// D3D12 implementation of the native-render RHI. A thin passthrough onto the // D3D12 command processor's deferred command list, barrier queue and // submission counters; the descriptor-field choices here reproduce exactly // what the scene renderer set when it talked to D3D12 directly, so migrating // the scene onto the RHI is behavior-preserving on this backend. #include #include #include #include #include #include #include #include #include #include #include #include namespace rex::graphics::d3d12 { namespace { using nrhi::Backend; using nrhi::Format; using nrhi::HeapKind; using nrhi::ResourceState; using nrhi::TextureKind; DXGI_FORMAT ToDxgi(Format format) { switch (format) { case Format::kR8G8B8A8_UNORM: return DXGI_FORMAT_R8G8B8A8_UNORM; case Format::kR8G8B8A8_UINT: return DXGI_FORMAT_R8G8B8A8_UINT; case Format::kR8_UNORM: return DXGI_FORMAT_R8_UNORM; case Format::kR8G8_UNORM: return DXGI_FORMAT_R8G8_UNORM; case Format::kB5G6R5_UNORM: return DXGI_FORMAT_B5G6R5_UNORM; case Format::kR16G16_UNORM: return DXGI_FORMAT_R16G16_UNORM; case Format::kR10G10B10A2_UNORM: return DXGI_FORMAT_R10G10B10A2_UNORM; case Format::kR32_FLOAT: return DXGI_FORMAT_R32_FLOAT; case Format::kR32G32_FLOAT: return DXGI_FORMAT_R32G32_FLOAT; case Format::kR32G32B32_FLOAT: return DXGI_FORMAT_R32G32B32_FLOAT; case Format::kR32G32B32A32_FLOAT: return DXGI_FORMAT_R32G32B32A32_FLOAT; case Format::kD32_FLOAT: return DXGI_FORMAT_D32_FLOAT; case Format::kBC1_UNORM: return DXGI_FORMAT_BC1_UNORM; case Format::kBC2_UNORM: return DXGI_FORMAT_BC2_UNORM; case Format::kBC3_UNORM: return DXGI_FORMAT_BC3_UNORM; case Format::kBC4_UNORM: return DXGI_FORMAT_BC4_UNORM; case Format::kBC5_UNORM: return DXGI_FORMAT_BC5_UNORM; default: return DXGI_FORMAT_UNKNOWN; } } Format FromDxgi(DXGI_FORMAT format) { switch (format) { case DXGI_FORMAT_R8G8B8A8_UNORM: return Format::kR8G8B8A8_UNORM; case DXGI_FORMAT_R10G10B10A2_UNORM: return Format::kR10G10B10A2_UNORM; default: return Format::kUnknown; } } D3D12_RESOURCE_STATES ToStates(ResourceState state, D3D12_RESOURCE_STATES guest_output_state) { switch (state) { case ResourceState::kRenderTarget: return D3D12_RESOURCE_STATE_RENDER_TARGET; case ResourceState::kDepthWrite: return D3D12_RESOURCE_STATE_DEPTH_WRITE; case ResourceState::kPixelShaderResource: return D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; case ResourceState::kCopySource: return D3D12_RESOURCE_STATE_COPY_SOURCE; case ResourceState::kCopyDest: return D3D12_RESOURCE_STATE_COPY_DEST; case ResourceState::kGenericRead: return D3D12_RESOURCE_STATE_GENERIC_READ; case ResourceState::kGuestOutput: return guest_output_state; case ResourceState::kCommon: default: return D3D12_RESOURCE_STATE_COMMON; } } UINT8 ToShaderComponentMapping(const nrhi::Swizzle swizzle) { switch (swizzle) { case nrhi::Swizzle::kX: return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_0; case nrhi::Swizzle::kY: return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_1; case nrhi::Swizzle::kZ: return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_2; case nrhi::Swizzle::kW: return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_3; case nrhi::Swizzle::kZero: return D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0; case nrhi::Swizzle::kOne: default: return D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1; } } D3D12_BLEND ToBlend(nrhi::BlendFactor factor) { switch (factor) { case nrhi::BlendFactor::kZero: return D3D12_BLEND_ZERO; case nrhi::BlendFactor::kSrcAlpha: return D3D12_BLEND_SRC_ALPHA; case nrhi::BlendFactor::kInvSrcAlpha: return D3D12_BLEND_INV_SRC_ALPHA; case nrhi::BlendFactor::kOne: default: return D3D12_BLEND_ONE; } } D3D12_BLEND_OP ToBlendOp(nrhi::BlendOp op) { return op == nrhi::BlendOp::kMin ? D3D12_BLEND_OP_MIN : D3D12_BLEND_OP_ADD; } D3D12_COMPARISON_FUNC ToCompare(nrhi::CompareFunc func) { switch (func) { case nrhi::CompareFunc::kLess: return D3D12_COMPARISON_FUNC_LESS; case nrhi::CompareFunc::kLessEqual: return D3D12_COMPARISON_FUNC_LESS_EQUAL; case nrhi::CompareFunc::kAlways: default: return D3D12_COMPARISON_FUNC_ALWAYS; } } class NrDeviceD3D12; class NrBufferD3D12 : public nrhi::Buffer { public: uint64_t size() const override { return size_; } ID3D12Resource* resource = nullptr; D3D12_GPU_VIRTUAL_ADDRESS gpu_va = 0; void* mapping = nullptr; uint64_t size_ = 0; HeapKind heap = HeapKind::kDefault; }; class NrTextureD3D12 : public nrhi::Texture { public: uint32_t width() const override { return desc.width; } uint32_t height() const override { return desc.height; } Format format() const override { return desc.format; } ID3D12Resource* resource = nullptr; nrhi::TextureDesc desc; DXGI_FORMAT dxgi_format = DXGI_FORMAT_UNKNOWN; // RTV/DSV slots in the device's non-shader-visible heaps, ~0u when absent. uint32_t rtv_slot = ~0u; uint32_t dsv_slot = ~0u; // Guest-output wrappers don't own their resource's lifetime semantics the // same way (they hold a ref, but are never destroyed via DestroyDeferred). bool is_guest_output = false; }; class NrTextureViewD3D12 : public nrhi::TextureView { public: // Slot in the device's non-shader-visible staging CBV_SRV_UAV heap; the // SRV descriptor is copied from here into shader-visible binding slots. uint32_t staging_slot = ~0u; NrTextureD3D12* texture = nullptr; }; class NrBindingLayoutD3D12 : public nrhi::BindingLayout { public: ID3D12RootSignature* root_signature = nullptr; }; class NrShaderD3D12 : public nrhi::Shader { public: ID3DBlob* blob = nullptr; }; class NrPipelineD3D12 : public nrhi::Pipeline { public: ID3D12PipelineState* pso = nullptr; }; // A simple slot allocator over a fixed-size descriptor heap with // submission-keyed retirement (mirrors the scene renderer's original // srv_next/srv_free/retired_srv_slots machinery). struct SlotAllocator { uint32_t capacity = 0; uint32_t next = 0; std::vector free_list; // (first_slot, count, submission) std::vector, uint64_t>> retired; bool Alloc(uint32_t count, uint32_t* out) { if (count == 1 && !free_list.empty()) { *out = free_list.back(); free_list.pop_back(); return true; } if (next + count <= capacity) { *out = next; next += count; return true; } return false; } void Retire(uint32_t first, uint32_t count, uint64_t submission) { retired.emplace_back(std::make_pair(first, count), submission); } void Drain(uint64_t completed) { std::erase_if(retired, [&](const auto& entry) { if (entry.second < completed) { for (uint32_t i = 0; i < entry.first.second; ++i) { free_list.push_back(entry.first.first + i); } return true; } return false; }); } }; class NrCmdD3D12 : public nrhi::Cmd { public: void SetBindingLayout(nrhi::BindingLayout* layout) override; void SetPipeline(nrhi::Pipeline* pipeline) override; void SetRootConstants(uint32_t param, uint32_t count, const void* values, uint32_t dest_offset_in_values) override; void SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) override; void SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) override; void SetTexture(uint32_t param, nrhi::TextureView* view) override; void SetTexturePair(uint32_t param, nrhi::TextureView* first, nrhi::TextureView* second) override; void SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) override; void SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) override; void ClearRenderTarget(nrhi::Texture* color, const float color4[4]) override; void ClearDepth(nrhi::Texture* depth, float value) override; void SetViewport(const nrhi::Viewport& viewport) override; void SetScissor(const nrhi::Rect& rect) override; void SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes, uint32_t stride) override; void SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes) override; void SetPrimitiveTopology(nrhi::PrimitiveTopology topology) override; void Draw(uint32_t vertex_count, uint32_t start_vertex) override; void DrawIndexed(uint32_t index_count, uint32_t start_index, int32_t base_vertex) override; void CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, uint32_t array_slice, nrhi::Buffer* src, uint64_t src_offset, uint32_t row_pitch, uint32_t width, uint32_t height, uint32_t depth) override; void CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, uint32_t row_pitch, nrhi::Texture* src, uint32_t mip, uint32_t width, uint32_t height) override; void Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) override; void FlushBarriers() override; NrDeviceD3D12* device = nullptr; }; class NrDeviceD3D12 : public nrhi::Device { public: explicit NrDeviceD3D12(D3D12CommandProcessor* cp) : cp_(cp) { const ui::d3d12::D3D12Provider& provider = cp_->GetD3D12Provider(); device_ = provider.GetDevice(); cmd_.device = this; D3D12_DESCRIPTOR_HEAP_DESC heap_desc{}; heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; heap_desc.NumDescriptors = kStagingViews; device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&staging_heap_)); heap_desc.NumDescriptors = kShaderVisibleViews; heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&srv_heap_)); heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; heap_desc.NumDescriptors = kRtvSlots; device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&rtv_heap_)); heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; heap_desc.NumDescriptors = kDsvSlots; device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&dsv_heap_)); view_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); rtv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); dsv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV); staging_slots_.capacity = kStagingViews; srv_slots_.capacity = kShaderVisibleViews; rtv_slots_.capacity = kRtvSlots; dsv_slots_.capacity = kDsvSlots; } ~NrDeviceD3D12() override { // Callers guarantee GPU idle; release everything immediately. DrainRetired(~0ull); for (auto& entry : guest_outputs_) { entry.second->resource->Release(); delete entry.second; } if (staging_heap_) staging_heap_->Release(); if (srv_heap_) srv_heap_->Release(); if (rtv_heap_) rtv_heap_->Release(); if (dsv_heap_) dsv_heap_->Release(); } Backend backend() const override { return Backend::kD3D12; } nrhi::Buffer* CreateBuffer(const nrhi::BufferDesc& desc) override { D3D12_RESOURCE_DESC rd{}; rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; rd.Width = desc.size; rd.Height = 1; rd.DepthOrArraySize = 1; rd.MipLevels = 1; rd.SampleDesc.Count = 1; rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; const D3D12_HEAP_PROPERTIES* heap = &ui::d3d12::util::kHeapPropertiesDefault; D3D12_RESOURCE_STATES state = D3D12_RESOURCE_STATE_COMMON; if (desc.heap == HeapKind::kUpload) { heap = &ui::d3d12::util::kHeapPropertiesUpload; state = D3D12_RESOURCE_STATE_GENERIC_READ; } else if (desc.heap == HeapKind::kReadback) { heap = &ui::d3d12::util::kHeapPropertiesReadback; state = D3D12_RESOURCE_STATE_COPY_DEST; } ID3D12Resource* resource = nullptr; if (FAILED(device_->CreateCommittedResource( heap, cp_->GetD3D12Provider().GetHeapFlagCreateNotZeroed(), &rd, state, nullptr, IID_PPV_ARGS(&resource)))) { REXLOG_ERROR("nrhi-d3d12: buffer creation failed ({} bytes)", desc.size); return nullptr; } auto* buffer = new NrBufferD3D12(); buffer->resource = resource; buffer->gpu_va = resource->GetGPUVirtualAddress(); buffer->size_ = desc.size; buffer->heap = desc.heap; return buffer; } nrhi::Texture* CreateTexture(const nrhi::TextureDesc& desc) override { D3D12_RESOURCE_DESC rd{}; rd.Dimension = desc.kind == TextureKind::k3D ? D3D12_RESOURCE_DIMENSION_TEXTURE3D : D3D12_RESOURCE_DIMENSION_TEXTURE2D; rd.Width = desc.width; rd.Height = desc.height; rd.DepthOrArraySize = UINT16(desc.kind == TextureKind::kCube ? 6 : desc.kind == TextureKind::k3D ? desc.depth : 1); rd.MipLevels = UINT16(desc.mip_levels); rd.Format = ToDxgi(desc.format); rd.SampleDesc.Count = desc.sample_count; rd.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; D3D12_CLEAR_VALUE clear{}; const D3D12_CLEAR_VALUE* clear_ptr = nullptr; if (desc.usage & nrhi::kTextureUsageRenderTarget) { rd.Flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; clear.Format = rd.Format; std::memcpy(clear.Color, desc.clear_color, sizeof(clear.Color)); clear_ptr = &clear; } if (desc.usage & nrhi::kTextureUsageDepthStencil) { // The depth resource is created typeless and viewed as D32_FLOAT / // R32_FLOAT; a typed depth SRV read the clear value on some drivers // (the photo-editor DoF smear); preserve the proven arrangement. rd.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; if (desc.format == Format::kD32_FLOAT) { rd.Format = DXGI_FORMAT_R32_TYPELESS; } clear.Format = DXGI_FORMAT_D32_FLOAT; clear.DepthStencil.Depth = desc.clear_depth; clear_ptr = &clear; } ID3D12Resource* resource = nullptr; if (FAILED(device_->CreateCommittedResource( &ui::d3d12::util::kHeapPropertiesDefault, cp_->GetD3D12Provider().GetHeapFlagCreateNotZeroed(), &rd, ToStates(desc.initial_state, guest_output_state_), clear_ptr, IID_PPV_ARGS(&resource)))) { REXLOG_ERROR("nrhi-d3d12: texture creation failed ({}x{} fmt {})", desc.width, desc.height, uint32_t(desc.format)); return nullptr; } auto* texture = new NrTextureD3D12(); texture->resource = resource; texture->desc = desc; texture->dxgi_format = ToDxgi(desc.format); if (desc.usage & nrhi::kTextureUsageRenderTarget) { std::lock_guard lock(mutex_); if (rtv_slots_.Alloc(1, &texture->rtv_slot)) { device_->CreateRenderTargetView(resource, nullptr, RtvHandle(texture->rtv_slot)); } } if (desc.usage & nrhi::kTextureUsageDepthStencil) { std::lock_guard lock(mutex_); if (dsv_slots_.Alloc(1, &texture->dsv_slot)) { D3D12_DEPTH_STENCIL_VIEW_DESC dsv{}; dsv.Format = DXGI_FORMAT_D32_FLOAT; dsv.ViewDimension = desc.sample_count > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DMS : D3D12_DSV_DIMENSION_TEXTURE2D; device_->CreateDepthStencilView(resource, &dsv, DsvHandle(texture->dsv_slot)); } } return texture; } void* Map(nrhi::Buffer* buffer) override { auto* b = static_cast(buffer); if (b->mapping == nullptr) { const D3D12_RANGE no_read{}; // Readback buffers are read through mapped memory; upload buffers are // write-only (pass an empty read range). b->resource->Map(0, b->heap == HeapKind::kReadback ? nullptr : &no_read, &b->mapping); } return b->mapping; } void Unmap(nrhi::Buffer* buffer) override { auto* b = static_cast(buffer); if (b->mapping != nullptr) { b->resource->Unmap(0, nullptr); b->mapping = nullptr; } } void InvalidateForRead(nrhi::Buffer*, uint64_t, uint64_t) override {} void DestroyDeferred(nrhi::Buffer* buffer) override { if (buffer == nullptr) return; auto* b = static_cast(buffer); std::lock_guard lock(mutex_); retired_.emplace_back(RetiredObject{b->resource, nullptr, cp_->GetCurrentSubmission()}); delete b; } void DestroyDeferred(nrhi::Texture* texture) override { if (texture == nullptr) return; auto* t = static_cast(texture); std::lock_guard lock(mutex_); const uint64_t submission = cp_->GetCurrentSubmission(); if (t->rtv_slot != ~0u) rtv_slots_.Retire(t->rtv_slot, 1, submission); if (t->dsv_slot != ~0u) dsv_slots_.Retire(t->dsv_slot, 1, submission); retired_.emplace_back(RetiredObject{t->resource, nullptr, submission}); delete t; } void DestroyDeferred(nrhi::TextureView* view) override { if (view == nullptr) return; auto* v = static_cast(view); std::lock_guard lock(mutex_); const uint64_t submission = cp_->GetCurrentSubmission(); staging_slots_.Retire(v->staging_slot, 1, submission); // Retire every shader-visible binding this view participates in. std::erase_if(bindings_, [&](auto& entry) { for (uint32_t i = 0; i < entry.first.count; ++i) { if (entry.first.views[i] == v) { srv_slots_.Retire(entry.second.first_slot, entry.second.count, submission); return true; } } return false; }); delete v; } void DestroyDeferred(nrhi::Pipeline* pipeline) override { if (pipeline == nullptr) return; auto* p = static_cast(pipeline); std::lock_guard lock(mutex_); retired_.emplace_back(RetiredObject{nullptr, p->pso, cp_->GetCurrentSubmission()}); delete p; } void DestroyDeferred(nrhi::Shader* shader) override { if (shader == nullptr) return; auto* s = static_cast(shader); if (s->blob) s->blob->Release(); delete s; } nrhi::TextureView* CreateTextureView(nrhi::Texture* texture, const nrhi::TextureViewDesc& desc) override { auto* t = static_cast(texture); uint32_t slot; { std::lock_guard lock(mutex_); if (!staging_slots_.Alloc(1, &slot)) { REXLOG_ERROR("nrhi-d3d12: staging view heap exhausted"); return nullptr; } } D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; srv.Format = desc.format != Format::kUnknown ? ToDxgi(desc.format) : t->desc.format == Format::kD32_FLOAT ? DXGI_FORMAT_R32_FLOAT : t->dxgi_format; srv.Shader4ComponentMapping = D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING( ToShaderComponentMapping(desc.swizzle[0]), ToShaderComponentMapping(desc.swizzle[1]), ToShaderComponentMapping(desc.swizzle[2]), ToShaderComponentMapping(desc.swizzle[3])); const uint32_t mips = desc.mip_levels == ~0u ? (t->desc.mip_levels - desc.base_mip) : desc.mip_levels; switch (desc.dimension) { case nrhi::ViewDimension::k2DMS: srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS; break; case nrhi::ViewDimension::kCube: srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; srv.TextureCube.MostDetailedMip = desc.base_mip; srv.TextureCube.MipLevels = mips; break; case nrhi::ViewDimension::k3D: srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; srv.Texture3D.MostDetailedMip = desc.base_mip; srv.Texture3D.MipLevels = mips; break; case nrhi::ViewDimension::k2D: default: srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv.Texture2D.MostDetailedMip = desc.base_mip; srv.Texture2D.MipLevels = mips; break; } device_->CreateShaderResourceView(t->resource, &srv, StagingHandle(slot)); auto* view = new NrTextureViewD3D12(); view->staging_slot = slot; view->texture = t; return view; } nrhi::BindingLayout* CreateBindingLayout(const nrhi::BindingLayoutDesc& desc) override { D3D12_ROOT_PARAMETER params[nrhi::kMaxBindingParams] = {}; D3D12_DESCRIPTOR_RANGE ranges[nrhi::kMaxBindingParams] = {}; for (uint32_t i = 0; i < desc.param_count; ++i) { const nrhi::BindingParamDesc& p = desc.params[i]; D3D12_SHADER_VISIBILITY visibility = p.visibility == nrhi::Visibility::kVertex ? D3D12_SHADER_VISIBILITY_VERTEX : p.visibility == nrhi::Visibility::kPixel ? D3D12_SHADER_VISIBILITY_PIXEL : D3D12_SHADER_VISIBILITY_ALL; params[i].ShaderVisibility = visibility; switch (p.kind) { case nrhi::BindingParamKind::kConstants: params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; params[i].Constants.ShaderRegister = p.shader_register; params[i].Constants.Num32BitValues = p.count; break; case nrhi::BindingParamKind::kConstantBuffer: params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV; params[i].Descriptor.ShaderRegister = p.shader_register; break; case nrhi::BindingParamKind::kBufferSrv: params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV; params[i].Descriptor.ShaderRegister = p.shader_register; break; case nrhi::BindingParamKind::kTextureTable: ranges[i].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; ranges[i].NumDescriptors = p.count; ranges[i].BaseShaderRegister = p.shader_register; params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; params[i].DescriptorTable.NumDescriptorRanges = 1; params[i].DescriptorTable.pDescriptorRanges = &ranges[i]; break; } } D3D12_STATIC_SAMPLER_DESC samplers[nrhi::kMaxStaticSamplers] = {}; for (uint32_t i = 0; i < desc.static_sampler_count; ++i) { const nrhi::StaticSamplerDesc& s = desc.static_samplers[i]; samplers[i].Filter = s.filter == nrhi::Filter::kAnisotropic ? D3D12_FILTER_ANISOTROPIC : s.filter == nrhi::Filter::kLinear ? D3D12_FILTER_MIN_MAG_MIP_LINEAR : D3D12_FILTER_MIN_MAG_MIP_POINT; samplers[i].MaxAnisotropy = s.max_anisotropy; D3D12_TEXTURE_ADDRESS_MODE mode = s.address == nrhi::AddressMode::kWrap ? D3D12_TEXTURE_ADDRESS_MODE_WRAP : D3D12_TEXTURE_ADDRESS_MODE_CLAMP; samplers[i].AddressU = mode; samplers[i].AddressV = mode; samplers[i].AddressW = mode; samplers[i].MaxLOD = D3D12_FLOAT32_MAX; samplers[i].ShaderRegister = s.shader_register; samplers[i].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; } D3D12_ROOT_SIGNATURE_DESC rs{}; rs.NumParameters = desc.param_count; rs.pParameters = params; rs.NumStaticSamplers = desc.static_sampler_count; rs.pStaticSamplers = samplers; rs.Flags = desc.allow_input_layout ? D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT : D3D12_ROOT_SIGNATURE_FLAG_NONE; ID3D12RootSignature* root_signature = ui::d3d12::util::CreateRootSignature(cp_->GetD3D12Provider(), rs); if (root_signature == nullptr) { REXLOG_ERROR("nrhi-d3d12: root signature creation failed"); return nullptr; } auto* layout = new NrBindingLayoutD3D12(); layout->root_signature = root_signature; return layout; } nrhi::Shader* CreateShader(const nrhi::ShaderDesc& desc) override { D3D_SHADER_MACRO macros[9] = {}; uint32_t macro_count = 0; if (desc.macros != nullptr) { while (desc.macros[macro_count].name != nullptr && macro_count < 8) { macros[macro_count].Name = desc.macros[macro_count].name; macros[macro_count].Definition = desc.macros[macro_count].value; ++macro_count; } } ID3DBlob* blob = nullptr; ID3DBlob* errors = nullptr; const char* target = desc.stage == nrhi::ShaderStage::kVertex ? "vs_5_0" : "ps_5_0"; HRESULT hr = D3DCompile(desc.hlsl_source, std::strlen(desc.hlsl_source), desc.name != nullptr ? desc.name : "nrhi_shader", macro_count != 0 ? macros : nullptr, nullptr, desc.entry_point, target, 0, 0, &blob, &errors); if (FAILED(hr)) { REXLOG_ERROR("nrhi-d3d12: shader compile failed ({} {}): {}", desc.name != nullptr ? desc.name : "?", desc.entry_point, errors != nullptr ? static_cast(errors->GetBufferPointer()) : "no error blob"); if (errors) errors->Release(); return nullptr; } if (errors) errors->Release(); auto* shader = new NrShaderD3D12(); shader->blob = blob; return shader; } nrhi::Pipeline* CreateGraphicsPipeline(const nrhi::GraphicsPipelineDesc& desc) override { auto* layout = static_cast(desc.layout); auto* vs = static_cast(desc.vs); auto* ps = static_cast(desc.ps); if (layout == nullptr || vs == nullptr || ps == nullptr) return nullptr; D3D12_GRAPHICS_PIPELINE_STATE_DESC pd{}; pd.pRootSignature = layout->root_signature; pd.VS = {vs->blob->GetBufferPointer(), vs->blob->GetBufferSize()}; pd.PS = {ps->blob->GetBufferPointer(), ps->blob->GetBufferSize()}; pd.BlendState.RenderTarget[0].BlendEnable = desc.blend.enable; pd.BlendState.RenderTarget[0].SrcBlend = ToBlend(desc.blend.src); pd.BlendState.RenderTarget[0].DestBlend = ToBlend(desc.blend.dst); pd.BlendState.RenderTarget[0].BlendOp = ToBlendOp(desc.blend.op); pd.BlendState.RenderTarget[0].SrcBlendAlpha = ToBlend(desc.blend.src_alpha); pd.BlendState.RenderTarget[0].DestBlendAlpha = ToBlend(desc.blend.dst_alpha); pd.BlendState.RenderTarget[0].BlendOpAlpha = ToBlendOp(desc.blend.op_alpha); pd.BlendState.RenderTarget[0].RenderTargetWriteMask = desc.blend.write_mask; pd.SampleMask = UINT_MAX; pd.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; pd.RasterizerState.CullMode = desc.cull == nrhi::CullMode::kFront ? D3D12_CULL_MODE_FRONT : desc.cull == nrhi::CullMode::kBack ? D3D12_CULL_MODE_BACK : D3D12_CULL_MODE_NONE; pd.RasterizerState.DepthClipEnable = desc.depth_clip; pd.DepthStencilState.DepthEnable = desc.depth.test_enable; pd.DepthStencilState.DepthWriteMask = desc.depth.write_enable ? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO; pd.DepthStencilState.DepthFunc = ToCompare(desc.depth.func); D3D12_INPUT_ELEMENT_DESC elements[16] = {}; if (desc.input_elements != nullptr && desc.input_element_count != 0 && desc.input_element_count <= 16) { for (uint32_t i = 0; i < desc.input_element_count; ++i) { const nrhi::InputElementDesc& e = desc.input_elements[i]; elements[i].SemanticName = e.semantic_name; elements[i].SemanticIndex = e.semantic_index; elements[i].Format = ToDxgi(e.format); elements[i].AlignedByteOffset = e.byte_offset; elements[i].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; } pd.InputLayout.pInputElementDescs = elements; pd.InputLayout.NumElements = desc.input_element_count; } pd.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; if (desc.rtv_format != Format::kUnknown) { pd.NumRenderTargets = 1; pd.RTVFormats[0] = ToDxgi(desc.rtv_format); } if (desc.dsv_format != Format::kUnknown) { pd.DSVFormat = ToDxgi(desc.dsv_format); } pd.SampleDesc.Count = desc.sample_count; ID3D12PipelineState* pso = nullptr; if (FAILED(device_->CreateGraphicsPipelineState(&pd, IID_PPV_ARGS(&pso)))) { REXLOG_ERROR("nrhi-d3d12: graphics pipeline creation failed"); return nullptr; } auto* pipeline = new NrPipelineD3D12(); pipeline->pso = pso; return pipeline; } uint32_t GetSupportedSampleCount(Format format, uint32_t desired) override { DXGI_FORMAT dxgi = ToDxgi(format); uint32_t count = desired; while (count > 1) { D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS levels{}; levels.Format = dxgi; levels.SampleCount = count; if (SUCCEEDED(device_->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &levels, sizeof(levels))) && levels.NumQualityLevels > 0) { break; } count >>= 1; } return std::max(count, 1u); } uint64_t CurrentSubmission() const override { return cp_->GetCurrentSubmission(); } uint64_t CompletedSubmission() const override { return cp_->GetCompletedSubmission(); } // --- frame handling (called from the command processor) --- nrhi::Cmd* BeginFrame(ID3D12Resource* guest_output_resource, DXGI_FORMAT guest_output_format, D3D12_RESOURCE_STATES guest_output_internal_state, uint32_t width, uint32_t height, nrhi::Texture** guest_output_out) { guest_output_state_ = guest_output_internal_state; { std::lock_guard lock(mutex_); DrainRetired(cp_->GetCompletedSubmission()); } NrTextureD3D12*& wrapper = guest_outputs_[guest_output_resource]; if (wrapper != nullptr && (wrapper->desc.width != width || wrapper->desc.height != height)) { // Same resource pointer, different geometry: a recreated image reusing // the address. Retire the stale wrapper's views and slots. DestroyGuestOutputWrapper(wrapper); wrapper = nullptr; } if (wrapper == nullptr) { wrapper = new NrTextureD3D12(); guest_output_resource->AddRef(); wrapper->resource = guest_output_resource; wrapper->desc.width = width; wrapper->desc.height = height; wrapper->desc.format = FromDxgi(guest_output_format); wrapper->desc.usage = nrhi::kTextureUsageRenderTarget; wrapper->dxgi_format = guest_output_format; wrapper->is_guest_output = true; std::lock_guard lock(mutex_); if (rtv_slots_.Alloc(1, &wrapper->rtv_slot)) { device_->CreateRenderTargetView(guest_output_resource, nullptr, RtvHandle(wrapper->rtv_slot)); } // The presenter keeps a small guest-output mailbox; evict wrappers of // resources it has replaced. if (guest_outputs_.size() > 6) { for (auto it = guest_outputs_.begin(); it != guest_outputs_.end();) { if (it->second != wrapper && it->first != guest_output_resource) { DestroyGuestOutputWrapperLocked(it->second); it = guest_outputs_.erase(it); } else { ++it; } } } } *guest_output_out = wrapper; return &cmd_; } // --- internals shared with NrCmdD3D12 --- D3D12_CPU_DESCRIPTOR_HANDLE StagingHandle(uint32_t slot) const { D3D12_CPU_DESCRIPTOR_HANDLE handle = staging_heap_->GetCPUDescriptorHandleForHeapStart(); handle.ptr += size_t(slot) * view_size_; return handle; } D3D12_CPU_DESCRIPTOR_HANDLE SrvCpuHandle(uint32_t slot) const { D3D12_CPU_DESCRIPTOR_HANDLE handle = srv_heap_->GetCPUDescriptorHandleForHeapStart(); handle.ptr += size_t(slot) * view_size_; return handle; } D3D12_GPU_DESCRIPTOR_HANDLE SrvGpuHandle(uint32_t slot) const { D3D12_GPU_DESCRIPTOR_HANDLE handle = srv_heap_->GetGPUDescriptorHandleForHeapStart(); handle.ptr += size_t(slot) * view_size_; return handle; } D3D12_CPU_DESCRIPTOR_HANDLE RtvHandle(uint32_t slot) const { D3D12_CPU_DESCRIPTOR_HANDLE handle = rtv_heap_->GetCPUDescriptorHandleForHeapStart(); handle.ptr += size_t(slot) * rtv_size_; return handle; } D3D12_CPU_DESCRIPTOR_HANDLE DsvHandle(uint32_t slot) const { D3D12_CPU_DESCRIPTOR_HANDLE handle = dsv_heap_->GetCPUDescriptorHandleForHeapStart(); handle.ptr += size_t(slot) * dsv_size_; return handle; } // Shader-visible binding for an ordered view tuple: consecutive slots // holding copies of the staging descriptors, cached and retired when a // participating view is destroyed. Render thread only. bool GetBinding(NrTextureViewD3D12* const* views, uint32_t count, D3D12_GPU_DESCRIPTOR_HANDLE* gpu_out) { if (count == 0 || count > nrhi::kMaxTextureTableSize || views[0] == nullptr) { return false; } BindingKey key{}; for (uint32_t i = 0; i < count; ++i) { key.views[i] = views[i]; } key.count = count; auto it = bindings_.find(key); if (it == bindings_.end()) { uint32_t first_slot; { std::lock_guard lock(mutex_); if (!srv_slots_.Alloc(count, &first_slot)) { REXLOG_ERROR("nrhi-d3d12: shader-visible view heap exhausted"); return false; } } for (uint32_t i = 0; i < count; ++i) { if (views[i] == nullptr) continue; device_->CopyDescriptorsSimple(1, SrvCpuHandle(first_slot + i), StagingHandle(views[i]->staging_slot), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); } it = bindings_.emplace(key, Binding{first_slot, count}).first; } *gpu_out = SrvGpuHandle(it->second.first_slot); return true; } D3D12CommandProcessor* cp() { return cp_; } ID3D12DescriptorHeap* srv_heap() { return srv_heap_; } D3D12_RESOURCE_STATES guest_output_state() const { return guest_output_state_; } private: static constexpr uint32_t kStagingViews = 16384; static constexpr uint32_t kShaderVisibleViews = 32768; static constexpr uint32_t kRtvSlots = 64; static constexpr uint32_t kDsvSlots = 8; struct RetiredObject { ID3D12Resource* resource; ID3D12PipelineState* pso; uint64_t submission; }; struct BindingKey { NrTextureViewD3D12* views[nrhi::kMaxTextureTableSize]; uint32_t count; bool operator<(const BindingKey& other) const { if (count != other.count) return count < other.count; for (uint32_t i = 0; i < count; ++i) { if (views[i] != other.views[i]) return views[i] < other.views[i]; } return false; } }; struct Binding { uint32_t first_slot; uint32_t count; }; void DrainRetired(uint64_t completed) { std::erase_if(retired_, [&](const RetiredObject& r) { if (r.submission < completed) { if (r.resource) r.resource->Release(); if (r.pso) r.pso->Release(); return true; } return false; }); staging_slots_.Drain(completed); srv_slots_.Drain(completed); rtv_slots_.Drain(completed); dsv_slots_.Drain(completed); } void DestroyGuestOutputWrapperLocked(NrTextureD3D12* wrapper) { const uint64_t submission = cp_->GetCurrentSubmission(); if (wrapper->rtv_slot != ~0u) rtv_slots_.Retire(wrapper->rtv_slot, 1, submission); retired_.emplace_back(RetiredObject{wrapper->resource, nullptr, submission}); delete wrapper; } void DestroyGuestOutputWrapper(NrTextureD3D12* wrapper) { std::lock_guard lock(mutex_); DestroyGuestOutputWrapperLocked(wrapper); } D3D12CommandProcessor* cp_; ID3D12Device* device_ = nullptr; NrCmdD3D12 cmd_; ID3D12DescriptorHeap* staging_heap_ = nullptr; ID3D12DescriptorHeap* srv_heap_ = nullptr; ID3D12DescriptorHeap* rtv_heap_ = nullptr; ID3D12DescriptorHeap* dsv_heap_ = nullptr; uint32_t view_size_ = 0; uint32_t rtv_size_ = 0; uint32_t dsv_size_ = 0; // mutex_ guards the slot allocators and retired_ (creation and deferred // destruction are thread-safe); bindings_ is render-thread-only. std::mutex mutex_; SlotAllocator staging_slots_; SlotAllocator srv_slots_; SlotAllocator rtv_slots_; SlotAllocator dsv_slots_; std::vector retired_; std::map bindings_; std::map guest_outputs_; D3D12_RESOURCE_STATES guest_output_state_ = D3D12_RESOURCE_STATE_COMMON; }; void NrCmdD3D12::SetBindingLayout(nrhi::BindingLayout* layout) { auto* l = static_cast(layout); DeferredCommandList& list = device->cp()->GetDeferredCommandList(); list.SetDescriptorHeaps(device->srv_heap(), nullptr); list.D3DSetGraphicsRootSignature(l->root_signature); } void NrCmdD3D12::SetPipeline(nrhi::Pipeline* pipeline) { device->cp()->GetDeferredCommandList().D3DSetPipelineState( static_cast(pipeline)->pso); } void NrCmdD3D12::SetRootConstants(uint32_t param, uint32_t count, const void* values, uint32_t dest_offset_in_values) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRoot32BitConstants( param, count, values, dest_offset_in_values); } void NrCmdD3D12::SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRootConstantBufferView( param, static_cast(buffer)->gpu_va + offset); } void NrCmdD3D12::SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRootShaderResourceView( param, static_cast(buffer)->gpu_va + offset); } void NrCmdD3D12::SetTexture(uint32_t param, nrhi::TextureView* view) { NrTextureViewD3D12* views[1] = {static_cast(view)}; D3D12_GPU_DESCRIPTOR_HANDLE handle; if (device->GetBinding(views, 1, &handle)) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); } } void NrCmdD3D12::SetTexturePair(uint32_t param, nrhi::TextureView* first, nrhi::TextureView* second) { NrTextureViewD3D12* views[2] = {static_cast(first), static_cast(second)}; D3D12_GPU_DESCRIPTOR_HANDLE handle; if (device->GetBinding(views, 2, &handle)) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); } } void NrCmdD3D12::SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) { NrTextureViewD3D12* typed[nrhi::kMaxTextureTableSize] = {}; if (count > nrhi::kMaxTextureTableSize) return; for (uint32_t i = 0; i < count; ++i) { typed[i] = static_cast(views[i]); } D3D12_GPU_DESCRIPTOR_HANDLE handle; if (device->GetBinding(typed, count, &handle)) { device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); } } void NrCmdD3D12::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) { D3D12_CPU_DESCRIPTOR_HANDLE rtv; D3D12_CPU_DESCRIPTOR_HANDLE dsv; const D3D12_CPU_DESCRIPTOR_HANDLE* rtv_ptr = nullptr; const D3D12_CPU_DESCRIPTOR_HANDLE* dsv_ptr = nullptr; UINT num_rtvs = 0; if (color != nullptr) { rtv = device->RtvHandle(static_cast(color)->rtv_slot); rtv_ptr = &rtv; num_rtvs = 1; } if (depth != nullptr) { dsv = device->DsvHandle(static_cast(depth)->dsv_slot); dsv_ptr = &dsv; } device->cp()->GetDeferredCommandList().D3DOMSetRenderTargets(num_rtvs, rtv_ptr, FALSE, dsv_ptr); } void NrCmdD3D12::ClearRenderTarget(nrhi::Texture* color, const float color4[4]) { device->cp()->GetDeferredCommandList().D3DClearRenderTargetView( device->RtvHandle(static_cast(color)->rtv_slot), color4, 0, nullptr); } void NrCmdD3D12::ClearDepth(nrhi::Texture* depth, float value) { device->cp()->GetDeferredCommandList().D3DClearDepthStencilView( device->DsvHandle(static_cast(depth)->dsv_slot), D3D12_CLEAR_FLAG_DEPTH, value, 0, 0, nullptr); } void NrCmdD3D12::SetViewport(const nrhi::Viewport& viewport) { D3D12_VIEWPORT vp; vp.TopLeftX = viewport.x; vp.TopLeftY = viewport.y; vp.Width = viewport.width; vp.Height = viewport.height; vp.MinDepth = viewport.min_depth; vp.MaxDepth = viewport.max_depth; device->cp()->GetDeferredCommandList().RSSetViewport(vp); } void NrCmdD3D12::SetScissor(const nrhi::Rect& rect) { D3D12_RECT r; r.left = rect.left; r.top = rect.top; r.right = rect.right; r.bottom = rect.bottom; device->cp()->GetDeferredCommandList().RSSetScissorRect(r); } void NrCmdD3D12::SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes, uint32_t stride) { D3D12_VERTEX_BUFFER_VIEW view; view.BufferLocation = static_cast(buffer)->gpu_va + offset; view.SizeInBytes = size_bytes; view.StrideInBytes = stride; device->cp()->GetDeferredCommandList().D3DIASetVertexBuffers(0, 1, &view); } void NrCmdD3D12::SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes) { D3D12_INDEX_BUFFER_VIEW view; view.BufferLocation = static_cast(buffer)->gpu_va + offset; view.SizeInBytes = size_bytes; view.Format = DXGI_FORMAT_R16_UINT; device->cp()->GetDeferredCommandList().D3DIASetIndexBuffer(&view); } void NrCmdD3D12::SetPrimitiveTopology(nrhi::PrimitiveTopology topology) { device->cp()->GetDeferredCommandList().D3DIASetPrimitiveTopology( topology == nrhi::PrimitiveTopology::kTriangleStrip ? D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP : D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); } void NrCmdD3D12::Draw(uint32_t vertex_count, uint32_t start_vertex) { device->cp()->GetDeferredCommandList().D3DDrawInstanced(vertex_count, 1, start_vertex, 0); } void NrCmdD3D12::DrawIndexed(uint32_t index_count, uint32_t start_index, int32_t base_vertex) { device->cp()->GetDeferredCommandList().D3DDrawIndexedInstanced(index_count, 1, start_index, base_vertex, 0); } void NrCmdD3D12::CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, uint32_t array_slice, nrhi::Buffer* src, uint64_t src_offset, uint32_t row_pitch, uint32_t width, uint32_t height, uint32_t depth) { auto* t = static_cast(dst); D3D12_TEXTURE_COPY_LOCATION dst_loc{}; dst_loc.pResource = t->resource; dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dst_loc.SubresourceIndex = mip + array_slice * t->desc.mip_levels; D3D12_TEXTURE_COPY_LOCATION src_loc{}; src_loc.pResource = static_cast(src)->resource; src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; src_loc.PlacedFootprint.Offset = src_offset; src_loc.PlacedFootprint.Footprint.Format = t->dxgi_format; src_loc.PlacedFootprint.Footprint.Width = width; src_loc.PlacedFootprint.Footprint.Height = height; src_loc.PlacedFootprint.Footprint.Depth = depth; src_loc.PlacedFootprint.Footprint.RowPitch = row_pitch; device->cp()->GetDeferredCommandList().D3DCopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nullptr); } void NrCmdD3D12::CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, uint32_t row_pitch, nrhi::Texture* src, uint32_t mip, uint32_t width, uint32_t height) { auto* t = static_cast(src); D3D12_TEXTURE_COPY_LOCATION src_loc{}; src_loc.pResource = t->resource; src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; src_loc.SubresourceIndex = mip; D3D12_TEXTURE_COPY_LOCATION dst_loc{}; dst_loc.pResource = static_cast(dst)->resource; dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; dst_loc.PlacedFootprint.Offset = dst_offset; dst_loc.PlacedFootprint.Footprint.Format = t->dxgi_format; dst_loc.PlacedFootprint.Footprint.Width = width; dst_loc.PlacedFootprint.Footprint.Height = height; dst_loc.PlacedFootprint.Footprint.Depth = 1; dst_loc.PlacedFootprint.Footprint.RowPitch = row_pitch; device->cp()->GetDeferredCommandList().D3DCopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nullptr); } void NrCmdD3D12::Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) { auto* t = static_cast(texture); device->cp()->PushTransitionBarrier(t->resource, ToStates(before, device->guest_output_state()), ToStates(after, device->guest_output_state())); } void NrCmdD3D12::FlushBarriers() { device->cp()->SubmitBarriers(); } } // namespace nrhi::Device* CreateNativeRhiDevice(D3D12CommandProcessor* command_processor) { return new NrDeviceD3D12(command_processor); } void DestroyNativeRhiDevice(nrhi::Device* device) { delete static_cast(device); } nrhi::Cmd* NativeRhiBeginFrame(nrhi::Device* device, ID3D12Resource* guest_output_resource, DXGI_FORMAT guest_output_format, D3D12_RESOURCE_STATES guest_output_internal_state, uint32_t width, uint32_t height, nrhi::Texture** guest_output_out) { return static_cast(device)->BeginFrame( guest_output_resource, guest_output_format, guest_output_internal_state, width, height, guest_output_out); } } // namespace rex::graphics::d3d12