diff --git a/docs/plan/NewEditor_D3D12_LegacyD2DClosurePlan_2026-04-22.md b/docs/plan/NewEditor_D3D12_LegacyD2DClosurePlan_2026-04-22.md new file mode 100644 index 00000000..0719e318 --- /dev/null +++ b/docs/plan/NewEditor_D3D12_LegacyD2DClosurePlan_2026-04-22.md @@ -0,0 +1,274 @@ +# NewEditor D3D12 Legacy D2D Closure Plan + +Date: 2026-04-22 + +## 1. Problem Statement + +`new_editor` main-window realtime presentation has already moved onto the native `D3D12` path, but the codebase still retains a legacy `D2D / D3D11On12` bridge in the host layer. + +That residual path is no longer the main realtime hot path, but it is still architecturally serious because it keeps a second rendering/composition implementation alive for the same UI draw model. + +Current residual seam: + +- `app/Rendering/Native/NativeRenderer.*` +- `app/Rendering/D3D12/D3D12WindowInteropContext.*` +- `app/Rendering/D3D12/D3D12WindowInteropHelpers.h` +- host/build references that still compile and link the old bridge + +## 2. Confirmed Current Facts + +### 2.1 Main window realtime presentation no longer depends on D2D + +Main realtime presentation flows through: + +- `D3D12WindowRenderer` +- `D3D12UiRenderer` +- `D3D12WindowRenderLoop` + +The active main path does not call `NativeRenderer::Render(...)`. + +### 2.2 Residual D2D bridge is concentrated, not scattered + +The old bridge is now effectively concentrated in: + +- screenshot/export flow +- dead hwnd-native D2D window rendering code that is no longer wired into the main frame loop + +### 2.3 NativeRenderer is no longer the main texture host + +`TexturePort` for the active editor path is already `D3D12UiTextureHost`. + +This means the remaining `NativeRenderer` responsibilities can be removed instead of preserved as an active subsystem. + +## 3. Root Architectural Issue + +The root issue is not “some D2D symbols still exist”. + +The root issue is that `new_editor` still retains two incompatible host-side ways to turn UI output into pixels: + +1. Active path: native `D3D12` rendering into the swapchain backbuffer. +2. Residual path: `UIDrawData -> D2D replay`, with optional `D3D11On12` resource interop. + +As long as both remain alive: + +- draw semantics can drift between live output and capture/export output +- future UI/rendering changes must be kept compatible with two host backends +- dead-window code remains available to be accidentally reused +- build/link dependencies preserve obsolete platform coupling + +So the correct goal is full single-path closure, not incremental patching. + +## 4. Target End State + +After this refactor: + +1. Main-window realtime rendering remains native `D3D12`. +2. Main-window screenshot capture also uses native `D3D12` backbuffer capture. +3. `new_editor` no longer contains any host-side `D3D11On12` or `D2D` bridge code for the main window. +4. Old hwnd `D2D` renderer code is deleted, not merely disconnected. +5. `XCUIEditorHost` no longer compiles obsolete interop units. +6. Host link dependencies only retain what is still genuinely required. + +## 5. Non-Goals + +- Do not roll the editor back to `D2D`. +- Do not reintroduce a fallback composition backend. +- Do not change the active `D3D12UiRenderer` architecture back into draw-data replay through another API. +- Do not remove `DirectWrite` from `D3D12UiTextSystem`; text shaping/raster generation remains valid there. + +## 6. Refactor Strategy + +### Phase A. Replace screenshot bridge with native D3D12 capture + +Introduce a native `D3D12` capture path that reads the final swapchain backbuffer and writes PNG through CPU-side WIC encoding. + +This capture path must: + +- capture the final composited editor frame +- avoid `UIDrawData` replay +- avoid `D3D11On12` +- avoid `D2D` +- operate on the same swapchain backbuffer the user actually sees + +Preferred integration point: + +- after UI rendering has finished for the active backbuffer +- after the frame has been submitted to the queue +- before `PresentFrame()` + +Reason: + +- the frame is already fully rendered +- the current backbuffer is still the one about to be presented +- capture remains aligned with the user-visible final image + +### Phase B. Collapse screenshot orchestration around the active D3D12 path + +Rework `AutoScreenshotController` so it only: + +- tracks pending capture requests +- resolves output/history paths +- finalizes success/error summaries + +It must no longer own the rendering backend choice. + +### Phase C. Delete obsolete NativeRenderer window-render path + +Delete the old hwnd `D2D` window renderer behavior: + +- `Initialize(HWND)` +- `Resize(...)` +- `Render(...)` +- hwnd render-target management +- D2D draw command replay entrypoint for window presentation + +If a remaining utility is still needed after this phase, it must live in a narrowly-scoped replacement component, not inside a zombie renderer class. + +### Phase D. Delete D3D11On12 interop bridge + +Once native `D3D12` capture is live: + +- delete `D3D12WindowInteropContext.*` +- delete `D3D12WindowInteropHelpers.h` +- delete all `NativeRenderer` interop code + +### Phase E. Build and link closure + +Update host build definitions so obsolete units and libraries are removed. + +Expected removals if no remaining consumer exists: + +- `app/Rendering/Native/NativeRenderer.cpp` +- `app/Rendering/D3D12/D3D12WindowInteropContext.cpp` +- `d2d1.lib` +- `d3d11.lib` + +Expected retained dependencies: + +- `d3d12.lib` +- `d3dcompiler.lib` +- `dwrite.lib` +- `dxgi.lib` +- `windowscodecs.lib` + +## 7. Concrete File-Level Work + +### 7.1 Add new native capture unit + +Add new host-side unit(s), likely under `app/Rendering/D3D12/`: + +- `D3D12WindowCapture.h` +- `D3D12WindowCapture.cpp` + +Responsibilities: + +- copy current swapchain backbuffer to readback +- map CPU pixels +- encode PNG via WIC +- return rich error text + +### 7.2 Extend window renderer / render loop + +Modify: + +- `app/Rendering/D3D12/D3D12WindowRenderer.h` +- `app/Rendering/D3D12/D3D12WindowRenderer.cpp` +- `app/Rendering/D3D12/D3D12WindowRenderLoop.h` +- `app/Rendering/D3D12/D3D12WindowRenderLoop.cpp` + +Needed changes: + +- expose a native capture operation on the active backbuffer +- extend present result with capture outcome +- allow present flow to execute capture before `PresentFrame()` + +### 7.3 Rework screenshot controller boundary + +Modify: + +- `app/Rendering/Native/AutoScreenshot.h` +- `app/Rendering/Native/AutoScreenshot.cpp` +- `app/Platform/Win32/EditorWindowRuntimeController.h` +- `app/Platform/Win32/EditorWindowRuntimeController.cpp` +- `app/Platform/Win32/EditorWindow.cpp` + +Needed changes: + +- stop passing `UIDrawData` into screenshot rendering +- stop depending on `NativeRenderer` +- move screenshot triggering into the D3D12 present path + +### 7.4 Delete obsolete renderer/interop code + +Delete if fully unreferenced after the above: + +- `app/Rendering/Native/NativeRenderer.h` +- `app/Rendering/Native/NativeRenderer.cpp` +- `app/Rendering/Native/NativeRendererHelpers.h` +- `app/Rendering/D3D12/D3D12WindowInteropContext.h` +- `app/Rendering/D3D12/D3D12WindowInteropContext.cpp` +- `app/Rendering/D3D12/D3D12WindowInteropHelpers.h` + +### 7.5 Build-system cleanup + +Modify: + +- `new_editor/CMakeLists.txt` + +Remove: + +- old source units +- dead link libraries + +## 8. Validation Requirements + +### 8.1 Build validation + +Must build: + +- `XCUIEditorHost` +- `XCUIEditorAppLib` +- `XCUIEditorApp` + +### 8.2 Runtime validation + +Must validate: + +1. editor launches normally +2. main window still presents correctly +3. manual screenshot still succeeds +4. startup auto-capture still succeeds if enabled +5. no screenshot path closes or stalls the main window unexpectedly + +### 8.3 Structural validation + +Searches after completion must show: + +- no `NativeRenderer` references in `new_editor/app` +- no `D3D12WindowInteropContext` references in `new_editor` +- no `D3D11On12` references in `new_editor` +- no `ID2D` / `D2D1` references in `new_editor/app` + +Exception: + +- `DirectWrite` references inside `D3D12UiTextSystem` remain allowed + +## 9. Execution Order + +1. Add native `D3D12` backbuffer capture path. +2. Route `AutoScreenshotController` through that new path. +3. Remove runtime-controller dependence on `NativeRenderer`. +4. Delete `NativeRenderer` and `D3D12WindowInteropContext`. +5. Clean CMake/link dependencies. +6. Build and smoke-test. +7. Run structural grep validation. + +## 10. Success Criteria + +This plan is complete only if all of the following are true: + +- screenshots come from the native `D3D12` backbuffer path +- no main-window host rendering code path can fall back to `D2D` +- no `D3D11On12` bridge remains in `new_editor` +- old dead window-render code is physically removed +- build and smoke validation pass diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index bfe3cbd7..6a2226fd 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -168,15 +168,14 @@ set(XCUI_EDITOR_HOST_PLATFORM_SOURCES set(XCUI_EDITOR_HOST_RENDERING_SOURCES app/Rendering/Native/AutoScreenshot.cpp app/Rendering/D3D12/D3D12HostDevice.cpp + app/Rendering/D3D12/D3D12WindowCapture.cpp app/Rendering/D3D12/D3D12UiRenderer.cpp app/Rendering/D3D12/D3D12UiTextSystem.cpp app/Rendering/D3D12/D3D12UiTextureHost.cpp app/Rendering/D3D12/D3D12ShaderResourceDescriptorAllocator.cpp - app/Rendering/D3D12/D3D12WindowInteropContext.cpp app/Rendering/D3D12/D3D12WindowRenderer.cpp app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp app/Rendering/D3D12/D3D12WindowRenderLoop.cpp - app/Rendering/Native/NativeRenderer.cpp ) add_library(XCUIEditorHost STATIC @@ -195,8 +194,6 @@ xcui_editor_apply_common_target_settings(XCUIEditorHost PRIVATE) target_link_libraries(XCUIEditorHost PRIVATE XCEngine - d2d1.lib - d3d11.lib d3d12.lib d3dcompiler.lib dwrite.lib @@ -353,8 +350,6 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) XCUIEditorAppCore XCUIEditorLib XCUIEditorHost - d2d1.lib - d3d11.lib d3d12.lib d3dcompiler.lib dwrite.lib diff --git a/new_editor/app/Platform/Win32/EditorWindow.cpp b/new_editor/app/Platform/Win32/EditorWindow.cpp index cfd338f9..2999fe6a 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.cpp +++ b/new_editor/app/Platform/Win32/EditorWindow.cpp @@ -652,12 +652,6 @@ EditorWindowFrameTransferRequests EditorWindow::RenderFrame( if (!presentResult.warning.empty()) { LogRuntimeTrace("present", presentResult.warning); } - - m_runtime->CaptureIfRequested( - drawData, - pixelWidth, - pixelHeight, - presentResult.framePresented); return transferRequests; } diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp index bd2f3613..bee022ba 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp @@ -75,7 +75,6 @@ void EditorWindowRuntimeController::ClearExternalDockHostDropPreview() { void EditorWindowRuntimeController::SetDpiScale(float dpiScale) { m_dpiScale = dpiScale > 0.0f ? dpiScale : 1.0f; - m_renderer.SetDpiScale(dpiScale); m_textSystem.SetDpiScale(m_dpiScale); m_uiRenderer.SetDpiScale(m_dpiScale); } @@ -199,8 +198,6 @@ void EditorWindowRuntimeController::Shutdown() { m_textureHost.Shutdown(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=WindowRenderer"); m_windowRenderer.Shutdown(); - LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=NativeRenderer"); - m_renderer.Shutdown(); m_dpiScale = 1.0f; LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown end"); } @@ -233,22 +230,35 @@ Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFram } Host::D3D12WindowRenderLoopPresentResult EditorWindowRuntimeController::Present( - const ::XCEngine::UI::UIDrawData& drawData) const { - return m_windowRenderLoop.Present(drawData); -} + const ::XCEngine::UI::UIDrawData& drawData) { + std::filesystem::path capturePath = {}; + const bool captureRequested = m_autoScreenshot.TryBeginCapture(capturePath); -void EditorWindowRuntimeController::CaptureIfRequested( - const ::XCEngine::UI::UIDrawData& drawData, - UINT pixelWidth, - UINT pixelHeight, - bool framePresented) { - m_autoScreenshot.CaptureIfRequested( - m_renderer, - m_windowRenderer, - drawData, - pixelWidth, - pixelHeight, - framePresented); + Host::D3D12WindowRenderLoopPresentResult result = + m_windowRenderLoop.Present( + drawData, + captureRequested ? &capturePath : nullptr); + + if (captureRequested) { + const bool captureSucceeded = result.framePresented && result.captureSucceeded; + if (captureSucceeded) { + m_autoScreenshot.CompleteCaptureSuccess(capturePath); + LogRuntimeTrace("capture", "native d3d12 capture succeeded: " + capturePath.string()); + } else { + std::string captureError = result.captureError; + if (captureError.empty()) { + captureError = !result.warning.empty() + ? result.warning + : "Screenshot capture did not complete."; + } + m_autoScreenshot.CompleteCaptureFailure(std::move(captureError)); + LogRuntimeTrace( + "capture", + "native d3d12 capture failed: " + m_autoScreenshot.GetLastCaptureError()); + } + } + + return result; } void EditorWindowRuntimeController::RequestManualScreenshot(std::string reason) { diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h index a527869a..35e93fd5 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h @@ -12,7 +12,6 @@ #include #include #include -#include #include @@ -82,12 +81,7 @@ public: Host::D3D12WindowRenderLoopFrameContext BeginFrame(); Host::D3D12WindowRenderLoopPresentResult Present( - const ::XCEngine::UI::UIDrawData& drawData) const; - void CaptureIfRequested( - const ::XCEngine::UI::UIDrawData& drawData, - UINT pixelWidth, - UINT pixelHeight, - bool framePresented); + const ::XCEngine::UI::UIDrawData& drawData); void RequestManualScreenshot(std::string reason); std::string BuildCaptureStatusText() const; @@ -98,7 +92,6 @@ private: void UpdateFrameTiming(); void RefreshDisplayedFrameStats(); - Host::NativeRenderer m_renderer = {}; Host::D3D12WindowRenderer m_windowRenderer = {}; Host::D3D12UiTextureHost m_textureHost = {}; Host::D3D12UiTextSystem m_textSystem = {}; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowCapture.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowCapture.cpp new file mode 100644 index 00000000..83344acf --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12WindowCapture.cpp @@ -0,0 +1,441 @@ +#include "D3D12WindowCapture.h" + +#include "D3D12WindowRenderer.h" + +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +using ::XCEngine::RHI::D3D12Texture; +using Microsoft::WRL::ComPtr; + +namespace { + +std::string HrToString(const char* operation, HRESULT hr) { + std::ostringstream stream = {}; + stream << operation << " failed with HRESULT 0x" + << std::hex << std::uppercase + << static_cast(hr); + return stream.str(); +} + +bool ReadbackTexturePixels( + ID3D12Device& device, + ID3D12CommandQueue& commandQueue, + ID3D12Resource& sourceResource, + UINT width, + UINT height, + std::vector& outPixels, + std::string& outError) { + outPixels.clear(); + + const D3D12_RESOURCE_DESC sourceDesc = sourceResource.GetDesc(); + D3D12_PLACED_SUBRESOURCE_FOOTPRINT readbackLayout = {}; + UINT rowCount = 0u; + UINT64 rowSizeInBytes = 0u; + UINT64 totalSize = 0u; + device.GetCopyableFootprints( + &sourceDesc, + 0u, + 1u, + 0u, + &readbackLayout, + &rowCount, + &rowSizeInBytes, + &totalSize); + if (totalSize == 0u || rowCount == 0u || rowSizeInBytes == 0u) { + outError = "GetCopyableFootprints returned an empty readback layout."; + return false; + } + + D3D12_HEAP_PROPERTIES heapProperties = {}; + heapProperties.Type = D3D12_HEAP_TYPE_READBACK; + + D3D12_RESOURCE_DESC readbackDesc = {}; + readbackDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + readbackDesc.Alignment = 0u; + readbackDesc.Width = totalSize; + readbackDesc.Height = 1u; + readbackDesc.DepthOrArraySize = 1u; + readbackDesc.MipLevels = 1u; + readbackDesc.Format = DXGI_FORMAT_UNKNOWN; + readbackDesc.SampleDesc.Count = 1u; + readbackDesc.SampleDesc.Quality = 0u; + readbackDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + readbackDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + + ComPtr readbackBuffer = {}; + HRESULT hr = device.CreateCommittedResource( + &heapProperties, + D3D12_HEAP_FLAG_NONE, + &readbackDesc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(readbackBuffer.ReleaseAndGetAddressOf())); + if (FAILED(hr) || readbackBuffer == nullptr) { + outError = HrToString("ID3D12Device::CreateCommittedResource(readback)", hr); + return false; + } + + ComPtr commandAllocator = {}; + hr = device.CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(commandAllocator.ReleaseAndGetAddressOf())); + if (FAILED(hr) || commandAllocator == nullptr) { + outError = HrToString("ID3D12Device::CreateCommandAllocator(capture)", hr); + return false; + } + + ComPtr commandList = {}; + hr = device.CreateCommandList( + 0u, + D3D12_COMMAND_LIST_TYPE_DIRECT, + commandAllocator.Get(), + nullptr, + IID_PPV_ARGS(commandList.ReleaseAndGetAddressOf())); + if (FAILED(hr) || commandList == nullptr) { + outError = HrToString("ID3D12Device::CreateCommandList(capture)", hr); + return false; + } + + D3D12_RESOURCE_BARRIER toCopyBarrier = {}; + toCopyBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + toCopyBarrier.Transition.pResource = &sourceResource; + toCopyBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; + toCopyBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + toCopyBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + commandList->ResourceBarrier(1u, &toCopyBarrier); + + D3D12_TEXTURE_COPY_LOCATION sourceLocation = {}; + sourceLocation.pResource = &sourceResource; + sourceLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + sourceLocation.SubresourceIndex = 0u; + + D3D12_TEXTURE_COPY_LOCATION destinationLocation = {}; + destinationLocation.pResource = readbackBuffer.Get(); + destinationLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + destinationLocation.PlacedFootprint = readbackLayout; + + D3D12_BOX sourceRegion = {}; + sourceRegion.left = 0u; + sourceRegion.top = 0u; + sourceRegion.front = 0u; + sourceRegion.right = width; + sourceRegion.bottom = height; + sourceRegion.back = 1u; + commandList->CopyTextureRegion( + &destinationLocation, + 0u, + 0u, + 0u, + &sourceLocation, + &sourceRegion); + + D3D12_RESOURCE_BARRIER restoreBarrier = {}; + restoreBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + restoreBarrier.Transition.pResource = &sourceResource; + restoreBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + restoreBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; + restoreBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + commandList->ResourceBarrier(1u, &restoreBarrier); + + hr = commandList->Close(); + if (FAILED(hr)) { + outError = HrToString("ID3D12GraphicsCommandList::Close(capture)", hr); + return false; + } + + ID3D12CommandList* commandLists[] = { commandList.Get() }; + commandQueue.ExecuteCommandLists(1u, commandLists); + + ComPtr fence = {}; + hr = device.CreateFence(0u, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence.ReleaseAndGetAddressOf())); + if (FAILED(hr) || fence == nullptr) { + outError = HrToString("ID3D12Device::CreateFence(capture)", hr); + return false; + } + + const HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (fenceEvent == nullptr) { + outError = "CreateEvent failed for D3D12 capture synchronization."; + return false; + } + + constexpr UINT64 kFenceValue = 1u; + hr = commandQueue.Signal(fence.Get(), kFenceValue); + if (FAILED(hr)) { + CloseHandle(fenceEvent); + outError = HrToString("ID3D12CommandQueue::Signal(capture)", hr); + return false; + } + + if (fence->GetCompletedValue() < kFenceValue) { + hr = fence->SetEventOnCompletion(kFenceValue, fenceEvent); + if (FAILED(hr)) { + CloseHandle(fenceEvent); + outError = HrToString("ID3D12Fence::SetEventOnCompletion(capture)", hr); + return false; + } + WaitForSingleObject(fenceEvent, INFINITE); + } + CloseHandle(fenceEvent); + + const UINT packedRowPitch = width * 4u; + outPixels.resize(static_cast(packedRowPitch) * static_cast(height)); + + D3D12_RANGE readRange = { 0u, static_cast(totalSize) }; + void* mappedData = nullptr; + hr = readbackBuffer->Map(0u, &readRange, &mappedData); + if (FAILED(hr) || mappedData == nullptr) { + outError = HrToString("ID3D12Resource::Map(readback)", hr); + outPixels.clear(); + return false; + } + + const auto* sourceBytes = static_cast(mappedData); + for (UINT rowIndex = 0u; rowIndex < height; ++rowIndex) { + std::memcpy( + outPixels.data() + static_cast(rowIndex) * packedRowPitch, + sourceBytes + static_cast(rowIndex) * readbackLayout.Footprint.RowPitch, + packedRowPitch); + } + + D3D12_RANGE writeRange = { 0u, 0u }; + readbackBuffer->Unmap(0u, &writeRange); + return true; +} + +void ConvertRgbaPixelsToBgra(std::vector& pixels) { + const std::size_t pixelCount = pixels.size() / 4u; + for (std::size_t pixelIndex = 0u; pixelIndex < pixelCount; ++pixelIndex) { + const std::size_t baseIndex = pixelIndex * 4u; + std::swap(pixels[baseIndex + 0u], pixels[baseIndex + 2u]); + } +} + +} // namespace + +bool D3D12WindowCapture::CaptureCurrentBackBufferToPng( + const D3D12WindowRenderer& windowRenderer, + const std::filesystem::path& outputPath, + std::string& outError) { + outError.clear(); + if (outputPath.empty()) { + outError = "CaptureCurrentBackBufferToPng rejected an empty output path."; + return false; + } + + ID3D12Device* device = windowRenderer.GetDevice(); + ID3D12CommandQueue* commandQueue = windowRenderer.GetCommandQueue(); + const D3D12Texture* backBufferTexture = windowRenderer.GetCurrentBackBufferTexture(); + if (device == nullptr || commandQueue == nullptr || backBufferTexture == nullptr || + backBufferTexture->GetResource() == nullptr) { + outError = "CaptureCurrentBackBufferToPng requires an active D3D12 backbuffer."; + return false; + } + + const DXGI_FORMAT backBufferFormat = backBufferTexture->GetDesc().Format; + const bool requiresRgbaToBgraConversion = + backBufferFormat == DXGI_FORMAT_R8G8B8A8_UNORM; + if (!requiresRgbaToBgraConversion && + backBufferFormat != DXGI_FORMAT_B8G8R8A8_UNORM) { + std::ostringstream stream = {}; + stream << "Unsupported backbuffer format for PNG capture: " + << static_cast(backBufferFormat); + outError = stream.str(); + return false; + } + + if (!EnsureWicFactory(outError)) { + return false; + } + + std::vector pixels = {}; + if (!ReadbackTexturePixels( + *device, + *commandQueue, + *backBufferTexture->GetResource(), + backBufferTexture->GetWidth(), + backBufferTexture->GetHeight(), + pixels, + outError)) { + return false; + } + + if (requiresRgbaToBgraConversion) { + ConvertRgbaPixelsToBgra(pixels); + } + + if (!EncodePng( + outputPath, + pixels.data(), + backBufferTexture->GetWidth(), + backBufferTexture->GetHeight(), + backBufferTexture->GetWidth() * 4u, + GUID_WICPixelFormat32bppBGRA, + outError)) { + return false; + } + + std::error_code fileError = {}; + const std::uintmax_t encodedFileSize = std::filesystem::file_size(outputPath, fileError); + if (fileError || encodedFileSize == 0u) { + outError = "Native D3D12 capture completed without producing a valid PNG file."; + return false; + } + + return true; +} + +void D3D12WindowCapture::Shutdown() { + m_wicFactory.Reset(); + if (m_wicComInitialized) { + CoUninitialize(); + m_wicComInitialized = false; + } +} + +bool D3D12WindowCapture::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) || m_wicFactory == nullptr) { + outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); + m_wicFactory.Reset(); + return false; + } + + return true; +} + +bool D3D12WindowCapture::EncodePng( + const std::filesystem::path& outputPath, + const std::uint8_t* pixels, + UINT width, + UINT height, + UINT rowPitch, + REFWICPixelFormatGUID pixelFormat, + std::string& outError) const { + outError.clear(); + if (m_wicFactory == nullptr || pixels == nullptr || width == 0u || height == 0u || rowPitch == 0u) { + outError = "EncodePng requires initialized WIC state and valid image data."; + return false; + } + + ComPtr stream = {}; + HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); + if (FAILED(hr) || stream == nullptr) { + outError = HrToString("IWICImagingFactory::CreateStream", hr); + return false; + } + + const std::wstring wideOutputPath = outputPath.wstring(); + hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE); + if (FAILED(hr)) { + outError = HrToString("IWICStream::InitializeFromFilename", hr); + return false; + } + + ComPtr encoder = {}; + hr = m_wicFactory->CreateEncoder( + GUID_ContainerFormatPng, + nullptr, + encoder.ReleaseAndGetAddressOf()); + if (FAILED(hr) || encoder == nullptr) { + outError = HrToString("IWICImagingFactory::CreateEncoder", hr); + return false; + } + + hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapEncoder::Initialize", hr); + return false; + } + + ComPtr frame = {}; + ComPtr propertyBag = {}; + hr = encoder->CreateNewFrame( + frame.ReleaseAndGetAddressOf(), + propertyBag.ReleaseAndGetAddressOf()); + if (FAILED(hr) || frame == nullptr) { + outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr); + return false; + } + + hr = frame->Initialize(propertyBag.Get()); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::Initialize", hr); + return false; + } + + hr = frame->SetSize(width, height); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::SetSize", hr); + return false; + } + + WICPixelFormatGUID resolvedPixelFormat = pixelFormat; + hr = frame->SetPixelFormat(&resolvedPixelFormat); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr); + return false; + } + if (!IsEqualGUID(resolvedPixelFormat, pixelFormat)) { + outError = "IWICBitmapFrameEncode::SetPixelFormat resolved an unexpected pixel format."; + return false; + } + + hr = frame->WritePixels( + height, + rowPitch, + rowPitch * height, + const_cast(reinterpret_cast(pixels))); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::WritePixels", hr); + return false; + } + + hr = frame->Commit(); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapFrameEncode::Commit", hr); + return false; + } + + hr = encoder->Commit(); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapEncoder::Commit", hr); + return false; + } + + hr = stream->Commit(STGC_DEFAULT); + if (FAILED(hr)) { + outError = HrToString("IWICStream::Commit", hr); + return false; + } + + return true; +} + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowCapture.h b/new_editor/app/Rendering/D3D12/D3D12WindowCapture.h new file mode 100644 index 00000000..3c0c3c76 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12WindowCapture.h @@ -0,0 +1,42 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +class D3D12WindowRenderer; + +class D3D12WindowCapture { +public: + bool CaptureCurrentBackBufferToPng( + const D3D12WindowRenderer& windowRenderer, + const std::filesystem::path& outputPath, + std::string& outError); + void Shutdown(); + +private: + bool EnsureWicFactory(std::string& outError); + bool EncodePng( + const std::filesystem::path& outputPath, + const std::uint8_t* pixels, + UINT width, + UINT height, + UINT rowPitch, + REFWICPixelFormatGUID pixelFormat, + std::string& outError) const; + + Microsoft::WRL::ComPtr m_wicFactory = {}; + bool m_wicComInitialized = false; +}; + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp deleted file mode 100644 index e847982d..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp +++ /dev/null @@ -1,310 +0,0 @@ -#include "D3D12WindowInteropHelpers.h" - -#include -#include - -#include -#include - -namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers { - -std::string HrToInteropString(const char* operation, HRESULT hr) { - char buffer[128] = {}; - sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast(hr)); - return buffer; -} - -D2D1_BITMAP_PROPERTIES1 BuildD2DBitmapProperties( - DXGI_FORMAT format, - D2D1_BITMAP_OPTIONS options) { - return D2D1::BitmapProperties1( - options, - D2D1::PixelFormat(format, D2D1_ALPHA_MODE_IGNORE), - 96.0f, - 96.0f); -} - -bool IsInteropTextureHandle(const ::XCEngine::UI::UITextureHandle& texture) { - return texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView && - texture.resourceHandle != 0u; -} - -void CollectInteropTextureHandles( - const ::XCEngine::UI::UIDrawData& drawData, - std::vector<::XCEngine::UI::UITextureHandle>& outTextures) { - outTextures.clear(); - std::unordered_set seenKeys = {}; - for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { - for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { - if (!IsInteropTextureHandle(command.texture) || - !seenKeys.insert(command.texture.resourceHandle).second) { - continue; - } - - outTextures.push_back(command.texture); - } - } -} - -} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers - -namespace XCEngine::UI::Editor::Host { - -using namespace D3D12WindowInteropHelpers; - -bool D3D12WindowInteropContext::Attach( - D3D12WindowRenderer& windowRenderer, - ID2D1Factory1& d2dFactory) { - if (m_windowRenderer != &windowRenderer) { - Detach(); - m_windowRenderer = &windowRenderer; - } - - m_d2dFactory = &d2dFactory; - return EnsureInterop(); -} - -void D3D12WindowInteropContext::Detach() { - ReleaseInteropState(); - m_windowRenderer = nullptr; - m_d2dFactory = nullptr; - m_lastError.clear(); -} - -D3D12WindowRenderer* D3D12WindowInteropContext::GetWindowRenderer() const { - return m_windowRenderer; -} - -ID3D11On12Device* D3D12WindowInteropContext::GetD3D11On12Device() const { - return m_d3d11On12Device.Get(); -} - -ID3D11DeviceContext* D3D12WindowInteropContext::GetD3D11DeviceContext() const { - return m_d3d11DeviceContext.Get(); -} - -ID2D1DeviceContext* D3D12WindowInteropContext::GetD2DDeviceContext() const { - return m_d2dDeviceContext.Get(); -} - -const std::string& D3D12WindowInteropContext::GetLastError() const { - return m_lastError; -} - -bool D3D12WindowInteropContext::EnsureInterop() { - if (m_windowRenderer == nullptr) { - m_lastError = "EnsureInterop requires an attached D3D12 window renderer."; - return false; - } - if (m_d2dFactory == nullptr) { - m_lastError = "EnsureInterop requires an initialized D2D factory."; - return false; - } - if (m_d3d11On12Device != nullptr && - m_d2dDeviceContext != nullptr) { - return true; - } - - ReleaseInteropState(); - - ID3D12Device* d3d12Device = m_windowRenderer->GetDevice(); - ID3D12CommandQueue* d3d12CommandQueue = m_windowRenderer->GetCommandQueue(); - if (d3d12Device == nullptr || d3d12CommandQueue == nullptr) { - m_lastError = "The attached D3D12 window renderer does not expose a native device/queue."; - return false; - } - - const std::array featureLevels = { - D3D_FEATURE_LEVEL_12_1, - D3D_FEATURE_LEVEL_12_0, - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0 - }; - const std::array commandQueues = { - reinterpret_cast(d3d12CommandQueue) - }; - - UINT createFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; -#ifdef _DEBUG - createFlags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - - D3D_FEATURE_LEVEL actualFeatureLevel = D3D_FEATURE_LEVEL_11_0; - HRESULT hr = D3D11On12CreateDevice( - d3d12Device, - createFlags, - featureLevels.data(), - static_cast(featureLevels.size()), - commandQueues.data(), - static_cast(commandQueues.size()), - 0u, - m_d3d11Device.ReleaseAndGetAddressOf(), - m_d3d11DeviceContext.ReleaseAndGetAddressOf(), - &actualFeatureLevel); -#ifdef _DEBUG - if (FAILED(hr)) { - createFlags &= ~D3D11_CREATE_DEVICE_DEBUG; - hr = D3D11On12CreateDevice( - d3d12Device, - createFlags, - featureLevels.data(), - static_cast(featureLevels.size()), - commandQueues.data(), - static_cast(commandQueues.size()), - 0u, - m_d3d11Device.ReleaseAndGetAddressOf(), - m_d3d11DeviceContext.ReleaseAndGetAddressOf(), - &actualFeatureLevel); - } -#endif - if (FAILED(hr) || m_d3d11Device == nullptr || m_d3d11DeviceContext == nullptr) { - m_lastError = HrToInteropString("D3D11On12CreateDevice", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d3d11Device.As(&m_d3d11On12Device); - if (FAILED(hr) || m_d3d11On12Device == nullptr) { - m_lastError = HrToInteropString("ID3D11Device::QueryInterface(ID3D11On12Device)", hr); - ReleaseInteropState(); - return false; - } - - Microsoft::WRL::ComPtr dxgiDevice = {}; - hr = m_d3d11Device.As(&dxgiDevice); - if (FAILED(hr) || dxgiDevice == nullptr) { - m_lastError = HrToInteropString("ID3D11Device::QueryInterface(IDXGIDevice)", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d2dFactory->CreateDevice(dxgiDevice.Get(), m_d2dDevice.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_d2dDevice == nullptr) { - m_lastError = HrToInteropString("ID2D1Factory1::CreateDevice", hr); - ReleaseInteropState(); - return false; - } - - hr = m_d2dDevice->CreateDeviceContext( - D2D1_DEVICE_CONTEXT_OPTIONS_NONE, - m_d2dDeviceContext.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_d2dDeviceContext == nullptr) { - m_lastError = HrToInteropString("ID2D1Device::CreateDeviceContext", hr); - ReleaseInteropState(); - return false; - } - - m_d2dDeviceContext->SetDpi(96.0f, 96.0f); - m_d2dDeviceContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - m_lastError.clear(); - return true; -} - -void D3D12WindowInteropContext::ReleaseInteropState() { - ClearSourceTextures(); - if (m_d2dDeviceContext != nullptr) { - m_d2dDeviceContext->SetTarget(nullptr); - } - if (m_d3d11DeviceContext != nullptr) { - m_d3d11DeviceContext->ClearState(); - m_d3d11DeviceContext->Flush(); - } - m_d2dDeviceContext.Reset(); - m_d2dDevice.Reset(); - m_d3d11On12Device.Reset(); - m_d3d11DeviceContext.Reset(); - m_d3d11Device.Reset(); -} - -bool D3D12WindowInteropContext::PrepareSourceTextures( - const ::XCEngine::UI::UIDrawData& drawData) { - ClearSourceTextures(); - if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { - return false; - } - - std::vector<::XCEngine::UI::UITextureHandle> textureHandles = {}; - CollectInteropTextureHandles(drawData, textureHandles); - m_activeSourceTextures.reserve(textureHandles.size()); - - for (const ::XCEngine::UI::UITextureHandle& textureHandle : textureHandles) { - auto* texture = - reinterpret_cast<::XCEngine::RHI::RHITexture*>(textureHandle.resourceHandle); - auto* nativeTexture = dynamic_cast<::XCEngine::RHI::D3D12Texture*>(texture); - if (nativeTexture == nullptr || nativeTexture->GetResource() == nullptr) { - m_lastError = "Failed to resolve a D3D12 source texture for UI composition."; - ClearSourceTextures(); - return false; - } - - D3D11_RESOURCE_FLAGS resourceFlags = {}; - resourceFlags.BindFlags = D3D11_BIND_SHADER_RESOURCE; - - SourceTextureResource resource = {}; - resource.key = textureHandle.resourceHandle; - HRESULT hr = m_d3d11On12Device->CreateWrappedResource( - nativeTexture->GetResource(), - &resourceFlags, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, - IID_PPV_ARGS(resource.wrappedResource.ReleaseAndGetAddressOf())); - if (FAILED(hr) || resource.wrappedResource == nullptr) { - m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(source)", hr); - ClearSourceTextures(); - return false; - } - - Microsoft::WRL::ComPtr dxgiSurface = {}; - hr = resource.wrappedResource.As(&dxgiSurface); - if (FAILED(hr) || dxgiSurface == nullptr) { - m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); - ClearSourceTextures(); - return false; - } - - const D2D1_BITMAP_PROPERTIES1 bitmapProperties = - BuildD2DBitmapProperties( - nativeTexture->GetDesc().Format, - D2D1_BITMAP_OPTIONS_NONE); - hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( - dxgiSurface.Get(), - &bitmapProperties, - resource.bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || resource.bitmap == nullptr) { - m_lastError = - HrToInteropString("ID2D1DeviceContext::CreateBitmapFromDxgiSurface(source)", hr); - ClearSourceTextures(); - return false; - } - - m_activeBitmaps.emplace(resource.key, resource.bitmap); - m_activeSourceTextures.push_back(std::move(resource)); - } - - m_lastError.clear(); - return true; -} - -void D3D12WindowInteropContext::ClearSourceTextures() { - m_activeBitmaps.clear(); - m_activeSourceTextures.clear(); -} - -bool D3D12WindowInteropContext::ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const { - outBitmap.Reset(); - if (!IsInteropTextureHandle(texture)) { - return false; - } - - const auto found = m_activeBitmaps.find(texture.resourceHandle); - if (found == m_activeBitmaps.end() || found->second == nullptr) { - return false; - } - - outBitmap = found->second; - return true; -} - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h deleted file mode 100644 index dced2112..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#ifndef NOMINMAX -#define NOMINMAX -#endif - -#include "D3D12WindowRenderer.h" - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Host { - -class D3D12WindowInteropContext { -public: - bool Attach(D3D12WindowRenderer& windowRenderer, ID2D1Factory1& d2dFactory); - void Detach(); - bool PrepareSourceTextures(const ::XCEngine::UI::UIDrawData& drawData); - void ClearSourceTextures(); - bool ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const; - - D3D12WindowRenderer* GetWindowRenderer() const; - ID3D11On12Device* GetD3D11On12Device() const; - ID3D11DeviceContext* GetD3D11DeviceContext() const; - ID2D1DeviceContext* GetD2DDeviceContext() const; - const std::string& GetLastError() const; - -private: - struct SourceTextureResource { - std::uintptr_t key = 0u; - Microsoft::WRL::ComPtr wrappedResource = {}; - Microsoft::WRL::ComPtr bitmap = {}; - }; - - bool EnsureInterop(); - void ReleaseInteropState(); - - D3D12WindowRenderer* m_windowRenderer = nullptr; - ID2D1Factory1* m_d2dFactory = nullptr; - Microsoft::WRL::ComPtr m_d3d11Device = {}; - Microsoft::WRL::ComPtr m_d3d11DeviceContext = {}; - Microsoft::WRL::ComPtr m_d3d11On12Device = {}; - Microsoft::WRL::ComPtr m_d2dDevice = {}; - Microsoft::WRL::ComPtr m_d2dDeviceContext = {}; - std::vector m_activeSourceTextures = {}; - std::unordered_map> m_activeBitmaps = {}; - std::string m_lastError = {}; -}; - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h b/new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h deleted file mode 100644 index 962749d5..00000000 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "D3D12WindowInteropContext.h" - -namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers { - -std::string HrToInteropString(const char* operation, HRESULT hr); - -D2D1_BITMAP_PROPERTIES1 BuildD2DBitmapProperties( - DXGI_FORMAT format, - D2D1_BITMAP_OPTIONS options); - -bool IsInteropTextureHandle(const ::XCEngine::UI::UITextureHandle& texture); - -void CollectInteropTextureHandles( - const ::XCEngine::UI::UIDrawData& drawData, - std::vector<::XCEngine::UI::UITextureHandle>& outTextures); - -} // namespace XCEngine::UI::Editor::Host::D3D12WindowInteropHelpers diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp index 122811d5..c7f2fe0f 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp @@ -66,7 +66,8 @@ D3D12WindowRenderLoopResizeResult D3D12WindowRenderLoop::ApplyResize(UINT width, } D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( - const ::XCEngine::UI::UIDrawData& drawData) const { + const ::XCEngine::UI::UIDrawData& drawData, + const std::filesystem::path* captureOutputPath) { D3D12WindowRenderLoopPresentResult result = {}; if (m_uiRenderer == nullptr) { result.warning = "window render loop has no ui renderer."; @@ -127,6 +128,17 @@ D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( return result; } + if (captureOutputPath != nullptr) { + std::string captureError = {}; + if (!m_windowRenderer->CaptureCurrentBackBufferToPng(*captureOutputPath, captureError)) { + result.captureError = captureError.empty() + ? "failed to capture the current D3D12 back buffer." + : std::move(captureError); + } else { + result.captureSucceeded = true; + } + } + if (!m_windowRenderer->PresentFrame()) { const std::string& error = m_windowRenderer->GetLastError(); result.warning = error.empty() diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h index 081695cd..3093a088 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h @@ -5,6 +5,7 @@ #include +#include #include namespace XCEngine::UI::Editor::Host { @@ -27,6 +28,8 @@ struct D3D12WindowRenderLoopResizeResult { struct D3D12WindowRenderLoopPresentResult { bool framePresented = false; + bool captureSucceeded = false; + std::string captureError = {}; std::string warning = {}; }; @@ -40,7 +43,8 @@ public: D3D12WindowRenderLoopFrameContext BeginFrame() const; D3D12WindowRenderLoopResizeResult ApplyResize(UINT width, UINT height); D3D12WindowRenderLoopPresentResult Present( - const ::XCEngine::UI::UIDrawData& drawData) const; + const ::XCEngine::UI::UIDrawData& drawData, + const std::filesystem::path* captureOutputPath = nullptr); bool HasViewportSurfacePresentation() const; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp index e1dd3d27..ab32b078 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp @@ -58,6 +58,7 @@ void D3D12WindowRenderer::Shutdown() { TraceRenderer("D3D12WindowRenderer::Shutdown begin", this); m_textureCpuHandles.clear(); m_textureAllocator.Shutdown(); + m_capture.Shutdown(); m_presenter.Shutdown(); m_hostDevice.Shutdown(); m_activeFrameSlot = 0u; @@ -153,6 +154,16 @@ bool D3D12WindowRenderer::PresentFrame() { return presented; } +bool D3D12WindowRenderer::CaptureCurrentBackBufferToPng( + const std::filesystem::path& outputPath, + std::string& outError) { + const bool captured = m_capture.CaptureCurrentBackBufferToPng(*this, outputPath, outError); + if (!captured && outError.empty()) { + outError = "CaptureCurrentBackBufferToPng failed."; + } + return captured; +} + void D3D12WindowRenderer::WaitForGpuIdle() { TraceRenderer("D3D12WindowRenderer::WaitForGpuIdle begin", this); m_hostDevice.WaitForGpuIdle(); diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h index 258dac66..a59733a9 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h @@ -7,6 +7,7 @@ #include "Ports/ViewportRenderPort.h" #include "D3D12HostDevice.h" #include "D3D12ShaderResourceDescriptorAllocator.h" +#include "D3D12WindowCapture.h" #include "D3D12WindowSwapChainPresenter.h" #include @@ -16,6 +17,7 @@ #include +#include #include #include @@ -37,6 +39,9 @@ public: bool SubmitFrame(); bool SignalFrameCompletion(); bool PresentFrame(); + bool CaptureCurrentBackBufferToPng( + const std::filesystem::path& outputPath, + std::string& outError); void WaitForGpuIdle(); ID3D12Device* GetDevice() const; @@ -63,6 +68,7 @@ public: private: D3D12HostDevice m_hostDevice = {}; D3D12WindowSwapChainPresenter m_presenter = {}; + D3D12WindowCapture m_capture = {}; D3D12ShaderResourceDescriptorAllocator m_textureAllocator = {}; std::uint32_t m_activeFrameSlot = 0u; std::uint32_t m_nextFrameSlot = 0u; diff --git a/new_editor/app/Rendering/Native/AutoScreenshot.cpp b/new_editor/app/Rendering/Native/AutoScreenshot.cpp index c372ed91..42454a49 100644 --- a/new_editor/app/Rendering/Native/AutoScreenshot.cpp +++ b/new_editor/app/Rendering/Native/AutoScreenshot.cpp @@ -1,6 +1,5 @@ #include "AutoScreenshot.h" -#include "NativeRenderer.h" #include "Support/EnvironmentFlags.h" #include "Support/ExecutablePath.h" @@ -29,6 +28,7 @@ void AutoScreenshotController::Initialize(const std::filesystem::path& captureRo m_captureRoot = ResolveBuildCaptureRoot(captureRoot); m_historyRoot = (m_captureRoot / "history").lexically_normal(); m_latestCapturePath = (m_captureRoot / "latest.png").lexically_normal(); + m_activeHistoryCapturePath.clear(); m_captureCount = 0; m_capturePending = false; m_pendingReason.clear(); @@ -40,6 +40,7 @@ void AutoScreenshotController::Initialize(const std::filesystem::path& captureRo } void AutoScreenshotController::Shutdown() { + m_activeHistoryCapturePath.clear(); m_capturePending = false; m_pendingReason.clear(); } @@ -49,19 +50,10 @@ void AutoScreenshotController::RequestCapture(std::string reason) { m_capturePending = true; } -void AutoScreenshotController::CaptureIfRequested( - NativeRenderer& renderer, - D3D12WindowRenderer& windowRenderer, - const ::XCEngine::UI::UIDrawData& drawData, - unsigned int width, - unsigned int height, - bool framePresented) { +bool AutoScreenshotController::TryBeginCapture(std::filesystem::path& outHistoryPath) { + outHistoryPath.clear(); if (!m_capturePending) { - return; - } - - if (!framePresented || drawData.Empty() || width == 0u || height == 0u) { - return; + return false; } std::error_code errorCode = {}; @@ -69,52 +61,70 @@ void AutoScreenshotController::CaptureIfRequested( if (errorCode) { m_lastCaptureError = "Failed to create screenshot directory: " + m_captureRoot.string(); m_lastCaptureSummary = "AutoShot failed"; - m_capturePending = false; - return; + ResetPendingRequest(); + return false; } std::filesystem::create_directories(m_historyRoot, errorCode); if (errorCode) { m_lastCaptureError = "Failed to create screenshot directory: " + m_historyRoot.string(); m_lastCaptureSummary = "AutoShot failed"; - m_capturePending = false; + ResetPendingRequest(); + return false; + } + + m_activeHistoryCapturePath = BuildHistoryCapturePath(m_pendingReason); + outHistoryPath = m_activeHistoryCapturePath; + return true; +} + +void AutoScreenshotController::CompleteCaptureSuccess(const std::filesystem::path& historyPath) { + const std::filesystem::path resolvedHistoryPath = + historyPath.empty() ? m_activeHistoryCapturePath : historyPath; + if (resolvedHistoryPath.empty()) { + CompleteCaptureFailure("Capture completed without a valid history path."); return; } - std::string captureError = {}; - const std::filesystem::path historyPath = BuildHistoryCapturePath(m_pendingReason); - if (!renderer.CaptureToPng( - &windowRenderer, - drawData, - width, - height, - historyPath, - captureError)) { - m_lastCaptureError = std::move(captureError); - m_lastCaptureSummary = "AutoShot failed"; - m_capturePending = false; + std::error_code errorCode = {}; + const std::uintmax_t historyFileSize = + std::filesystem::file_size(resolvedHistoryPath, errorCode); + if (errorCode || historyFileSize == 0u) { + CompleteCaptureFailure("Capture completed without a valid PNG payload."); return; } errorCode.clear(); std::filesystem::copy_file( - historyPath, + resolvedHistoryPath, m_latestCapturePath, std::filesystem::copy_options::overwrite_existing, errorCode); if (errorCode) { m_lastCaptureError = "Failed to update latest screenshot: " + m_latestCapturePath.string(); m_lastCaptureSummary = "AutoShot failed"; - m_capturePending = false; + ResetPendingRequest(); return; } ++m_captureCount; m_lastCaptureError.clear(); m_lastCaptureSummary = - "Shot: latest.png | " + historyPath.filename().string(); - m_capturePending = false; - m_pendingReason.clear(); + "Shot: latest.png | " + resolvedHistoryPath.filename().string(); + ResetPendingRequest(); +} + +void AutoScreenshotController::CompleteCaptureFailure(std::string error) { + if (!m_activeHistoryCapturePath.empty()) { + std::error_code deleteError = {}; + std::filesystem::remove(m_activeHistoryCapturePath, deleteError); + } + + m_lastCaptureError = error.empty() + ? "Screenshot capture failed." + : std::move(error); + m_lastCaptureSummary = "AutoShot failed"; + ResetPendingRequest(); } bool AutoScreenshotController::HasPendingCapture() const { @@ -133,6 +143,12 @@ const std::string& AutoScreenshotController::GetLastCaptureError() const { return m_lastCaptureError; } +void AutoScreenshotController::ResetPendingRequest() { + m_capturePending = false; + m_pendingReason.clear(); + m_activeHistoryCapturePath.clear(); +} + std::filesystem::path AutoScreenshotController::BuildHistoryCapturePath(std::string_view reason) const { std::ostringstream filename; filename << BuildTimestampString() diff --git a/new_editor/app/Rendering/Native/AutoScreenshot.h b/new_editor/app/Rendering/Native/AutoScreenshot.h index b5dd7162..46d661fe 100644 --- a/new_editor/app/Rendering/Native/AutoScreenshot.h +++ b/new_editor/app/Rendering/Native/AutoScreenshot.h @@ -4,8 +4,6 @@ #define NOMINMAX #endif -#include - #include #include #include @@ -13,22 +11,15 @@ namespace XCEngine::UI::Editor::Host { -class NativeRenderer; -class D3D12WindowRenderer; - class AutoScreenshotController { public: void Initialize(const std::filesystem::path& captureRoot); void Shutdown(); void RequestCapture(std::string reason); - void CaptureIfRequested( - NativeRenderer& renderer, - D3D12WindowRenderer& windowRenderer, - const ::XCEngine::UI::UIDrawData& drawData, - unsigned int width, - unsigned int height, - bool framePresented); + bool TryBeginCapture(std::filesystem::path& outHistoryPath); + void CompleteCaptureSuccess(const std::filesystem::path& historyPath); + void CompleteCaptureFailure(std::string error); bool HasPendingCapture() const; const std::filesystem::path& GetLatestCapturePath() const; @@ -37,6 +28,7 @@ public: private: std::filesystem::path BuildHistoryCapturePath(std::string_view reason) const; + void ResetPendingRequest(); static std::string BuildTimestampString(); static std::string SanitizeReason(std::string_view reason); @@ -44,6 +36,7 @@ private: std::filesystem::path m_captureRoot = {}; std::filesystem::path m_historyRoot = {}; std::filesystem::path m_latestCapturePath = {}; + std::filesystem::path m_activeHistoryCapturePath = {}; std::string m_pendingReason = {}; std::string m_lastCaptureSummary = {}; std::string m_lastCaptureError = {}; diff --git a/new_editor/app/Rendering/Native/NativeRenderer.cpp b/new_editor/app/Rendering/Native/NativeRenderer.cpp deleted file mode 100644 index 10475fa0..00000000 --- a/new_editor/app/Rendering/Native/NativeRenderer.cpp +++ /dev/null @@ -1,1166 +0,0 @@ -#include "NativeRendererHelpers.h" -#include -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::Initialize(HWND hwnd) { - Shutdown(); - - if (hwnd == nullptr) { - m_lastRenderError = "Initialize rejected a null hwnd."; - return false; - } - - m_hwnd = hwnd; - std::string error = {}; - if (!EnsureCoreFactories(error)) { - m_lastRenderError = error; - Shutdown(); - return false; - } - - m_lastRenderError.clear(); - return true; -} - -void NativeRenderer::Shutdown() { - m_windowInterop.Detach(); - while (!m_liveTextures.empty()) { - auto it = m_liveTextures.begin(); - delete *it; - m_liveTextures.erase(it); - } - m_textFormats.clear(); - m_solidBrush.Reset(); - m_renderTarget.Reset(); - m_wicFactory.Reset(); - m_dwriteFactory.Reset(); - m_d2dFactory.Reset(); - if (m_wicComInitialized) { - CoUninitialize(); - m_wicComInitialized = false; - } - m_hwnd = nullptr; -} - -void NativeRenderer::SetDpiScale(float dpiScale) { - m_dpiScale = ClampDpiScale(dpiScale); - if (m_renderTarget) { - m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); - } -} - -float NativeRenderer::GetDpiScale() const { - return m_dpiScale; -} - -const std::string& NativeRenderer::GetLastRenderError() const { - return m_lastRenderError; -} - -bool NativeRenderer::EnsureWicFactory(std::string& outError) { - outError.clear(); - if (m_wicFactory) { - 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; -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) { - if (!EnsureRenderTarget()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "EnsureRenderTarget failed."; - } - return false; - } - - const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData); - const HRESULT hr = m_renderTarget->EndDraw(); - if (hr == D2DERR_RECREATE_TARGET) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); - DiscardRenderTarget(); - return false; - } - - if (!rendered || FAILED(hr)) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); - return false; - } - - m_lastRenderError.clear(); - return true; -} - -bool NativeRenderer::EnsureCoreFactories(std::string& outError) { - outError.clear(); - - if (m_d2dFactory != nullptr && m_dwriteFactory != nullptr) { - return true; - } - - if (m_d2dFactory == nullptr) { - D2D1_FACTORY_OPTIONS factoryOptions = {}; -#ifdef _DEBUG - factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; -#endif - const HRESULT factoryHr = D2D1CreateFactory( - D2D1_FACTORY_TYPE_SINGLE_THREADED, - __uuidof(ID2D1Factory1), - &factoryOptions, - reinterpret_cast(m_d2dFactory.ReleaseAndGetAddressOf())); - if (FAILED(factoryHr) || m_d2dFactory == nullptr) { - outError = HrToString("D2D1CreateFactory", factoryHr); - m_d2dFactory.Reset(); - return false; - } - } - - if (m_dwriteFactory == nullptr) { - const HRESULT writeHr = DWriteCreateFactory( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof(IDWriteFactory), - reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); - if (FAILED(writeHr) || m_dwriteFactory == nullptr) { - outError = HrToString("DWriteCreateFactory", writeHr); - m_dwriteFactory.Reset(); - return false; - } - } - - return true; -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -void NativeRenderer::Resize(UINT width, UINT height) { - if (!m_renderTarget || width == 0 || height == 0) { - return; - } - - const HRESULT hr = m_renderTarget->Resize(D2D1::SizeU(width, height)); - if (hr == D2DERR_RECREATE_TARGET) { - DiscardRenderTarget(); - } -} - -bool NativeRenderer::EnsureRenderTarget() { - if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) { - m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory."; - return false; - } - - return CreateDeviceResources(); -} - -void NativeRenderer::DiscardRenderTarget() { - InvalidateCachedTextureBitmaps(m_renderTarget.Get()); - m_solidBrush.Reset(); - m_renderTarget.Reset(); -} - -bool NativeRenderer::CreateDeviceResources() { - if (m_renderTarget) { - return true; - } - - RECT clientRect = {}; - GetClientRect(m_hwnd, &clientRect); - const UINT width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); - const UINT height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); - - const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties( - D2D1_RENDER_TARGET_TYPE_DEFAULT, - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties( - m_hwnd, - D2D1::SizeU(width, height)); - - const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget( - renderTargetProps, - hwndProps, - m_renderTarget.ReleaseAndGetAddressOf()); - if (FAILED(renderTargetHr)) { - m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr); - return false; - } - - const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - m_solidBrush.ReleaseAndGetAddressOf()); - if (FAILED(brushHr)) { - m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr); - DiscardRenderTarget(); - return false; - } - - m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); - m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - m_lastRenderError.clear(); - return true; -} - -void NativeRenderer::InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget) { - for (NativeTextureResource* texture : m_liveTextures) { - if (texture == nullptr) { - continue; - } - - if (renderTarget == nullptr || texture->cachedTarget == renderTarget) { - texture->cachedBitmap.Reset(); - texture->cachedTarget = nullptr; - } - } -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::RenderToTarget( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawData& drawData) { - renderTarget.SetDpi(kBaseDpi, kBaseDpi); - renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - renderTarget.BeginDraw(); - renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f)); - - std::vector clipStack = {}; - for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { - for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { - RenderCommand(renderTarget, solidBrush, command, clipStack); - } - } - - while (!clipStack.empty()) { - renderTarget.PopAxisAlignedClip(); - clipStack.pop_back(); - } - - return true; -} - -void NativeRenderer::RenderCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command, - std::vector& clipStack) { - solidBrush.SetColor(ToD2DColor(command.color)); - - switch (command.type) { - case ::XCEngine::UI::UIDrawCommandType::FilledRect: - RenderFilledRectCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: - RenderFilledRectLinearGradientCommand(renderTarget, command); - break; - case ::XCEngine::UI::UIDrawCommandType::RectOutline: - RenderRectOutlineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Line: - RenderLineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: - RenderFilledTriangleCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::FilledCircle: - RenderFilledCircleCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::CircleOutline: - RenderCircleOutlineCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Text: - RenderTextCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::Image: - RenderImageCommand(renderTarget, solidBrush, command); - break; - case ::XCEngine::UI::UIDrawCommandType::PushClipRect: { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); - clipStack.push_back(rect); - break; - } - case ::XCEngine::UI::UIDrawCommandType::PopClipRect: { - if (!clipStack.empty()) { - renderTarget.PopAxisAlignedClip(); - clipStack.pop_back(); - } - break; - } - default: - break; - } -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -void NativeRenderer::RenderFilledRectCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - if (command.rounding > 0.0f) { - renderTarget.FillRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - &solidBrush); - return; - } - - renderTarget.FillRectangle(rect, &solidBrush); -} - -void NativeRenderer::RenderFilledRectLinearGradientCommand( - ID2D1RenderTarget& renderTarget, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - - const D2D1_GRADIENT_STOP stops[2] = { - D2D1::GradientStop(0.0f, ToD2DColor(command.color)), - D2D1::GradientStop(1.0f, ToD2DColor(command.secondaryColor)) - }; - - Microsoft::WRL::ComPtr stopCollection; - HRESULT hr = renderTarget.CreateGradientStopCollection( - stops, - 2u, - stopCollection.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !stopCollection) { - return; - } - - const D2D1_POINT_2F startPoint = - command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical - ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.top) - : D2D1::Point2F(rect.left, (rect.top + rect.bottom) * 0.5f); - const D2D1_POINT_2F endPoint = - command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical - ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.bottom) - : D2D1::Point2F(rect.right, (rect.top + rect.bottom) * 0.5f); - - Microsoft::WRL::ComPtr gradientBrush; - hr = renderTarget.CreateLinearGradientBrush( - D2D1::LinearGradientBrushProperties(startPoint, endPoint), - stopCollection.Get(), - gradientBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !gradientBrush) { - return; - } - - if (command.rounding > 0.0f) { - renderTarget.FillRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - gradientBrush.Get()); - return; - } - - renderTarget.FillRectangle(rect, gradientBrush.Get()); -} - -void NativeRenderer::RenderRectOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; - if (command.rounding > 0.0f) { - renderTarget.DrawRoundedRectangle( - D2D1::RoundedRect(rect, rounding, rounding), - &solidBrush, - thickness); - return; - } - - renderTarget.DrawRectangle(rect, &solidBrush, thickness); -} - -void NativeRenderer::RenderLineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - const float pixelOffset = ResolveStrokePixelOffset(thickness); - const D2D1_POINT_2F start = ToD2DPoint(command.position, dpiScale, pixelOffset); - const D2D1_POINT_2F end = ToD2DPoint(command.uvMin, dpiScale, pixelOffset); - renderTarget.DrawLine(start, end, &solidBrush, thickness); -} - -void NativeRenderer::RenderFilledTriangleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - Microsoft::WRL::ComPtr geometry; - HRESULT hr = m_d2dFactory->CreatePathGeometry(geometry.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !geometry) { - return; - } - - Microsoft::WRL::ComPtr sink; - hr = geometry->Open(sink.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !sink) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const D2D1_POINT_2F points[3] = { - ToD2DPoint(command.position, dpiScale), - ToD2DPoint(command.uvMin, dpiScale), - ToD2DPoint(command.uvMax, dpiScale) - }; - - sink->BeginFigure(points[0], D2D1_FIGURE_BEGIN_FILLED); - sink->AddLines(points + 1, 2u); - sink->EndFigure(D2D1_FIGURE_END_CLOSED); - hr = sink->Close(); - if (FAILED(hr)) { - return; - } - - renderTarget.FillGeometry(geometry.Get(), &solidBrush); -} - -void NativeRenderer::RenderFilledCircleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float radius = command.radius * dpiScale; - renderTarget.FillEllipse( - D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), - &solidBrush); -} - -void NativeRenderer::RenderCircleOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - const float dpiScale = ClampDpiScale(m_dpiScale); - const float radius = command.radius * dpiScale; - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; - renderTarget.DrawEllipse( - D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), - &solidBrush, - thickness); -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -void NativeRenderer::RenderTextCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - if (command.text.empty()) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const float fontSize = ResolveFontSize(command.fontSize); - const float scaledFontSize = fontSize * dpiScale; - IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); - if (textFormat == nullptr) { - return; - } - - const std::wstring text = Utf8ToWide(command.text); - if (text.empty()) { - return; - } - - const D2D1_SIZE_F targetSize = renderTarget.GetSize(); - const float originX = SnapToPixel(command.position.x, dpiScale); - const float originY = SnapToPixel(command.position.y, dpiScale); - const float lineHeight = std::ceil(scaledFontSize * 1.6f); - const D2D1_RECT_F layoutRect = D2D1::RectF( - originX, - originY, - targetSize.width, - originY + lineHeight); - renderTarget.DrawTextW( - text.c_str(), - static_cast(text.size()), - textFormat, - layoutRect, - &solidBrush, - D2D1_DRAW_TEXT_OPTIONS_CLIP, - DWRITE_MEASURING_MODE_GDI_NATURAL); -} - -void NativeRenderer::RenderImageCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command) { - if (!command.texture.IsValid()) { - return; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - Microsoft::WRL::ComPtr bitmap; - if (command.texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) { - if (!ResolveInteropBitmap(command.texture, bitmap) || !bitmap) { - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); - return; - } - } else { - auto* texture = reinterpret_cast(command.texture.nativeHandle); - if (texture == nullptr || m_liveTextures.find(texture) == m_liveTextures.end()) { - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); - return; - } - - if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) { - return; - } - } - - const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float sourceWidth = static_cast(command.texture.width); - const float sourceHeight = static_cast(command.texture.height); - const float sourceLeft = sourceWidth * std::clamp(command.uvMin.x, 0.0f, 1.0f); - const float sourceTop = sourceHeight * std::clamp(command.uvMin.y, 0.0f, 1.0f); - const float sourceRight = sourceWidth * std::clamp(command.uvMax.x, 0.0f, 1.0f); - const float sourceBottom = sourceHeight * std::clamp(command.uvMax.y, 0.0f, 1.0f); - renderTarget.DrawBitmap( - bitmap.Get(), - rect, - std::clamp(command.color.a, 0.0f, 1.0f), - D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, - D2D1::RectF(sourceLeft, sourceTop, sourceRight, sourceBottom)); -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const { - if (!m_dwriteFactory) { - return nullptr; - } - - const float resolvedFontSize = ResolveFontSize(fontSize); - const int key = static_cast(std::lround(resolvedFontSize * 10.0f)); - const auto found = m_textFormats.find(key); - if (found != m_textFormats.end()) { - return found->second.Get(); - } - - Microsoft::WRL::ComPtr textFormat; - const HRESULT hr = m_dwriteFactory->CreateTextFormat( - L"Segoe UI", - nullptr, - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - resolvedFontSize, - L"", - textFormat.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - return nullptr; - } - - textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); - textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); - textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); - - IDWriteTextFormat* result = textFormat.Get(); - m_textFormats.emplace(key, std::move(textFormat)); - return result; -} - -D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) { - return D2D1::ColorF(color.r, color.g, color.b, color.a); -} - -std::wstring NativeRenderer::Utf8ToWide(std::string_view text) { - if (text.empty()) { - return {}; - } - - const int sizeNeeded = MultiByteToWideChar( - CP_UTF8, - 0, - text.data(), - static_cast(text.size()), - nullptr, - 0); - if (sizeNeeded <= 0) { - return {}; - } - - std::wstring wideText(static_cast(sizeNeeded), L'\0'); - MultiByteToWideChar( - CP_UTF8, - 0, - text.data(), - static_cast(text.size()), - wideText.data(), - sizeNeeded); - return wideText; -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::DecodeTextureFile( - const std::filesystem::path& path, - NativeTextureResource& outTexture, - std::string& outError) { - outError.clear(); - if (!EnsureWicFactory(outError)) { - return false; - } - - const std::wstring widePath = path.wstring(); - Microsoft::WRL::ComPtr decoder; - HRESULT hr = m_wicFactory->CreateDecoderFromFilename( - widePath.c_str(), - nullptr, - GENERIC_READ, - WICDecodeMetadataCacheOnLoad, - decoder.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !decoder) { - outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !frame) { - outError = HrToString("IWICBitmapDecoder::GetFrame", hr); - return false; - } - - return DecodeTextureFrame(*frame.Get(), outTexture, outError); -} - -bool NativeRenderer::DecodeTextureMemory( - const std::uint8_t* data, - std::size_t size, - NativeTextureResource& outTexture, - std::string& outError) { - 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) { - 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) { - outError = HrToString("IWICImagingFactory::CreateDecoderFromStream", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !frame) { - outError = HrToString("IWICBitmapDecoder::GetFrame", hr); - return false; - } - - return DecodeTextureFrame(*frame.Get(), outTexture, outError); -} - -bool NativeRenderer::DecodeTextureFrame( - IWICBitmapSource& source, - NativeTextureResource& outTexture, - std::string& outError) { - outError.clear(); - - Microsoft::WRL::ComPtr converter; - HRESULT hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !converter) { - outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); - return false; - } - - hr = converter->Initialize( - &source, - GUID_WICPixelFormat32bppPBGRA, - 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; - } - - outTexture.pixels = std::move(pixels); - outTexture.width = width; - outTexture.height = height; - outTexture.cachedBitmap.Reset(); - outTexture.cachedTarget = nullptr; - return true; -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::LoadTextureFromFile( - const std::filesystem::path& path, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) { - outError.clear(); - ReleaseTexture(outTexture); - - auto texture = std::make_unique(); - if (!DecodeTextureFile(path, *texture, outError)) { - outTexture = {}; - return false; - } - - outTexture.nativeHandle = reinterpret_cast(texture.get()); - outTexture.width = texture->width; - outTexture.height = texture->height; - outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; - m_liveTextures.insert(texture.get()); - texture.release(); - return true; -} - -bool NativeRenderer::LoadTextureFromMemory( - const std::uint8_t* data, - std::size_t size, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) { - outError.clear(); - ReleaseTexture(outTexture); - - auto texture = std::make_unique(); - if (!DecodeTextureMemory(data, size, *texture, outError)) { - outTexture = {}; - return false; - } - - outTexture.nativeHandle = reinterpret_cast(texture.get()); - outTexture.width = texture->width; - outTexture.height = texture->height; - outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; - m_liveTextures.insert(texture.get()); - texture.release(); - return true; -} - -bool NativeRenderer::LoadTextureFromRgba( - const std::uint8_t* rgbaPixels, - std::uint32_t width, - std::uint32_t height, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) { - outError.clear(); - ReleaseTexture(outTexture); - - if (rgbaPixels == nullptr || width == 0u || height == 0u) { - outError = "LoadTextureFromRgba rejected an empty RGBA payload."; - return false; - } - - const std::uint64_t pixelCount = - static_cast(width) * static_cast(height); - if (pixelCount == 0u || - pixelCount > - static_cast((std::numeric_limits::max)() / 4u)) { - outError = "LoadTextureFromRgba payload exceeds supported limits."; - return false; - } - - auto texture = std::make_unique(); - texture->pixels.resize(static_cast(pixelCount) * 4u); - for (std::uint64_t pixelIndex = 0u; pixelIndex < pixelCount; ++pixelIndex) { - const std::size_t srcOffset = static_cast(pixelIndex) * 4u; - const std::uint8_t red = rgbaPixels[srcOffset + 0u]; - const std::uint8_t green = rgbaPixels[srcOffset + 1u]; - const std::uint8_t blue = rgbaPixels[srcOffset + 2u]; - const std::uint8_t alpha = rgbaPixels[srcOffset + 3u]; - - texture->pixels[srcOffset + 0u] = - static_cast((static_cast(blue) * alpha + 127u) / 255u); - texture->pixels[srcOffset + 1u] = - static_cast((static_cast(green) * alpha + 127u) / 255u); - texture->pixels[srcOffset + 2u] = - static_cast((static_cast(red) * alpha + 127u) / 255u); - texture->pixels[srcOffset + 3u] = alpha; - } - - texture->width = width; - texture->height = height; - texture->cachedBitmap.Reset(); - texture->cachedTarget = nullptr; - - outTexture.nativeHandle = reinterpret_cast(texture.get()); - outTexture.width = texture->width; - outTexture.height = texture->height; - outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; - m_liveTextures.insert(texture.get()); - texture.release(); - return true; -} - -void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { - if (!texture.IsValid()) { - texture = {}; - return; - } - - auto* resource = reinterpret_cast(texture.nativeHandle); - if (resource != nullptr) { - const auto found = m_liveTextures.find(resource); - if (found != m_liveTextures.end()) { - m_liveTextures.erase(found); - delete resource; - } - } - - texture = {}; -} - -bool NativeRenderer::ResolveTextureBitmap( - ID2D1RenderTarget& renderTarget, - NativeTextureResource& texture, - Microsoft::WRL::ComPtr& outBitmap) { - outBitmap.Reset(); - if (texture.width == 0u || texture.height == 0u || texture.pixels.empty()) { - return false; - } - - if (texture.cachedBitmap && texture.cachedTarget == &renderTarget) { - outBitmap = texture.cachedBitmap; - return true; - } - - Microsoft::WRL::ComPtr bitmap; - const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties( - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - const HRESULT hr = renderTarget.CreateBitmap( - D2D1::SizeU(texture.width, texture.height), - texture.pixels.data(), - texture.width * 4u, - &properties, - bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !bitmap) { - return false; - } - - if (&renderTarget == m_renderTarget.Get()) { - texture.cachedBitmap = bitmap; - texture.cachedTarget = &renderTarget; - } - - outBitmap = std::move(bitmap); - return true; -} - -bool NativeRenderer::ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const { - return m_windowInterop.ResolveInteropBitmap(texture, outBitmap); -} - -} // namespace XCEngine::UI::Editor::Host - -namespace XCEngine::UI::Editor::Host { - -using namespace NativeRendererHelpers; - -bool NativeRenderer::CaptureToPng( - D3D12WindowRenderer* windowRenderer, - const ::XCEngine::UI::UIDrawData& drawData, - UINT width, - UINT height, - const std::filesystem::path& outputPath, - std::string& outError) { - outError.clear(); - if (width == 0 || height == 0) { - outError = "CaptureToPng rejected an empty render size."; - return false; - } - - if (!EnsureCoreFactories(outError)) { - return false; - } - - if (!EnsureWicFactory(outError)) { - return false; - } - - std::vector<::XCEngine::UI::UITextureHandle> interopTextures = {}; - CollectInteropTextureHandles(drawData, interopTextures); - const bool requiresInterop = !interopTextures.empty(); - if (requiresInterop) { - if (windowRenderer == nullptr) { - outError = - "CaptureToPng requires a D3D12 window renderer to resolve GPU UI textures."; - return false; - } - if (!m_windowInterop.Attach(*windowRenderer, *m_d2dFactory.Get())) { - outError = m_windowInterop.GetLastError(); - return false; - } - if (!m_windowInterop.PrepareSourceTextures(drawData)) { - outError = m_windowInterop.GetLastError(); - m_windowInterop.Detach(); - return false; - } - } - - std::error_code errorCode = {}; - std::filesystem::create_directories(outputPath.parent_path(), errorCode); - if (errorCode) { - outError = "Failed to create screenshot directory: " + outputPath.parent_path().string(); - if (requiresInterop) { - m_windowInterop.ClearSourceTextures(); - m_windowInterop.Detach(); - } - return false; - } - - Microsoft::WRL::ComPtr bitmap; - HRESULT hr = m_wicFactory->CreateBitmap( - width, - height, - GUID_WICPixelFormat32bppPBGRA, - WICBitmapCacheOnLoad, - bitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateBitmap", hr); - return false; - } - - const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties( - D2D1_RENDER_TARGET_TYPE_DEFAULT, - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), - kBaseDpi, - kBaseDpi); - - Microsoft::WRL::ComPtr offscreenRenderTarget; - hr = m_d2dFactory->CreateWicBitmapRenderTarget( - bitmap.Get(), - renderTargetProperties, - offscreenRenderTarget.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); - if (requiresInterop) { - m_windowInterop.ClearSourceTextures(); - m_windowInterop.Detach(); - } - return false; - } - - Microsoft::WRL::ComPtr offscreenBrush; - hr = offscreenRenderTarget->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - offscreenBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("ID2D1RenderTarget::CreateSolidColorBrush", hr); - if (requiresInterop) { - m_windowInterop.ClearSourceTextures(); - m_windowInterop.Detach(); - } - return false; - } - - const bool rendered = RenderToTarget(*offscreenRenderTarget.Get(), *offscreenBrush.Get(), drawData); - hr = offscreenRenderTarget->EndDraw(); - if (requiresInterop) { - m_windowInterop.ClearSourceTextures(); - m_windowInterop.Detach(); - } - if (!rendered || FAILED(hr)) { - outError = HrToString("ID2D1RenderTarget::EndDraw", hr); - return false; - } - - const std::wstring wideOutputPath = outputPath.wstring(); - Microsoft::WRL::ComPtr stream; - hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateStream", hr); - return false; - } - - hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE); - if (FAILED(hr)) { - outError = HrToString("IWICStream::InitializeFromFilename", hr); - return false; - } - - Microsoft::WRL::ComPtr encoder; - hr = m_wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICImagingFactory::CreateEncoder", hr); - return false; - } - - hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::Initialize", hr); - return false; - } - - Microsoft::WRL::ComPtr frame; - Microsoft::WRL::ComPtr propertyBag; - hr = encoder->CreateNewFrame(frame.ReleaseAndGetAddressOf(), propertyBag.ReleaseAndGetAddressOf()); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr); - return false; - } - - hr = frame->Initialize(propertyBag.Get()); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::Initialize", hr); - return false; - } - - hr = frame->SetSize(width, height); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::SetSize", hr); - return false; - } - - WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppPBGRA; - hr = frame->SetPixelFormat(&pixelFormat); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr); - return false; - } - - hr = frame->WriteSource(bitmap.Get(), nullptr); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::WriteSource", hr); - return false; - } - - hr = frame->Commit(); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapFrameEncode::Commit", hr); - return false; - } - - hr = encoder->Commit(); - if (FAILED(hr)) { - outError = HrToString("IWICBitmapEncoder::Commit", hr); - return false; - } - - return true; -} - -} // namespace XCEngine::UI::Editor::Host - - diff --git a/new_editor/app/Rendering/Native/NativeRenderer.h b/new_editor/app/Rendering/Native/NativeRenderer.h deleted file mode 100644 index bcceeada..00000000 --- a/new_editor/app/Rendering/Native/NativeRenderer.h +++ /dev/null @@ -1,164 +0,0 @@ -#pragma once - -#ifndef NOMINMAX -#define NOMINMAX -#endif - -#include "Ports/TexturePort.h" - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Host { - -class D3D12WindowRenderer; - -class NativeRenderer : public Ports::TexturePort { -public: - bool Initialize(HWND hwnd); - void Shutdown(); - void SetDpiScale(float dpiScale); - float GetDpiScale() const; - void Resize(UINT width, UINT height); - bool Render(const ::XCEngine::UI::UIDrawData& drawData); - const std::string& GetLastRenderError() const; - bool LoadTextureFromFile( - const std::filesystem::path& path, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) override; - bool LoadTextureFromMemory( - const std::uint8_t* data, - std::size_t size, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) override; - bool LoadTextureFromRgba( - const std::uint8_t* rgbaPixels, - std::uint32_t width, - std::uint32_t height, - ::XCEngine::UI::UITextureHandle& outTexture, - std::string& outError) override; - void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) override; - bool CaptureToPng( - D3D12WindowRenderer* windowRenderer, - const ::XCEngine::UI::UIDrawData& drawData, - UINT width, - UINT height, - const std::filesystem::path& outputPath, - std::string& outError); - -private: - bool EnsureCoreFactories(std::string& outError); - struct NativeTextureResource { - std::vector pixels = {}; - Microsoft::WRL::ComPtr cachedBitmap = {}; - const ID2D1RenderTarget* cachedTarget = nullptr; - UINT width = 0u; - UINT height = 0u; - }; - - bool EnsureRenderTarget(); - bool EnsureWicFactory(std::string& outError); - void DiscardRenderTarget(); - bool CreateDeviceResources(); - void InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget); - bool RenderToTarget( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawData& drawData); - bool DecodeTextureFile( - const std::filesystem::path& path, - NativeTextureResource& outTexture, - std::string& outError); - bool DecodeTextureMemory( - const std::uint8_t* data, - std::size_t size, - NativeTextureResource& outTexture, - std::string& outError); - bool DecodeTextureFrame( - IWICBitmapSource& source, - NativeTextureResource& outTexture, - std::string& outError); - bool ResolveTextureBitmap( - ID2D1RenderTarget& renderTarget, - NativeTextureResource& texture, - Microsoft::WRL::ComPtr& outBitmap); - bool ResolveInteropBitmap( - const ::XCEngine::UI::UITextureHandle& texture, - Microsoft::WRL::ComPtr& outBitmap) const; - void RenderCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command, - std::vector& clipStack); - void RenderFilledRectCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderFilledRectLinearGradientCommand( - ID2D1RenderTarget& renderTarget, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderRectOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderLineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderFilledTriangleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderFilledCircleCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderCircleOutlineCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderTextCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - void RenderImageCommand( - ID2D1RenderTarget& renderTarget, - ID2D1SolidColorBrush& solidBrush, - const ::XCEngine::UI::UIDrawCommand& command); - - IDWriteTextFormat* GetTextFormat(float fontSize) const; - static D2D1_COLOR_F ToD2DColor(const ::XCEngine::UI::UIColor& color); - static std::wstring Utf8ToWide(std::string_view text); - - HWND m_hwnd = nullptr; - Microsoft::WRL::ComPtr m_d2dFactory; - Microsoft::WRL::ComPtr m_dwriteFactory; - Microsoft::WRL::ComPtr m_wicFactory; - Microsoft::WRL::ComPtr m_renderTarget; - Microsoft::WRL::ComPtr m_solidBrush; - mutable std::unordered_map> m_textFormats; - std::unordered_set m_liveTextures; - D3D12WindowInteropContext m_windowInterop = {}; - std::string m_lastRenderError = {}; - bool m_wicComInitialized = false; - float m_dpiScale = 1.0f; -}; - -} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/Native/NativeRendererHelpers.h b/new_editor/app/Rendering/Native/NativeRendererHelpers.h deleted file mode 100644 index 322039fc..00000000 --- a/new_editor/app/Rendering/Native/NativeRendererHelpers.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include "NativeRenderer.h" - -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::Host::NativeRendererHelpers { - -inline constexpr float kBaseDpi = 96.0f; -inline constexpr float kDefaultFontSize = 16.0f; - -inline 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; -} - -inline float ClampDpiScale(float dpiScale) { - return dpiScale > 0.0f ? dpiScale : 1.0f; -} - -inline float ResolveFontSize(float fontSize) { - return fontSize > 0.0f ? fontSize : kDefaultFontSize; -} - -inline float SnapToPixel(float value, float dpiScale) { - const float scale = ClampDpiScale(dpiScale); - return std::round(value * scale); -} - -inline D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect, float dpiScale) { - const float left = SnapToPixel(rect.x, dpiScale); - const float top = SnapToPixel(rect.y, dpiScale); - const float right = SnapToPixel(rect.x + rect.width, dpiScale); - const float bottom = SnapToPixel(rect.y + rect.height, dpiScale); - return D2D1::RectF(left, top, right, bottom); -} - -inline D2D1_POINT_2F ToD2DPoint( - const ::XCEngine::UI::UIPoint& point, - float dpiScale, - float pixelOffset = 0.0f) { - return D2D1::Point2F( - SnapToPixel(point.x, dpiScale) + pixelOffset, - SnapToPixel(point.y, dpiScale) + pixelOffset); -} - -inline float ResolveStrokePixelOffset(float thickness) { - const float roundedThickness = std::round(thickness); - return std::fmod(roundedThickness, 2.0f) == 1.0f ? 0.5f : 0.0f; -} - -inline D2D1_BITMAP_PROPERTIES1 BuildD2DBitmapProperties( - DXGI_FORMAT format, - D2D1_BITMAP_OPTIONS options, - D2D1_ALPHA_MODE alphaMode = D2D1_ALPHA_MODE_IGNORE) { - return D2D1::BitmapProperties1( - options, - D2D1::PixelFormat(format, alphaMode), - kBaseDpi, - kBaseDpi); -} - -inline bool IsInteropTextureHandle(const ::XCEngine::UI::UITextureHandle& texture) { - return texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView && - texture.resourceHandle != 0u; -} - -inline void CollectInteropTextureHandles( - const ::XCEngine::UI::UIDrawData& drawData, - std::vector<::XCEngine::UI::UITextureHandle>& outTextures) { - outTextures.clear(); - std::unordered_set seenKeys = {}; - for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { - for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { - if (!IsInteropTextureHandle(command.texture) || - !seenKeys.insert(command.texture.resourceHandle).second) { - continue; - } - - outTextures.push_back(command.texture); - } - } -} - -} // namespace XCEngine::UI::Editor::Host::NativeRendererHelpers