#include "D3D12UiTextureHost.h" #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::Host { namespace { std::string HrToString(const char* operation, HRESULT hr) { char buffer[128] = {}; sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast(hr)); return buffer; } constexpr std::uint32_t kTextureBytesPerPixel = 4u; std::uint32_t AlignTextureRowPitch(std::uint32_t rowPitch) { constexpr std::uint32_t alignment = D3D12_TEXTURE_DATA_PITCH_ALIGNMENT; return (rowPitch + alignment - 1u) & ~(alignment - 1u); } } // namespace bool D3D12UiTextureHost::Initialize(D3D12WindowRenderer& windowRenderer) { Shutdown(); if (windowRenderer.GetRHIDevice() == nullptr) { m_lastError = "Initialize requires an initialized D3D12 window renderer."; return false; } m_windowRenderer = &windowRenderer; m_lastError.clear(); return true; } void D3D12UiTextureHost::BeginFrame(std::uint32_t frameSlot) { if (frameSlot >= m_frameUploadBuffers.size()) { m_hasActiveFrameSlot = false; return; } if (!m_retiredTextures[frameSlot].empty()) { std::ostringstream stream = {}; stream << "D3D12UiTextureHost::BeginFrame reclaim frameSlot=" << frameSlot << " retiredCount=" << m_retiredTextures[frameSlot].size(); AppendUIEditorRuntimeTrace("window-close", stream.str()); } DestroyQueuedRetiredTextures(frameSlot); m_frameUploadBuffers[frameSlot].clear(); m_activeFrameSlot = frameSlot; m_hasActiveFrameSlot = true; } void D3D12UiTextureHost::Shutdown() { std::size_t retiredTextureCount = 0u; for (const auto& retiredTextures : m_retiredTextures) { retiredTextureCount += retiredTextures.size(); } { std::ostringstream stream = {}; stream << "D3D12UiTextureHost::Shutdown begin liveTextures=" << m_liveTextures.size() << " retiredTextures=" << retiredTextureCount << " activeFrameSlot=" << m_activeFrameSlot << " hasActiveFrameSlot=" << (m_hasActiveFrameSlot ? 1 : 0); AppendUIEditorRuntimeTrace("window-close", stream.str()); } if (m_windowRenderer != nullptr) { m_windowRenderer->WaitForGpuIdle(); } for (auto& entry : m_liveTextures) { if (entry.second != nullptr) { DestroyTextureResource(*entry.second); } } m_liveTextures.clear(); for (auto& retiredTextures : m_retiredTextures) { for (auto& textureResource : retiredTextures) { if (textureResource != nullptr) { DestroyTextureResource(*textureResource); } } retiredTextures.clear(); } for (auto& uploadBuffers : m_frameUploadBuffers) { uploadBuffers.clear(); } m_wicFactory.Reset(); if (m_wicComInitialized) { CoUninitialize(); m_wicComInitialized = false; } m_windowRenderer = nullptr; m_activeFrameSlot = 0u; m_hasActiveFrameSlot = false; m_lastError.clear(); AppendUIEditorRuntimeTrace("window-close", "D3D12UiTextureHost::Shutdown end"); } bool D3D12UiTextureHost::IsInitialized() const { return m_windowRenderer != nullptr && m_windowRenderer->GetRHIDevice() != nullptr; } const std::string& D3D12UiTextureHost::GetLastError() const { return m_lastError; } bool D3D12UiTextureHost::EnsureWicFactory(std::string& outError) { outError.clear(); if (m_wicFactory != nullptr) { return true; } const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { outError = HrToString("CoInitializeEx", initHr); return false; } if (SUCCEEDED(initHr)) { m_wicComInitialized = true; } const HRESULT factoryHr = CoCreateInstance( CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf())); if (FAILED(factoryHr)) { outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); return false; } return true; } bool D3D12UiTextureHost::DecodeTextureFile( const std::filesystem::path& path, std::vector& outPixels, std::uint32_t& outWidth, std::uint32_t& outHeight, std::string& outError) { outPixels.clear(); outWidth = 0u; outHeight = 0u; outError.clear(); if (!EnsureWicFactory(outError)) { return false; } Microsoft::WRL::ComPtr decoder = {}; const HRESULT hr = m_wicFactory->CreateDecoderFromFilename( path.wstring().c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnLoad, decoder.ReleaseAndGetAddressOf()); if (FAILED(hr) || decoder == nullptr) { outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); return false; } Microsoft::WRL::ComPtr frame = {}; const HRESULT frameHr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); if (FAILED(frameHr) || frame == nullptr) { outError = HrToString("IWICBitmapDecoder::GetFrame", frameHr); return false; } return DecodeTextureFrame(*frame.Get(), outPixels, outWidth, outHeight, outError); } bool D3D12UiTextureHost::DecodeTextureMemory( const std::uint8_t* data, std::size_t size, std::vector& outPixels, std::uint32_t& outWidth, std::uint32_t& outHeight, std::string& outError) { outPixels.clear(); outWidth = 0u; outHeight = 0u; outError.clear(); if (data == nullptr || size == 0u) { outError = "DecodeTextureMemory rejected an empty image payload."; return false; } if (size > static_cast((std::numeric_limits::max)())) { outError = "DecodeTextureMemory payload exceeds WIC stream limits."; return false; } if (!EnsureWicFactory(outError)) { return false; } Microsoft::WRL::ComPtr stream = {}; HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); if (FAILED(hr) || stream == nullptr) { outError = HrToString("IWICImagingFactory::CreateStream", hr); return false; } hr = stream->InitializeFromMemory( const_cast(reinterpret_cast(data)), static_cast(size)); if (FAILED(hr)) { outError = HrToString("IWICStream::InitializeFromMemory", hr); return false; } Microsoft::WRL::ComPtr decoder = {}; hr = m_wicFactory->CreateDecoderFromStream( stream.Get(), nullptr, WICDecodeMetadataCacheOnLoad, decoder.ReleaseAndGetAddressOf()); if (FAILED(hr) || decoder == nullptr) { outError = HrToString("IWICImagingFactory::CreateDecoderFromStream", hr); return false; } Microsoft::WRL::ComPtr frame = {}; hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); if (FAILED(hr) || frame == nullptr) { outError = HrToString("IWICBitmapDecoder::GetFrame", hr); return false; } return DecodeTextureFrame(*frame.Get(), outPixels, outWidth, outHeight, outError); } bool D3D12UiTextureHost::DecodeTextureFrame( IWICBitmapSource& source, std::vector& outPixels, std::uint32_t& outWidth, std::uint32_t& outHeight, std::string& outError) { outPixels.clear(); outWidth = 0u; outHeight = 0u; outError.clear(); Microsoft::WRL::ComPtr converter = {}; HRESULT hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); if (FAILED(hr) || converter == nullptr) { outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); return false; } hr = converter->Initialize( &source, GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone, nullptr, 0.0f, WICBitmapPaletteTypeCustom); if (FAILED(hr)) { outError = HrToString("IWICFormatConverter::Initialize", hr); return false; } UINT width = 0u; UINT height = 0u; hr = converter->GetSize(&width, &height); if (FAILED(hr) || width == 0u || height == 0u) { outError = HrToString("IWICBitmapSource::GetSize", hr); return false; } std::vector pixels( static_cast(width) * static_cast(height) * 4u); hr = converter->CopyPixels( nullptr, width * 4u, static_cast(pixels.size()), pixels.data()); if (FAILED(hr)) { outError = HrToString("IWICBitmapSource::CopyPixels", hr); return false; } outPixels = std::move(pixels); outWidth = static_cast(width); outHeight = static_cast(height); return true; } bool D3D12UiTextureHost::CreateTextureResource( const std::uint8_t* rgbaPixels, std::uint32_t width, std::uint32_t height, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { ReleaseTexture(outTexture); outTexture = {}; outError.clear(); if (!IsInitialized()) { outError = "CreateTextureResource requires an initialized D3D12 UI texture host."; return false; } if (rgbaPixels == nullptr || width == 0u || height == 0u) { outError = "CreateTextureResource rejected an empty RGBA payload."; return false; } ::XCEngine::RHI::TextureDesc textureDesc = {}; textureDesc.width = width; textureDesc.height = height; textureDesc.depth = 1u; textureDesc.mipLevels = 1u; textureDesc.arraySize = 1u; textureDesc.format = static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); textureDesc.textureType = static_cast(::XCEngine::RHI::TextureType::Texture2D); textureDesc.sampleCount = 1u; textureDesc.sampleQuality = 0u; textureDesc.flags = 0u; const std::size_t pixelBytes = static_cast(width) * static_cast(height) * 4u; ::XCEngine::RHI::RHITexture* texture = nullptr; Microsoft::WRL::ComPtr uploadBuffer = {}; if (m_hasActiveFrameSlot && m_windowRenderer->GetDevice() != nullptr) { const ::XCEngine::Rendering::RenderContext renderContext = m_windowRenderer->GetRenderContext(); auto* d3d12CommandList = renderContext.commandList != nullptr ? static_cast<::XCEngine::RHI::D3D12CommandList*>(renderContext.commandList) : nullptr; if (d3d12CommandList != nullptr && d3d12CommandList->GetCommandList() != nullptr) { auto* d3d12Texture = new ::XCEngine::RHI::D3D12Texture(); D3D12_RESOURCE_DESC nativeTextureDesc = {}; nativeTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; nativeTextureDesc.Alignment = 0; nativeTextureDesc.Width = width; nativeTextureDesc.Height = height; nativeTextureDesc.DepthOrArraySize = 1u; nativeTextureDesc.MipLevels = 1u; nativeTextureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; nativeTextureDesc.SampleDesc.Count = 1u; nativeTextureDesc.SampleDesc.Quality = 0u; nativeTextureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; nativeTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; if (d3d12Texture->InitializeFromData( m_windowRenderer->GetDevice(), d3d12CommandList->GetCommandList(), nativeTextureDesc, ::XCEngine::RHI::TextureType::Texture2D, rgbaPixels, pixelBytes, width * 4u, &uploadBuffer)) { texture = d3d12Texture; } else { delete d3d12Texture; } } } if (texture == nullptr) { texture = m_windowRenderer->GetRHIDevice()->CreateTexture( textureDesc, rgbaPixels, pixelBytes, width * 4u); } if (texture == nullptr) { outError = "Failed to create the GPU texture."; return false; } D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; if (!m_windowRenderer->GetTextureDescriptorAllocator().CreateTextureDescriptor( texture, &cpuHandle, &gpuHandle) || cpuHandle.ptr == 0u || gpuHandle.ptr == 0u) { texture->Shutdown(); delete texture; outError = "Failed to allocate the shader resource descriptor for the GPU texture."; return false; } auto textureResource = std::make_unique(); textureResource->texture = texture; textureResource->cpuHandle = cpuHandle; textureResource->gpuHandle = gpuHandle; textureResource->width = width; textureResource->height = height; if (uploadBuffer != nullptr && m_hasActiveFrameSlot && m_activeFrameSlot < m_frameUploadBuffers.size()) { m_frameUploadBuffers[m_activeFrameSlot].push_back(std::move(uploadBuffer)); } const std::uintptr_t resourceKey = reinterpret_cast(textureResource.get()); outTexture.nativeHandle = static_cast(gpuHandle.ptr); outTexture.width = width; outTexture.height = height; outTexture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView; outTexture.resourceHandle = resourceKey; m_liveTextures.emplace(resourceKey, std::move(textureResource)); return true; } void D3D12UiTextureHost::DestroyQueuedRetiredTextures(std::uint32_t frameSlot) { if (frameSlot >= m_retiredTextures.size()) { return; } for (auto& textureResource : m_retiredTextures[frameSlot]) { if (textureResource != nullptr) { DestroyTextureResource(*textureResource); } } m_retiredTextures[frameSlot].clear(); } D3D12UiTextureHost::TextureResource* D3D12UiTextureHost::ResolveTextureResource( const ::XCEngine::UI::UITextureHandle& texture) const { if (!texture.IsValid() || texture.resourceHandle == 0u) { return nullptr; } const auto found = m_liveTextures.find(texture.resourceHandle); if (found == m_liveTextures.end() || found->second == nullptr) { return nullptr; } return found->second.get(); } void D3D12UiTextureHost::DestroyTextureResource(TextureResource& textureResource) { if (m_windowRenderer != nullptr && textureResource.cpuHandle.ptr != 0u && textureResource.gpuHandle.ptr != 0u) { m_windowRenderer->GetTextureDescriptorAllocator().Free( textureResource.cpuHandle, textureResource.gpuHandle); } if (textureResource.texture != nullptr) { textureResource.texture->Shutdown(); delete textureResource.texture; textureResource.texture = nullptr; } textureResource.cpuHandle = {}; textureResource.gpuHandle = {}; textureResource.width = 0u; textureResource.height = 0u; } void D3D12UiTextureHost::RetireTextureResource( std::unique_ptr textureResource) { if (textureResource == nullptr) { return; } if (m_hasActiveFrameSlot && m_activeFrameSlot < m_retiredTextures.size()) { m_retiredTextures[m_activeFrameSlot].push_back(std::move(textureResource)); return; } AppendUIEditorRuntimeTrace( "window-close", "D3D12UiTextureHost::RetireTextureResource forcing WaitForGpuIdle for immediate destruction"); if (m_windowRenderer != nullptr) { m_windowRenderer->WaitForGpuIdle(); } DestroyTextureResource(*textureResource); } bool D3D12UiTextureHost::UpdateTextureRegionRgba( const ::XCEngine::UI::UITextureHandle& texture, std::uint32_t dstX, std::uint32_t dstY, const std::uint8_t* rgbaPixels, std::uint32_t width, std::uint32_t height, std::uint32_t rowPitch, std::string& outError) { outError.clear(); if (!IsInitialized()) { outError = "UpdateTextureRegionRgba requires an initialized D3D12 UI texture host."; return false; } if (!m_hasActiveFrameSlot || m_activeFrameSlot >= m_frameUploadBuffers.size()) { outError = "UpdateTextureRegionRgba requires an active frame slot."; return false; } if (rgbaPixels == nullptr || width == 0u || height == 0u) { outError = "UpdateTextureRegionRgba rejected an empty RGBA payload."; return false; } TextureResource* textureResource = ResolveTextureResource(texture); if (textureResource == nullptr || textureResource->texture == nullptr) { outError = "UpdateTextureRegionRgba could not resolve the destination texture."; return false; } if (dstX + width > textureResource->width || dstY + height > textureResource->height) { outError = "UpdateTextureRegionRgba target rectangle exceeds texture bounds."; return false; } const ::XCEngine::Rendering::RenderContext renderContext = m_windowRenderer->GetRenderContext(); auto* d3d12CommandList = renderContext.commandList != nullptr ? static_cast<::XCEngine::RHI::D3D12CommandList*>(renderContext.commandList) : nullptr; if (d3d12CommandList == nullptr || d3d12CommandList->GetCommandList() == nullptr) { outError = "UpdateTextureRegionRgba requires an active D3D12 command list."; return false; } auto* d3d12Texture = dynamic_cast<::XCEngine::RHI::D3D12Texture*>(textureResource->texture); if (d3d12Texture == nullptr || d3d12Texture->GetResource() == nullptr) { outError = "UpdateTextureRegionRgba requires a native D3D12 destination texture."; return false; } const std::uint32_t resolvedRowPitch = rowPitch > 0u ? rowPitch : width * kTextureBytesPerPixel; if (resolvedRowPitch < width * kTextureBytesPerPixel) { outError = "UpdateTextureRegionRgba row pitch is smaller than the texture row width."; return false; } const std::uint32_t alignedRowPitch = AlignTextureRowPitch(width * kTextureBytesPerPixel); const std::uint64_t uploadBytes = static_cast(alignedRowPitch) * static_cast(height); D3D12_HEAP_PROPERTIES uploadHeapProperties = {}; uploadHeapProperties.Type = D3D12_HEAP_TYPE_UPLOAD; D3D12_RESOURCE_DESC uploadDesc = {}; uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; uploadDesc.Alignment = 0; uploadDesc.Width = uploadBytes; uploadDesc.Height = 1; uploadDesc.DepthOrArraySize = 1; uploadDesc.MipLevels = 1; uploadDesc.Format = DXGI_FORMAT_UNKNOWN; uploadDesc.SampleDesc.Count = 1; uploadDesc.SampleDesc.Quality = 0; uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; uploadDesc.Flags = D3D12_RESOURCE_FLAG_NONE; Microsoft::WRL::ComPtr uploadBuffer = {}; HRESULT hr = m_windowRenderer->GetDevice()->CreateCommittedResource( &uploadHeapProperties, D3D12_HEAP_FLAG_NONE, &uploadDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(uploadBuffer.ReleaseAndGetAddressOf())); if (FAILED(hr) || uploadBuffer == nullptr) { outError = HrToString("ID3D12Device::CreateCommittedResource(upload)", hr); return false; } std::uint8_t* mappedData = nullptr; hr = uploadBuffer->Map(0u, nullptr, reinterpret_cast(&mappedData)); if (FAILED(hr) || mappedData == nullptr) { outError = HrToString("ID3D12Resource::Map(upload)", hr); return false; } for (std::uint32_t row = 0u; row < height; ++row) { std::memcpy( mappedData + static_cast(row) * alignedRowPitch, rgbaPixels + static_cast(row) * resolvedRowPitch, static_cast(width) * kTextureBytesPerPixel); } uploadBuffer->Unmap(0u, nullptr); ID3D12GraphicsCommandList* nativeCommandList = d3d12CommandList->GetCommandList(); const ::XCEngine::RHI::ResourceStates originalState = d3d12Texture->GetState(); if (originalState != ::XCEngine::RHI::ResourceStates::CopyDst) { D3D12_RESOURCE_BARRIER toCopyBarrier = {}; toCopyBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; toCopyBarrier.Transition.pResource = d3d12Texture->GetResource(); toCopyBarrier.Transition.StateBefore = ::XCEngine::RHI::ToD3D12(originalState); toCopyBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; toCopyBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; nativeCommandList->ResourceBarrier(1u, &toCopyBarrier); d3d12Texture->SetState(::XCEngine::RHI::ResourceStates::CopyDst); } D3D12_TEXTURE_COPY_LOCATION dstLocation = {}; dstLocation.pResource = d3d12Texture->GetResource(); dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dstLocation.SubresourceIndex = 0u; D3D12_TEXTURE_COPY_LOCATION srcLocation = {}; srcLocation.pResource = uploadBuffer.Get(); srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; srcLocation.PlacedFootprint.Offset = 0u; srcLocation.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM; srcLocation.PlacedFootprint.Footprint.Width = width; srcLocation.PlacedFootprint.Footprint.Height = height; srcLocation.PlacedFootprint.Footprint.Depth = 1u; srcLocation.PlacedFootprint.Footprint.RowPitch = alignedRowPitch; D3D12_BOX srcBox = {}; srcBox.left = 0u; srcBox.top = 0u; srcBox.front = 0u; srcBox.right = width; srcBox.bottom = height; srcBox.back = 1u; nativeCommandList->CopyTextureRegion( &dstLocation, dstX, dstY, 0u, &srcLocation, &srcBox); if (originalState != ::XCEngine::RHI::ResourceStates::CopyDst) { D3D12_RESOURCE_BARRIER restoreBarrier = {}; restoreBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; restoreBarrier.Transition.pResource = d3d12Texture->GetResource(); restoreBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; restoreBarrier.Transition.StateAfter = ::XCEngine::RHI::ToD3D12(originalState); restoreBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; nativeCommandList->ResourceBarrier(1u, &restoreBarrier); d3d12Texture->SetState(originalState); } m_frameUploadBuffers[m_activeFrameSlot].push_back(std::move(uploadBuffer)); return true; } bool D3D12UiTextureHost::LoadTextureFromFile( const std::filesystem::path& path, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { std::vector pixels = {}; std::uint32_t width = 0u; std::uint32_t height = 0u; if (!DecodeTextureFile(path, pixels, width, height, outError)) { outTexture = {}; m_lastError = outError; return false; } const bool created = CreateTextureResource( pixels.data(), width, height, outTexture, outError); m_lastError = created ? std::string() : outError; return created; } bool D3D12UiTextureHost::LoadTextureFromMemory( const std::uint8_t* data, std::size_t size, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { std::vector pixels = {}; std::uint32_t width = 0u; std::uint32_t height = 0u; if (!DecodeTextureMemory(data, size, pixels, width, height, outError)) { outTexture = {}; m_lastError = outError; return false; } const bool created = CreateTextureResource( pixels.data(), width, height, outTexture, outError); m_lastError = created ? std::string() : outError; return created; } bool D3D12UiTextureHost::LoadTextureFromRgba( const std::uint8_t* rgbaPixels, std::uint32_t width, std::uint32_t height, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { const bool created = CreateTextureResource( rgbaPixels, width, height, outTexture, outError); m_lastError = created ? std::string() : outError; return created; } void D3D12UiTextureHost::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { if (!texture.IsValid()) { texture = {}; return; } const auto found = m_liveTextures.find(texture.resourceHandle); if (found != m_liveTextures.end() && found->second != nullptr) { std::unique_ptr retiredTexture = std::move(found->second); m_liveTextures.erase(found); RetireTextureResource(std::move(retiredTexture)); } texture = {}; } } // namespace XCEngine::UI::Editor::Host