From 6e9265e92e89847432d628ffac237b6963f74537 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 21 Apr 2026 20:49:18 +0800 Subject: [PATCH] checkpoint(new_editor): native d3d12 ui path Key node 1: move main-window UI presentation onto the D3D12 render loop with native UI renderer, text system, and texture host. Key node 2: wire frame timing/FPS display, window runtime, swapchain presentation, and native screenshot capture around the new path. Key node 3: carry editor shell/workspace/viewport/panel interaction updates needed by the new renderer and detached window flow. Key node 4: pump async resource loads and scene bridge follow-up needed for scene content visibility in new_editor. --- engine/include/XCEngine/UI/Types.h | 1 + engine/src/RHI/D3D12/D3D12PipelineState.cpp | 10 +- engine/src/RHI/Vulkan/VulkanPipelineState.cpp | 11 +- new_editor/CMakeLists.txt | 3 + new_editor/app/Bootstrap/Application.cpp | 1 + new_editor/app/Composition/EditorContext.cpp | 13 + .../app/Composition/EditorShellRuntime.cpp | 128 +- .../app/Composition/EditorShellRuntime.h | 2 +- .../Features/Project/ProjectBrowserModel.cpp | 41 + .../app/Features/Project/ProjectPanel.cpp | 4 + .../app/Platform/Win32/EditorWindow.cpp | 22 +- new_editor/app/Platform/Win32/EditorWindow.h | 3 +- .../Win32/EditorWindowChromeController.cpp | 2 +- .../Win32/EditorWindowFrameOrchestrator.cpp | 17 +- .../Win32/EditorWindowFrameOrchestrator.h | 3 +- .../Win32/EditorWindowRuntimeController.cpp | 108 +- .../Win32/EditorWindowRuntimeController.h | 13 +- .../app/Rendering/D3D12/D3D12UiRenderer.cpp | 2492 +++++++++++++++++ .../app/Rendering/D3D12/D3D12UiRenderer.h | 246 ++ .../app/Rendering/D3D12/D3D12UiTextSystem.cpp | 752 +++++ .../app/Rendering/D3D12/D3D12UiTextSystem.h | 85 + .../Rendering/D3D12/D3D12UiTextureHost.cpp | 676 +++++ .../app/Rendering/D3D12/D3D12UiTextureHost.h | 113 + .../D3D12/D3D12WindowInteropContext.cpp | 151 +- .../D3D12/D3D12WindowInteropContext.h | 18 - .../Rendering/D3D12/D3D12WindowRenderLoop.cpp | 134 +- .../Rendering/D3D12/D3D12WindowRenderLoop.h | 9 +- .../Rendering/D3D12/D3D12WindowRenderer.cpp | 61 +- .../app/Rendering/D3D12/D3D12WindowRenderer.h | 12 +- .../D3D12/D3D12WindowSwapChainPresenter.cpp | 40 +- .../D3D12/D3D12WindowSwapChainPresenter.h | 5 +- .../app/Rendering/Native/AutoScreenshot.cpp | 9 +- .../app/Rendering/Native/AutoScreenshot.h | 2 + .../app/Rendering/Native/NativeRenderer.cpp | 322 +-- .../app/Rendering/Native/NativeRenderer.h | 22 +- new_editor/app/Scene/EditorSceneBridge.cpp | 27 +- .../Panels/UIEditorPanelContentHost.h | 4 + .../XCEditor/Shell/UIEditorShellCompose.h | 27 +- .../XCEditor/Shell/UIEditorShellInteraction.h | 1 - .../XCEditor/Viewport/UIEditorViewportShell.h | 7 + .../XCEditor/Viewport/UIEditorViewportSlot.h | 13 +- .../Workspace/UIEditorWorkspaceCompose.h | 20 + .../Docking/DockHostInteractionHelpers.cpp | 13 +- .../src/Docking/DockHostInteractionInternal.h | 2 +- .../Docking/UIEditorDockHostInteraction.cpp | 49 +- .../src/Panels/UIEditorPanelContentHost.cpp | 91 +- .../src/Shell/ShellInteractionInternal.h | 1 - new_editor/src/Shell/UIEditorShellCompose.cpp | 99 +- .../src/Shell/UIEditorShellInteraction.cpp | 26 +- .../src/Viewport/UIEditorViewportShell.cpp | 24 +- .../src/Viewport/UIEditorViewportSlot.cpp | 22 +- .../Workspace/UIEditorWorkspaceCompose.cpp | 169 +- .../UIEditorWorkspaceInteraction.cpp | 62 +- 53 files changed, 5330 insertions(+), 858 deletions(-) create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiRenderer.h create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiTextSystem.h create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp create mode 100644 new_editor/app/Rendering/D3D12/D3D12UiTextureHost.h diff --git a/engine/include/XCEngine/UI/Types.h b/engine/include/XCEngine/UI/Types.h index c3ad9c41..039b71cf 100644 --- a/engine/include/XCEngine/UI/Types.h +++ b/engine/include/XCEngine/UI/Types.h @@ -108,6 +108,7 @@ struct UIInputEvent { UIInputModifiers modifiers = {}; bool repeat = false; bool synthetic = false; + bool doubleClick = false; }; } // namespace UI diff --git a/engine/src/RHI/D3D12/D3D12PipelineState.cpp b/engine/src/RHI/D3D12/D3D12PipelineState.cpp index a8dfa528..6330715a 100644 --- a/engine/src/RHI/D3D12/D3D12PipelineState.cpp +++ b/engine/src/RHI/D3D12/D3D12PipelineState.cpp @@ -140,8 +140,14 @@ void D3D12PipelineState::SetInputLayout(const InputLayoutDesc& layout) { desc.Format = ToD3D12(static_cast(elem.format)); desc.InputSlot = elem.inputSlot; desc.AlignedByteOffset = elem.alignedByteOffset; - desc.InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; - desc.InstanceDataStepRate = 0; + desc.InputSlotClass = + elem.inputSlotClass != 0u + ? D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA + : D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; + desc.InstanceDataStepRate = + elem.inputSlotClass != 0u + ? (elem.instanceDataStepRate != 0u ? elem.instanceDataStepRate : 1u) + : 0u; m_inputElements.push_back(desc); } } diff --git a/engine/src/RHI/Vulkan/VulkanPipelineState.cpp b/engine/src/RHI/Vulkan/VulkanPipelineState.cpp index 61cd1a0d..8c97ba73 100644 --- a/engine/src/RHI/Vulkan/VulkanPipelineState.cpp +++ b/engine/src/RHI/Vulkan/VulkanPipelineState.cpp @@ -273,9 +273,14 @@ bool VulkanPipelineState::CreateGraphicsPipeline(const GraphicsPipelineDesc& des } std::map strideBySlot; + std::map inputRateBySlot; for (const InputElementDesc& element : m_inputLayoutDesc.elements) { const uint32_t attributeSize = GetFormatSize(static_cast(element.format)); strideBySlot[element.inputSlot] = (std::max)(strideBySlot[element.inputSlot], element.alignedByteOffset + attributeSize); + inputRateBySlot[element.inputSlot] = + element.inputSlotClass != 0u + ? VK_VERTEX_INPUT_RATE_INSTANCE + : VK_VERTEX_INPUT_RATE_VERTEX; } std::vector bindings; @@ -284,7 +289,11 @@ bool VulkanPipelineState::CreateGraphicsPipeline(const GraphicsPipelineDesc& des VkVertexInputBindingDescription binding = {}; binding.binding = entry.first; binding.stride = entry.second; - binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + const auto rateFound = inputRateBySlot.find(entry.first); + binding.inputRate = + rateFound != inputRateBySlot.end() + ? rateFound->second + : VK_VERTEX_INPUT_RATE_VERTEX; bindings.push_back(binding); } diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index e8673e72..bfe3cbd7 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -168,6 +168,9 @@ 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/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 diff --git a/new_editor/app/Bootstrap/Application.cpp b/new_editor/app/Bootstrap/Application.cpp index 46a544a1..dd2d78df 100644 --- a/new_editor/app/Bootstrap/Application.cpp +++ b/new_editor/app/Bootstrap/Application.cpp @@ -228,6 +228,7 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { } if (m_windowManager != nullptr) { + ::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads(); m_windowManager->DestroyClosedWindows(); if (!m_windowManager->HasWindows()) { break; diff --git a/new_editor/app/Composition/EditorContext.cpp b/new_editor/app/Composition/EditorContext.cpp index 23e99ae0..37443716 100644 --- a/new_editor/app/Composition/EditorContext.cpp +++ b/new_editor/app/Composition/EditorContext.cpp @@ -2,6 +2,7 @@ #include "Composition/EditorShellAssetBuilder.h" #include "Scene/EditorSceneRuntime.h" #include "Composition/EditorPanelIds.h" +#include #include #include @@ -11,6 +12,7 @@ namespace { using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager; using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; +using ::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace; std::string ComposeStatusText( std::string_view status, @@ -41,8 +43,14 @@ UIEditorWorkspacePanelPresentationModel* FindMutablePresentation( } // namespace bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { + AppendUIEditorRuntimeTrace("startup", "EditorContext::Initialize begin"); m_shellAsset = BuildEditorApplicationShellAsset(repoRoot); + AppendUIEditorRuntimeTrace("startup", "BuildEditorApplicationShellAsset complete"); m_shellValidation = ValidateEditorShellAsset(m_shellAsset); + AppendUIEditorRuntimeTrace( + "startup", + std::string("ValidateEditorShellAsset complete valid=") + + (m_shellValidation.IsValid() ? "1" : "0")); if (!m_shellValidation.IsValid()) { return false; } @@ -53,10 +61,14 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { m_commandFocusService = {}; m_selectionService = {}; m_projectRuntime = {}; + AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize begin"); m_projectRuntime.Initialize(repoRoot); + AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize end"); m_projectRuntime.BindSelectionService(&m_selectionService); m_sceneRuntime = {}; + AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize begin"); m_sceneRuntime.Initialize(m_session.projectRoot); + AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize end"); m_sceneRuntime.BindSelectionService(&m_selectionService); ResetEditorColorPickerToolState(m_colorPickerToolState); SyncSessionFromSelectionService(); @@ -69,6 +81,7 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); m_shellServices.shortcutManager = &m_shortcutManager; SetReadyStatus(); + AppendUIEditorRuntimeTrace("startup", "EditorContext::Initialize end"); return true; } diff --git a/new_editor/app/Composition/EditorShellRuntime.cpp b/new_editor/app/Composition/EditorShellRuntime.cpp index 62e57f21..65203aa4 100644 --- a/new_editor/app/Composition/EditorShellRuntime.cpp +++ b/new_editor/app/Composition/EditorShellRuntime.cpp @@ -49,7 +49,6 @@ void EditorShellRuntime::Initialize( m_projectPanel.SetBuiltInIcons(&m_builtInIcons); m_projectPanel.SetTextMeasurer(&textMeasurer); m_hierarchyPanel.Initialize(); - m_projectPanel.Initialize(repoRoot); } void EditorShellRuntime::AttachViewportWindowRenderer(Ports::ViewportRenderPort& renderer) { @@ -257,36 +256,120 @@ void AppendShellPopups( namespace XCEngine::UI::Editor::App { using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIDrawData; + +template +void AppendDrawPacket( + UIDrawData& drawData, + std::string debugName, + AppendFn&& appendFn) { + UIDrawList drawList(std::move(debugName)); + appendFn(drawList); + if (!drawList.Empty()) { + drawData.AddDrawList(std::move(drawList)); + } +} + void EditorShellRuntime::RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext) { m_viewportHostService.RenderRequestedViewports(renderContext); } -void EditorShellRuntime::Append(UIDrawList& drawList) const { +void EditorShellRuntime::Append(UIDrawData& drawData) const { const auto& metrics = ResolveUIEditorShellInteractionMetrics(); const auto& palette = ResolveUIEditorShellInteractionPalette(); const UIEditorShellComposeModel shellComposeModel = BuildShellComposeModelFromFrame(m_shellFrame); - AppendUIEditorShellCompose( - drawList, - m_shellFrame.shellFrame, - shellComposeModel, - m_shellInteractionState.composeState, - palette.shellPalette, - metrics.shellMetrics, - &m_builtInIcons); - m_consolePanel.Append(drawList); - m_colorPickerPanel.Append(drawList); - m_hierarchyPanel.Append(drawList); - m_inspectorPanel.Append(drawList); - m_projectPanel.Append(drawList); - m_sceneViewportFeature.Append(drawList); - AppendUIEditorShellComposeOverlay( - drawList, - m_shellFrame.shellFrame, - palette.shellPalette, - metrics.shellMetrics); - AppendShellPopups(drawList, m_shellFrame, palette, metrics); + AppendDrawPacket( + drawData, + "XCEditorShell.Compose.Base", + [&](UIDrawList& drawList) { + AppendUIEditorShellComposeBase( + drawList, + m_shellFrame.shellFrame, + shellComposeModel, + m_shellInteractionState.composeState, + palette.shellPalette, + metrics.shellMetrics, + &m_builtInIcons); + }); + AppendDrawPacket( + drawData, + "XCEditorShell.Compose.Workspace", + [&](UIDrawList& drawList) { + AppendUIEditorWorkspaceCompose( + drawList, + m_shellFrame.workspaceInteractionFrame.composeFrame, + palette.shellPalette.dockHostPalette, + metrics.shellMetrics.dockHostMetrics, + palette.shellPalette.viewportPalette, + metrics.shellMetrics.viewportMetrics, + UIEditorWorkspaceComposeAppendOptions{ + true, + false + }); + }); + AppendDrawPacket( + drawData, + "XCEditorShell.Compose.ViewportTextures", + [&](UIDrawList& drawList) { + AppendUIEditorWorkspaceComposeViewportTextures( + drawList, + m_shellFrame.workspaceInteractionFrame.composeFrame, + palette.shellPalette.viewportPalette); + }); + AppendDrawPacket( + drawData, + "XCEditorShell.Compose.StatusBar", + [&](UIDrawList& drawList) { + AppendUIEditorShellComposeStatusBar( + drawList, + m_shellFrame.shellFrame, + shellComposeModel, + m_shellInteractionState.composeState, + palette.shellPalette, + metrics.shellMetrics); + }); + AppendDrawPacket( + drawData, + "XCEditorPanel.Console", + [&](UIDrawList& drawList) { m_consolePanel.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorPanel.ColorPicker", + [&](UIDrawList& drawList) { m_colorPickerPanel.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorPanel.Hierarchy", + [&](UIDrawList& drawList) { m_hierarchyPanel.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorPanel.Inspector", + [&](UIDrawList& drawList) { m_inspectorPanel.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorPanel.Project", + [&](UIDrawList& drawList) { m_projectPanel.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorPanel.SceneOverlay", + [&](UIDrawList& drawList) { m_sceneViewportFeature.Append(drawList); }); + AppendDrawPacket( + drawData, + "XCEditorShell.Compose.Overlay", + [&](UIDrawList& drawList) { + AppendUIEditorShellComposeOverlay( + drawList, + m_shellFrame.workspaceInteractionFrame.composeFrame, + palette.shellPalette, + metrics.shellMetrics); + }); + AppendDrawPacket( + drawData, + "XCEditorShell.Popups", + [&](UIDrawList& drawList) { + AppendShellPopups(drawList, m_shellFrame, palette, metrics); + }); } } // namespace XCEngine::UI::Editor::App @@ -664,7 +747,6 @@ void ApplyViewportFramesToShellFrame( }; applyToViewportFrames(shellFrame.workspaceInteractionFrame.composeFrame.viewportFrames); - applyToViewportFrames(shellFrame.shellFrame.workspaceFrame.viewportFrames); } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellRuntime.h b/new_editor/app/Composition/EditorShellRuntime.h index e8de9bfa..7c301ce3 100644 --- a/new_editor/app/Composition/EditorShellRuntime.h +++ b/new_editor/app/Composition/EditorShellRuntime.h @@ -70,7 +70,7 @@ public: float detachedWindowChromeHeight = 0.0f); void RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext); - void Append(::XCEngine::UI::UIDrawList& drawList) const; + void Append(::XCEngine::UI::UIDrawData& drawData) const; const UIEditorShellInteractionFrame& GetShellFrame() const; const UIEditorShellInteractionState& GetShellInteractionState() const; diff --git a/new_editor/app/Features/Project/ProjectBrowserModel.cpp b/new_editor/app/Features/Project/ProjectBrowserModel.cpp index ed8fc185..b858f436 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModel.cpp +++ b/new_editor/app/Features/Project/ProjectBrowserModel.cpp @@ -1,5 +1,6 @@ #include "ProjectBrowserModel.h" #include +#include #include #include #include @@ -89,6 +90,10 @@ std::u8string BuildU8String(std::string_view value) { return result; } +void TraceProjectBrowser(std::string message) { + ::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace("startup", std::move(message)); +} + } // namespace std::string ToLowerCopy(std::string value) { @@ -550,14 +555,31 @@ std::string RemapMovedItemId( void ProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) { m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); + TraceProjectBrowser("ProjectBrowserModel::Initialize assetsRoot=" + PathToUtf8String(m_assetsRootPath)); std::error_code errorCode = {}; if (!std::filesystem::exists(m_assetsRootPath, errorCode)) { std::filesystem::create_directories(m_assetsRootPath / "Scenes", errorCode); } + if (errorCode) { + TraceProjectBrowser("ProjectBrowserModel::Initialize filesystem error=" + errorCode.message()); + } Refresh(); + TraceProjectBrowser( + "ProjectBrowserModel::Initialize complete folders=" + + std::to_string(m_folderEntries.size()) + + " assets=" + + std::to_string(m_assetEntries.size())); } void ProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { + if (m_folderIcon.nativeHandle == icon.nativeHandle && + m_folderIcon.width == icon.width && + m_folderIcon.height == icon.height && + m_folderIcon.kind == icon.kind && + m_folderIcon.resourceHandle == icon.resourceHandle) { + return; + } + m_folderIcon = icon; if (!m_assetsRootPath.empty()) { RefreshFolderTree(); @@ -565,9 +587,17 @@ void ProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& i } void ProjectBrowserModel::Refresh() { + TraceProjectBrowser("ProjectBrowserModel::Refresh begin"); RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); + TraceProjectBrowser( + "ProjectBrowserModel::Refresh end currentFolder=" + + m_currentFolderId + + " folders=" + + std::to_string(m_folderEntries.size()) + + " assets=" + + std::to_string(m_assetEntries.size())); } bool ProjectBrowserModel::Empty() const { @@ -1186,6 +1216,7 @@ namespace XCEngine::UI::Editor::App { void ProjectBrowserModel::RefreshAssetList() { + TraceProjectBrowser("ProjectBrowserModel::RefreshAssetList begin"); EnsureValidCurrentFolder(); m_assetEntries.clear(); @@ -1239,6 +1270,12 @@ void ProjectBrowserModel::RefreshAssetList() { CanPreviewItem(entry.path(), assetEntry.kind, assetEntry.directory); m_assetEntries.push_back(std::move(assetEntry)); } + + TraceProjectBrowser( + "ProjectBrowserModel::RefreshAssetList end currentFolder=" + + m_currentFolderId + + " assets=" + + std::to_string(m_assetEntries.size())); } } // namespace XCEngine::UI::Editor::App @@ -1247,6 +1284,7 @@ namespace XCEngine::UI::Editor::App { void ProjectBrowserModel::RefreshFolderTree() { + TraceProjectBrowser("ProjectBrowserModel::RefreshFolderTree begin"); m_folderEntries.clear(); m_treeItems.clear(); @@ -1280,6 +1318,9 @@ void ProjectBrowserModel::RefreshFolderTree() { }; appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); + TraceProjectBrowser( + "ProjectBrowserModel::RefreshFolderTree end folders=" + + std::to_string(m_folderEntries.size())); } std::vector ProjectBrowserModel::CollectCurrentFolderAncestorIds() const { diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index 8c3bc863..1c0b5785 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -317,6 +317,10 @@ void ProjectPanel::Initialize(const std::filesystem::path& repoRoot) { } void ProjectPanel::SetProjectRuntime(EditorProjectRuntime* projectRuntime) { + if (m_projectRuntime == projectRuntime) { + return; + } + m_projectRuntime = projectRuntime; if (m_projectRuntime != nullptr && m_icons != nullptr) { m_projectRuntime->SetFolderIcon(ResolveFolderIcon(m_icons)); diff --git a/new_editor/app/Platform/Win32/EditorWindow.cpp b/new_editor/app/Platform/Win32/EditorWindow.cpp index abc2cdcf..1c867652 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.cpp +++ b/new_editor/app/Platform/Win32/EditorWindow.cpp @@ -613,20 +613,22 @@ EditorWindowFrameTransferRequests EditorWindow::RenderFrame( const UIRect workspaceBounds = ResolveWorkspaceBounds(width, height); UIDrawData drawData = {}; - UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); - drawList.AddFilledRect( + UIDrawList& backgroundDrawList = drawData.EmplaceDrawList("XCEditorWindow.Surface"); + backgroundDrawList.AddFilledRect( UIRect(0.0f, 0.0f, width, height), kShellSurfaceColor); EditorWindowFrameTransferRequests transferRequests = {}; if (editorContext.IsValid()) { transferRequests = - RenderRuntimeFrame(editorContext, globalTabDragActive, workspaceBounds, drawList); + RenderRuntimeFrame(editorContext, globalTabDragActive, workspaceBounds, drawData); } else { - m_frameOrchestrator->AppendInvalidFrame(editorContext, drawList); + UIDrawList& invalidDrawList = drawData.EmplaceDrawList("XCEditorWindow.Invalid"); + m_frameOrchestrator->AppendInvalidFrame(editorContext, invalidDrawList); } - AppendBorderlessWindowChrome(drawList, width); + UIDrawList& windowChromeDrawList = drawData.EmplaceDrawList("XCEditorWindow.Chrome"); + AppendBorderlessWindowChrome(windowChromeDrawList, width); const Host::D3D12WindowRenderLoopPresentResult presentResult = m_runtime->Present(drawData); if (!presentResult.warning.empty()) { @@ -677,7 +679,7 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( EditorContext& editorContext, bool globalTabDragActive, const UIRect& workspaceBounds, - UIDrawList& drawList) { + UIDrawData& drawData) { SyncShellCapturedPointerButtonsFromSystemState(); std::vector frameEvents = m_inputController->TakePendingEvents(); const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); @@ -685,13 +687,13 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( m_frameOrchestrator->UpdateAndAppend( editorContext, *m_runtime, - workspaceBounds, - frameEvents, + workspaceBounds, + frameEvents, m_runtime->BuildCaptureStatusText(), m_state->window.primary, globalTabDragActive, - useDetachedTitleBarTabStrip, - drawList); + useDetachedTitleBarTabStrip, + drawData); ApplyShellRuntimePointerCapture(); ApplyCurrentCursor(); diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index fd58012e..dfad4ae9 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -18,6 +18,7 @@ namespace XCEngine::UI { +class UIDrawData; class UIDrawList; struct UIPoint; @@ -215,7 +216,7 @@ private: EditorContext& editorContext, bool globalTabDragActive, const ::XCEngine::UI::UIRect& workspaceBounds, - ::XCEngine::UI::UIDrawList& drawList); + ::XCEngine::UI::UIDrawData& drawData); bool IsPointerInsideClientArea() const; LPCWSTR ResolveCurrentCursorResource() const; float GetDpiScale() const; diff --git a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp index d7ae3aaa..97211569 100644 --- a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp @@ -818,7 +818,7 @@ void EditorWindowChromeController::AppendChrome( kBorderlessTitleBarFontSize); if (!frameRateText.empty()) { const float frameRateTextWidth = - window.m_runtime->GetRenderer().MeasureTextWidth( + window.m_runtime->GetTextMeasurer().MeasureTextWidth( UIEditorTextMeasureRequest{ frameRateText, kBorderlessTitleBarFontSize }); diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp index a5e27421..80697542 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp @@ -13,6 +13,7 @@ namespace XCEngine::UI::Editor::App { using namespace EditorWindowSupport; +using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; @@ -64,12 +65,12 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend EditorContext& editorContext, EditorWindowRuntimeController& runtimeController, const ::XCEngine::UI::UIRect& workspaceBounds, - const std::vector& frameEvents, - std::string_view captureStatusText, - bool primary, - bool globalTabDragActive, - bool useDetachedTitleBarTabStrip, - UIDrawList& drawList) const { + const std::vector& frameEvents, + std::string_view captureStatusText, + bool primary, + bool globalTabDragActive, + bool useDetachedTitleBarTabStrip, + UIDrawData& drawData) const { LogInputTrace(editorContext, runtimeController, frameEvents); const Host::D3D12WindowRenderLoopFrameContext frameContext = runtimeController.BeginFrame(); @@ -77,7 +78,7 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend LogRuntimeTrace("viewport", frameContext.warning); } - editorContext.AttachTextMeasurer(runtimeController.GetRenderer()); + editorContext.AttachTextMeasurer(runtimeController.GetTextMeasurer()); UIEditorWorkspaceController& workspaceController = runtimeController.GetMutableWorkspaceController(); EditorShellRuntime& shellRuntime = runtimeController.GetShellRuntime(); @@ -120,7 +121,7 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend LogRuntimeTrace(entry.channel, entry.message); } - shellRuntime.Append(drawList); + shellRuntime.Append(drawData); if (frameContext.canRenderViewports) { shellRuntime.RenderRequestedViewports(frameContext.renderContext); } diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h index 74d559f4..cc703ab0 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h @@ -11,6 +11,7 @@ namespace XCEngine::UI { class UIDrawList; +class UIDrawData; struct UIRect; struct UIInputEvent; @@ -50,7 +51,7 @@ public: bool primary, bool globalTabDragActive, bool useDetachedTitleBarTabStrip, - ::XCEngine::UI::UIDrawList& drawList) const; + ::XCEngine::UI::UIDrawData& drawData) const; void AppendInvalidFrame( EditorContext& editorContext, ::XCEngine::UI::UIDrawList& drawList) const; diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp index 36398302..82bd5153 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace XCEngine::UI::Editor::App { @@ -18,6 +19,7 @@ using namespace EditorWindowSupport; namespace { constexpr float kFrameTimeSmoothingFactor = 0.12f; +constexpr float kFrameStatsDisplayRefreshIntervalSeconds = 0.25f; } @@ -73,14 +75,17 @@ void EditorWindowRuntimeController::ClearExternalDockHostDropPreview() { void EditorWindowRuntimeController::SetDpiScale(float dpiScale) { m_renderer.SetDpiScale(dpiScale); + m_textSystem.SetDpiScale(dpiScale); + m_uiRenderer.SetDpiScale(dpiScale); } -Host::NativeRenderer& EditorWindowRuntimeController::GetRenderer() { - return m_renderer; +::XCEngine::UI::Editor::UIEditorTextMeasurer& EditorWindowRuntimeController::GetTextMeasurer() { + return m_textSystem; } -const Host::NativeRenderer& EditorWindowRuntimeController::GetRenderer() const { - return m_renderer; +const ::XCEngine::UI::Editor::UIEditorTextMeasurer& +EditorWindowRuntimeController::GetTextMeasurer() const { + return m_textSystem; } const ::XCEngine::UI::UITextureHandle& EditorWindowRuntimeController::GetTitleBarLogoIcon() const { @@ -98,36 +103,53 @@ bool EditorWindowRuntimeController::Initialize( return false; } - if (!m_renderer.Initialize(hwnd)) { - LogRuntimeTrace("app", "renderer initialization failed"); - return false; - } - RECT clientRect = {}; GetClientRect(hwnd, &clientRect); const int clientWidth = (std::max)(clientRect.right - clientRect.left, 1L); const int clientHeight = (std::max)(clientRect.bottom - clientRect.top, 1L); if (!m_windowRenderer.Initialize(hwnd, clientWidth, clientHeight)) { LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); - m_renderer.Shutdown(); return false; } - const Host::D3D12WindowRenderLoopAttachResult attachResult = - m_windowRenderLoop.Attach(m_renderer, m_windowRenderer); - if (!attachResult.interopWarning.empty()) { - LogRuntimeTrace("app", attachResult.interopWarning); + if (!m_textureHost.Initialize(m_windowRenderer)) { + LogRuntimeTrace("app", "d3d12 ui texture host initialization failed"); + m_windowRenderer.Shutdown(); + return false; } - editorContext.AttachTextMeasurer(m_renderer); - m_shellRuntime.Initialize(repoRoot, m_renderer, m_renderer); + if (!m_textSystem.Initialize()) { + LogRuntimeTrace("app", "d3d12 ui text system initialization failed"); + m_textureHost.Shutdown(); + m_windowRenderer.Shutdown(); + return false; + } + m_textSystem.SetDpiScale(m_renderer.GetDpiScale()); + + if (!m_uiRenderer.Initialize(m_windowRenderer, m_textureHost, m_textSystem)) { + LogRuntimeTrace("app", "d3d12 ui renderer initialization failed"); + m_textSystem.Shutdown(); + m_textureHost.Shutdown(); + m_windowRenderer.Shutdown(); + return false; + } + m_uiRenderer.SetDpiScale(m_renderer.GetDpiScale()); + + const Host::D3D12WindowRenderLoopAttachResult attachResult = + m_windowRenderLoop.Attach(m_uiRenderer, m_windowRenderer); + if (!attachResult.warning.empty()) { + LogRuntimeTrace("app", attachResult.warning); + } + + editorContext.AttachTextMeasurer(m_textSystem); + m_shellRuntime.Initialize(repoRoot, m_textureHost, m_textSystem); m_shellRuntime.AttachViewportWindowRenderer(m_windowRenderer); m_shellRuntime.SetViewportSurfacePresentationEnabled( attachResult.hasViewportSurfacePresentation); std::string titleBarLogoError = {}; if (!LoadEmbeddedPngTexture( - m_renderer, + m_textureHost, IDR_PNG_LOGO_ICON, m_titleBarLogoIcon, titleBarLogoError)) { @@ -160,8 +182,11 @@ void EditorWindowRuntimeController::Shutdown() { ResetFrameTiming(); m_autoScreenshot.Shutdown(); m_shellRuntime.Shutdown(); - m_renderer.ReleaseTexture(m_titleBarLogoIcon); m_windowRenderLoop.Detach(); + m_uiRenderer.Shutdown(); + m_textSystem.Shutdown(); + m_textureHost.ReleaseTexture(m_titleBarLogoIcon); + m_textureHost.Shutdown(); m_windowRenderer.Shutdown(); m_renderer.Shutdown(); } @@ -185,10 +210,6 @@ bool EditorWindowRuntimeController::ApplyResize(UINT width, UINT height) { LogRuntimeTrace("present", resizeResult.windowRendererWarning); } - if (!resizeResult.interopWarning.empty()) { - LogRuntimeTrace("present", resizeResult.interopWarning); - } - return resizeResult.hasViewportSurfacePresentation; } @@ -209,6 +230,7 @@ void EditorWindowRuntimeController::CaptureIfRequested( bool framePresented) { m_autoScreenshot.CaptureIfRequested( m_renderer, + m_windowRenderer, drawData, pixelWidth, pixelHeight, @@ -236,26 +258,17 @@ std::string EditorWindowRuntimeController::BuildCaptureStatusText() const { } std::string EditorWindowRuntimeController::BuildFrameRateText() const { - if (m_displayFps <= 0.0f || m_displayFrameTimeMs <= 0.0f) { - return {}; - } - - char buffer[48] = {}; - std::snprintf( - buffer, - sizeof(buffer), - "FPS %.1f | %.2f ms", - m_displayFps, - m_displayFrameTimeMs); - return buffer; + return m_frameRateText; } void EditorWindowRuntimeController::ResetFrameTiming() { m_lastFrameTime = {}; m_hasLastFrameTime = false; m_smoothedDeltaTimeSeconds = 0.0f; + m_frameStatsDisplayAccumulatorSeconds = 0.0f; m_displayFps = 0.0f; m_displayFrameTimeMs = 0.0f; + m_frameRateText.clear(); } void EditorWindowRuntimeController::UpdateFrameTiming() { @@ -283,6 +296,33 @@ void EditorWindowRuntimeController::UpdateFrameTiming() { m_displayFps = m_smoothedDeltaTimeSeconds > 0.0f ? 1.0f / m_smoothedDeltaTimeSeconds : 0.0f; + + m_frameStatsDisplayAccumulatorSeconds += deltaTime; + if (m_frameRateText.empty() || + m_frameStatsDisplayAccumulatorSeconds >= kFrameStatsDisplayRefreshIntervalSeconds) { + RefreshDisplayedFrameStats(); + m_frameStatsDisplayAccumulatorSeconds = 0.0f; + } +} + +void EditorWindowRuntimeController::RefreshDisplayedFrameStats() { + if (m_displayFps <= 0.0f || m_displayFrameTimeMs <= 0.0f) { + m_frameRateText.clear(); + return; + } + + const int roundedFps = (std::max)(0, static_cast(std::lround(m_displayFps))); + const int roundedFrameTimeMs = + (std::max)(0, static_cast(std::lround(m_displayFrameTimeMs))); + + char buffer[48] = {}; + std::snprintf( + buffer, + sizeof(buffer), + "FPS %3d | %2d ms", + roundedFps, + roundedFrameTimeMs); + m_frameRateText = buffer; } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h index 17cc11dc..f84ebad2 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h @@ -6,6 +6,9 @@ #include "Composition/EditorShellRuntime.h" +#include +#include +#include #include #include #include @@ -63,8 +66,8 @@ public: void ClearExternalDockHostDropPreview(); void SetDpiScale(float dpiScale); - Host::NativeRenderer& GetRenderer(); - const Host::NativeRenderer& GetRenderer() const; + ::XCEngine::UI::Editor::UIEditorTextMeasurer& GetTextMeasurer(); + const ::XCEngine::UI::Editor::UIEditorTextMeasurer& GetTextMeasurer() const; const ::XCEngine::UI::UITextureHandle& GetTitleBarLogoIcon() const; bool Initialize( @@ -93,9 +96,13 @@ public: private: void ResetFrameTiming(); void UpdateFrameTiming(); + void RefreshDisplayedFrameStats(); Host::NativeRenderer m_renderer = {}; Host::D3D12WindowRenderer m_windowRenderer = {}; + Host::D3D12UiTextureHost m_textureHost = {}; + Host::D3D12UiTextSystem m_textSystem = {}; + Host::D3D12UiRenderer m_uiRenderer = {}; Host::D3D12WindowRenderLoop m_windowRenderLoop = {}; Host::AutoScreenshotController m_autoScreenshot = {}; ::XCEngine::UI::UITextureHandle m_titleBarLogoIcon = {}; @@ -104,8 +111,10 @@ private: std::chrono::steady_clock::time_point m_lastFrameTime = {}; bool m_hasLastFrameTime = false; float m_smoothedDeltaTimeSeconds = 0.0f; + float m_frameStatsDisplayAccumulatorSeconds = 0.0f; float m_displayFps = 0.0f; float m_displayFrameTimeMs = 0.0f; + std::string m_frameRateText = {}; bool m_ready = false; }; diff --git a/new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp new file mode 100644 index 00000000..e942dff9 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp @@ -0,0 +1,2492 @@ +#include "D3D12UiRenderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +namespace { + +using ::XCEngine::RHI::BlendFactor; +using ::XCEngine::RHI::BlendOp; +using ::XCEngine::RHI::BufferDesc; +using ::XCEngine::RHI::BufferType; +using ::XCEngine::RHI::ColorWriteMask; +using ::XCEngine::RHI::ComparisonFunc; +using ::XCEngine::RHI::CullMode; +using ::XCEngine::RHI::DescriptorHeapType; +using ::XCEngine::RHI::DescriptorPoolDesc; +using ::XCEngine::RHI::DescriptorSetLayoutBinding; +using ::XCEngine::RHI::DescriptorSetLayoutDesc; +using ::XCEngine::RHI::DescriptorType; +using ::XCEngine::RHI::D3D12CommandList; +using ::XCEngine::RHI::D3D12DescriptorSet; +using ::XCEngine::RHI::D3D12DescriptorHeap; +using ::XCEngine::RHI::FilterMode; +using ::XCEngine::RHI::FillMode; +using ::XCEngine::RHI::Format; +using ::XCEngine::RHI::FrontFace; +using ::XCEngine::RHI::GraphicsPipelineDesc; +using ::XCEngine::RHI::PrimitiveTopology; +using ::XCEngine::RHI::PrimitiveTopologyType; +using ::XCEngine::RHI::RHICommandList; +using ::XCEngine::RHI::Rect; +using ::XCEngine::RHI::ResourceViewDesc; +using ::XCEngine::RHI::ResourceViewDimension; +using ::XCEngine::RHI::RHIPipelineLayoutDesc; +using ::XCEngine::RHI::SamplerDesc; +using ::XCEngine::RHI::ShaderVisibility; +using ::XCEngine::RHI::TextureAddressMode; +using ::XCEngine::RHI::Viewport; +using ::XCEngine::Rendering::RenderContext; +using ::XCEngine::Rendering::RenderSurface; +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawCommand; +using ::XCEngine::UI::UIDrawCommandType; +using ::XCEngine::UI::UIDrawData; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UILinearGradientDirection; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UITextureHandle; + +constexpr std::uint64_t kMinDynamicVertexBufferBytes = 16u * 1024u; +constexpr std::uint64_t kMinDynamicIndexBufferBytes = 16u * 1024u; +constexpr std::uint64_t kMinDynamicInstanceBufferBytes = 16u * 1024u; +constexpr std::uint32_t kGlyphAtlasPageWidth = 1024u; +constexpr std::uint32_t kGlyphAtlasPageHeight = 1024u; +constexpr std::uint32_t kGlyphAtlasPadding = 1u; + +enum class UiQuadKind : int { + Textured = 0, + RectFill = 1, + RectOutline = 2, + CircleFill = 3, + CircleOutline = 4 +}; + +struct UiQuadVertex { + float corner[2] = {}; + float uv[2] = {}; +}; + +const UiQuadVertex kUnitQuadVertices[] = { + { { 0.0f, 0.0f }, { 0.0f, 0.0f } }, + { { 1.0f, 0.0f }, { 1.0f, 0.0f } }, + { { 1.0f, 1.0f }, { 1.0f, 1.0f } }, + { { 0.0f, 1.0f }, { 0.0f, 1.0f } } +}; + +const std::uint32_t kUnitQuadIndices[] = { 0u, 1u, 2u, 0u, 2u, 3u }; + +const char kLegacyUiPassHlsl[] = R"( +cbuffer UiConstants : register(b0) { + float2 gInvViewportSize; + float2 gPadding; +}; + +struct VSInput { + float2 position : POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +Texture2D gTexture : register(t0); +SamplerState gSampler : register(s0); + +VSOutput MainVS(VSInput input) { + VSOutput output; + float2 ndc; + ndc.x = input.position.x * gInvViewportSize.x * 2.0f - 1.0f; + ndc.y = 1.0f - input.position.y * gInvViewportSize.y * 2.0f; + output.position = float4(ndc, 0.0f, 1.0f); + output.uv = input.uv; + output.color = input.color; + return output; +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + return gTexture.Sample(gSampler, input.uv) * input.color; +} +)"; + +const char kQuadUiPassHlsl[] = R"( +cbuffer UiConstants : register(b0) { + float2 gInvViewportSize; + float2 gPadding; +}; + +struct VSInput { + float2 corner : POSITION0; + float2 unitUv : TEXCOORD0; + float2 origin : POSITION1; + float2 axisX : TEXCOORD1; + float2 axisY : TEXCOORD2; + float2 uvMin : TEXCOORD3; + float2 uvMax : TEXCOORD4; + float4 color : COLOR0; + float4 secondaryColor : COLOR1; + float4 params : TEXCOORD5; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float2 unitUv : TEXCOORD0; + float2 localPos : TEXCOORD1; + float2 size : TEXCOORD2; + float2 sampleUv : TEXCOORD3; + float4 color : COLOR0; + float4 secondaryColor : COLOR1; + float4 params : TEXCOORD4; +}; + +Texture2D gTexture : register(t0); +SamplerState gSampler : register(s0); + +float2 ToNdc(float2 pixelPos) { + float2 ndc; + ndc.x = pixelPos.x * gInvViewportSize.x * 2.0f - 1.0f; + ndc.y = 1.0f - pixelPos.y * gInvViewportSize.y * 2.0f; + return ndc; +} + +VSOutput MainVS(VSInput input) { + VSOutput output; + const float2 pixelPos = + input.origin + + input.axisX * input.corner.x + + input.axisY * input.corner.y; + const float2 axisLength = float2(length(input.axisX), length(input.axisY)); + output.position = float4(ToNdc(pixelPos), 0.0f, 1.0f); + output.unitUv = input.corner; + output.localPos = axisLength * input.corner; + output.size = axisLength; + output.sampleUv = lerp(input.uvMin, input.uvMax, input.corner); + output.color = input.color; + output.secondaryColor = input.secondaryColor; + output.params = input.params; + return output; +} + +float CoverageFromSignedDistance(float distance) { + const float aa = max(fwidth(distance), 1.0f); + return saturate(0.5f - distance / aa); +} + +float RoundedRectSignedDistance(float2 localPos, float2 size, float radius) { + const float2 halfSize = max(size * 0.5f, float2(0.0f, 0.0f)); + const float resolvedRadius = min(radius, min(halfSize.x, halfSize.y)); + const float2 centered = localPos - halfSize; + const float2 radiusVector = float2(resolvedRadius, resolvedRadius); + const float2 q = abs(centered) - (halfSize - radiusVector); + return length(max(q, float2(0.0f, 0.0f))) + min(max(q.x, q.y), 0.0f) - resolvedRadius; +} + +float CircleSignedDistance(float2 localPos, float2 center, float radius) { + return length(localPos - center) - radius; +} + +float4 ResolveGradientColor(VSOutput input) { + const bool verticalGradient = input.params.w > 0.5f; + const float span = verticalGradient ? input.size.y : input.size.x; + const float axisValue = verticalGradient ? input.localPos.y : input.localPos.x; + const float t = span > 0.0f ? saturate(axisValue / span) : 0.0f; + return lerp(input.color, input.secondaryColor, t); +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + const int quadKind = (int)round(input.params.x); + if (quadKind == 0) { + return gTexture.Sample(gSampler, input.sampleUv) * input.color; + } + + float4 baseColor = ResolveGradientColor(input); + float coverage = 1.0f; + + if (quadKind == 1) { + const float distance = RoundedRectSignedDistance(input.localPos, input.size, input.params.y); + coverage = CoverageFromSignedDistance(distance); + } else if (quadKind == 2) { + const float outerDistance = + RoundedRectSignedDistance(input.localPos, input.size, input.params.y); + const float innerRadius = max(0.0f, input.params.y - input.params.z); + const float2 insetVector = float2(input.params.z, input.params.z); + const float2 innerSize = max(input.size - insetVector * 2.0f, float2(0.0f, 0.0f)); + const float2 innerLocalPos = input.localPos - insetVector; + const float innerDistance = + RoundedRectSignedDistance(innerLocalPos, innerSize, innerRadius); + coverage = + CoverageFromSignedDistance(outerDistance) * + (1.0f - CoverageFromSignedDistance(innerDistance)); + } else if (quadKind == 3) { + const float2 center = input.size * 0.5f; + const float distance = CircleSignedDistance(input.localPos, center, input.params.y); + coverage = CoverageFromSignedDistance(distance); + } else if (quadKind == 4) { + const float2 center = input.size * 0.5f; + const float distance = + abs(length(input.localPos - center) - input.params.y) - + input.params.z * 0.5f; + coverage = CoverageFromSignedDistance(distance); + } + + return float4(baseColor.rgb, baseColor.a * coverage); +} +)"; + +struct FloatPoint { + float x = 0.0f; + float y = 0.0f; +}; + +struct FloatRect { + float left = 0.0f; + float top = 0.0f; + float right = 0.0f; + float bottom = 0.0f; +}; + +struct ClipState { + Rect scissor = {}; + FloatRect rect = {}; +}; + +struct GradientContext { + FloatRect bounds = {}; + UIColor startColor = {}; + UIColor endColor = {}; + UILinearGradientDirection direction = UILinearGradientDirection::Horizontal; +}; + +struct UiConstants { + float invViewportSize[2] = {}; + float padding[2] = {}; +}; + +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; +} + +float ClampDpiScale(float dpiScale) { + return dpiScale > 0.0f ? dpiScale : 1.0f; +} + +float ResolveFontSize(float fontSize) { + return fontSize > 0.0f ? fontSize : 16.0f; +} + +float SnapToPixel(float value, float dpiScale) { + return std::round(value * ClampDpiScale(dpiScale)); +} + +float ClampRadius(float radius, float width, float height) { + return std::clamp(radius, 0.0f, (std::min)(width, height) * 0.5f); +} + +FloatRect ToPixelRect(const UIRect& rect, float dpiScale) { + return FloatRect{ + SnapToPixel(rect.x, dpiScale), + SnapToPixel(rect.y, dpiScale), + SnapToPixel(rect.x + rect.width, dpiScale), + SnapToPixel(rect.y + rect.height, dpiScale) + }; +} + +FloatPoint ToPixelPoint(const UIPoint& point, float dpiScale) { + return FloatPoint{ + SnapToPixel(point.x, dpiScale), + SnapToPixel(point.y, dpiScale) + }; +} + +FloatRect IntersectFloatRect(const FloatRect& lhs, const FloatRect& rhs) { + return FloatRect{ + (std::max)(lhs.left, rhs.left), + (std::max)(lhs.top, rhs.top), + (std::min)(lhs.right, rhs.right), + (std::min)(lhs.bottom, rhs.bottom) + }; +} + +bool IsEmptyRect(const FloatRect& rect) { + return rect.right <= rect.left || rect.bottom <= rect.top; +} + +Rect ClampScissor(const FloatRect& rect, const Rect& renderArea) { + Rect clamped = {}; + clamped.left = (std::max)(renderArea.left, static_cast(std::floor(rect.left))); + clamped.top = (std::max)(renderArea.top, static_cast(std::floor(rect.top))); + clamped.right = (std::min)(renderArea.right, static_cast(std::ceil(rect.right))); + clamped.bottom = (std::min)(renderArea.bottom, static_cast(std::ceil(rect.bottom))); + if (clamped.right < clamped.left) { + clamped.right = clamped.left; + } + if (clamped.bottom < clamped.top) { + clamped.bottom = clamped.top; + } + return clamped; +} + +bool IsEmptyScissor(const Rect& rect) { + return rect.right <= rect.left || rect.bottom <= rect.top; +} + +constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ull; +constexpr std::uint64_t kFnvPrime = 1099511628211ull; + +void HashBytes( + std::uint64_t& hash, + const void* data, + std::size_t size) { + const auto* bytes = static_cast(data); + for (std::size_t index = 0u; index < size; ++index) { + hash ^= static_cast(bytes[index]); + hash *= kFnvPrime; + } +} + +void HashFloat(std::uint64_t& hash, float value) { + std::uint32_t bits = 0u; + static_assert(sizeof(bits) == sizeof(value)); + std::memcpy(&bits, &value, sizeof(bits)); + HashBytes(hash, &bits, sizeof(bits)); +} + +void HashBool(std::uint64_t& hash, bool value) { + const std::uint8_t encoded = value ? 1u : 0u; + HashBytes(hash, &encoded, sizeof(encoded)); +} + +void HashString(std::uint64_t& hash, std::string_view value) { + const std::size_t size = value.size(); + HashBytes(hash, &size, sizeof(size)); + if (!value.empty()) { + HashBytes(hash, value.data(), value.size()); + } +} + +void HashTextureHandle(std::uint64_t& hash, const UITextureHandle& texture) { + HashBytes(hash, &texture.nativeHandle, sizeof(texture.nativeHandle)); + HashBytes(hash, &texture.width, sizeof(texture.width)); + HashBytes(hash, &texture.height, sizeof(texture.height)); + HashBytes(hash, &texture.kind, sizeof(texture.kind)); + HashBytes(hash, &texture.resourceHandle, sizeof(texture.resourceHandle)); +} + +void HashPoint(std::uint64_t& hash, const UIPoint& point) { + HashFloat(hash, point.x); + HashFloat(hash, point.y); +} + +void HashRect(std::uint64_t& hash, const UIRect& rect) { + HashFloat(hash, rect.x); + HashFloat(hash, rect.y); + HashFloat(hash, rect.width); + HashFloat(hash, rect.height); +} + +std::uint64_t BuildDrawListContentHash(const UIDrawList& drawList) { + std::uint64_t hash = kFnvOffsetBasis; + HashString(hash, drawList.GetDebugName()); + + const std::size_t commandCount = drawList.GetCommandCount(); + HashBytes(hash, &commandCount, sizeof(commandCount)); + + for (const UIDrawCommand& command : drawList.GetCommands()) { + HashBytes(hash, &command.type, sizeof(command.type)); + HashRect(hash, command.rect); + HashPoint(hash, command.position); + HashPoint(hash, command.uvMin); + HashPoint(hash, command.uvMax); + HashFloat(hash, command.color.r); + HashFloat(hash, command.color.g); + HashFloat(hash, command.color.b); + HashFloat(hash, command.color.a); + HashFloat(hash, command.secondaryColor.r); + HashFloat(hash, command.secondaryColor.g); + HashFloat(hash, command.secondaryColor.b); + HashFloat(hash, command.secondaryColor.a); + HashFloat(hash, command.thickness); + HashFloat(hash, command.rounding); + HashFloat(hash, command.radius); + HashFloat(hash, command.fontSize); + HashBool(hash, command.intersectWithCurrentClip); + HashBytes(hash, &command.gradientDirection, sizeof(command.gradientDirection)); + HashTextureHandle(hash, command.texture); + HashString(hash, command.text); + } + + return hash; +} + +void WriteColor(float outColor[4], const UIColor& color) { + outColor[0] = color.r; + outColor[1] = color.g; + outColor[2] = color.b; + outColor[3] = color.a; +} + +UIColor LerpColor( + const UIColor& startColor, + const UIColor& endColor, + float t) { + return UIColor( + startColor.r + (endColor.r - startColor.r) * t, + startColor.g + (endColor.g - startColor.g) * t, + startColor.b + (endColor.b - startColor.b) * t, + startColor.a + (endColor.a - startColor.a) * t); +} + +UIColor ResolveGradientColor( + const GradientContext& gradient, + const FloatPoint& point) { + if (gradient.direction == UILinearGradientDirection::Vertical) { + const float span = gradient.bounds.bottom - gradient.bounds.top; + const float t = span > 0.0f + ? std::clamp((point.y - gradient.bounds.top) / span, 0.0f, 1.0f) + : 0.0f; + return LerpColor(gradient.startColor, gradient.endColor, t); + } + + const float span = gradient.bounds.right - gradient.bounds.left; + const float t = span > 0.0f + ? std::clamp((point.x - gradient.bounds.left) / span, 0.0f, 1.0f) + : 0.0f; + return LerpColor(gradient.startColor, gradient.endColor, t); +} + +std::uint32_t AppendVertex( + std::vector& vertices, + const FloatPoint& position, + const UIPoint& uv, + const UIColor& color) { + D3D12UiRenderer::UiVertex vertex = {}; + vertex.position[0] = position.x; + vertex.position[1] = position.y; + vertex.uv[0] = uv.x; + vertex.uv[1] = uv.y; + WriteColor(vertex.color, color); + vertices.push_back(vertex); + return static_cast(vertices.size() - 1u); +} + +void AppendBatchInternal( + std::vector& batches, + D3D12UiRenderer::UiBatchKind kind, + std::uint32_t firstElement, + std::uint32_t elementCount, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + if (elementCount == 0u || textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { + return; + } + + if (!batches.empty()) { + D3D12UiRenderer::UiBatch& lastBatch = batches.back(); + if (lastBatch.kind == kind && + lastBatch.textureHandle.ptr == textureHandle.ptr && + lastBatch.scissorRect.left == scissor.left && + lastBatch.scissorRect.top == scissor.top && + lastBatch.scissorRect.right == scissor.right && + lastBatch.scissorRect.bottom == scissor.bottom && + lastBatch.firstElement + lastBatch.elementCount == firstElement) { + lastBatch.elementCount += elementCount; + return; + } + } + + D3D12UiRenderer::UiBatch batch = {}; + batch.kind = kind; + batch.firstElement = firstElement; + batch.elementCount = elementCount; + batch.textureHandle = textureHandle; + batch.scissorRect = scissor; + batches.push_back(batch); +} + +void AppendBatch( + std::vector& batches, + std::uint32_t firstIndex, + std::uint32_t indexCount, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + AppendBatchInternal( + batches, + D3D12UiRenderer::UiBatchKind::LegacyIndexed, + firstIndex, + indexCount, + textureHandle, + scissor); +} + +void AppendQuadBatch( + std::vector& batches, + std::uint32_t firstInstance, + std::uint32_t instanceCount, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + AppendBatchInternal( + batches, + D3D12UiRenderer::UiBatchKind::QuadInstanced, + firstInstance, + instanceCount, + textureHandle, + scissor); +} + +void AppendQuadInstance( + std::vector& instances, + std::vector& batches, + const FloatPoint& origin, + const FloatPoint& axisX, + const FloatPoint& axisY, + const UIPoint& uvMin, + const UIPoint& uvMax, + const UIColor& color, + const UIColor& secondaryColor, + UiQuadKind quadKind, + float radius, + float thickness, + float gradientDirection, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + if (textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { + return; + } + + D3D12UiRenderer::UiQuadInstance instance = {}; + instance.origin[0] = origin.x; + instance.origin[1] = origin.y; + instance.axisX[0] = axisX.x; + instance.axisX[1] = axisX.y; + instance.axisY[0] = axisY.x; + instance.axisY[1] = axisY.y; + instance.uvMin[0] = uvMin.x; + instance.uvMin[1] = uvMin.y; + instance.uvMax[0] = uvMax.x; + instance.uvMax[1] = uvMax.y; + WriteColor(instance.color, color); + WriteColor(instance.secondaryColor, secondaryColor); + instance.params[0] = static_cast(static_cast(quadKind)); + instance.params[1] = radius; + instance.params[2] = thickness; + instance.params[3] = gradientDirection; + const std::uint32_t firstInstance = static_cast(instances.size()); + instances.push_back(instance); + AppendQuadBatch(batches, firstInstance, 1u, textureHandle, scissor); +} + +std::uint32_t ResolveArcSegmentCount(float radius) { + return static_cast(std::clamp(std::ceil(radius * 0.12f), 3.0f, 12.0f)); +} + +void AppendArc( + std::vector& outPoints, + float centerX, + float centerY, + float radius, + float startAngle, + float endAngle, + std::uint32_t segmentCount, + bool includeFirstPoint) { + if (radius <= 0.0f) { + return; + } + + for (std::uint32_t segmentIndex = 0u; segmentIndex <= segmentCount; ++segmentIndex) { + if (segmentIndex == 0u && !includeFirstPoint) { + continue; + } + + const float t = static_cast(segmentIndex) / static_cast(segmentCount); + const float angle = startAngle + (endAngle - startAngle) * t; + outPoints.push_back(FloatPoint{ + centerX + std::cos(angle) * radius, + centerY + std::sin(angle) * radius + }); + } +} + +std::vector BuildRoundedRectPolygon( + const FloatRect& rect, + float radius) { + std::vector points = {}; + if (IsEmptyRect(rect)) { + return points; + } + + const float width = rect.right - rect.left; + const float height = rect.bottom - rect.top; + const float resolvedRadius = ClampRadius(radius, width, height); + if (resolvedRadius <= 0.0f) { + points.push_back(FloatPoint{ rect.left, rect.top }); + points.push_back(FloatPoint{ rect.right, rect.top }); + points.push_back(FloatPoint{ rect.right, rect.bottom }); + points.push_back(FloatPoint{ rect.left, rect.bottom }); + return points; + } + + const std::uint32_t segmentCount = ResolveArcSegmentCount(resolvedRadius); + points.reserve(static_cast(segmentCount + 1u) * 4u); + AppendArc( + points, + rect.left + resolvedRadius, + rect.top + resolvedRadius, + resolvedRadius, + std::numbers::pi_v, + std::numbers::pi_v * 1.5f, + segmentCount, + true); + AppendArc( + points, + rect.right - resolvedRadius, + rect.top + resolvedRadius, + resolvedRadius, + std::numbers::pi_v * 1.5f, + std::numbers::pi_v * 2.0f, + segmentCount, + false); + AppendArc( + points, + rect.right - resolvedRadius, + rect.bottom - resolvedRadius, + resolvedRadius, + 0.0f, + std::numbers::pi_v * 0.5f, + segmentCount, + false); + AppendArc( + points, + rect.left + resolvedRadius, + rect.bottom - resolvedRadius, + resolvedRadius, + std::numbers::pi_v * 0.5f, + std::numbers::pi_v, + segmentCount, + false); + return points; +} + +std::vector BuildCirclePolygon( + const FloatPoint& center, + float radius) { + std::vector points = {}; + if (radius <= 0.0f) { + return points; + } + + const std::uint32_t segmentCount = + static_cast(std::clamp(std::ceil(radius * 0.45f), 12.0f, 48.0f)); + points.reserve(segmentCount); + for (std::uint32_t index = 0u; index < segmentCount; ++index) { + const float angle = + static_cast(index) / static_cast(segmentCount) * + std::numbers::pi_v * 2.0f; + points.push_back(FloatPoint{ + center.x + std::cos(angle) * radius, + center.y + std::sin(angle) * radius + }); + } + return points; +} + +void AppendConvexFill( + std::vector& vertices, + std::vector& indices, + const std::vector& polygon, + const GradientContext* gradient, + const UIColor& fallbackColor) { + if (polygon.size() < 3u) { + return; + } + + FloatPoint center = {}; + for (const FloatPoint& point : polygon) { + center.x += point.x; + center.y += point.y; + } + center.x /= static_cast(polygon.size()); + center.y /= static_cast(polygon.size()); + + const UIColor centerColor = + gradient != nullptr + ? ResolveGradientColor(*gradient, center) + : fallbackColor; + const std::uint32_t centerIndex = AppendVertex( + vertices, + center, + UIPoint(0.5f, 0.5f), + centerColor); + + std::vector ringIndices = {}; + ringIndices.reserve(polygon.size()); + for (const FloatPoint& point : polygon) { + const UIColor vertexColor = + gradient != nullptr + ? ResolveGradientColor(*gradient, point) + : fallbackColor; + ringIndices.push_back(AppendVertex( + vertices, + point, + UIPoint(0.0f, 0.0f), + vertexColor)); + } + + for (std::size_t index = 0u; index < ringIndices.size(); ++index) { + const std::size_t nextIndex = (index + 1u) % ringIndices.size(); + indices.push_back(centerIndex); + indices.push_back(ringIndices[index]); + indices.push_back(ringIndices[nextIndex]); + } +} + +void AppendRing( + std::vector& vertices, + std::vector& indices, + const std::vector& outerPolygon, + const std::vector& innerPolygon, + const UIColor& color) { + if (outerPolygon.size() < 3u || + outerPolygon.size() != innerPolygon.size()) { + return; + } + + const std::size_t pointCount = outerPolygon.size(); + std::vector outerIndices = {}; + std::vector innerIndices = {}; + outerIndices.reserve(pointCount); + innerIndices.reserve(pointCount); + + for (std::size_t index = 0u; index < pointCount; ++index) { + outerIndices.push_back(AppendVertex( + vertices, + outerPolygon[index], + UIPoint(0.0f, 0.0f), + color)); + innerIndices.push_back(AppendVertex( + vertices, + innerPolygon[index], + UIPoint(0.0f, 0.0f), + color)); + } + + for (std::size_t index = 0u; index < pointCount; ++index) { + const std::size_t nextIndex = (index + 1u) % pointCount; + indices.push_back(outerIndices[index]); + indices.push_back(outerIndices[nextIndex]); + indices.push_back(innerIndices[nextIndex]); + + indices.push_back(outerIndices[index]); + indices.push_back(innerIndices[nextIndex]); + indices.push_back(innerIndices[index]); + } +} + +GraphicsPipelineDesc BuildBaseUiPipelineDesc( + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const RenderSurface& surface) { + GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = + static_cast(PrimitiveTopologyType::Triangle); + ::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + surface, + pipelineDesc); + + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(Format::R32G32_Float), 0, 0, 0, 0 }, + { "TEXCOORD", 0, static_cast(Format::R32G32_Float), 0, 8, 0, 0 }, + { "COLOR", 0, static_cast(Format::R32G32B32A32_Float), 0, 16, 0, 0 } + }; + + pipelineDesc.rasterizerState.fillMode = + static_cast(FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = + static_cast(CullMode::None); + pipelineDesc.rasterizerState.frontFace = + static_cast(FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + pipelineDesc.rasterizerState.scissorTestEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = + static_cast(BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = + static_cast(BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = + static_cast(BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = + static_cast(BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = + static_cast(BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = + static_cast(BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = + static_cast(ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = + static_cast(ComparisonFunc::Always); + return pipelineDesc; +} + +GraphicsPipelineDesc BuildLegacyUiPipelineDesc( + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const RenderSurface& surface) { + GraphicsPipelineDesc pipelineDesc = BuildBaseUiPipelineDesc(pipelineLayout, surface); + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(Format::R32G32_Float), 0, 0, 0, 0 }, + { "TEXCOORD", 0, static_cast(Format::R32G32_Float), 0, 8, 0, 0 }, + { "COLOR", 0, static_cast(Format::R32G32B32A32_Float), 0, 16, 0, 0 } + }; + + pipelineDesc.vertexShader.source.assign( + kLegacyUiPassHlsl, + kLegacyUiPassHlsl + std::strlen(kLegacyUiPassHlsl)); + pipelineDesc.vertexShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kLegacyUiPassHlsl, + kLegacyUiPassHlsl + std::strlen(kLegacyUiPassHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + return pipelineDesc; +} + +GraphicsPipelineDesc BuildQuadUiPipelineDesc( + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const RenderSurface& surface) { + GraphicsPipelineDesc pipelineDesc = BuildBaseUiPipelineDesc(pipelineLayout, surface); + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(Format::R32G32_Float), 0, 0, 0, 0 }, + { "TEXCOORD", 0, static_cast(Format::R32G32_Float), 0, 8, 0, 0 }, + { "POSITION", 1, static_cast(Format::R32G32_Float), 1, 0, 1, 1 }, + { "TEXCOORD", 1, static_cast(Format::R32G32_Float), 1, 8, 1, 1 }, + { "TEXCOORD", 2, static_cast(Format::R32G32_Float), 1, 16, 1, 1 }, + { "TEXCOORD", 3, static_cast(Format::R32G32_Float), 1, 24, 1, 1 }, + { "TEXCOORD", 4, static_cast(Format::R32G32_Float), 1, 32, 1, 1 }, + { "COLOR", 0, static_cast(Format::R32G32B32A32_Float), 1, 40, 1, 1 }, + { "COLOR", 1, static_cast(Format::R32G32B32A32_Float), 1, 56, 1, 1 }, + { "TEXCOORD", 5, static_cast(Format::R32G32B32A32_Float), 1, 72, 1, 1 } + }; + + pipelineDesc.vertexShader.source.assign( + kQuadUiPassHlsl, + kQuadUiPassHlsl + std::strlen(kQuadUiPassHlsl)); + pipelineDesc.vertexShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kQuadUiPassHlsl, + kQuadUiPassHlsl + std::strlen(kQuadUiPassHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + return pipelineDesc; +} + +} // namespace + +bool D3D12UiRenderer::GlyphAtlasKey::operator==(const GlyphAtlasKey& other) const { + return fontFaceKey == other.fontFaceKey && + glyphIndex == other.glyphIndex && + fontEmSizeTenths == other.fontEmSizeTenths && + isSideways == other.isSideways; +} + +std::size_t D3D12UiRenderer::GlyphAtlasKeyHash::operator()( + const GlyphAtlasKey& key) const { + const std::size_t faceHash = std::hash{}(key.fontFaceKey); + const std::size_t glyphHash = std::hash{}(key.glyphIndex); + const std::size_t fontHash = std::hash{}(key.fontEmSizeTenths); + const std::size_t sidewaysHash = std::hash{}(key.isSideways); + return faceHash ^ + (glyphHash << 1u) ^ + (fontHash << 2u) ^ + (sidewaysHash << 3u); +} + +bool D3D12UiRenderer::TextRunCacheKey::operator==(const TextRunCacheKey& other) const { + return text == other.text && + fontSizeTenths == other.fontSizeTenths && + dpiScaleMilli == other.dpiScaleMilli; +} + +std::size_t D3D12UiRenderer::TextRunCacheKeyHash::operator()( + const TextRunCacheKey& key) const { + const std::size_t textHash = std::hash{}(key.text); + const std::size_t fontHash = std::hash{}(key.fontSizeTenths); + const std::size_t dpiHash = std::hash{}(key.dpiScaleMilli); + return textHash ^ (fontHash << 1u) ^ (dpiHash << 2u); +} + +bool D3D12UiRenderer::CompiledDrawListKey::operator==( + const CompiledDrawListKey& other) const { + return contentHash == other.contentHash && + renderWidth == other.renderWidth && + renderHeight == other.renderHeight && + dpiScaleMilli == other.dpiScaleMilli; +} + +std::size_t D3D12UiRenderer::CompiledDrawListKeyHash::operator()( + const CompiledDrawListKey& key) const { + const std::size_t contentHashValue = std::hash{}(key.contentHash); + const std::size_t widthHash = std::hash{}(key.renderWidth); + const std::size_t heightHash = std::hash{}(key.renderHeight); + const std::size_t dpiHash = std::hash{}(key.dpiScaleMilli); + return contentHashValue ^ + (widthHash << 1u) ^ + (heightHash << 2u) ^ + (dpiHash << 3u); +} + +bool D3D12UiRenderer::Initialize( + D3D12WindowRenderer& windowRenderer, + D3D12UiTextureHost& textureHost, + D3D12UiTextSystem& textSystem) { + const float dpiScale = m_dpiScale; + Shutdown(); + + m_windowRenderer = &windowRenderer; + m_textureHost = &textureHost; + m_textSystem = &textSystem; + m_dpiScale = ClampDpiScale(dpiScale); + m_lastError.clear(); + return true; +} + +void D3D12UiRenderer::Shutdown() { + ReleaseTextRunCache(); + ReleaseGlyphAtlas(); + ReleaseCompiledDrawListCache(); + if (m_textureHost != nullptr) { + m_textureHost->ReleaseTexture(m_whiteTexture); + } else { + m_whiteTexture = {}; + } + + DestroyResources(); + m_windowRenderer = nullptr; + m_textureHost = nullptr; + m_textSystem = nullptr; + m_dpiScale = 1.0f; + m_lastError.clear(); +} + +void D3D12UiRenderer::SetDpiScale(float dpiScale) { + const float resolvedScale = ClampDpiScale(dpiScale); + if (std::abs(m_dpiScale - resolvedScale) > 0.0001f) { + ReleaseTextRunCache(); + ReleaseGlyphAtlas(); + ReleaseCompiledDrawListCache(); + } + m_dpiScale = resolvedScale; +} + +float D3D12UiRenderer::GetDpiScale() const { + return m_dpiScale; +} + +const std::string& D3D12UiRenderer::GetLastError() const { + return m_lastError; +} + +bool D3D12UiRenderer::EnsureWhiteTexture() { + if (m_whiteTexture.IsValid()) { + return true; + } + + if (m_textureHost == nullptr) { + m_lastError = "EnsureWhiteTexture requires an initialized texture host."; + return false; + } + + const std::uint8_t whitePixel[4] = { 255u, 255u, 255u, 255u }; + std::string error = {}; + if (!m_textureHost->LoadTextureFromRgba( + whitePixel, + 1u, + 1u, + m_whiteTexture, + error)) { + m_lastError = error.empty() + ? "Failed to create the UI white texture." + : error; + return false; + } + + return true; +} + +bool D3D12UiRenderer::EnsureGlyphAtlasPage() { + if (m_textureHost == nullptr) { + m_lastError = "EnsureGlyphAtlasPage requires an initialized texture host."; + return false; + } + + if (!m_glyphAtlasPages.empty()) { + return true; + } + + GlyphAtlasPage page = {}; + page.width = kGlyphAtlasPageWidth; + page.height = kGlyphAtlasPageHeight; + + std::vector initialPixels( + static_cast(page.width) * static_cast(page.height) * 4u, + 0u); + std::string error = {}; + if (!m_textureHost->LoadTextureFromRgba( + initialPixels.data(), + page.width, + page.height, + page.texture, + error)) { + m_lastError = error.empty() + ? "Failed to create the glyph atlas page texture." + : error; + return false; + } + + m_glyphAtlasPages.push_back(std::move(page)); + return true; +} + +bool D3D12UiRenderer::AllocateGlyphAtlasRegion( + std::uint32_t width, + std::uint32_t height, + std::size_t& outPageIndex, + std::uint32_t& outPageX, + std::uint32_t& outPageY) { + outPageIndex = 0u; + outPageX = 0u; + outPageY = 0u; + + if (width == 0u || height == 0u) { + return false; + } + + if (!EnsureGlyphAtlasPage()) { + return false; + } + + const std::uint32_t paddedWidth = width + kGlyphAtlasPadding * 2u; + const std::uint32_t paddedHeight = height + kGlyphAtlasPadding * 2u; + if (paddedWidth > kGlyphAtlasPageWidth || paddedHeight > kGlyphAtlasPageHeight) { + m_lastError = "Glyph atlas entry exceeds the atlas page size."; + return false; + } + + auto tryAllocateInPage = + [&](GlyphAtlasPage& page, std::size_t pageIndex) -> bool { + if (page.cursorX + paddedWidth > page.width) { + page.cursorX = 0u; + page.cursorY += page.rowHeight; + page.rowHeight = 0u; + } + + if (page.cursorY + paddedHeight > page.height) { + return false; + } + + outPageIndex = pageIndex; + outPageX = page.cursorX + kGlyphAtlasPadding; + outPageY = page.cursorY + kGlyphAtlasPadding; + page.cursorX += paddedWidth; + page.rowHeight = (std::max)(page.rowHeight, paddedHeight); + return true; + }; + + for (std::size_t pageIndex = 0u; pageIndex < m_glyphAtlasPages.size(); ++pageIndex) { + if (tryAllocateInPage(m_glyphAtlasPages[pageIndex], pageIndex)) { + return true; + } + } + + GlyphAtlasPage newPage = {}; + newPage.width = kGlyphAtlasPageWidth; + newPage.height = kGlyphAtlasPageHeight; + + std::vector initialPixels( + static_cast(newPage.width) * static_cast(newPage.height) * 4u, + 0u); + std::string error = {}; + if (!m_textureHost->LoadTextureFromRgba( + initialPixels.data(), + newPage.width, + newPage.height, + newPage.texture, + error)) { + m_lastError = error.empty() + ? "Failed to create an additional glyph atlas page." + : error; + return false; + } + + m_glyphAtlasPages.push_back(std::move(newPage)); + return tryAllocateInPage(m_glyphAtlasPages.back(), m_glyphAtlasPages.size() - 1u); +} + +const D3D12UiRenderer::GlyphAtlasEntry* D3D12UiRenderer::ResolveGlyphAtlasEntry( + const D3D12UiTextSystem::ShapedGlyph& glyph) { + if (m_textureHost == nullptr || m_textSystem == nullptr || glyph.fontFace == nullptr) { + return nullptr; + } + + GlyphAtlasKey key = {}; + key.fontFaceKey = reinterpret_cast(glyph.fontFace.Get()); + key.glyphIndex = glyph.glyphIndex; + key.fontEmSizeTenths = static_cast(std::lround(glyph.fontEmSize * 10.0f)); + key.isSideways = glyph.isSideways; + + const auto found = m_glyphAtlas.find(key); + if (found != m_glyphAtlas.end()) { + return &found->second; + } + + D3D12UiTextSystem::RasterizedGlyph rasterizedGlyph = {}; + std::string error = {}; + if (!m_textSystem->RasterizeGlyph(glyph, rasterizedGlyph, error)) { + m_lastError = error.empty() + ? "Failed to rasterize the glyph atlas entry." + : error; + return nullptr; + } + + GlyphAtlasEntry entry = {}; + entry.boundsLeft = rasterizedGlyph.boundsLeft; + entry.boundsTop = rasterizedGlyph.boundsTop; + entry.boundsRight = rasterizedGlyph.boundsRight; + entry.boundsBottom = rasterizedGlyph.boundsBottom; + entry.width = rasterizedGlyph.width; + entry.height = rasterizedGlyph.height; + entry.hasPixels = + !rasterizedGlyph.rgbaPixels.empty() && + rasterizedGlyph.width > 0u && + rasterizedGlyph.height > 0u; + + if (entry.hasPixels) { + std::size_t pageIndex = 0u; + std::uint32_t pageX = 0u; + std::uint32_t pageY = 0u; + if (!AllocateGlyphAtlasRegion( + rasterizedGlyph.width, + rasterizedGlyph.height, + pageIndex, + pageX, + pageY)) { + return nullptr; + } + + GlyphAtlasPage& page = m_glyphAtlasPages[pageIndex]; + if (!page.texture.IsValid()) { + m_lastError = "Glyph atlas page texture is invalid."; + return nullptr; + } + + if (!m_textureHost->UpdateTextureRegionRgba( + page.texture, + pageX, + pageY, + rasterizedGlyph.rgbaPixels.data(), + rasterizedGlyph.width, + rasterizedGlyph.height, + rasterizedGlyph.width * 4u, + error)) { + m_lastError = error.empty() + ? "Failed to upload the glyph atlas region." + : error; + return nullptr; + } + + entry.pageIndex = pageIndex; + entry.pageX = pageX; + entry.pageY = pageY; + entry.textureHandle.ptr = static_cast(page.texture.nativeHandle); + } + + const auto [insertedIt, inserted] = m_glyphAtlas.emplace(std::move(key), std::move(entry)); + (void)inserted; + return &insertedIt->second; +} + +const D3D12UiRenderer::CachedTextRun* D3D12UiRenderer::ResolveTextRun( + std::string_view text, + float fontSize) { + if (m_textureHost == nullptr || m_textSystem == nullptr) { + return nullptr; + } + + TextRunCacheKey cacheKey = {}; + cacheKey.text.assign(text); + cacheKey.fontSizeTenths = + static_cast(std::lround(ResolveFontSize(fontSize) * 10.0f)); + cacheKey.dpiScaleMilli = + static_cast(std::lround(ClampDpiScale(m_dpiScale) * 1000.0f)); + + const auto found = m_textRunCache.find(cacheKey); + if (found != m_textRunCache.end()) { + return &found->second; + } + + std::string error = {}; + D3D12UiTextSystem::ShapedTextRun shapedText = {}; + if (!m_textSystem->ShapeTextRun(text, fontSize, shapedText, error)) { + m_lastError = error.empty() + ? "Failed to shape the UI text run." + : error; + return nullptr; + } + + CachedTextRun cachedText = {}; + cachedText.glyphs.reserve(shapedText.glyphs.size()); + for (const D3D12UiTextSystem::ShapedGlyph& glyph : shapedText.glyphs) { + const GlyphAtlasEntry* atlasEntry = ResolveGlyphAtlasEntry(glyph); + if (atlasEntry == nullptr) { + return nullptr; + } + + if (!atlasEntry->hasPixels || atlasEntry->textureHandle.ptr == 0u) { + continue; + } + + const GlyphAtlasPage& page = m_glyphAtlasPages[atlasEntry->pageIndex]; + if (!page.texture.IsValid() || page.width == 0u || page.height == 0u) { + m_lastError = "Glyph atlas page metadata is invalid."; + return nullptr; + } + + GlyphQuadPlan glyphQuad = {}; + glyphQuad.left = glyph.originX + static_cast(atlasEntry->boundsLeft); + glyphQuad.top = glyph.originY + static_cast(atlasEntry->boundsTop); + glyphQuad.right = glyph.originX + static_cast(atlasEntry->boundsRight); + glyphQuad.bottom = glyph.originY + static_cast(atlasEntry->boundsBottom); + glyphQuad.uvMinX = static_cast(atlasEntry->pageX) / static_cast(page.width); + glyphQuad.uvMinY = static_cast(atlasEntry->pageY) / static_cast(page.height); + glyphQuad.uvMaxX = + static_cast(atlasEntry->pageX + atlasEntry->width) / + static_cast(page.width); + glyphQuad.uvMaxY = + static_cast(atlasEntry->pageY + atlasEntry->height) / + static_cast(page.height); + glyphQuad.textureHandle = atlasEntry->textureHandle; + cachedText.glyphs.push_back(glyphQuad); + } + + const auto [insertedIt, inserted] = + m_textRunCache.emplace(std::move(cacheKey), std::move(cachedText)); + (void)inserted; + return &insertedIt->second; +} + +void D3D12UiRenderer::ReleaseGlyphAtlas() { + if (m_textureHost != nullptr) { + for (GlyphAtlasPage& page : m_glyphAtlasPages) { + m_textureHost->ReleaseTexture(page.texture); + } + } + + m_glyphAtlasPages.clear(); + m_glyphAtlas.clear(); +} + +void D3D12UiRenderer::ReleaseTextRunCache() { + m_textRunCache.clear(); +} + +void D3D12UiRenderer::ReleaseCompiledDrawListCache() { + m_compiledDrawListCache.clear(); + m_compiledDrawListFrameCounter = 0u; +} + +bool D3D12UiRenderer::EnsureInitialized( + const RenderContext& renderContext, + const RenderSurface& surface) { + const Format renderTargetFormat = + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); + const std::uint32_t sampleCount = + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); + const std::uint32_t sampleQuality = + ::XCEngine::Rendering::ResolveSurfaceSampleQuality(surface); + + if (m_device == renderContext.device && + m_backendType == renderContext.backendType && + m_pipelineLayout != nullptr && + m_legacyPipelineState != nullptr && + m_quadPipelineState != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_samplerPool != nullptr && + m_samplerSet != nullptr && + m_sampler != nullptr && + m_quadVertexBuffer != nullptr && + m_quadVertexBufferView != nullptr && + m_quadIndexBuffer != nullptr && + m_quadIndexBufferView != nullptr && + m_renderTargetFormat == renderTargetFormat && + m_renderTargetSampleCount == sampleCount && + m_renderTargetSampleQuality == sampleQuality) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext, surface); +} + +bool D3D12UiRenderer::CreateResources( + const RenderContext& renderContext, + const RenderSurface& surface) { + if (!renderContext.IsValid() || + renderContext.device == nullptr || + !::XCEngine::Rendering::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u) == Format::Unknown) { + m_lastError = "CreateResources requires a valid single-color render surface."; + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + m_renderTargetFormat = + ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); + m_renderTargetSampleCount = + ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); + m_renderTargetSampleQuality = + ::XCEngine::Rendering::ResolveSurfaceSampleQuality(surface); + + DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0u; + constantBinding.type = static_cast(DescriptorType::CBV); + constantBinding.count = 1u; + constantBinding.visibility = static_cast(ShaderVisibility::All); + + DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0u; + textureBinding.type = static_cast(DescriptorType::SRV); + textureBinding.count = 1u; + textureBinding.visibility = static_cast(ShaderVisibility::All); + + DescriptorSetLayoutBinding samplerBinding = {}; + samplerBinding.binding = 0u; + samplerBinding.type = static_cast(DescriptorType::Sampler); + samplerBinding.count = 1u; + samplerBinding.visibility = static_cast(ShaderVisibility::All); + + DescriptorSetLayoutDesc setLayouts[3] = {}; + setLayouts[0].bindings = &constantBinding; + setLayouts[0].bindingCount = 1u; + setLayouts[1].bindings = &textureBinding; + setLayouts[1].bindingCount = 1u; + setLayouts[2].bindings = &samplerBinding; + setLayouts[2].bindingCount = 1u; + + RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = 3u; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + m_lastError = "Failed to create the D3D12 UI pipeline layout."; + DestroyResources(); + return false; + } + + DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1u; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + m_lastError = "Failed to create the D3D12 UI constant descriptor pool."; + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(setLayouts[0]); + if (m_constantSet == nullptr) { + m_lastError = "Failed to allocate the D3D12 UI constant descriptor set."; + DestroyResources(); + return false; + } + + SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(FilterMode::Linear); + samplerDesc.addressU = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(TextureAddressMode::Clamp); + samplerDesc.maxAnisotropy = 1u; + samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 16.0f; + m_sampler = m_device->CreateSampler(samplerDesc); + if (m_sampler == nullptr) { + m_lastError = "Failed to create the D3D12 UI sampler."; + DestroyResources(); + return false; + } + + DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1u; + samplerPoolDesc.shaderVisible = true; + m_samplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); + if (m_samplerPool == nullptr) { + m_lastError = "Failed to create the D3D12 UI sampler descriptor pool."; + DestroyResources(); + return false; + } + + m_samplerSet = m_samplerPool->AllocateSet(setLayouts[2]); + if (m_samplerSet == nullptr) { + m_lastError = "Failed to allocate the D3D12 UI sampler descriptor set."; + DestroyResources(); + return false; + } + + m_samplerSet->UpdateSampler(0u, m_sampler); + auto* samplerDescriptorSet = static_cast(m_samplerSet); + m_samplerGpuHandle = samplerDescriptorSet->GetGPUHandleForBinding(0u); + if (m_samplerGpuHandle.ptr == 0u) { + m_lastError = "Failed to resolve the D3D12 UI sampler GPU descriptor handle."; + DestroyResources(); + return false; + } + + m_legacyPipelineState = m_device->CreatePipelineState( + BuildLegacyUiPipelineDesc(m_pipelineLayout, surface)); + if (m_legacyPipelineState == nullptr || !m_legacyPipelineState->IsValid()) { + m_lastError = "Failed to create the D3D12 UI legacy pipeline state."; + DestroyResources(); + return false; + } + + m_quadPipelineState = m_device->CreatePipelineState( + BuildQuadUiPipelineDesc(m_pipelineLayout, surface)); + if (m_quadPipelineState == nullptr || !m_quadPipelineState->IsValid()) { + m_lastError = "Failed to create the D3D12 UI quad pipeline state."; + DestroyResources(); + return false; + } + + BufferDesc quadVertexBufferDesc = {}; + quadVertexBufferDesc.size = sizeof(kUnitQuadVertices); + quadVertexBufferDesc.stride = static_cast(sizeof(UiQuadVertex)); + quadVertexBufferDesc.bufferType = static_cast(BufferType::Vertex); + m_quadVertexBuffer = + m_device->CreateBuffer(quadVertexBufferDesc, kUnitQuadVertices, sizeof(kUnitQuadVertices)); + if (m_quadVertexBuffer == nullptr) { + m_lastError = "Failed to create the D3D12 UI unit-quad vertex buffer."; + DestroyResources(); + return false; + } + m_quadVertexBuffer->SetStride(quadVertexBufferDesc.stride); + m_quadVertexBuffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc quadVertexViewDesc = {}; + quadVertexViewDesc.dimension = ResourceViewDimension::Buffer; + quadVertexViewDesc.structureByteStride = quadVertexBufferDesc.stride; + m_quadVertexBufferView = + m_device->CreateVertexBufferView(m_quadVertexBuffer, quadVertexViewDesc); + if (m_quadVertexBufferView == nullptr) { + m_lastError = "Failed to create the D3D12 UI unit-quad vertex view."; + DestroyResources(); + return false; + } + + BufferDesc quadIndexBufferDesc = {}; + quadIndexBufferDesc.size = sizeof(kUnitQuadIndices); + quadIndexBufferDesc.stride = static_cast(sizeof(std::uint32_t)); + quadIndexBufferDesc.bufferType = static_cast(BufferType::Index); + m_quadIndexBuffer = + m_device->CreateBuffer(quadIndexBufferDesc, kUnitQuadIndices, sizeof(kUnitQuadIndices)); + if (m_quadIndexBuffer == nullptr) { + m_lastError = "Failed to create the D3D12 UI unit-quad index buffer."; + DestroyResources(); + return false; + } + m_quadIndexBuffer->SetStride(quadIndexBufferDesc.stride); + m_quadIndexBuffer->SetBufferType(BufferType::Index); + + ResourceViewDesc quadIndexViewDesc = {}; + quadIndexViewDesc.dimension = ResourceViewDimension::Buffer; + quadIndexViewDesc.format = static_cast(Format::R32_UInt); + m_quadIndexBufferView = + m_device->CreateIndexBufferView(m_quadIndexBuffer, quadIndexViewDesc); + if (m_quadIndexBufferView == nullptr) { + m_lastError = "Failed to create the D3D12 UI unit-quad index view."; + DestroyResources(); + return false; + } + + return true; +} + +void D3D12UiRenderer::DestroyFrameResources(FrameResources& frameResources) { + if (frameResources.legacyVertexBufferView != nullptr) { + frameResources.legacyVertexBufferView->Shutdown(); + delete frameResources.legacyVertexBufferView; + frameResources.legacyVertexBufferView = nullptr; + } + if (frameResources.legacyVertexBuffer != nullptr) { + frameResources.legacyVertexBuffer->Shutdown(); + delete frameResources.legacyVertexBuffer; + frameResources.legacyVertexBuffer = nullptr; + } + if (frameResources.legacyIndexBufferView != nullptr) { + frameResources.legacyIndexBufferView->Shutdown(); + delete frameResources.legacyIndexBufferView; + frameResources.legacyIndexBufferView = nullptr; + } + if (frameResources.legacyIndexBuffer != nullptr) { + frameResources.legacyIndexBuffer->Shutdown(); + delete frameResources.legacyIndexBuffer; + frameResources.legacyIndexBuffer = nullptr; + } + if (frameResources.quadInstanceBufferView != nullptr) { + frameResources.quadInstanceBufferView->Shutdown(); + delete frameResources.quadInstanceBufferView; + frameResources.quadInstanceBufferView = nullptr; + } + if (frameResources.quadInstanceBuffer != nullptr) { + frameResources.quadInstanceBuffer->Shutdown(); + delete frameResources.quadInstanceBuffer; + frameResources.quadInstanceBuffer = nullptr; + } + frameResources.legacyVertexCapacityBytes = 0u; + frameResources.legacyIndexCapacityBytes = 0u; + frameResources.quadInstanceCapacityBytes = 0u; +} + +void D3D12UiRenderer::DestroyResources() { + for (FrameResources& frameResources : m_frameResources) { + DestroyFrameResources(frameResources); + } + + if (m_quadIndexBufferView != nullptr) { + m_quadIndexBufferView->Shutdown(); + delete m_quadIndexBufferView; + m_quadIndexBufferView = nullptr; + } + if (m_quadIndexBuffer != nullptr) { + m_quadIndexBuffer->Shutdown(); + delete m_quadIndexBuffer; + m_quadIndexBuffer = nullptr; + } + if (m_quadVertexBufferView != nullptr) { + m_quadVertexBufferView->Shutdown(); + delete m_quadVertexBufferView; + m_quadVertexBufferView = nullptr; + } + if (m_quadVertexBuffer != nullptr) { + m_quadVertexBuffer->Shutdown(); + delete m_quadVertexBuffer; + m_quadVertexBuffer = nullptr; + } + if (m_quadPipelineState != nullptr) { + m_quadPipelineState->Shutdown(); + delete m_quadPipelineState; + m_quadPipelineState = nullptr; + } + if (m_legacyPipelineState != nullptr) { + m_legacyPipelineState->Shutdown(); + delete m_legacyPipelineState; + m_legacyPipelineState = nullptr; + } + if (m_samplerSet != nullptr) { + m_samplerSet->Shutdown(); + delete m_samplerSet; + m_samplerSet = nullptr; + } + if (m_samplerPool != nullptr) { + m_samplerPool->Shutdown(); + delete m_samplerPool; + m_samplerPool = nullptr; + } + if (m_sampler != nullptr) { + m_sampler->Shutdown(); + delete m_sampler; + m_sampler = nullptr; + } + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_samplerGpuHandle = {}; + m_device = nullptr; + m_backendType = ::XCEngine::RHI::RHIType::D3D12; + m_renderTargetFormat = Format::Unknown; + m_renderTargetSampleCount = 1u; + m_renderTargetSampleQuality = 0u; +} + +bool D3D12UiRenderer::EnsureFrameBufferCapacity( + std::uint32_t frameSlot, + std::size_t legacyVertexBytes, + std::size_t legacyIndexBytes, + std::size_t quadInstanceBytes) { + if (m_device == nullptr || frameSlot >= m_frameResources.size()) { + return false; + } + + FrameResources& frameResources = m_frameResources[frameSlot]; + auto ensureVertexBuffer = + [&](std::size_t requiredBytes, + std::uint64_t minimumBytes, + std::uint32_t stride, + ::XCEngine::RHI::RHIBuffer*& buffer, + ::XCEngine::RHI::RHIResourceView*& bufferView, + std::uint64_t& capacityBytes) -> bool { + if (requiredBytes == 0u) { + return true; + } + + const std::uint64_t resolvedRequiredBytes = + (std::max)(static_cast(requiredBytes), minimumBytes); + if (buffer != nullptr && capacityBytes >= resolvedRequiredBytes) { + return true; + } + + if (bufferView != nullptr) { + bufferView->Shutdown(); + delete bufferView; + bufferView = nullptr; + } + if (buffer != nullptr) { + buffer->Shutdown(); + delete buffer; + buffer = nullptr; + } + + capacityBytes = resolvedRequiredBytes; + BufferDesc bufferDesc = {}; + bufferDesc.size = capacityBytes; + bufferDesc.stride = stride; + bufferDesc.bufferType = static_cast(BufferType::Vertex); + buffer = m_device->CreateBuffer(bufferDesc); + if (buffer == nullptr) { + capacityBytes = 0u; + return false; + } + buffer->SetStride(bufferDesc.stride); + buffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc viewDesc = {}; + viewDesc.dimension = ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + bufferView = m_device->CreateVertexBufferView(buffer, viewDesc); + if (bufferView == nullptr) { + buffer->Shutdown(); + delete buffer; + buffer = nullptr; + capacityBytes = 0u; + return false; + } + + return true; + }; + + auto ensureIndexBuffer = + [&](std::size_t requiredBytes, + std::uint64_t minimumBytes, + ::XCEngine::RHI::RHIBuffer*& buffer, + ::XCEngine::RHI::RHIResourceView*& bufferView, + std::uint64_t& capacityBytes) -> bool { + if (requiredBytes == 0u) { + return true; + } + + const std::uint64_t resolvedRequiredBytes = + (std::max)(static_cast(requiredBytes), minimumBytes); + if (buffer != nullptr && capacityBytes >= resolvedRequiredBytes) { + return true; + } + + if (bufferView != nullptr) { + bufferView->Shutdown(); + delete bufferView; + bufferView = nullptr; + } + if (buffer != nullptr) { + buffer->Shutdown(); + delete buffer; + buffer = nullptr; + } + + capacityBytes = resolvedRequiredBytes; + BufferDesc bufferDesc = {}; + bufferDesc.size = capacityBytes; + bufferDesc.stride = static_cast(sizeof(std::uint32_t)); + bufferDesc.bufferType = static_cast(BufferType::Index); + buffer = m_device->CreateBuffer(bufferDesc); + if (buffer == nullptr) { + capacityBytes = 0u; + return false; + } + buffer->SetStride(bufferDesc.stride); + buffer->SetBufferType(BufferType::Index); + + ResourceViewDesc viewDesc = {}; + viewDesc.dimension = ResourceViewDimension::Buffer; + viewDesc.format = static_cast(Format::R32_UInt); + bufferView = m_device->CreateIndexBufferView(buffer, viewDesc); + if (bufferView == nullptr) { + buffer->Shutdown(); + delete buffer; + buffer = nullptr; + capacityBytes = 0u; + return false; + } + + return true; + }; + + return ensureVertexBuffer( + legacyVertexBytes, + kMinDynamicVertexBufferBytes, + static_cast(sizeof(UiVertex)), + frameResources.legacyVertexBuffer, + frameResources.legacyVertexBufferView, + frameResources.legacyVertexCapacityBytes) && + ensureIndexBuffer( + legacyIndexBytes, + kMinDynamicIndexBufferBytes, + frameResources.legacyIndexBuffer, + frameResources.legacyIndexBufferView, + frameResources.legacyIndexCapacityBytes) && + ensureVertexBuffer( + quadInstanceBytes, + kMinDynamicInstanceBufferBytes, + static_cast(sizeof(UiQuadInstance)), + frameResources.quadInstanceBuffer, + frameResources.quadInstanceBufferView, + frameResources.quadInstanceCapacityBytes); +} + +bool D3D12UiRenderer::BuildDrawBatches( + const UIDrawData& drawData, + const RenderSurface& surface, + std::vector& outVertices, + std::vector& outIndices, + std::vector& outQuadInstances, + std::vector& outBatches) { + outVertices.clear(); + outIndices.clear(); + outQuadInstances.clear(); + outBatches.clear(); + + if (!EnsureWhiteTexture()) { + return false; + } + + const auto renderArea = surface.GetRenderArea(); + const Rect fullScissor = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + const FloatRect fullRect = { + static_cast(fullScissor.left), + static_cast(fullScissor.top), + static_cast(fullScissor.right), + static_cast(fullScissor.bottom) + }; + + const float dpiScale = ClampDpiScale(m_dpiScale); + const D3D12_GPU_DESCRIPTOR_HANDLE whiteTextureHandle = { + static_cast(m_whiteTexture.nativeHandle) + }; + + auto buildCompiledDrawList = + [&](const UIDrawList& drawList, + std::vector& compiledVertices, + std::vector& compiledIndices, + std::vector& compiledQuadInstances, + std::vector& compiledBatches) { + compiledVertices.clear(); + compiledIndices.clear(); + compiledQuadInstances.clear(); + compiledBatches.clear(); + + std::vector localClipStack = {}; + localClipStack.push_back({ fullScissor, fullRect }); + + auto appendAxisAlignedQuad = + [&](const FloatRect& rect, + const UIPoint& uvMin, + const UIPoint& uvMax, + const UIColor& color, + const UIColor& secondaryColor, + UiQuadKind quadKind, + float radius, + float thickness, + float gradientDirection, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + if (IsEmptyRect(rect) || textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { + return; + } + + AppendQuadInstance( + compiledQuadInstances, + compiledBatches, + FloatPoint{ rect.left, rect.top }, + FloatPoint{ rect.right - rect.left, 0.0f }, + FloatPoint{ 0.0f, rect.bottom - rect.top }, + uvMin, + uvMax, + color, + secondaryColor, + quadKind, + radius, + thickness, + gradientDirection, + textureHandle, + scissor); + }; + + auto appendOrientedQuad = + [&](const FloatPoint& origin, + const FloatPoint& axisX, + const FloatPoint& axisY, + const UIColor& color, + UiQuadKind quadKind, + float radius, + float thickness, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const Rect& scissor) { + if (textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { + return; + } + + AppendQuadInstance( + compiledQuadInstances, + compiledBatches, + origin, + axisX, + axisY, + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + color, + color, + quadKind, + radius, + thickness, + 0.0f, + textureHandle, + scissor); + }; + + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (localClipStack.empty()) { + localClipStack.push_back({ fullScissor, fullRect }); + } + + if (command.type == UIDrawCommandType::PushClipRect) { + FloatRect clipRect = ToPixelRect(command.rect, dpiScale); + clipRect = command.intersectWithCurrentClip + ? IntersectFloatRect(localClipStack.back().rect, clipRect) + : IntersectFloatRect(fullRect, clipRect); + ClipState clipState = {}; + clipState.rect = clipRect; + clipState.scissor = ClampScissor(clipRect, fullScissor); + localClipStack.push_back(clipState); + continue; + } + + if (command.type == UIDrawCommandType::PopClipRect) { + if (localClipStack.size() > 1u) { + localClipStack.pop_back(); + } + continue; + } + + const ClipState currentClip = localClipStack.back(); + if (IsEmptyScissor(currentClip.scissor)) { + continue; + } + + switch (command.type) { + case UIDrawCommandType::FilledRect: { + appendAxisAlignedQuad( + ToPixelRect(command.rect, dpiScale), + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + command.color, + command.color, + UiQuadKind::RectFill, + command.rounding * dpiScale, + 0.0f, + 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::FilledRectLinearGradient: { + appendAxisAlignedQuad( + ToPixelRect(command.rect, dpiScale), + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + command.color, + command.secondaryColor, + UiQuadKind::RectFill, + command.rounding * dpiScale, + 0.0f, + command.gradientDirection == UILinearGradientDirection::Vertical + ? 1.0f + : 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::RectOutline: { + const FloatRect rect = ToPixelRect(command.rect, dpiScale); + const float thickness = + (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float halfThickness = thickness * 0.5f; + appendAxisAlignedQuad( + FloatRect{ + rect.left - halfThickness, + rect.top - halfThickness, + rect.right + halfThickness, + rect.bottom + halfThickness + }, + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + command.color, + command.color, + UiQuadKind::RectOutline, + command.rounding * dpiScale + halfThickness, + thickness, + 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::Line: { + const FloatPoint start = ToPixelPoint(command.position, dpiScale); + const FloatPoint end = ToPixelPoint(command.uvMin, dpiScale); + const float dx = end.x - start.x; + const float dy = end.y - start.y; + const float length = std::sqrt(dx * dx + dy * dy); + if (length <= 0.0f) { + break; + } + + const float thickness = + (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float halfThickness = thickness * 0.5f; + const float nx = -dy / length; + const float ny = dx / length; + appendOrientedQuad( + FloatPoint{ start.x + nx * halfThickness, start.y + ny * halfThickness }, + FloatPoint{ dx, dy }, + FloatPoint{ -nx * thickness, -ny * thickness }, + command.color, + UiQuadKind::RectFill, + 0.0f, + 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::FilledTriangle: { + const std::uint32_t firstIndex = + static_cast(compiledIndices.size()); + const std::uint32_t baseVertex = + static_cast(compiledVertices.size()); + AppendVertex( + compiledVertices, + ToPixelPoint(command.position, dpiScale), + UIPoint(0.0f, 0.0f), + command.color); + AppendVertex( + compiledVertices, + ToPixelPoint(command.uvMin, dpiScale), + UIPoint(1.0f, 0.0f), + command.color); + AppendVertex( + compiledVertices, + ToPixelPoint(command.uvMax, dpiScale), + UIPoint(0.5f, 1.0f), + command.color); + compiledIndices.push_back(baseVertex + 0u); + compiledIndices.push_back(baseVertex + 1u); + compiledIndices.push_back(baseVertex + 2u); + AppendBatch( + compiledBatches, + firstIndex, + 3u, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::FilledCircle: { + const FloatPoint center = ToPixelPoint(command.position, dpiScale); + const float radius = command.radius * dpiScale; + if (radius <= 0.0f) { + break; + } + + appendAxisAlignedQuad( + FloatRect{ + center.x - radius, + center.y - radius, + center.x + radius, + center.y + radius + }, + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + command.color, + command.color, + UiQuadKind::CircleFill, + radius, + 0.0f, + 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::CircleOutline: { + const FloatPoint center = ToPixelPoint(command.position, dpiScale); + const float thickness = + (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float radius = command.radius * dpiScale; + const float outerRadius = radius + thickness * 0.5f; + if (outerRadius <= 0.0f) { + break; + } + + appendAxisAlignedQuad( + FloatRect{ + center.x - outerRadius, + center.y - outerRadius, + center.x + outerRadius, + center.y + outerRadius + }, + UIPoint(0.0f, 0.0f), + UIPoint(1.0f, 1.0f), + command.color, + command.color, + UiQuadKind::CircleOutline, + radius, + thickness, + 0.0f, + whiteTextureHandle, + currentClip.scissor); + break; + } + case UIDrawCommandType::Text: { + if (command.text.empty()) { + break; + } + + const CachedTextRun* textRun = + ResolveTextRun(command.text, command.fontSize); + if (textRun == nullptr || textRun->glyphs.empty()) { + break; + } + + const FloatPoint position = ToPixelPoint(command.position, dpiScale); + for (const GlyphQuadPlan& glyphQuad : textRun->glyphs) { + appendAxisAlignedQuad( + FloatRect{ + position.x + glyphQuad.left, + position.y + glyphQuad.top, + position.x + glyphQuad.right, + position.y + glyphQuad.bottom + }, + UIPoint(glyphQuad.uvMinX, glyphQuad.uvMinY), + UIPoint(glyphQuad.uvMaxX, glyphQuad.uvMaxY), + command.color, + command.color, + UiQuadKind::Textured, + 0.0f, + 0.0f, + 0.0f, + glyphQuad.textureHandle, + currentClip.scissor); + } + break; + } + case UIDrawCommandType::Image: { + if (!command.texture.IsValid()) { + break; + } + + appendAxisAlignedQuad( + ToPixelRect(command.rect, dpiScale), + command.uvMin, + command.uvMax, + command.color, + command.color, + UiQuadKind::Textured, + 0.0f, + 0.0f, + 0.0f, + D3D12_GPU_DESCRIPTOR_HANDLE{ + static_cast(command.texture.nativeHandle) + }, + currentClip.scissor); + break; + } + default: + break; + } + } + + return true; + }; + + auto appendCompiledDrawList = + [&](const CompiledDrawList& compiled) { + if (compiled.batches.empty()) { + return; + } + + const std::uint32_t baseVertex = + static_cast(outVertices.size()); + const std::uint32_t baseIndex = + static_cast(outIndices.size()); + const std::uint32_t baseInstance = + static_cast(outQuadInstances.size()); + + if (!compiled.vertices.empty()) { + outVertices.insert( + outVertices.end(), + compiled.vertices.begin(), + compiled.vertices.end()); + } + + if (!compiled.indices.empty()) { + outIndices.reserve(outIndices.size() + compiled.indices.size()); + for (std::uint32_t index : compiled.indices) { + outIndices.push_back(baseVertex + index); + } + } + + if (!compiled.quadInstances.empty()) { + outQuadInstances.insert( + outQuadInstances.end(), + compiled.quadInstances.begin(), + compiled.quadInstances.end()); + } + + for (const UiBatch& batch : compiled.batches) { + if (batch.kind == UiBatchKind::QuadInstanced) { + AppendQuadBatch( + outBatches, + baseInstance + batch.firstElement, + batch.elementCount, + batch.textureHandle, + batch.scissorRect); + } else { + AppendBatch( + outBatches, + baseIndex + batch.firstElement, + batch.elementCount, + batch.textureHandle, + batch.scissorRect); + } + } + }; + + const std::uint64_t currentFrameId = ++m_compiledDrawListFrameCounter; + const std::uint32_t renderWidth = renderArea.width > 0 ? renderArea.width : 0u; + const std::uint32_t renderHeight = renderArea.height > 0 ? renderArea.height : 0u; + const int dpiScaleMilli = + static_cast(std::lround(dpiScale * 1000.0f)); + + for (const UIDrawList& drawList : drawData.GetDrawLists()) { + if (drawList.Empty()) { + continue; + } + + CompiledDrawListKey key = {}; + key.contentHash = BuildDrawListContentHash(drawList); + key.renderWidth = renderWidth; + key.renderHeight = renderHeight; + key.dpiScaleMilli = dpiScaleMilli; + + auto found = m_compiledDrawListCache.find(key); + if (found == m_compiledDrawListCache.end()) { + CompiledDrawList compiled = {}; + if (!buildCompiledDrawList( + drawList, + compiled.vertices, + compiled.indices, + compiled.quadInstances, + compiled.batches)) { + return false; + } + + compiled.lastUsedFrame = currentFrameId; + const auto [insertedIt, inserted] = + m_compiledDrawListCache.emplace(key, std::move(compiled)); + (void)inserted; + found = insertedIt; + } else { + found->second.lastUsedFrame = currentFrameId; + } + + appendCompiledDrawList(found->second); + } + + constexpr std::size_t kCompiledDrawListCacheMaxEntries = 256u; + constexpr std::uint64_t kCompiledDrawListCacheRetentionFrames = 240u; + if (m_compiledDrawListCache.size() > kCompiledDrawListCacheMaxEntries) { + const std::uint64_t pruneBeforeFrame = + currentFrameId > kCompiledDrawListCacheRetentionFrames + ? currentFrameId - kCompiledDrawListCacheRetentionFrames + : 0u; + for (auto it = m_compiledDrawListCache.begin(); + it != m_compiledDrawListCache.end();) { + if (it->second.lastUsedFrame <= pruneBeforeFrame) { + it = m_compiledDrawListCache.erase(it); + } else { + ++it; + } + } + } + + return true; +} + +bool D3D12UiRenderer::Render( + const UIDrawData& drawData, + const RenderContext& renderContext, + const RenderSurface& surface) { + if (m_windowRenderer == nullptr || + m_textureHost == nullptr || + m_textSystem == nullptr) { + m_lastError = "Render requires an initialized D3D12 UI renderer."; + return false; + } + + if (!renderContext.IsValid() || renderContext.commandList == nullptr) { + m_lastError = "Render requires a valid D3D12 render context."; + return false; + } + + if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || + surface.GetColorAttachments().empty() || + surface.GetColorAttachments()[0] == nullptr) { + m_lastError = "Render requires a valid single-color render surface."; + return false; + } + + if (!EnsureInitialized(renderContext, surface)) { + if (m_lastError.empty()) { + m_lastError = "Failed to initialize the D3D12 UI renderer resources."; + } + return false; + } + + m_textureHost->BeginFrame(m_windowRenderer->GetActiveFrameSlot()); + + std::vector legacyVertices = {}; + std::vector legacyIndices = {}; + std::vector quadInstances = {}; + std::vector batches = {}; + if (!BuildDrawBatches(drawData, surface, legacyVertices, legacyIndices, quadInstances, batches)) { + if (m_lastError.empty()) { + m_lastError = "Failed to build the D3D12 UI draw batches."; + } + return false; + } + + if (batches.empty()) { + m_lastError.clear(); + return true; + } + + const std::uint32_t frameSlot = m_windowRenderer->GetActiveFrameSlot(); + if (!EnsureFrameBufferCapacity( + frameSlot, + legacyVertices.size() * sizeof(UiVertex), + legacyIndices.size() * sizeof(std::uint32_t), + quadInstances.size() * sizeof(UiQuadInstance))) { + m_lastError = "Failed to allocate the D3D12 UI dynamic buffers."; + return false; + } + + FrameResources& frameResources = m_frameResources[frameSlot]; + if (!legacyVertices.empty() && frameResources.legacyVertexBuffer != nullptr) { + frameResources.legacyVertexBuffer->SetData( + legacyVertices.data(), + legacyVertices.size() * sizeof(UiVertex)); + } + if (!legacyIndices.empty() && frameResources.legacyIndexBuffer != nullptr) { + frameResources.legacyIndexBuffer->SetData( + legacyIndices.data(), + legacyIndices.size() * sizeof(std::uint32_t)); + } + if (!quadInstances.empty() && frameResources.quadInstanceBuffer != nullptr) { + frameResources.quadInstanceBuffer->SetData( + quadInstances.data(), + quadInstances.size() * sizeof(UiQuadInstance)); + } + + const auto renderArea = surface.GetRenderArea(); + UiConstants constants = {}; + constants.invViewportSize[0] = + renderArea.width > 0 ? 1.0f / static_cast(renderArea.width) : 0.0f; + constants.invViewportSize[1] = + renderArea.height > 0 ? 1.0f / static_cast(renderArea.height) : 0.0f; + m_constantSet->WriteConstant(0u, &constants, sizeof(constants)); + + auto* d3d12CommandList = + static_cast(renderContext.commandList); + auto* renderTarget = surface.GetColorAttachments()[0]; + RHICommandList* commandList = renderContext.commandList; + commandList->SetRenderTargets(1u, &renderTarget, nullptr); + commandList->SetViewport(Viewport{ + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }); + commandList->SetScissorRect(Rect{ + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }); + commandList->SetPrimitiveTopology(PrimitiveTopology::TriangleList); + + ::XCEngine::RHI::RHIDescriptorSet* constantSets[] = { m_constantSet }; + commandList->SetGraphicsDescriptorSets(0u, 1u, constantSets, m_pipelineLayout); + + auto* textureHeap = m_windowRenderer->GetTextureDescriptorAllocator().GetDescriptorHeap(); + auto* samplerHeap = static_cast(m_samplerPool)->GetDescriptorHeap(); + ID3D12DescriptorHeap* descriptorHeaps[] = { textureHeap, samplerHeap }; + d3d12CommandList->SetDescriptorHeaps(2u, descriptorHeaps); + + const auto* pipelineLayout = + static_cast(m_pipelineLayout); + const std::uint32_t textureRootIndex = + pipelineLayout->GetShaderResourceTableRootParameterIndex(1u); + const std::uint32_t samplerRootIndex = + pipelineLayout->GetSamplerTableRootParameterIndex(2u); + + enum class BoundPipeline : std::uint8_t { + None = 0u, + Legacy, + Quad + }; + + BoundPipeline boundPipeline = BoundPipeline::None; + for (const UiBatch& batch : batches) { + commandList->SetScissorRect(batch.scissorRect); + d3d12CommandList->SetGraphicsRootDescriptorTable( + textureRootIndex, + batch.textureHandle); + d3d12CommandList->SetGraphicsRootDescriptorTable( + samplerRootIndex, + m_samplerGpuHandle); + + if (batch.kind == UiBatchKind::QuadInstanced) { + if (boundPipeline != BoundPipeline::Quad) { + if (m_quadPipelineState == nullptr || + m_quadVertexBufferView == nullptr || + m_quadIndexBufferView == nullptr || + frameResources.quadInstanceBufferView == nullptr) { + m_lastError = "D3D12 UI quad instancing resources are unavailable."; + return false; + } + + commandList->SetPipelineState(m_quadPipelineState); + const std::uint64_t quadVertexOffsets[] = { 0u, 0u }; + const std::uint32_t quadVertexStrides[] = { + static_cast(sizeof(UiQuadVertex)), + static_cast(sizeof(UiQuadInstance)) + }; + ::XCEngine::RHI::RHIResourceView* quadVertexViews[] = { + m_quadVertexBufferView, + frameResources.quadInstanceBufferView + }; + commandList->SetVertexBuffers(0u, 2u, quadVertexViews, quadVertexOffsets, quadVertexStrides); + commandList->SetIndexBuffer(m_quadIndexBufferView, 0u); + boundPipeline = BoundPipeline::Quad; + } + + commandList->DrawIndexed( + static_cast(sizeof(kUnitQuadIndices) / sizeof(kUnitQuadIndices[0])), + batch.elementCount, + 0u, + 0, + batch.firstElement); + } else { + if (boundPipeline != BoundPipeline::Legacy) { + if (m_legacyPipelineState == nullptr || + frameResources.legacyVertexBufferView == nullptr || + frameResources.legacyIndexBufferView == nullptr) { + m_lastError = "D3D12 UI legacy geometry resources are unavailable."; + return false; + } + + commandList->SetPipelineState(m_legacyPipelineState); + const std::uint64_t legacyVertexOffsets[] = { 0u }; + const std::uint32_t legacyVertexStrides[] = { + static_cast(sizeof(UiVertex)) + }; + ::XCEngine::RHI::RHIResourceView* legacyVertexViews[] = { + frameResources.legacyVertexBufferView + }; + commandList->SetVertexBuffers( + 0u, + 1u, + legacyVertexViews, + legacyVertexOffsets, + legacyVertexStrides); + commandList->SetIndexBuffer(frameResources.legacyIndexBufferView, 0u); + boundPipeline = BoundPipeline::Legacy; + } + + commandList->DrawIndexed(batch.elementCount, 1u, batch.firstElement, 0, 0u); + } + } + + m_lastError.clear(); + return true; +} + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12UiRenderer.h b/new_editor/app/Rendering/D3D12/D3D12UiRenderer.h new file mode 100644 index 00000000..06a9b382 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiRenderer.h @@ -0,0 +1,246 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "D3D12UiTextSystem.h" +#include "D3D12UiTextureHost.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +class D3D12UiRenderer { +public: + enum class UiBatchKind : std::uint8_t { + LegacyIndexed = 0u, + QuadInstanced + }; + + struct UiVertex { + float position[2] = {}; + float uv[2] = {}; + float color[4] = {}; + }; + + struct UiQuadInstance { + float origin[2] = {}; + float axisX[2] = {}; + float axisY[2] = {}; + float uvMin[2] = {}; + float uvMax[2] = {}; + float color[4] = {}; + float secondaryColor[4] = {}; + float params[4] = {}; + }; + + struct UiBatch { + UiBatchKind kind = UiBatchKind::LegacyIndexed; + std::uint32_t firstElement = 0u; + std::uint32_t elementCount = 0u; + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle = {}; + ::XCEngine::RHI::Rect scissorRect = {}; + }; + + bool Initialize( + D3D12WindowRenderer& windowRenderer, + D3D12UiTextureHost& textureHost, + D3D12UiTextSystem& textSystem); + void Shutdown(); + + void SetDpiScale(float dpiScale); + float GetDpiScale() const; + const std::string& GetLastError() const; + + bool Render( + const ::XCEngine::UI::UIDrawData& drawData, + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface); + +private: + struct FrameResources { + ::XCEngine::RHI::RHIBuffer* legacyVertexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* legacyVertexBufferView = nullptr; + ::XCEngine::RHI::RHIBuffer* legacyIndexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* legacyIndexBufferView = nullptr; + ::XCEngine::RHI::RHIBuffer* quadInstanceBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* quadInstanceBufferView = nullptr; + std::uint64_t legacyVertexCapacityBytes = 0u; + std::uint64_t legacyIndexCapacityBytes = 0u; + std::uint64_t quadInstanceCapacityBytes = 0u; + }; + + struct GlyphAtlasKey { + std::uintptr_t fontFaceKey = 0u; + std::uint16_t glyphIndex = 0u; + int fontEmSizeTenths = 0; + bool isSideways = false; + + bool operator==(const GlyphAtlasKey& other) const; + }; + + struct GlyphAtlasKeyHash { + std::size_t operator()(const GlyphAtlasKey& key) const; + }; + + struct GlyphAtlasEntry { + std::size_t pageIndex = 0u; + std::uint32_t pageX = 0u; + std::uint32_t pageY = 0u; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + LONG boundsLeft = 0; + LONG boundsTop = 0; + LONG boundsRight = 0; + LONG boundsBottom = 0; + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle = {}; + bool hasPixels = false; + }; + + struct GlyphAtlasPage { + ::XCEngine::UI::UITextureHandle texture = {}; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + std::uint32_t cursorX = 0u; + std::uint32_t cursorY = 0u; + std::uint32_t rowHeight = 0u; + }; + + struct TextRunCacheKey { + std::string text = {}; + int fontSizeTenths = 0; + int dpiScaleMilli = 0; + + bool operator==(const TextRunCacheKey& other) const; + }; + + struct TextRunCacheKeyHash { + std::size_t operator()(const TextRunCacheKey& key) const; + }; + + struct GlyphQuadPlan { + float left = 0.0f; + float top = 0.0f; + float right = 0.0f; + float bottom = 0.0f; + float uvMinX = 0.0f; + float uvMinY = 0.0f; + float uvMaxX = 0.0f; + float uvMaxY = 0.0f; + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle = {}; + }; + + struct CachedTextRun { + std::vector glyphs = {}; + }; + + struct CompiledDrawListKey { + std::uint64_t contentHash = 0u; + std::uint32_t renderWidth = 0u; + std::uint32_t renderHeight = 0u; + int dpiScaleMilli = 0; + + bool operator==(const CompiledDrawListKey& other) const; + }; + + struct CompiledDrawListKeyHash { + std::size_t operator()(const CompiledDrawListKey& key) const; + }; + + struct CompiledDrawList { + std::vector vertices = {}; + std::vector indices = {}; + std::vector quadInstances = {}; + std::vector batches = {}; + std::uint64_t lastUsedFrame = 0u; + }; + + bool EnsureInitialized( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface); + bool CreateResources( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface); + void DestroyResources(); + bool EnsureFrameBufferCapacity( + std::uint32_t frameSlot, + std::size_t legacyVertexBytes, + std::size_t legacyIndexBytes, + std::size_t quadInstanceBytes); + void DestroyFrameResources(FrameResources& frameResources); + bool EnsureWhiteTexture(); + bool EnsureGlyphAtlasPage(); + bool AllocateGlyphAtlasRegion( + std::uint32_t width, + std::uint32_t height, + std::size_t& outPageIndex, + std::uint32_t& outPageX, + std::uint32_t& outPageY); + const GlyphAtlasEntry* ResolveGlyphAtlasEntry( + const D3D12UiTextSystem::ShapedGlyph& glyph); + const CachedTextRun* ResolveTextRun( + std::string_view text, + float fontSize); + void ReleaseGlyphAtlas(); + void ReleaseTextRunCache(); + void ReleaseCompiledDrawListCache(); + bool BuildDrawBatches( + const ::XCEngine::UI::UIDrawData& drawData, + const ::XCEngine::Rendering::RenderSurface& surface, + std::vector& outLegacyVertices, + std::vector& outLegacyIndices, + std::vector& outQuadInstances, + std::vector& outBatches); + + D3D12WindowRenderer* m_windowRenderer = nullptr; + D3D12UiTextureHost* m_textureHost = nullptr; + D3D12UiTextSystem* m_textSystem = nullptr; + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_legacyPipelineState = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_quadPipelineState = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_constantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_constantSet = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_samplerPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_samplerSet = nullptr; + ::XCEngine::RHI::RHISampler* m_sampler = nullptr; + D3D12_GPU_DESCRIPTOR_HANDLE m_samplerGpuHandle = {}; + ::XCEngine::UI::UITextureHandle m_whiteTexture = {}; + ::XCEngine::RHI::RHIBuffer* m_quadVertexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* m_quadVertexBufferView = nullptr; + ::XCEngine::RHI::RHIBuffer* m_quadIndexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* m_quadIndexBufferView = nullptr; + std::array m_frameResources = {}; + std::vector m_glyphAtlasPages = {}; + std::unordered_map m_glyphAtlas = {}; + std::unordered_map m_textRunCache = {}; + std::unordered_map + m_compiledDrawListCache = {}; + std::uint64_t m_compiledDrawListFrameCounter = 0u; + ::XCEngine::RHI::Format m_renderTargetFormat = ::XCEngine::RHI::Format::Unknown; + std::uint32_t m_renderTargetSampleCount = 1u; + std::uint32_t m_renderTargetSampleQuality = 0u; + float m_dpiScale = 1.0f; + std::string m_lastError = {}; +}; + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp b/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp new file mode 100644 index 00000000..e54dbdc3 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp @@ -0,0 +1,752 @@ +#include "D3D12UiTextSystem.h" + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +namespace { + +constexpr float kDefaultFontSize = 16.0f; + +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; +} + +float ClampDpiScale(float dpiScale) { + return dpiScale > 0.0f ? dpiScale : 1.0f; +} + +float ResolveFontSize(float fontSize) { + return fontSize > 0.0f ? fontSize : kDefaultFontSize; +} + +struct GlyphRunRecord { + Microsoft::WRL::ComPtr fontFace = {}; + std::vector glyphIndices = {}; + std::vector glyphAdvances = {}; + std::vector glyphOffsets = {}; + FLOAT fontEmSize = 0.0f; + FLOAT baselineOriginX = 0.0f; + FLOAT baselineOriginY = 0.0f; + DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL; + BOOL isSideways = FALSE; + UINT32 bidiLevel = 0u; +}; + +class GlyphRunCollector final : public IDWriteTextRenderer { +public: + explicit GlyphRunCollector(float pixelsPerDip) + : m_pixelsPerDip(ClampDpiScale(pixelsPerDip)) { + } + + const std::vector& GetGlyphRuns() const { + return m_glyphRuns; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** object) override { + if (object == nullptr) { + return E_INVALIDARG; + } + + *object = nullptr; + if (riid == __uuidof(IUnknown) || + riid == __uuidof(IDWritePixelSnapping) || + riid == __uuidof(IDWriteTextRenderer)) { + *object = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return ++m_refCount; + } + + ULONG STDMETHODCALLTYPE Release() override { + const ULONG refCount = --m_refCount; + if (refCount == 0u) { + delete this; + } + + return refCount; + } + + HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled( + void*, + BOOL* isDisabled) override { + if (isDisabled == nullptr) { + return E_INVALIDARG; + } + + *isDisabled = FALSE; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetCurrentTransform( + void*, + DWRITE_MATRIX* transform) override { + if (transform == nullptr) { + return E_INVALIDARG; + } + + *transform = DWRITE_MATRIX{ 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetPixelsPerDip( + void*, + FLOAT* pixelsPerDip) override { + if (pixelsPerDip == nullptr) { + return E_INVALIDARG; + } + + *pixelsPerDip = m_pixelsPerDip; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE DrawGlyphRun( + void*, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + DWRITE_MEASURING_MODE measuringMode, + const DWRITE_GLYPH_RUN* glyphRun, + const DWRITE_GLYPH_RUN_DESCRIPTION*, + IUnknown*) override { + if (glyphRun == nullptr || + glyphRun->fontFace == nullptr || + glyphRun->glyphCount == 0u || + glyphRun->glyphIndices == nullptr) { + return S_OK; + } + + GlyphRunRecord record = {}; + record.fontFace = glyphRun->fontFace; + record.glyphIndices.assign( + glyphRun->glyphIndices, + glyphRun->glyphIndices + glyphRun->glyphCount); + record.glyphAdvances.resize(glyphRun->glyphCount, 0.0f); + record.glyphOffsets.resize(glyphRun->glyphCount, DWRITE_GLYPH_OFFSET{}); + if (glyphRun->glyphAdvances != nullptr) { + std::copy( + glyphRun->glyphAdvances, + glyphRun->glyphAdvances + glyphRun->glyphCount, + record.glyphAdvances.begin()); + } + if (glyphRun->glyphOffsets != nullptr) { + std::copy( + glyphRun->glyphOffsets, + glyphRun->glyphOffsets + glyphRun->glyphCount, + record.glyphOffsets.begin()); + } + + record.fontEmSize = glyphRun->fontEmSize; + record.baselineOriginX = baselineOriginX; + record.baselineOriginY = baselineOriginY; + record.measuringMode = measuringMode; + record.isSideways = glyphRun->isSideways; + record.bidiLevel = glyphRun->bidiLevel; + m_glyphRuns.push_back(std::move(record)); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE DrawUnderline( + void*, + FLOAT, + FLOAT, + const DWRITE_UNDERLINE*, + IUnknown*) override { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE DrawStrikethrough( + void*, + FLOAT, + FLOAT, + const DWRITE_STRIKETHROUGH*, + IUnknown*) override { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE DrawInlineObject( + void*, + FLOAT, + FLOAT, + IDWriteInlineObject*, + BOOL, + BOOL, + IUnknown*) override { + return S_OK; + } + +private: + std::atomic m_refCount = 1u; + float m_pixelsPerDip = 1.0f; + std::vector m_glyphRuns = {}; +}; + +void CompositeCoverage( + std::vector& rgbaPixels, + UINT targetWidth, + UINT targetHeight, + const RECT& bounds, + const std::uint8_t* alphaTexture, + std::size_t textureStride, + std::size_t channelCount) { + if (alphaTexture == nullptr || channelCount == 0u) { + return; + } + + const LONG width = bounds.right - bounds.left; + const LONG height = bounds.bottom - bounds.top; + if (width <= 0 || height <= 0) { + return; + } + + for (LONG row = 0; row < height; ++row) { + const LONG dstY = bounds.top + row; + if (dstY < 0 || dstY >= static_cast(targetHeight)) { + continue; + } + + for (LONG column = 0; column < width; ++column) { + const LONG dstX = bounds.left + column; + if (dstX < 0 || dstX >= static_cast(targetWidth)) { + continue; + } + + const std::size_t srcOffset = + static_cast(row) * textureStride + + static_cast(column) * channelCount; + std::uint8_t coverage = 0u; + for (std::size_t channel = 0u; channel < channelCount; ++channel) { + coverage = (std::max)(coverage, alphaTexture[srcOffset + channel]); + } + + if (coverage == 0u) { + continue; + } + + const std::size_t dstOffset = + (static_cast(dstY) * static_cast(targetWidth) + + static_cast(dstX)) * + 4u; + std::uint8_t& dstAlpha = rgbaPixels[dstOffset + 3u]; + const std::uint32_t blendedAlpha = + static_cast(dstAlpha) + + ((255u - static_cast(dstAlpha)) * + static_cast(coverage) + + 127u) / + 255u; + dstAlpha = static_cast((std::min)(blendedAlpha, 255u)); + rgbaPixels[dstOffset + 0u] = 255u; + rgbaPixels[dstOffset + 1u] = 255u; + rgbaPixels[dstOffset + 2u] = 255u; + } + } +} + +} // namespace + +bool D3D12UiTextSystem::Initialize() { + Shutdown(); + + const HRESULT hr = DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr)) { + const std::string error = HrToString("DWriteCreateFactory", hr); + Shutdown(); + m_lastError = error; + return false; + } + + m_lastError.clear(); + return true; +} + +void D3D12UiTextSystem::Shutdown() { + m_textFormats.clear(); + m_dwriteFactory.Reset(); + m_lastError.clear(); +} + +void D3D12UiTextSystem::SetDpiScale(float dpiScale) { + m_dpiScale = ClampDpiScale(dpiScale); +} + +float D3D12UiTextSystem::GetDpiScale() const { + return m_dpiScale; +} + +const std::string& D3D12UiTextSystem::GetLastError() const { + return m_lastError; +} + +bool D3D12UiTextSystem::ShapeTextRun( + std::string_view text, + float fontSize, + ShapedTextRun& outRun, + std::string& outError) const { + outRun = {}; + outError.clear(); + + if (!m_dwriteFactory) { + outError = "ShapeTextRun requires an initialized DirectWrite factory."; + return false; + } + + if (text.empty()) { + return true; + } + + const std::wstring wideText = Utf8ToWide(text); + if (wideText.empty()) { + outError = "ShapeTextRun could not convert UTF-8 text to UTF-16."; + return false; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float scaledFontSize = ResolveFontSize(fontSize) * dpiScale; + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + outError = "ShapeTextRun could not resolve the DirectWrite text format."; + return false; + } + + Microsoft::WRL::ComPtr textLayout = {}; + HRESULT hr = m_dwriteFactory->CreateTextLayout( + wideText.c_str(), + static_cast(wideText.size()), + textFormat, + 4096.0f, + scaledFontSize * 2.0f, + textLayout.ReleaseAndGetAddressOf()); + if (FAILED(hr) || textLayout == nullptr) { + outError = HrToString("IDWriteFactory::CreateTextLayout", hr); + return false; + } + + DWRITE_TEXT_METRICS textMetrics = {}; + hr = textLayout->GetMetrics(&textMetrics); + if (FAILED(hr)) { + outError = HrToString("IDWriteTextLayout::GetMetrics", hr); + return false; + } + outRun.width = textMetrics.widthIncludingTrailingWhitespace; + outRun.height = (std::max)(textMetrics.height, scaledFontSize * 1.6f); + + GlyphRunCollector* collector = new GlyphRunCollector(dpiScale); + hr = textLayout->Draw( + nullptr, + collector, + 0.0f, + 0.0f); + const std::vector glyphRuns = collector->GetGlyphRuns(); + collector->Release(); + if (FAILED(hr)) { + outError = HrToString("IDWriteTextLayout::Draw", hr); + return false; + } + + for (const GlyphRunRecord& record : glyphRuns) { + if (record.fontFace == nullptr || record.glyphIndices.empty()) { + continue; + } + + float advanceCursor = 0.0f; + for (std::size_t glyphIndex = 0u; glyphIndex < record.glyphIndices.size(); ++glyphIndex) { + const DWRITE_GLYPH_OFFSET glyphOffset = + glyphIndex < record.glyphOffsets.size() + ? record.glyphOffsets[glyphIndex] + : DWRITE_GLYPH_OFFSET{}; + ShapedGlyph shapedGlyph = {}; + shapedGlyph.fontFace = record.fontFace; + shapedGlyph.glyphIndex = record.glyphIndices[glyphIndex]; + shapedGlyph.fontEmSize = record.fontEmSize; + shapedGlyph.originX = + record.baselineOriginX + + advanceCursor + + glyphOffset.advanceOffset; + shapedGlyph.originY = + record.baselineOriginY + + glyphOffset.ascenderOffset; + shapedGlyph.measuringMode = record.measuringMode; + shapedGlyph.isSideways = record.isSideways != FALSE; + outRun.glyphs.push_back(std::move(shapedGlyph)); + + if (glyphIndex < record.glyphAdvances.size()) { + advanceCursor += record.glyphAdvances[glyphIndex]; + } + } + } + + return true; +} + +bool D3D12UiTextSystem::RasterizeGlyph( + const ShapedGlyph& glyph, + RasterizedGlyph& outGlyph, + std::string& outError) const { + outGlyph = {}; + outError.clear(); + + if (!m_dwriteFactory) { + outError = "RasterizeGlyph requires an initialized DirectWrite factory."; + return false; + } + + if (glyph.fontFace == nullptr) { + outError = "RasterizeGlyph requires a valid font face."; + return false; + } + + const UINT16 glyphIndices[] = { glyph.glyphIndex }; + const FLOAT glyphAdvances[] = { 0.0f }; + const DWRITE_GLYPH_OFFSET glyphOffsets[] = { DWRITE_GLYPH_OFFSET{} }; + + DWRITE_GLYPH_RUN glyphRun = {}; + glyphRun.fontFace = glyph.fontFace.Get(); + glyphRun.fontEmSize = glyph.fontEmSize; + glyphRun.glyphCount = 1u; + glyphRun.glyphIndices = glyphIndices; + glyphRun.glyphAdvances = glyphAdvances; + glyphRun.glyphOffsets = glyphOffsets; + glyphRun.isSideways = glyph.isSideways ? TRUE : FALSE; + glyphRun.bidiLevel = 0u; + + Microsoft::WRL::ComPtr analysis = {}; + const float dpiScale = ClampDpiScale(m_dpiScale); + HRESULT hr = m_dwriteFactory->CreateGlyphRunAnalysis( + &glyphRun, + dpiScale, + nullptr, + DWRITE_RENDERING_MODE_NATURAL, + glyph.measuringMode, + 0.0f, + 0.0f, + analysis.ReleaseAndGetAddressOf()); + if (FAILED(hr) || analysis == nullptr) { + outError = HrToString("IDWriteFactory::CreateGlyphRunAnalysis", hr); + return false; + } + + RECT bounds = {}; + hr = analysis->GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1, &bounds); + if (FAILED(hr)) { + outError = HrToString("IDWriteGlyphRunAnalysis::GetAlphaTextureBounds", hr); + return false; + } + + outGlyph.boundsLeft = bounds.left; + outGlyph.boundsTop = bounds.top; + outGlyph.boundsRight = bounds.right; + outGlyph.boundsBottom = bounds.bottom; + + const LONG boundsWidth = bounds.right - bounds.left; + const LONG boundsHeight = bounds.bottom - bounds.top; + if (boundsWidth <= 0 || boundsHeight <= 0) { + return true; + } + + std::vector alphaTexture( + static_cast(boundsWidth) * static_cast(boundsHeight) * 3u); + hr = analysis->CreateAlphaTexture( + DWRITE_TEXTURE_CLEARTYPE_3x1, + &bounds, + alphaTexture.data(), + static_cast(alphaTexture.size())); + if (FAILED(hr)) { + outError = HrToString("IDWriteGlyphRunAnalysis::CreateAlphaTexture", hr); + return false; + } + + outGlyph.width = static_cast(boundsWidth); + outGlyph.height = static_cast(boundsHeight); + outGlyph.rgbaPixels.assign( + static_cast(outGlyph.width) * static_cast(outGlyph.height) * 4u, + 0u); + CompositeCoverage( + outGlyph.rgbaPixels, + outGlyph.width, + outGlyph.height, + RECT{ 0, 0, boundsWidth, boundsHeight }, + alphaTexture.data(), + static_cast(boundsWidth) * 3u, + 3u); + return true; +} + +float D3D12UiTextSystem::MeasureTextWidth( + const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const { + if (!m_dwriteFactory || request.text.empty()) { + return 0.0f; + } + + const std::wstring text = Utf8ToWide(request.text); + if (text.empty()) { + return 0.0f; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale; + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + return 0.0f; + } + + Microsoft::WRL::ComPtr textLayout = {}; + HRESULT hr = m_dwriteFactory->CreateTextLayout( + text.c_str(), + static_cast(text.size()), + textFormat, + 4096.0f, + scaledFontSize * 2.0f, + textLayout.ReleaseAndGetAddressOf()); + if (FAILED(hr) || textLayout == nullptr) { + return 0.0f; + } + + DWRITE_TEXT_METRICS textMetrics = {}; + hr = textLayout->GetMetrics(&textMetrics); + if (FAILED(hr)) { + return 0.0f; + } + + DWRITE_OVERHANG_METRICS overhangMetrics = {}; + float width = textMetrics.widthIncludingTrailingWhitespace; + if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) { + width += (std::max)(overhangMetrics.left, 0.0f); + width += (std::max)(overhangMetrics.right, 0.0f); + } + + return std::ceil(width) / dpiScale; +} + +bool D3D12UiTextSystem::RasterizeTextMask( + std::string_view text, + float fontSize, + std::vector& outRgbaPixels, + UINT& outWidth, + UINT& outHeight, + std::string& outError) { + outRgbaPixels.clear(); + outWidth = 0u; + outHeight = 0u; + outError.clear(); + + if (!m_dwriteFactory) { + outError = "RasterizeTextMask requires an initialized DirectWrite factory."; + return false; + } + + if (text.empty()) { + outError = "RasterizeTextMask rejected an empty text payload."; + return false; + } + + const std::wstring wideText = Utf8ToWide(text); + if (wideText.empty()) { + outError = "RasterizeTextMask could not convert UTF-8 text to UTF-16."; + return false; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float scaledFontSize = ResolveFontSize(fontSize) * dpiScale; + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + outError = "RasterizeTextMask could not resolve the DirectWrite text format."; + return false; + } + + Microsoft::WRL::ComPtr textLayout = {}; + HRESULT hr = m_dwriteFactory->CreateTextLayout( + wideText.c_str(), + static_cast(wideText.size()), + textFormat, + 4096.0f, + scaledFontSize * 2.0f, + textLayout.ReleaseAndGetAddressOf()); + if (FAILED(hr) || textLayout == nullptr) { + outError = HrToString("IDWriteFactory::CreateTextLayout", hr); + return false; + } + + DWRITE_TEXT_METRICS textMetrics = {}; + hr = textLayout->GetMetrics(&textMetrics); + if (FAILED(hr)) { + outError = HrToString("IDWriteTextLayout::GetMetrics", hr); + return false; + } + + DWRITE_OVERHANG_METRICS overhangMetrics = {}; + textLayout->GetOverhangMetrics(&overhangMetrics); + + const float leftPad = (std::max)(overhangMetrics.left, 0.0f); + const float topPad = (std::max)(overhangMetrics.top, 0.0f); + const float rightPad = (std::max)(overhangMetrics.right, 0.0f); + const float bottomPad = (std::max)(overhangMetrics.bottom, 0.0f); + outWidth = (std::max)( + 1u, + static_cast(std::ceil( + textMetrics.widthIncludingTrailingWhitespace + leftPad + rightPad))); + outHeight = (std::max)( + 1u, + static_cast(std::ceil( + (std::max)(textMetrics.height, scaledFontSize * 1.6f) + topPad + bottomPad))); + outRgbaPixels.assign( + static_cast(outWidth) * static_cast(outHeight) * 4u, + 0u); + + GlyphRunCollector* collector = new GlyphRunCollector(dpiScale); + hr = textLayout->Draw( + nullptr, + collector, + leftPad, + topPad); + const std::vector glyphRuns = collector->GetGlyphRuns(); + collector->Release(); + if (FAILED(hr)) { + outError = HrToString("IDWriteTextLayout::Draw", hr); + return false; + } + + for (const GlyphRunRecord& record : glyphRuns) { + if (record.fontFace == nullptr || record.glyphIndices.empty()) { + continue; + } + + DWRITE_GLYPH_RUN glyphRun = {}; + glyphRun.fontFace = record.fontFace.Get(); + glyphRun.fontEmSize = record.fontEmSize; + glyphRun.glyphCount = static_cast(record.glyphIndices.size()); + glyphRun.glyphIndices = record.glyphIndices.data(); + glyphRun.glyphAdvances = record.glyphAdvances.data(); + glyphRun.glyphOffsets = + record.glyphOffsets.empty() ? nullptr : record.glyphOffsets.data(); + glyphRun.isSideways = record.isSideways; + glyphRun.bidiLevel = record.bidiLevel; + + Microsoft::WRL::ComPtr analysis = {}; + hr = m_dwriteFactory->CreateGlyphRunAnalysis( + &glyphRun, + dpiScale, + nullptr, + DWRITE_RENDERING_MODE_NATURAL, + record.measuringMode, + record.baselineOriginX, + record.baselineOriginY, + analysis.ReleaseAndGetAddressOf()); + if (FAILED(hr) || analysis == nullptr) { + outError = HrToString("IDWriteFactory::CreateGlyphRunAnalysis", hr); + return false; + } + + RECT bounds = {}; + hr = analysis->GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1, &bounds); + if (FAILED(hr)) { + outError = HrToString("IDWriteGlyphRunAnalysis::GetAlphaTextureBounds", hr); + return false; + } + + const LONG boundsWidth = bounds.right - bounds.left; + const LONG boundsHeight = bounds.bottom - bounds.top; + if (boundsWidth <= 0 || boundsHeight <= 0) { + continue; + } + + std::vector alphaTexture( + static_cast(boundsWidth) * static_cast(boundsHeight) * 3u); + hr = analysis->CreateAlphaTexture( + DWRITE_TEXTURE_CLEARTYPE_3x1, + &bounds, + alphaTexture.data(), + static_cast(alphaTexture.size())); + if (FAILED(hr)) { + outError = HrToString("IDWriteGlyphRunAnalysis::CreateAlphaTexture", hr); + return false; + } + + CompositeCoverage( + outRgbaPixels, + outWidth, + outHeight, + bounds, + alphaTexture.data(), + static_cast(boundsWidth) * 3u, + 3u); + } + + return true; +} + +IDWriteTextFormat* D3D12UiTextSystem::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) || textFormat == nullptr) { + 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; +} + +std::wstring D3D12UiTextSystem::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 diff --git a/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.h b/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.h new file mode 100644 index 00000000..2887c2b2 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiTextSystem.h @@ -0,0 +1,85 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +class D3D12UiTextSystem final : public ::XCEngine::UI::Editor::UIEditorTextMeasurer { +public: + struct ShapedGlyph { + Microsoft::WRL::ComPtr fontFace = {}; + std::uint16_t glyphIndex = 0u; + float fontEmSize = 0.0f; + float originX = 0.0f; + float originY = 0.0f; + DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL; + bool isSideways = false; + }; + + struct ShapedTextRun { + std::vector glyphs = {}; + float width = 0.0f; + float height = 0.0f; + }; + + struct RasterizedGlyph { + std::vector rgbaPixels = {}; + UINT width = 0u; + UINT height = 0u; + LONG boundsLeft = 0; + LONG boundsTop = 0; + LONG boundsRight = 0; + LONG boundsBottom = 0; + }; + + bool Initialize(); + void Shutdown(); + + void SetDpiScale(float dpiScale); + float GetDpiScale() const; + const std::string& GetLastError() const; + + float MeasureTextWidth( + const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override; + bool ShapeTextRun( + std::string_view text, + float fontSize, + ShapedTextRun& outRun, + std::string& outError) const; + bool RasterizeGlyph( + const ShapedGlyph& glyph, + RasterizedGlyph& outGlyph, + std::string& outError) const; + bool RasterizeTextMask( + std::string_view text, + float fontSize, + std::vector& outRgbaPixels, + UINT& outWidth, + UINT& outHeight, + std::string& outError); + +private: + IDWriteTextFormat* GetTextFormat(float fontSize) const; + static std::wstring Utf8ToWide(std::string_view text); + + Microsoft::WRL::ComPtr m_dwriteFactory = {}; + mutable std::unordered_map> m_textFormats = {}; + float m_dpiScale = 1.0f; + std::string m_lastError = {}; +}; + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp b/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp new file mode 100644 index 00000000..a1d782e7 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp @@ -0,0 +1,676 @@ +#include "D3D12UiTextureHost.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +namespace { + +std::string HrToString(const char* operation, HRESULT hr) { + char buffer[128] = {}; + sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast(hr)); + return buffer; +} + +constexpr std::uint32_t kTextureBytesPerPixel = 4u; + +std::uint32_t AlignTextureRowPitch(std::uint32_t rowPitch) { + constexpr std::uint32_t alignment = D3D12_TEXTURE_DATA_PITCH_ALIGNMENT; + return (rowPitch + alignment - 1u) & ~(alignment - 1u); +} + +} // namespace + +bool D3D12UiTextureHost::Initialize(D3D12WindowRenderer& windowRenderer) { + Shutdown(); + + if (windowRenderer.GetRHIDevice() == nullptr) { + m_lastError = "Initialize requires an initialized D3D12 window renderer."; + return false; + } + + m_windowRenderer = &windowRenderer; + m_lastError.clear(); + return true; +} + +void D3D12UiTextureHost::BeginFrame(std::uint32_t frameSlot) { + if (frameSlot >= m_frameUploadBuffers.size()) { + m_hasActiveFrameSlot = false; + return; + } + + m_frameUploadBuffers[frameSlot].clear(); + m_activeFrameSlot = frameSlot; + m_hasActiveFrameSlot = true; +} + +void D3D12UiTextureHost::Shutdown() { + for (auto& entry : m_liveTextures) { + if (entry.second != nullptr) { + DestroyTextureResource(*entry.second); + } + } + m_liveTextures.clear(); + for (auto& uploadBuffers : m_frameUploadBuffers) { + uploadBuffers.clear(); + } + + m_wicFactory.Reset(); + if (m_wicComInitialized) { + CoUninitialize(); + m_wicComInitialized = false; + } + + m_windowRenderer = nullptr; + m_activeFrameSlot = 0u; + m_hasActiveFrameSlot = false; + m_lastError.clear(); +} + +bool D3D12UiTextureHost::IsInitialized() const { + return m_windowRenderer != nullptr && + m_windowRenderer->GetRHIDevice() != nullptr; +} + +const std::string& D3D12UiTextureHost::GetLastError() const { + return m_lastError; +} + +bool D3D12UiTextureHost::EnsureWicFactory(std::string& outError) { + outError.clear(); + if (m_wicFactory != nullptr) { + return true; + } + + const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { + outError = HrToString("CoInitializeEx", initHr); + return false; + } + if (SUCCEEDED(initHr)) { + m_wicComInitialized = true; + } + + const HRESULT factoryHr = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf())); + if (FAILED(factoryHr)) { + outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); + return false; + } + + return true; +} + +bool D3D12UiTextureHost::DecodeTextureFile( + const std::filesystem::path& path, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError) { + outPixels.clear(); + outWidth = 0u; + outHeight = 0u; + outError.clear(); + + if (!EnsureWicFactory(outError)) { + return false; + } + + Microsoft::WRL::ComPtr decoder = {}; + const HRESULT hr = m_wicFactory->CreateDecoderFromFilename( + path.wstring().c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnLoad, + decoder.ReleaseAndGetAddressOf()); + if (FAILED(hr) || decoder == nullptr) { + outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); + return false; + } + + Microsoft::WRL::ComPtr frame = {}; + const HRESULT frameHr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); + if (FAILED(frameHr) || frame == nullptr) { + outError = HrToString("IWICBitmapDecoder::GetFrame", frameHr); + return false; + } + + return DecodeTextureFrame(*frame.Get(), outPixels, outWidth, outHeight, outError); +} + +bool D3D12UiTextureHost::DecodeTextureMemory( + const std::uint8_t* data, + std::size_t size, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError) { + outPixels.clear(); + outWidth = 0u; + outHeight = 0u; + outError.clear(); + + if (data == nullptr || size == 0u) { + outError = "DecodeTextureMemory rejected an empty image payload."; + return false; + } + + if (size > static_cast((std::numeric_limits::max)())) { + outError = "DecodeTextureMemory payload exceeds WIC stream limits."; + return false; + } + + if (!EnsureWicFactory(outError)) { + return false; + } + + Microsoft::WRL::ComPtr stream = {}; + HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); + if (FAILED(hr) || stream == nullptr) { + outError = HrToString("IWICImagingFactory::CreateStream", hr); + return false; + } + + hr = stream->InitializeFromMemory( + const_cast(reinterpret_cast(data)), + static_cast(size)); + if (FAILED(hr)) { + outError = HrToString("IWICStream::InitializeFromMemory", hr); + return false; + } + + Microsoft::WRL::ComPtr decoder = {}; + hr = m_wicFactory->CreateDecoderFromStream( + stream.Get(), + nullptr, + WICDecodeMetadataCacheOnLoad, + decoder.ReleaseAndGetAddressOf()); + if (FAILED(hr) || decoder == nullptr) { + outError = HrToString("IWICImagingFactory::CreateDecoderFromStream", hr); + return false; + } + + Microsoft::WRL::ComPtr frame = {}; + hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); + if (FAILED(hr) || frame == nullptr) { + outError = HrToString("IWICBitmapDecoder::GetFrame", hr); + return false; + } + + return DecodeTextureFrame(*frame.Get(), outPixels, outWidth, outHeight, outError); +} + +bool D3D12UiTextureHost::DecodeTextureFrame( + IWICBitmapSource& source, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError) { + outPixels.clear(); + outWidth = 0u; + outHeight = 0u; + outError.clear(); + + Microsoft::WRL::ComPtr converter = {}; + HRESULT hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); + if (FAILED(hr) || converter == nullptr) { + outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); + return false; + } + + hr = converter->Initialize( + &source, + GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, + nullptr, + 0.0f, + WICBitmapPaletteTypeCustom); + if (FAILED(hr)) { + outError = HrToString("IWICFormatConverter::Initialize", hr); + return false; + } + + UINT width = 0u; + UINT height = 0u; + hr = converter->GetSize(&width, &height); + if (FAILED(hr) || width == 0u || height == 0u) { + outError = HrToString("IWICBitmapSource::GetSize", hr); + return false; + } + + std::vector pixels( + static_cast(width) * static_cast(height) * 4u); + hr = converter->CopyPixels( + nullptr, + width * 4u, + static_cast(pixels.size()), + pixels.data()); + if (FAILED(hr)) { + outError = HrToString("IWICBitmapSource::CopyPixels", hr); + return false; + } + + outPixels = std::move(pixels); + outWidth = static_cast(width); + outHeight = static_cast(height); + return true; +} + +bool D3D12UiTextureHost::CreateTextureResource( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + ReleaseTexture(outTexture); + outTexture = {}; + outError.clear(); + + if (!IsInitialized()) { + outError = "CreateTextureResource requires an initialized D3D12 UI texture host."; + return false; + } + + if (rgbaPixels == nullptr || width == 0u || height == 0u) { + outError = "CreateTextureResource rejected an empty RGBA payload."; + return false; + } + + ::XCEngine::RHI::TextureDesc textureDesc = {}; + textureDesc.width = width; + textureDesc.height = height; + textureDesc.depth = 1u; + textureDesc.mipLevels = 1u; + textureDesc.arraySize = 1u; + textureDesc.format = static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); + textureDesc.textureType = + static_cast(::XCEngine::RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1u; + textureDesc.sampleQuality = 0u; + textureDesc.flags = 0u; + + const std::size_t pixelBytes = + static_cast(width) * static_cast(height) * 4u; + ::XCEngine::RHI::RHITexture* texture = nullptr; + Microsoft::WRL::ComPtr uploadBuffer = {}; + if (m_hasActiveFrameSlot && m_windowRenderer->GetDevice() != nullptr) { + const ::XCEngine::Rendering::RenderContext renderContext = + m_windowRenderer->GetRenderContext(); + auto* d3d12CommandList = + renderContext.commandList != nullptr + ? static_cast<::XCEngine::RHI::D3D12CommandList*>(renderContext.commandList) + : nullptr; + if (d3d12CommandList != nullptr && d3d12CommandList->GetCommandList() != nullptr) { + auto* d3d12Texture = new ::XCEngine::RHI::D3D12Texture(); + D3D12_RESOURCE_DESC nativeTextureDesc = {}; + nativeTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + nativeTextureDesc.Alignment = 0; + nativeTextureDesc.Width = width; + nativeTextureDesc.Height = height; + nativeTextureDesc.DepthOrArraySize = 1u; + nativeTextureDesc.MipLevels = 1u; + nativeTextureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + nativeTextureDesc.SampleDesc.Count = 1u; + nativeTextureDesc.SampleDesc.Quality = 0u; + nativeTextureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + nativeTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + if (d3d12Texture->InitializeFromData( + m_windowRenderer->GetDevice(), + d3d12CommandList->GetCommandList(), + nativeTextureDesc, + ::XCEngine::RHI::TextureType::Texture2D, + rgbaPixels, + pixelBytes, + width * 4u, + &uploadBuffer)) { + texture = d3d12Texture; + } else { + delete d3d12Texture; + } + } + } + + if (texture == nullptr) { + texture = m_windowRenderer->GetRHIDevice()->CreateTexture( + textureDesc, + rgbaPixels, + pixelBytes, + width * 4u); + } + + if (texture == nullptr) { + outError = "Failed to create the GPU texture."; + return false; + } + + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; + if (!m_windowRenderer->GetTextureDescriptorAllocator().CreateTextureDescriptor( + texture, + &cpuHandle, + &gpuHandle) || + cpuHandle.ptr == 0u || + gpuHandle.ptr == 0u) { + texture->Shutdown(); + delete texture; + outError = "Failed to allocate the shader resource descriptor for the GPU texture."; + return false; + } + + auto textureResource = std::make_unique(); + textureResource->texture = texture; + textureResource->cpuHandle = cpuHandle; + textureResource->gpuHandle = gpuHandle; + textureResource->width = width; + textureResource->height = height; + if (uploadBuffer != nullptr && + m_hasActiveFrameSlot && + m_activeFrameSlot < m_frameUploadBuffers.size()) { + m_frameUploadBuffers[m_activeFrameSlot].push_back(std::move(uploadBuffer)); + } + + const std::uintptr_t resourceKey = + reinterpret_cast(textureResource.get()); + outTexture.nativeHandle = static_cast(gpuHandle.ptr); + outTexture.width = width; + outTexture.height = height; + outTexture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView; + outTexture.resourceHandle = resourceKey; + m_liveTextures.emplace(resourceKey, std::move(textureResource)); + return true; +} + +D3D12UiTextureHost::TextureResource* D3D12UiTextureHost::ResolveTextureResource( + const ::XCEngine::UI::UITextureHandle& texture) const { + if (!texture.IsValid() || texture.resourceHandle == 0u) { + return nullptr; + } + + const auto found = m_liveTextures.find(texture.resourceHandle); + if (found == m_liveTextures.end() || found->second == nullptr) { + return nullptr; + } + + return found->second.get(); +} + +void D3D12UiTextureHost::DestroyTextureResource(TextureResource& textureResource) { + if (m_windowRenderer != nullptr && + textureResource.cpuHandle.ptr != 0u && + textureResource.gpuHandle.ptr != 0u) { + m_windowRenderer->GetTextureDescriptorAllocator().Free( + textureResource.cpuHandle, + textureResource.gpuHandle); + } + + if (textureResource.texture != nullptr) { + textureResource.texture->Shutdown(); + delete textureResource.texture; + textureResource.texture = nullptr; + } + + textureResource.cpuHandle = {}; + textureResource.gpuHandle = {}; + textureResource.width = 0u; + textureResource.height = 0u; +} + +bool D3D12UiTextureHost::UpdateTextureRegionRgba( + const ::XCEngine::UI::UITextureHandle& texture, + std::uint32_t dstX, + std::uint32_t dstY, + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + std::uint32_t rowPitch, + std::string& outError) { + outError.clear(); + + if (!IsInitialized()) { + outError = "UpdateTextureRegionRgba requires an initialized D3D12 UI texture host."; + return false; + } + + if (!m_hasActiveFrameSlot || m_activeFrameSlot >= m_frameUploadBuffers.size()) { + outError = "UpdateTextureRegionRgba requires an active frame slot."; + return false; + } + + if (rgbaPixels == nullptr || width == 0u || height == 0u) { + outError = "UpdateTextureRegionRgba rejected an empty RGBA payload."; + return false; + } + + TextureResource* textureResource = ResolveTextureResource(texture); + if (textureResource == nullptr || textureResource->texture == nullptr) { + outError = "UpdateTextureRegionRgba could not resolve the destination texture."; + return false; + } + + if (dstX + width > textureResource->width || dstY + height > textureResource->height) { + outError = "UpdateTextureRegionRgba target rectangle exceeds texture bounds."; + return false; + } + + const ::XCEngine::Rendering::RenderContext renderContext = m_windowRenderer->GetRenderContext(); + auto* d3d12CommandList = + renderContext.commandList != nullptr + ? static_cast<::XCEngine::RHI::D3D12CommandList*>(renderContext.commandList) + : nullptr; + if (d3d12CommandList == nullptr || d3d12CommandList->GetCommandList() == nullptr) { + outError = "UpdateTextureRegionRgba requires an active D3D12 command list."; + return false; + } + + auto* d3d12Texture = + dynamic_cast<::XCEngine::RHI::D3D12Texture*>(textureResource->texture); + if (d3d12Texture == nullptr || d3d12Texture->GetResource() == nullptr) { + outError = "UpdateTextureRegionRgba requires a native D3D12 destination texture."; + return false; + } + + const std::uint32_t resolvedRowPitch = + rowPitch > 0u ? rowPitch : width * kTextureBytesPerPixel; + if (resolvedRowPitch < width * kTextureBytesPerPixel) { + outError = "UpdateTextureRegionRgba row pitch is smaller than the texture row width."; + return false; + } + + const std::uint32_t alignedRowPitch = AlignTextureRowPitch(width * kTextureBytesPerPixel); + const std::uint64_t uploadBytes = + static_cast(alignedRowPitch) * static_cast(height); + + D3D12_HEAP_PROPERTIES uploadHeapProperties = {}; + uploadHeapProperties.Type = D3D12_HEAP_TYPE_UPLOAD; + + D3D12_RESOURCE_DESC uploadDesc = {}; + uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + uploadDesc.Alignment = 0; + uploadDesc.Width = uploadBytes; + uploadDesc.Height = 1; + uploadDesc.DepthOrArraySize = 1; + uploadDesc.MipLevels = 1; + uploadDesc.Format = DXGI_FORMAT_UNKNOWN; + uploadDesc.SampleDesc.Count = 1; + uploadDesc.SampleDesc.Quality = 0; + uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + uploadDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + + Microsoft::WRL::ComPtr uploadBuffer = {}; + HRESULT hr = m_windowRenderer->GetDevice()->CreateCommittedResource( + &uploadHeapProperties, + D3D12_HEAP_FLAG_NONE, + &uploadDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(uploadBuffer.ReleaseAndGetAddressOf())); + if (FAILED(hr) || uploadBuffer == nullptr) { + outError = HrToString("ID3D12Device::CreateCommittedResource(upload)", hr); + return false; + } + + std::uint8_t* mappedData = nullptr; + hr = uploadBuffer->Map(0u, nullptr, reinterpret_cast(&mappedData)); + if (FAILED(hr) || mappedData == nullptr) { + outError = HrToString("ID3D12Resource::Map(upload)", hr); + return false; + } + + for (std::uint32_t row = 0u; row < height; ++row) { + std::memcpy( + mappedData + static_cast(row) * alignedRowPitch, + rgbaPixels + static_cast(row) * resolvedRowPitch, + static_cast(width) * kTextureBytesPerPixel); + } + uploadBuffer->Unmap(0u, nullptr); + + ID3D12GraphicsCommandList* nativeCommandList = d3d12CommandList->GetCommandList(); + const ::XCEngine::RHI::ResourceStates originalState = d3d12Texture->GetState(); + if (originalState != ::XCEngine::RHI::ResourceStates::CopyDst) { + D3D12_RESOURCE_BARRIER toCopyBarrier = {}; + toCopyBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + toCopyBarrier.Transition.pResource = d3d12Texture->GetResource(); + toCopyBarrier.Transition.StateBefore = ::XCEngine::RHI::ToD3D12(originalState); + toCopyBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + toCopyBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + nativeCommandList->ResourceBarrier(1u, &toCopyBarrier); + d3d12Texture->SetState(::XCEngine::RHI::ResourceStates::CopyDst); + } + + D3D12_TEXTURE_COPY_LOCATION dstLocation = {}; + dstLocation.pResource = d3d12Texture->GetResource(); + dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLocation.SubresourceIndex = 0u; + + D3D12_TEXTURE_COPY_LOCATION srcLocation = {}; + srcLocation.pResource = uploadBuffer.Get(); + srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLocation.PlacedFootprint.Offset = 0u; + srcLocation.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srcLocation.PlacedFootprint.Footprint.Width = width; + srcLocation.PlacedFootprint.Footprint.Height = height; + srcLocation.PlacedFootprint.Footprint.Depth = 1u; + srcLocation.PlacedFootprint.Footprint.RowPitch = alignedRowPitch; + + D3D12_BOX srcBox = {}; + srcBox.left = 0u; + srcBox.top = 0u; + srcBox.front = 0u; + srcBox.right = width; + srcBox.bottom = height; + srcBox.back = 1u; + nativeCommandList->CopyTextureRegion( + &dstLocation, + dstX, + dstY, + 0u, + &srcLocation, + &srcBox); + + if (originalState != ::XCEngine::RHI::ResourceStates::CopyDst) { + D3D12_RESOURCE_BARRIER restoreBarrier = {}; + restoreBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + restoreBarrier.Transition.pResource = d3d12Texture->GetResource(); + restoreBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + restoreBarrier.Transition.StateAfter = ::XCEngine::RHI::ToD3D12(originalState); + restoreBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + nativeCommandList->ResourceBarrier(1u, &restoreBarrier); + d3d12Texture->SetState(originalState); + } + + m_frameUploadBuffers[m_activeFrameSlot].push_back(std::move(uploadBuffer)); + return true; +} + +bool D3D12UiTextureHost::LoadTextureFromFile( + const std::filesystem::path& path, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + std::vector pixels = {}; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + if (!DecodeTextureFile(path, pixels, width, height, outError)) { + outTexture = {}; + m_lastError = outError; + return false; + } + + const bool created = CreateTextureResource( + pixels.data(), + width, + height, + outTexture, + outError); + m_lastError = created ? std::string() : outError; + return created; +} + +bool D3D12UiTextureHost::LoadTextureFromMemory( + const std::uint8_t* data, + std::size_t size, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + std::vector pixels = {}; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + if (!DecodeTextureMemory(data, size, pixels, width, height, outError)) { + outTexture = {}; + m_lastError = outError; + return false; + } + + const bool created = CreateTextureResource( + pixels.data(), + width, + height, + outTexture, + outError); + m_lastError = created ? std::string() : outError; + return created; +} + +bool D3D12UiTextureHost::LoadTextureFromRgba( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError) { + const bool created = CreateTextureResource( + rgbaPixels, + width, + height, + outTexture, + outError); + m_lastError = created ? std::string() : outError; + return created; +} + +void D3D12UiTextureHost::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { + if (!texture.IsValid()) { + texture = {}; + return; + } + + const auto found = m_liveTextures.find(texture.resourceHandle); + if (found != m_liveTextures.end() && found->second != nullptr) { + DestroyTextureResource(*found->second); + m_liveTextures.erase(found); + } + + texture = {}; +} + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.h b/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.h new file mode 100644 index 00000000..f9698467 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UiTextureHost.h @@ -0,0 +1,113 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "Ports/TexturePort.h" + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +class D3D12UiTextureHost final : public Ports::TexturePort { +public: + bool Initialize(D3D12WindowRenderer& windowRenderer); + void Shutdown(); + + bool IsInitialized() const; + const std::string& GetLastError() const; + void BeginFrame(std::uint32_t frameSlot); + + 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; + bool UpdateTextureRegionRgba( + const ::XCEngine::UI::UITextureHandle& texture, + std::uint32_t dstX, + std::uint32_t dstY, + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + std::uint32_t rowPitch, + std::string& outError); + void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) override; + +private: + struct TextureResource { + ::XCEngine::RHI::RHITexture* texture = nullptr; + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + }; + + bool EnsureWicFactory(std::string& outError); + bool DecodeTextureFile( + const std::filesystem::path& path, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError); + bool DecodeTextureMemory( + const std::uint8_t* data, + std::size_t size, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError); + bool DecodeTextureFrame( + IWICBitmapSource& source, + std::vector& outPixels, + std::uint32_t& outWidth, + std::uint32_t& outHeight, + std::string& outError); + bool CreateTextureResource( + const std::uint8_t* rgbaPixels, + std::uint32_t width, + std::uint32_t height, + ::XCEngine::UI::UITextureHandle& outTexture, + std::string& outError); + TextureResource* ResolveTextureResource(const ::XCEngine::UI::UITextureHandle& texture) const; + void DestroyTextureResource(TextureResource& textureResource); + + D3D12WindowRenderer* m_windowRenderer = nullptr; + Microsoft::WRL::ComPtr m_wicFactory = {}; + bool m_wicComInitialized = false; + std::array< + std::vector>, + D3D12WindowRenderer::kFrameContextCount> + m_frameUploadBuffers = {}; + std::uint32_t m_activeFrameSlot = 0u; + bool m_hasActiveFrameSlot = false; + std::unordered_map> m_liveTextures = {}; + std::string m_lastError = {}; +}; + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp index 9ba1fb86..e847982d 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp @@ -61,15 +61,7 @@ bool D3D12WindowInteropContext::Attach( } m_d2dFactory = &d2dFactory; - if (!EnsureInterop()) { - return false; - } - - if (HasBackBufferTargets()) { - return true; - } - - return RebuildBackBufferTargets(); + return EnsureInterop(); } void D3D12WindowInteropContext::Detach() { @@ -95,10 +87,6 @@ ID2D1DeviceContext* D3D12WindowInteropContext::GetD2DDeviceContext() const { return m_d2dDeviceContext.Get(); } -ID2D1SolidColorBrush* D3D12WindowInteropContext::GetInteropBrush() const { - return m_interopBrush.Get(); -} - const std::string& D3D12WindowInteropContext::GetLastError() const { return m_lastError; } @@ -113,8 +101,7 @@ bool D3D12WindowInteropContext::EnsureInterop() { return false; } if (m_d3d11On12Device != nullptr && - m_d2dDeviceContext != nullptr && - m_interopBrush != nullptr) { + m_d2dDeviceContext != nullptr) { return true; } @@ -207,15 +194,6 @@ bool D3D12WindowInteropContext::EnsureInterop() { return false; } - hr = m_d2dDeviceContext->CreateSolidColorBrush( - D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), - m_interopBrush.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_interopBrush == nullptr) { - m_lastError = HrToInteropString("ID2D1DeviceContext::CreateSolidColorBrush", hr); - ReleaseInteropState(); - return false; - } - m_d2dDeviceContext->SetDpi(96.0f, 96.0f); m_d2dDeviceContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); m_lastError.clear(); @@ -223,134 +201,19 @@ bool D3D12WindowInteropContext::EnsureInterop() { } void D3D12WindowInteropContext::ReleaseInteropState() { - ReleaseBackBufferTargets(); - m_interopBrush.Reset(); - m_d2dDeviceContext.Reset(); - m_d2dDevice.Reset(); - m_d3d11On12Device.Reset(); - m_d3d11DeviceContext.Reset(); - m_d3d11Device.Reset(); -} - -void D3D12WindowInteropContext::ReleaseBackBufferTargets() { ClearSourceTextures(); if (m_d2dDeviceContext != nullptr) { m_d2dDeviceContext->SetTarget(nullptr); } if (m_d3d11DeviceContext != nullptr) { m_d3d11DeviceContext->ClearState(); - } - m_backBufferTargets.clear(); - if (m_d2dDeviceContext != nullptr) { - D2D1_TAG firstTag = 0u; - D2D1_TAG secondTag = 0u; - m_d2dDeviceContext->Flush(&firstTag, &secondTag); - } - if (m_d3d11DeviceContext != nullptr) { m_d3d11DeviceContext->Flush(); } -} - -bool D3D12WindowInteropContext::RebuildBackBufferTargets() { - m_backBufferTargets.clear(); - if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) { - return false; - } - - const std::uint32_t backBufferCount = m_windowRenderer->GetBackBufferCount(); - m_backBufferTargets.resize(backBufferCount); - for (std::uint32_t index = 0u; index < backBufferCount; ++index) { - const ::XCEngine::RHI::D3D12Texture* backBufferTexture = - m_windowRenderer->GetBackBufferTexture(index); - if (backBufferTexture == nullptr || backBufferTexture->GetResource() == nullptr) { - m_lastError = "Failed to resolve a D3D12 swap chain back buffer."; - m_backBufferTargets.clear(); - return false; - } - - D3D11_RESOURCE_FLAGS resourceFlags = {}; - resourceFlags.BindFlags = D3D11_BIND_RENDER_TARGET; - HRESULT hr = m_d3d11On12Device->CreateWrappedResource( - backBufferTexture->GetResource(), - &resourceFlags, - D3D12_RESOURCE_STATE_RENDER_TARGET, - D3D12_RESOURCE_STATE_PRESENT, - IID_PPV_ARGS(m_backBufferTargets[index].wrappedResource.ReleaseAndGetAddressOf())); - if (FAILED(hr) || m_backBufferTargets[index].wrappedResource == nullptr) { - m_lastError = HrToInteropString("ID3D11On12Device::CreateWrappedResource(backbuffer)", hr); - m_backBufferTargets.clear(); - return false; - } - - Microsoft::WRL::ComPtr dxgiSurface = {}; - hr = m_backBufferTargets[index].wrappedResource.As(&dxgiSurface); - if (FAILED(hr) || dxgiSurface == nullptr) { - m_lastError = HrToInteropString("ID3D11Resource::QueryInterface(IDXGISurface)", hr); - m_backBufferTargets.clear(); - return false; - } - - const D2D1_BITMAP_PROPERTIES1 bitmapProperties = - BuildD2DBitmapProperties( - backBufferTexture->GetDesc().Format, - D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW); - hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface( - dxgiSurface.Get(), - &bitmapProperties, - m_backBufferTargets[index].targetBitmap.ReleaseAndGetAddressOf()); - if (FAILED(hr) || m_backBufferTargets[index].targetBitmap == nullptr) { - m_lastError = HrToInteropString( - "ID2D1DeviceContext::CreateBitmapFromDxgiSurface(backbuffer)", - hr); - m_backBufferTargets.clear(); - return false; - } - } - - m_lastError.clear(); - return true; -} - -bool D3D12WindowInteropContext::HasAttachedWindowRenderer() const { - return m_windowRenderer != nullptr && - m_d3d11On12Device != nullptr && - m_d2dDeviceContext != nullptr && - !m_backBufferTargets.empty(); -} - -bool D3D12WindowInteropContext::HasBackBufferTargets() const { - return !m_backBufferTargets.empty(); -} - -void D3D12WindowInteropContext::BuildAcquiredResources( - std::uint32_t backBufferIndex, - std::vector& outResources) const { - outResources.clear(); - - ID3D11Resource* backBufferResource = GetWrappedBackBufferResource(backBufferIndex); - if (backBufferResource != nullptr) { - outResources.push_back(backBufferResource); - } - - for (const SourceTextureResource& resource : m_activeSourceTextures) { - if (resource.wrappedResource != nullptr) { - outResources.push_back(resource.wrappedResource.Get()); - } - } -} - -ID3D11Resource* D3D12WindowInteropContext::GetWrappedBackBufferResource(std::uint32_t index) const { - return index < m_backBufferTargets.size() ? m_backBufferTargets[index].wrappedResource.Get() : nullptr; -} - -ID2D1Bitmap1* D3D12WindowInteropContext::GetBackBufferTargetBitmap(std::uint32_t index) const { - return index < m_backBufferTargets.size() ? m_backBufferTargets[index].targetBitmap.Get() : nullptr; -} - -std::uint32_t D3D12WindowInteropContext::GetCurrentBackBufferIndex() const { - return m_windowRenderer != nullptr && m_windowRenderer->GetSwapChain() != nullptr - ? m_windowRenderer->GetSwapChain()->GetCurrentBackBufferIndex() - : 0u; + m_d2dDeviceContext.Reset(); + m_d2dDevice.Reset(); + m_d3d11On12Device.Reset(); + m_d3d11DeviceContext.Reset(); + m_d3d11Device.Reset(); } bool D3D12WindowInteropContext::PrepareSourceTextures( diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h index 7112b431..dced2112 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h @@ -24,10 +24,6 @@ class D3D12WindowInteropContext { public: bool Attach(D3D12WindowRenderer& windowRenderer, ID2D1Factory1& d2dFactory); void Detach(); - void ReleaseBackBufferTargets(); - bool RebuildBackBufferTargets(); - bool HasAttachedWindowRenderer() const; - bool HasBackBufferTargets() const; bool PrepareSourceTextures(const ::XCEngine::UI::UIDrawData& drawData); void ClearSourceTextures(); bool ResolveInteropBitmap( @@ -38,21 +34,9 @@ public: ID3D11On12Device* GetD3D11On12Device() const; ID3D11DeviceContext* GetD3D11DeviceContext() const; ID2D1DeviceContext* GetD2DDeviceContext() const; - ID2D1SolidColorBrush* GetInteropBrush() const; - void BuildAcquiredResources( - std::uint32_t backBufferIndex, - std::vector& outResources) const; - ID3D11Resource* GetWrappedBackBufferResource(std::uint32_t index) const; - ID2D1Bitmap1* GetBackBufferTargetBitmap(std::uint32_t index) const; - std::uint32_t GetCurrentBackBufferIndex() const; const std::string& GetLastError() const; private: - struct BackBufferTarget { - Microsoft::WRL::ComPtr wrappedResource = {}; - Microsoft::WRL::ComPtr targetBitmap = {}; - }; - struct SourceTextureResource { std::uintptr_t key = 0u; Microsoft::WRL::ComPtr wrappedResource = {}; @@ -69,8 +53,6 @@ private: Microsoft::WRL::ComPtr m_d3d11On12Device = {}; Microsoft::WRL::ComPtr m_d2dDevice = {}; Microsoft::WRL::ComPtr m_d2dDeviceContext = {}; - Microsoft::WRL::ComPtr m_interopBrush = {}; - std::vector m_backBufferTargets = {}; std::vector m_activeSourceTextures = {}; std::unordered_map> m_activeBitmaps = {}; std::string m_lastError = {}; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp index 04e09832..122811d5 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp @@ -3,28 +3,22 @@ namespace XCEngine::UI::Editor::Host { D3D12WindowRenderLoopAttachResult D3D12WindowRenderLoop::Attach( - NativeRenderer& uiRenderer, + D3D12UiRenderer& uiRenderer, D3D12WindowRenderer& windowRenderer) { m_uiRenderer = &uiRenderer; m_windowRenderer = &windowRenderer; D3D12WindowRenderLoopAttachResult result = {}; - result.hasViewportSurfacePresentation = m_uiRenderer->AttachWindowRenderer(*m_windowRenderer); + result.hasViewportSurfacePresentation = + m_windowRenderer->GetSwapChain() != nullptr && + m_windowRenderer->GetRHIDevice() != nullptr; if (!result.hasViewportSurfacePresentation) { - const std::string& interopError = m_uiRenderer->GetLastRenderError(); - result.interopWarning = interopError.empty() - ? "native renderer d3d12 interop unavailable; falling back to hwnd renderer." - : "native renderer d3d12 interop unavailable; falling back to hwnd renderer: " + - interopError; + result.warning = "window render loop attach requires an initialized D3D12 window renderer."; } return result; } void D3D12WindowRenderLoop::Detach() { - if (m_uiRenderer != nullptr) { - m_uiRenderer->DetachWindowRenderer(); - } - m_uiRenderer = nullptr; m_windowRenderer = nullptr; } @@ -51,16 +45,10 @@ D3D12WindowRenderLoopFrameContext D3D12WindowRenderLoop::BeginFrame() const { D3D12WindowRenderLoopResizeResult D3D12WindowRenderLoop::ApplyResize(UINT width, UINT height) { D3D12WindowRenderLoopResizeResult result = {}; if (m_uiRenderer == nullptr || m_windowRenderer == nullptr) { - result.interopWarning = "window render loop is detached."; + result.windowRendererWarning = "window render loop is detached."; return result; } - m_uiRenderer->Resize(width, height); - const bool hadViewportSurfacePresentation = m_uiRenderer->HasAttachedWindowRenderer(); - if (hadViewportSurfacePresentation) { - m_uiRenderer->ReleaseWindowRendererBackBufferTargets(); - } - const bool resizedWindowRenderer = m_windowRenderer->Resize(static_cast(width), static_cast(height)); if (!resizedWindowRenderer || !m_windowRenderer->GetLastError().empty()) { @@ -70,36 +58,10 @@ D3D12WindowRenderLoopResizeResult D3D12WindowRenderLoop::ApplyResize(UINT width, : "window renderer resize warning: " + resizeError; } - if (!resizedWindowRenderer) { - if (hadViewportSurfacePresentation) { - result.hasViewportSurfacePresentation = - m_uiRenderer->RebuildWindowRendererBackBufferTargets(); - if (!result.hasViewportSurfacePresentation) { - const D3D12WindowRenderLoopAttachResult attachResult = - Attach(*m_uiRenderer, *m_windowRenderer); - result.hasViewportSurfacePresentation = attachResult.hasViewportSurfacePresentation; - result.interopWarning = attachResult.interopWarning; - } - } - return result; - } - - if (hadViewportSurfacePresentation) { - result.hasViewportSurfacePresentation = - m_uiRenderer->RebuildWindowRendererBackBufferTargets(); - if (!result.hasViewportSurfacePresentation) { - const D3D12WindowRenderLoopAttachResult attachResult = - Attach(*m_uiRenderer, *m_windowRenderer); - result.hasViewportSurfacePresentation = attachResult.hasViewportSurfacePresentation; - result.interopWarning = attachResult.interopWarning; - } - return result; - } - - const D3D12WindowRenderLoopAttachResult attachResult = - Attach(*m_uiRenderer, *m_windowRenderer); - result.hasViewportSurfacePresentation = attachResult.hasViewportSurfacePresentation; - result.interopWarning = attachResult.interopWarning; + result.hasViewportSurfacePresentation = + resizedWindowRenderer && + m_windowRenderer->GetSwapChain() != nullptr && + m_windowRenderer->GetCurrentRenderSurface() != nullptr; return result; } @@ -111,31 +73,77 @@ D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( return result; } - if (HasViewportSurfacePresentation()) { - result.framePresented = m_uiRenderer->RenderToWindowRenderer(drawData); - if (!result.framePresented) { - const std::string& composeError = m_uiRenderer->GetLastRenderError(); - result.warning = composeError.empty() - ? "d3d12 window composition failed, falling back to hwnd renderer." - : "d3d12 window composition failed, falling back to hwnd renderer: " + - composeError; - } + if (!HasViewportSurfacePresentation()) { + result.warning = "window render loop has no active D3D12 presentation path."; + return result; } - if (!result.framePresented) { - result.framePresented = m_uiRenderer->Render(drawData); - if (!result.framePresented && result.warning.empty()) { - result.warning = m_uiRenderer->GetLastRenderError(); - } + if (!m_windowRenderer->PrepareCurrentBackBufferForUiRender()) { + const std::string& error = m_windowRenderer->GetLastError(); + result.warning = error.empty() + ? "failed to prepare the current back buffer for UI rendering." + : error; + return result; } + const ::XCEngine::Rendering::RenderContext renderContext = + m_windowRenderer->GetRenderContext(); + const ::XCEngine::Rendering::RenderSurface* surface = + m_windowRenderer->GetCurrentRenderSurface(); + if (surface == nullptr) { + result.warning = "window render loop could not resolve the current back buffer surface."; + return result; + } + + if (!m_uiRenderer->Render(drawData, renderContext, *surface)) { + const std::string& error = m_uiRenderer->GetLastError(); + result.warning = error.empty() + ? "d3d12 ui renderer failed to render the current frame." + : error; + return result; + } + + if (!m_windowRenderer->FinalizeCurrentBackBufferForPresent()) { + const std::string& error = m_windowRenderer->GetLastError(); + result.warning = error.empty() + ? "failed to finalize the current back buffer for present." + : error; + return result; + } + + if (!m_windowRenderer->SubmitFrame()) { + const std::string& error = m_windowRenderer->GetLastError(); + result.warning = error.empty() + ? "failed to submit the current D3D12 frame." + : error; + return result; + } + + if (!m_windowRenderer->SignalFrameCompletion()) { + const std::string& error = m_windowRenderer->GetLastError(); + result.warning = error.empty() + ? "failed to signal current frame completion." + : error; + return result; + } + + if (!m_windowRenderer->PresentFrame()) { + const std::string& error = m_windowRenderer->GetLastError(); + result.warning = error.empty() + ? "failed to present the swap chain." + : error; + return result; + } + + result.framePresented = true; return result; } bool D3D12WindowRenderLoop::HasViewportSurfacePresentation() const { return m_uiRenderer != nullptr && m_windowRenderer != nullptr && - m_uiRenderer->HasAttachedWindowRenderer(); + m_windowRenderer->GetSwapChain() != nullptr && + m_windowRenderer->GetCurrentRenderSurface() != nullptr; } } // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h index f8dd4655..081695cd 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include @@ -11,7 +11,7 @@ namespace XCEngine::UI::Editor::Host { struct D3D12WindowRenderLoopAttachResult { bool hasViewportSurfacePresentation = false; - std::string interopWarning = {}; + std::string warning = {}; }; struct D3D12WindowRenderLoopFrameContext { @@ -23,7 +23,6 @@ struct D3D12WindowRenderLoopFrameContext { struct D3D12WindowRenderLoopResizeResult { bool hasViewportSurfacePresentation = false; std::string windowRendererWarning = {}; - std::string interopWarning = {}; }; struct D3D12WindowRenderLoopPresentResult { @@ -34,7 +33,7 @@ struct D3D12WindowRenderLoopPresentResult { class D3D12WindowRenderLoop { public: D3D12WindowRenderLoopAttachResult Attach( - NativeRenderer& uiRenderer, + D3D12UiRenderer& uiRenderer, D3D12WindowRenderer& windowRenderer); void Detach(); @@ -46,7 +45,7 @@ public: bool HasViewportSurfacePresentation() const; private: - NativeRenderer* m_uiRenderer = nullptr; + D3D12UiRenderer* m_uiRenderer = nullptr; D3D12WindowRenderer* m_windowRenderer = nullptr; }; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp index e785bbf0..155780e2 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp @@ -27,8 +27,8 @@ bool D3D12WindowRenderer::Initialize(HWND hwnd, int width, int height) { auto* device = m_hostDevice.GetRHIDevice(); if (device == nullptr || - !m_viewportTextureAllocator.Initialize(*device)) { - m_lastError = "Failed to initialize the viewport texture allocator."; + !m_textureAllocator.Initialize(*device, 512u)) { + m_lastError = "Failed to initialize the UI texture descriptor allocator."; Shutdown(); return false; } @@ -38,8 +38,8 @@ bool D3D12WindowRenderer::Initialize(HWND hwnd, int width, int height) { } void D3D12WindowRenderer::Shutdown() { - m_viewportTextureCpuHandles.clear(); - m_viewportTextureAllocator.Shutdown(); + m_textureCpuHandles.clear(); + m_textureAllocator.Shutdown(); m_presenter.Shutdown(); m_hostDevice.Shutdown(); m_activeFrameSlot = 0u; @@ -84,35 +84,36 @@ bool D3D12WindowRenderer::BeginFrame() { return true; } -bool D3D12WindowRenderer::PreparePresentSurface() { +bool D3D12WindowRenderer::PrepareCurrentBackBufferForUiRender() { ::XCEngine::Rendering::RenderContext renderContext = GetRenderContext(); if (!renderContext.IsValid() || renderContext.commandList == nullptr) { - m_lastError = "PreparePresentSurface requires a valid render context."; + m_lastError = "PrepareCurrentBackBufferForUiRender requires a valid render context."; return false; } - const bool prepared = m_presenter.PreparePresentSurface(renderContext); + const bool prepared = m_presenter.PrepareCurrentBackBufferForRender(renderContext); m_lastError = prepared ? std::string() : m_presenter.GetLastError(); return prepared; } -bool D3D12WindowRenderer::SubmitFrame(bool presentSwapChain) { - if (presentSwapChain && m_presenter.GetSwapChain() == nullptr) { - m_lastError = "SubmitFrame requested present without a swap chain."; +bool D3D12WindowRenderer::FinalizeCurrentBackBufferForPresent() { + ::XCEngine::Rendering::RenderContext renderContext = GetRenderContext(); + if (!renderContext.IsValid() || renderContext.commandList == nullptr) { + m_lastError = "FinalizeCurrentBackBufferForPresent requires a valid render context."; return false; } + const bool finalized = m_presenter.FinalizeCurrentBackBufferForPresent(renderContext); + m_lastError = finalized ? std::string() : m_presenter.GetLastError(); + return finalized; +} + +bool D3D12WindowRenderer::SubmitFrame() { if (!m_hostDevice.SubmitFrame(m_activeFrameSlot)) { m_lastError = m_hostDevice.GetLastError(); return false; } - if (presentSwapChain) { - if (!m_presenter.PresentFrame()) { - m_lastError = m_presenter.GetLastError(); - return false; - } - } m_lastError.clear(); return true; } @@ -157,13 +158,13 @@ bool D3D12WindowRenderer::CreateViewportTextureHandle( outTexture = {}; if (width == 0u || height == 0u || - !m_viewportTextureAllocator.IsInitialized()) { + !m_textureAllocator.IsInitialized()) { return false; } D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; - if (!m_viewportTextureAllocator.CreateTextureDescriptor( + if (!m_textureAllocator.CreateTextureDescriptor( &texture, &cpuHandle, &gpuHandle) || @@ -177,7 +178,7 @@ bool D3D12WindowRenderer::CreateViewportTextureHandle( outTexture.height = height; outTexture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView; outTexture.resourceHandle = reinterpret_cast(&texture); - m_viewportTextureCpuHandles[outTexture.nativeHandle] = cpuHandle; + m_textureCpuHandles[outTexture.nativeHandle] = cpuHandle; return true; } @@ -188,12 +189,12 @@ void D3D12WindowRenderer::ReleaseViewportTextureHandle( return; } - const auto found = m_viewportTextureCpuHandles.find(texture.nativeHandle); - if (found != m_viewportTextureCpuHandles.end()) { + const auto found = m_textureCpuHandles.find(texture.nativeHandle); + if (found != m_textureCpuHandles.end()) { D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; gpuHandle.ptr = static_cast(texture.nativeHandle); - m_viewportTextureAllocator.Free(found->second, gpuHandle); - m_viewportTextureCpuHandles.erase(found); + m_textureAllocator.Free(found->second, gpuHandle); + m_textureCpuHandles.erase(found); } texture = {}; @@ -219,8 +220,22 @@ std::uint32_t D3D12WindowRenderer::GetBackBufferCount() const { return m_presenter.GetBackBufferCount(); } +std::uint32_t D3D12WindowRenderer::GetActiveFrameSlot() const { + return m_activeFrameSlot; +} + ::XCEngine::Rendering::RenderContext D3D12WindowRenderer::GetRenderContext() const { return m_hostDevice.GetRenderContext(m_activeFrameSlot); } +D3D12ShaderResourceDescriptorAllocator& +D3D12WindowRenderer::GetTextureDescriptorAllocator() { + return m_textureAllocator; +} + +const D3D12ShaderResourceDescriptorAllocator& +D3D12WindowRenderer::GetTextureDescriptorAllocator() const { + return m_textureAllocator; +} + } // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h index 6fc7997a..cc2d1e73 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h @@ -32,8 +32,9 @@ public: void Shutdown(); bool Resize(int width, int height); bool BeginFrame(); - bool PreparePresentSurface(); - bool SubmitFrame(bool presentSwapChain); + bool PrepareCurrentBackBufferForUiRender(); + bool FinalizeCurrentBackBufferForPresent(); + bool SubmitFrame(); bool SignalFrameCompletion(); bool PresentFrame(); @@ -53,16 +54,19 @@ public: const ::XCEngine::RHI::D3D12Texture* GetCurrentBackBufferTexture() const; const ::XCEngine::RHI::D3D12Texture* GetBackBufferTexture(std::uint32_t index) const; std::uint32_t GetBackBufferCount() const; + std::uint32_t GetActiveFrameSlot() const; ::XCEngine::Rendering::RenderContext GetRenderContext() const; + D3D12ShaderResourceDescriptorAllocator& GetTextureDescriptorAllocator(); + const D3D12ShaderResourceDescriptorAllocator& GetTextureDescriptorAllocator() const; private: D3D12HostDevice m_hostDevice = {}; D3D12WindowSwapChainPresenter m_presenter = {}; - D3D12ShaderResourceDescriptorAllocator m_viewportTextureAllocator = {}; + D3D12ShaderResourceDescriptorAllocator m_textureAllocator = {}; std::uint32_t m_activeFrameSlot = 0u; std::uint32_t m_nextFrameSlot = 0u; std::uint32_t m_activeBackBufferIndex = 0u; - std::unordered_map m_viewportTextureCpuHandles = {}; + std::unordered_map m_textureCpuHandles = {}; std::string m_lastError = {}; }; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp index aa568999..3b3fbff5 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp @@ -240,15 +240,15 @@ bool D3D12WindowSwapChainPresenter::RecreateBackBufferViews() { return true; } -bool D3D12WindowSwapChainPresenter::PreparePresentSurface( +bool D3D12WindowSwapChainPresenter::PrepareCurrentBackBufferForRender( const ::XCEngine::Rendering::RenderContext& renderContext) { if (!renderContext.IsValid() || renderContext.commandList == nullptr) { - m_lastError = "PreparePresentSurface requires a valid render context."; + m_lastError = "PrepareCurrentBackBufferForRender requires a valid render context."; return false; } if (m_swapChain == nullptr) { - m_lastError = "PreparePresentSurface requires an initialized swap chain."; + m_lastError = "PrepareCurrentBackBufferForRender requires an initialized swap chain."; return false; } @@ -256,7 +256,7 @@ bool D3D12WindowSwapChainPresenter::PreparePresentSurface( if (backBufferIndex >= m_backBufferViews.size() || m_backBufferViews[backBufferIndex] == nullptr) { std::ostringstream error = {}; - error << "PreparePresentSurface could not find the current swap chain RTV. index=" + error << "PrepareCurrentBackBufferForRender could not find the current swap chain RTV. index=" << backBufferIndex << " rtvCount=" << m_backBufferViews.size(); @@ -272,6 +272,38 @@ bool D3D12WindowSwapChainPresenter::PreparePresentSurface( return true; } +bool D3D12WindowSwapChainPresenter::FinalizeCurrentBackBufferForPresent( + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.commandList == nullptr) { + m_lastError = "FinalizeCurrentBackBufferForPresent requires a valid render context."; + return false; + } + + if (m_swapChain == nullptr) { + m_lastError = "FinalizeCurrentBackBufferForPresent requires an initialized swap chain."; + return false; + } + + const std::uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); + if (backBufferIndex >= m_backBufferViews.size() || + m_backBufferViews[backBufferIndex] == nullptr) { + std::ostringstream error = {}; + error << "FinalizeCurrentBackBufferForPresent could not find the current swap chain RTV. index=" + << backBufferIndex + << " rtvCount=" + << m_backBufferViews.size(); + m_lastError = error.str(); + return false; + } + + renderContext.commandList->TransitionBarrier( + m_backBufferViews[backBufferIndex], + ::XCEngine::RHI::ResourceStates::RenderTarget, + ::XCEngine::RHI::ResourceStates::Present); + m_lastError.clear(); + return true; +} + bool D3D12WindowSwapChainPresenter::PresentFrame() { if (m_swapChain == nullptr) { m_lastError = "PresentFrame requires an initialized swap chain."; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h index 8faef2b8..d8afa7f8 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h @@ -28,7 +28,10 @@ public: bool Initialize(D3D12HostDevice& hostDevice, HWND hwnd, int width, int height); void Shutdown(); bool Resize(int width, int height); - bool PreparePresentSurface(const ::XCEngine::Rendering::RenderContext& renderContext); + bool PrepareCurrentBackBufferForRender( + const ::XCEngine::Rendering::RenderContext& renderContext); + bool FinalizeCurrentBackBufferForPresent( + const ::XCEngine::Rendering::RenderContext& renderContext); bool PresentFrame(); const std::string& GetLastError() const; diff --git a/new_editor/app/Rendering/Native/AutoScreenshot.cpp b/new_editor/app/Rendering/Native/AutoScreenshot.cpp index 33af907e..c372ed91 100644 --- a/new_editor/app/Rendering/Native/AutoScreenshot.cpp +++ b/new_editor/app/Rendering/Native/AutoScreenshot.cpp @@ -51,6 +51,7 @@ void AutoScreenshotController::RequestCapture(std::string reason) { void AutoScreenshotController::CaptureIfRequested( NativeRenderer& renderer, + D3D12WindowRenderer& windowRenderer, const ::XCEngine::UI::UIDrawData& drawData, unsigned int width, unsigned int height, @@ -82,7 +83,13 @@ void AutoScreenshotController::CaptureIfRequested( std::string captureError = {}; const std::filesystem::path historyPath = BuildHistoryCapturePath(m_pendingReason); - if (!renderer.CaptureToPng(drawData, width, height, historyPath, captureError)) { + if (!renderer.CaptureToPng( + &windowRenderer, + drawData, + width, + height, + historyPath, + captureError)) { m_lastCaptureError = std::move(captureError); m_lastCaptureSummary = "AutoShot failed"; m_capturePending = false; diff --git a/new_editor/app/Rendering/Native/AutoScreenshot.h b/new_editor/app/Rendering/Native/AutoScreenshot.h index cb04e5f3..b5dd7162 100644 --- a/new_editor/app/Rendering/Native/AutoScreenshot.h +++ b/new_editor/app/Rendering/Native/AutoScreenshot.h @@ -14,6 +14,7 @@ namespace XCEngine::UI::Editor::Host { class NativeRenderer; +class D3D12WindowRenderer; class AutoScreenshotController { public: @@ -23,6 +24,7 @@ public: void RequestCapture(std::string reason); void CaptureIfRequested( NativeRenderer& renderer, + D3D12WindowRenderer& windowRenderer, const ::XCEngine::UI::UIDrawData& drawData, unsigned int width, unsigned int height, diff --git a/new_editor/app/Rendering/Native/NativeRenderer.cpp b/new_editor/app/Rendering/Native/NativeRenderer.cpp index 620295ae..10475fa0 100644 --- a/new_editor/app/Rendering/Native/NativeRenderer.cpp +++ b/new_editor/app/Rendering/Native/NativeRenderer.cpp @@ -18,27 +18,9 @@ bool NativeRenderer::Initialize(HWND hwnd) { } m_hwnd = hwnd; - D2D1_FACTORY_OPTIONS factoryOptions = {}; -#ifdef _DEBUG - factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; -#endif - HRESULT hr = D2D1CreateFactory( - D2D1_FACTORY_TYPE_SINGLE_THREADED, - __uuidof(ID2D1Factory1), - &factoryOptions, - reinterpret_cast(m_d2dFactory.ReleaseAndGetAddressOf())); - if (FAILED(hr)) { - m_lastRenderError = HrToString("D2D1CreateFactory", hr); - Shutdown(); - return false; - } - - hr = DWriteCreateFactory( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof(IDWriteFactory), - reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); - if (FAILED(hr)) { - m_lastRenderError = HrToString("DWriteCreateFactory", hr); + std::string error = {}; + if (!EnsureCoreFactories(error)) { + m_lastRenderError = error; Shutdown(); return false; } @@ -48,7 +30,7 @@ bool NativeRenderer::Initialize(HWND hwnd) { } void NativeRenderer::Shutdown() { - DetachWindowRenderer(); + m_windowInterop.Detach(); while (!m_liveTextures.empty()) { auto it = m_liveTextures.begin(); delete *it; @@ -141,121 +123,42 @@ bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) { return true; } -bool NativeRenderer::RenderToWindowRenderer(const ::XCEngine::UI::UIDrawData& drawData) { - if (!EnsureWindowRendererInterop()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "Window renderer interop is not available."; +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; } - return false; } - if (!m_windowInterop.HasBackBufferTargets() && - !m_windowInterop.RebuildBackBufferTargets()) { - if (m_lastRenderError.empty()) { - m_lastRenderError = "Window renderer back buffer interop targets are unavailable."; + 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 false; } - ID3D11On12Device* d3d11On12Device = m_windowInterop.GetD3D11On12Device(); - ID3D11DeviceContext* d3d11DeviceContext = m_windowInterop.GetD3D11DeviceContext(); - ID2D1DeviceContext* d2dDeviceContext = m_windowInterop.GetD2DDeviceContext(); - ID2D1SolidColorBrush* interopBrush = m_windowInterop.GetInteropBrush(); - if (d3d11On12Device == nullptr || - d3d11DeviceContext == nullptr || - d2dDeviceContext == nullptr || - interopBrush == nullptr) { - m_lastRenderError = "Window renderer interop resources are incomplete."; - return false; - } - - const std::uint32_t backBufferIndex = m_windowInterop.GetCurrentBackBufferIndex(); - if (m_windowInterop.GetWrappedBackBufferResource(backBufferIndex) == nullptr || - m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex) == nullptr) { - m_lastRenderError = "Back buffer interop target index is out of range."; - return false; - } - - if (!m_windowRenderer->PreparePresentSurface()) { - m_lastRenderError = "Failed to prepare the D3D12 present surface: " + - m_windowRenderer->GetLastError(); - return false; - } - - if (!m_windowRenderer->SubmitFrame(false)) { - m_lastRenderError = "Failed to submit the D3D12 frame before UI composition."; - return false; - } - - if (!m_windowInterop.PrepareSourceTextures(drawData)) { - ID3D11Resource* backBufferResource = - m_windowInterop.GetWrappedBackBufferResource(backBufferIndex); - if (backBufferResource != nullptr) { - d3d11On12Device->AcquireWrappedResources(&backBufferResource, 1u); - d3d11On12Device->ReleaseWrappedResources(&backBufferResource, 1u); - } - d3d11DeviceContext->Flush(); - m_windowInterop.ClearSourceTextures(); - const bool signaled = m_windowRenderer->SignalFrameCompletion(); - ReleaseWindowRendererInterop(); - if (!signaled) { - m_lastRenderError = - "Failed to signal D3D12 frame completion after interop preparation failed."; - } - return false; - } - - std::vector acquiredResources = {}; - m_windowInterop.BuildAcquiredResources(backBufferIndex, acquiredResources); - if (acquiredResources.empty()) { - m_lastRenderError = "No wrapped interop resources were prepared for UI composition."; - return false; - } - - d3d11On12Device->AcquireWrappedResources( - acquiredResources.data(), - static_cast(acquiredResources.size())); - - d2dDeviceContext->SetTarget(m_windowInterop.GetBackBufferTargetBitmap(backBufferIndex)); - const bool rendered = RenderToTarget(*d2dDeviceContext, *interopBrush, drawData); - const HRESULT hr = d2dDeviceContext->EndDraw(); - - d3d11On12Device->ReleaseWrappedResources( - acquiredResources.data(), - static_cast(acquiredResources.size())); - d3d11DeviceContext->Flush(); - d2dDeviceContext->SetTarget(nullptr); - m_windowInterop.ClearSourceTextures(); - - if (!rendered || FAILED(hr)) { - m_lastRenderError = FAILED(hr) - ? HrToString("ID2D1DeviceContext::EndDraw", hr) - : "RenderToTarget failed during D3D11On12 composition."; - const bool signaled = m_windowRenderer->SignalFrameCompletion(); - if (hr == D2DERR_RECREATE_TARGET) { - ReleaseWindowRendererBackBufferTargets(); - } else { - ReleaseWindowRendererInterop(); - } - if (!signaled) { - m_lastRenderError = - "Failed to signal D3D12 frame completion after UI composition failed."; - } - return false; - } - - if (!m_windowRenderer->SignalFrameCompletion()) { - m_lastRenderError = "Failed to signal D3D12 frame completion after UI composition."; - ReleaseWindowRendererInterop(); - return false; - } - if (!m_windowRenderer->PresentFrame()) { - m_lastRenderError = "Failed to present the D3D12 swap chain."; - ReleaseWindowRendererInterop(); - return false; - } - - m_lastRenderError.clear(); return true; } @@ -688,52 +591,6 @@ namespace XCEngine::UI::Editor::Host { using namespace NativeRendererHelpers; -float NativeRenderer::MeasureTextWidth( - const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const { - if (!m_dwriteFactory || request.text.empty()) { - return 0.0f; - } - - const std::wstring text = Utf8ToWide(request.text); - if (text.empty()) { - return 0.0f; - } - - const float dpiScale = ClampDpiScale(m_dpiScale); - const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale; - IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); - if (textFormat == nullptr) { - return 0.0f; - } - - Microsoft::WRL::ComPtr textLayout; - HRESULT hr = m_dwriteFactory->CreateTextLayout( - text.c_str(), - static_cast(text.size()), - textFormat, - 4096.0f, - scaledFontSize * 2.0f, - textLayout.ReleaseAndGetAddressOf()); - if (FAILED(hr) || !textLayout) { - return 0.0f; - } - - DWRITE_TEXT_METRICS textMetrics = {}; - hr = textLayout->GetMetrics(&textMetrics); - if (FAILED(hr)) { - return 0.0f; - } - - DWRITE_OVERHANG_METRICS overhangMetrics = {}; - float width = textMetrics.widthIncludingTrailingWhitespace; - if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) { - width += (std::max)(overhangMetrics.left, 0.0f); - width += (std::max)(overhangMetrics.right, 0.0f); - } - - return std::ceil(width) / dpiScale; -} - IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const { if (!m_dwriteFactory) { return nullptr; @@ -1121,78 +978,8 @@ namespace XCEngine::UI::Editor::Host { using namespace NativeRendererHelpers; -bool NativeRenderer::AttachWindowRenderer(D3D12WindowRenderer& windowRenderer) { - if (m_windowRenderer != &windowRenderer) { - ReleaseWindowRendererInterop(); - m_windowRenderer = &windowRenderer; - } - - if (!EnsureWindowRendererInterop()) { - return false; - } - - DiscardRenderTarget(); - - if (m_windowInterop.HasBackBufferTargets()) { - return true; - } - - return m_windowInterop.RebuildBackBufferTargets(); -} - -void NativeRenderer::DetachWindowRenderer() { - ReleaseWindowRendererInterop(); - m_windowRenderer = nullptr; -} - -void NativeRenderer::ReleaseWindowRendererBackBufferTargets() { - m_windowInterop.ReleaseBackBufferTargets(); -} - -bool NativeRenderer::RebuildWindowRendererBackBufferTargets() { - if (!EnsureWindowRendererInterop()) { - return false; - } - - DiscardRenderTarget(); - ReleaseWindowRendererBackBufferTargets(); - return m_windowInterop.RebuildBackBufferTargets(); -} - -bool NativeRenderer::HasAttachedWindowRenderer() const { - return m_windowInterop.HasAttachedWindowRenderer(); -} - -bool NativeRenderer::EnsureWindowRendererInterop() { - if (m_windowRenderer == nullptr) { - m_lastRenderError = "EnsureWindowRendererInterop requires an attached D3D12 window renderer."; - return false; - } - if (m_d2dFactory == nullptr || m_dwriteFactory == nullptr) { - m_lastRenderError = "EnsureWindowRendererInterop requires initialized D2D and DWrite factories."; - return false; - } - - const bool attached = m_windowInterop.Attach(*m_windowRenderer, *m_d2dFactory.Get()); - if (!attached) { - m_lastRenderError = m_windowInterop.GetLastError(); - } else { - m_lastRenderError.clear(); - } - return attached; -} - -void NativeRenderer::ReleaseWindowRendererInterop() { - m_windowInterop.Detach(); -} - -} // 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, @@ -1204,8 +991,7 @@ bool NativeRenderer::CaptureToPng( return false; } - if (!m_d2dFactory || !m_dwriteFactory) { - outError = "CaptureToPng requires an initialized NativeRenderer."; + if (!EnsureCoreFactories(outError)) { return false; } @@ -1213,10 +999,34 @@ bool NativeRenderer::CaptureToPng( 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; } @@ -1245,6 +1055,10 @@ bool NativeRenderer::CaptureToPng( offscreenRenderTarget.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); + if (requiresInterop) { + m_windowInterop.ClearSourceTextures(); + m_windowInterop.Detach(); + } return false; } @@ -1254,11 +1068,19 @@ bool NativeRenderer::CaptureToPng( 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; diff --git a/new_editor/app/Rendering/Native/NativeRenderer.h b/new_editor/app/Rendering/Native/NativeRenderer.h index 623f439f..bcceeada 100644 --- a/new_editor/app/Rendering/Native/NativeRenderer.h +++ b/new_editor/app/Rendering/Native/NativeRenderer.h @@ -7,9 +7,6 @@ #include "Ports/TexturePort.h" #include -#include - -#include #include @@ -31,22 +28,16 @@ namespace XCEngine::UI::Editor::Host { -class NativeRenderer - : public Ports::TexturePort - , public ::XCEngine::UI::Editor::UIEditorTextMeasurer { +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 AttachWindowRenderer(D3D12WindowRenderer& windowRenderer); - void DetachWindowRenderer(); - void ReleaseWindowRendererBackBufferTargets(); - bool RebuildWindowRendererBackBufferTargets(); - bool HasAttachedWindowRenderer() const; bool Render(const ::XCEngine::UI::UIDrawData& drawData); - bool RenderToWindowRenderer(const ::XCEngine::UI::UIDrawData& drawData); const std::string& GetLastRenderError() const; bool LoadTextureFromFile( const std::filesystem::path& path, @@ -64,9 +55,8 @@ public: ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) override; void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) override; - float MeasureTextWidth( - const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override; bool CaptureToPng( + D3D12WindowRenderer* windowRenderer, const ::XCEngine::UI::UIDrawData& drawData, UINT width, UINT height, @@ -74,6 +64,7 @@ public: std::string& outError); private: + bool EnsureCoreFactories(std::string& outError); struct NativeTextureResource { std::vector pixels = {}; Microsoft::WRL::ComPtr cachedBitmap = {}; @@ -83,11 +74,9 @@ private: }; bool EnsureRenderTarget(); - bool EnsureWindowRendererInterop(); bool EnsureWicFactory(std::string& outError); void DiscardRenderTarget(); bool CreateDeviceResources(); - void ReleaseWindowRendererInterop(); void InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget); bool RenderToTarget( ID2D1RenderTarget& renderTarget, @@ -159,7 +148,6 @@ private: static std::wstring Utf8ToWide(std::string_view text); HWND m_hwnd = nullptr; - D3D12WindowRenderer* m_windowRenderer = nullptr; Microsoft::WRL::ComPtr m_d2dFactory; Microsoft::WRL::ComPtr m_dwriteFactory; Microsoft::WRL::ComPtr m_wicFactory; diff --git a/new_editor/app/Scene/EditorSceneBridge.cpp b/new_editor/app/Scene/EditorSceneBridge.cpp index d280d21a..43a90f9a 100644 --- a/new_editor/app/Scene/EditorSceneBridge.cpp +++ b/new_editor/app/Scene/EditorSceneBridge.cpp @@ -1,5 +1,6 @@ #include "Scene/EditorSceneBridge.h" +#include #include #include #include @@ -28,6 +29,10 @@ using ::XCEngine::Components::SceneManager; using ::XCEngine::Components::TransformComponent; using ::XCEngine::Resources::ResourceManager; +void TraceSceneStartup(std::string message) { + ::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace("startup", std::move(message)); +} + struct ClipboardNode { std::string name = {}; std::string transformPayload = {}; @@ -145,16 +150,19 @@ bool WouldCreateCycle( EditorStartupSceneResult EnsureEditorStartupScene( const std::filesystem::path& projectRoot) { EditorStartupSceneResult result = {}; + TraceSceneStartup("EnsureEditorStartupScene begin projectRoot=" + projectRoot.string()); if (projectRoot.empty()) { return result; } ResourceManager::Get().SetResourceRoot(projectRoot.string().c_str()); + TraceSceneStartup("ResourceManager::SetResourceRoot complete"); if (Scene* activeScene = ResolvePrimaryScene(); activeScene != nullptr) { result.ready = true; result.sceneName = activeScene->GetName(); + TraceSceneStartup("EnsureEditorStartupScene reused active scene=" + result.sceneName); return result; } @@ -164,7 +172,13 @@ EditorStartupSceneResult EnsureEditorStartupScene( if (std::filesystem::exists(startupScenePath) && std::filesystem::is_regular_file(startupScenePath)) { - sceneManager.LoadScene(startupScenePath.string()); + TraceSceneStartup("SceneManager::LoadScene begin path=" + startupScenePath.string()); + { + ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( + ResourceManager::Get()); + sceneManager.LoadScene(startupScenePath.string()); + } + TraceSceneStartup("SceneManager::LoadScene end"); Scene* loadedScene = sceneManager.GetScene(startupScenePath.stem().string()); if (loadedScene == nullptr) { loadedScene = ResolvePrimaryScene(); @@ -177,6 +191,7 @@ EditorStartupSceneResult EnsureEditorStartupScene( result.loadedFromDisk = true; result.scenePath = startupScenePath; result.sceneName = loadedScene->GetName(); + TraceSceneStartup("EnsureEditorStartupScene loaded scene=" + result.sceneName); return result; } } @@ -186,8 +201,12 @@ EditorStartupSceneResult EnsureEditorStartupScene( sceneManager.SetActiveScene(scene); result.ready = true; result.sceneName = scene->GetName(); + TraceSceneStartup("EnsureEditorStartupScene created scene=" + result.sceneName); } + TraceSceneStartup( + std::string("EnsureEditorStartupScene end ready=") + + (result.ready ? "1" : "0")); return result; } @@ -208,7 +227,11 @@ bool OpenEditorSceneAsset(const std::filesystem::path& scenePath) { } SceneManager& sceneManager = SceneManager::Get(); - sceneManager.LoadScene(scenePath.string()); + { + ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( + ResourceManager::Get()); + sceneManager.LoadScene(scenePath.string()); + } Scene* loadedScene = sceneManager.GetScene(scenePath.stem().string()); if (loadedScene == nullptr) { loadedScene = ResolvePrimaryScene(); diff --git a/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h b/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h index cb52cb0b..d97dd5be 100644 --- a/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h +++ b/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h @@ -70,6 +70,10 @@ UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( const Widgets::UIEditorDockHostLayout& dockHostLayout, const UIEditorPanelRegistry& panelRegistry); +UIEditorPanelContentHostFrame BuildUIEditorPanelContentHostFrame( + const UIEditorPanelContentHostRequest& request, + const UIEditorPanelRegistry& panelRegistry); + UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( UIEditorPanelContentHostState& state, const UIEditorPanelContentHostRequest& request, diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h index 1f101214..a2896ca0 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h @@ -100,12 +100,10 @@ struct UIEditorShellComposeLayout { struct UIEditorShellComposeRequest { UIEditorShellComposeLayout layout = {}; - UIEditorWorkspaceComposeRequest workspaceRequest = {}; }; struct UIEditorShellComposeFrame { UIEditorShellComposeLayout layout = {}; - UIEditorWorkspaceComposeFrame workspaceFrame = {}; }; UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( @@ -123,12 +121,7 @@ UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( const ::XCEngine::UI::UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, const UIEditorShellComposeModel& model, - const Widgets::UIEditorDockHostState& dockHostState = {}, - const UIEditorShellComposeState& state = {}, const UIEditorShellComposeMetrics& metrics = {}); UIEditorShellComposeFrame UpdateUIEditorShellCompose( @@ -145,15 +138,33 @@ UIEditorShellComposeFrame UpdateUIEditorShellCompose( void AppendUIEditorShellCompose( ::XCEngine::UI::UIDrawList& drawList, const UIEditorShellComposeFrame& frame, + const UIEditorWorkspaceComposeFrame& workspaceFrame, const UIEditorShellComposeModel& model, const UIEditorShellComposeState& state, const UIEditorShellComposePalette& palette = {}, const UIEditorShellComposeMetrics& metrics = {}, const App::BuiltInIcons* builtInIcons = nullptr); -void AppendUIEditorShellComposeOverlay( +void AppendUIEditorShellComposeBase( ::XCEngine::UI::UIDrawList& drawList, const UIEditorShellComposeFrame& frame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette = {}, + const UIEditorShellComposeMetrics& metrics = {}, + const App::BuiltInIcons* builtInIcons = nullptr); + +void AppendUIEditorShellComposeStatusBar( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellComposeFrame& frame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette = {}, + const UIEditorShellComposeMetrics& metrics = {}); + +void AppendUIEditorShellComposeOverlay( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorWorkspaceComposeFrame& workspaceFrame, const UIEditorShellComposePalette& palette = {}, const UIEditorShellComposeMetrics& metrics = {}); diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h b/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h index 4880183a..e529a6e9 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h @@ -143,7 +143,6 @@ UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const ::XCEngine::UI::UIRect& bounds, - const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state = {}, const UIEditorShellInteractionMetrics& metrics = {}, diff --git a/new_editor/include/XCEditor/Viewport/UIEditorViewportShell.h b/new_editor/include/XCEditor/Viewport/UIEditorViewportShell.h index 8a4fcfd9..631513d4 100644 --- a/new_editor/include/XCEditor/Viewport/UIEditorViewportShell.h +++ b/new_editor/include/XCEditor/Viewport/UIEditorViewportShell.h @@ -47,6 +47,13 @@ UIEditorViewportShellRequest ResolveUIEditorViewportShellRequest( const UIEditorViewportShellSpec& spec, const Widgets::UIEditorViewportSlotMetrics& metrics = {}); +UIEditorViewportShellFrame UpdateUIEditorViewportShell( + UIEditorViewportShellState& state, + const UIEditorViewportShellRequest& request, + const UIEditorViewportShellModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const UIEditorViewportInputBridgeRequest& inputRequest = {}); + UIEditorViewportShellFrame UpdateUIEditorViewportShell( UIEditorViewportShellState& state, const ::XCEngine::UI::UIRect& bounds, diff --git a/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h b/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h index a0426c57..5e5b231a 100644 --- a/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h +++ b/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h @@ -153,6 +153,10 @@ struct UIEditorViewportSlotHitTarget { std::size_t index = UIEditorViewportSlotInvalidIndex; }; +struct UIEditorViewportSlotForegroundAppendOptions { + bool includeSurfaceTexture = true; +}; + float ResolveUIEditorViewportSlotDesiredToolWidth( const UIEditorViewportSlotToolItem& item, const UIEditorViewportSlotMetrics& metrics = {}); @@ -187,7 +191,14 @@ void AppendUIEditorViewportSlotForeground( const std::vector& statusSegments, const UIEditorViewportSlotState& state, const UIEditorViewportSlotPalette& palette = {}, - const UIEditorViewportSlotMetrics& metrics = {}); + const UIEditorViewportSlotMetrics& metrics = {}, + const UIEditorViewportSlotForegroundAppendOptions& options = {}); + +void AppendUIEditorViewportSlotSurfaceTexture( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const UIEditorViewportSlotFrame& frame, + const UIEditorViewportSlotPalette& palette = {}); void AppendUIEditorViewportSlot( ::XCEngine::UI::UIDrawList& drawList, diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceCompose.h b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceCompose.h index 1efeaffa..a5bea042 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceCompose.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceCompose.h @@ -58,6 +58,7 @@ struct UIEditorWorkspaceComposeFrame { struct UIEditorWorkspaceComposeAppendOptions { bool deferDockPreviewOverlay = false; + bool includeViewportTextures = true; }; const UIEditorWorkspacePanelPresentationModel* FindUIEditorWorkspacePanelPresentationModel( @@ -76,6 +77,12 @@ const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresen const UIEditorWorkspaceComposeFrame& frame, std::string_view panelId); +UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( + const Widgets::UIEditorDockHostLayout& dockHostLayout, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& presentations, + const Widgets::UIEditorViewportSlotMetrics& viewportMetrics = {}); + UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( const ::XCEngine::UI::UIRect& bounds, const UIEditorPanelRegistry& panelRegistry, @@ -86,6 +93,14 @@ UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( const Widgets::UIEditorDockHostMetrics& dockHostMetrics = {}, const Widgets::UIEditorViewportSlotMetrics& viewportMetrics = {}); +UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( + UIEditorWorkspaceComposeState& state, + const UIEditorWorkspaceComposeRequest& request, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& presentations, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const UIEditorWorkspaceInputOwner* inputOwner = nullptr); + UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( UIEditorWorkspaceComposeState& state, const ::XCEngine::UI::UIRect& bounds, @@ -117,4 +132,9 @@ void AppendUIEditorWorkspaceComposeOverlay( const Widgets::UIEditorDockHostPalette& dockHostPalette = {}, const Widgets::UIEditorDockHostMetrics& dockHostMetrics = {}); +void AppendUIEditorWorkspaceComposeViewportTextures( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorWorkspaceComposeFrame& frame, + const Widgets::UIEditorViewportSlotPalette& viewportPalette = {}); + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Docking/DockHostInteractionHelpers.cpp b/new_editor/src/Docking/DockHostInteractionHelpers.cpp index 844efc2d..baa24a93 100644 --- a/new_editor/src/Docking/DockHostInteractionHelpers.cpp +++ b/new_editor/src/Docking/DockHostInteractionHelpers.cpp @@ -83,7 +83,7 @@ UIEditorDockHostTabStripInteractionEntry& FindOrCreateTabStripInteractionEntry( return entry; } -void PruneTabStripInteractionEntries( +bool PruneTabStripInteractionEntries( UIEditorDockHostInteractionState& state, const Widgets::UIEditorDockHostLayout& layout) { const auto isVisibleNodeId = [&layout](std::string_view nodeId) { @@ -95,6 +95,8 @@ void PruneTabStripInteractionEntries( }) != layout.tabStacks.end(); }; + bool changed = false; + const std::size_t previousInteractionCount = state.tabStripInteractions.size(); state.tabStripInteractions.erase( std::remove_if( state.tabStripInteractions.begin(), @@ -103,7 +105,11 @@ void PruneTabStripInteractionEntries( return !isVisibleNodeId(entry.nodeId); }), state.tabStripInteractions.end()); + changed = changed || + previousInteractionCount != state.tabStripInteractions.size(); + const std::size_t previousVisualStateCount = + state.dockHostState.tabStripStates.size(); state.dockHostState.tabStripStates.erase( std::remove_if( state.dockHostState.tabStripStates.begin(), @@ -112,13 +118,18 @@ void PruneTabStripInteractionEntries( return !isVisibleNodeId(entry.nodeId); }), state.dockHostState.tabStripStates.end()); + changed = changed || + previousVisualStateCount != state.dockHostState.tabStripStates.size(); if (!state.activeTabDragNodeId.empty() && !isVisibleNodeId(state.activeTabDragNodeId)) { state.activeTabDragNodeId.clear(); state.activeTabDragPanelId.clear(); state.dockHostState.dropPreview = {}; + changed = true; } + + return changed; } void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) { diff --git a/new_editor/src/Docking/DockHostInteractionInternal.h b/new_editor/src/Docking/DockHostInteractionInternal.h index 36549025..7fed24d8 100644 --- a/new_editor/src/Docking/DockHostInteractionInternal.h +++ b/new_editor/src/Docking/DockHostInteractionInternal.h @@ -35,7 +35,7 @@ UIEditorWorkspaceCommandResult DispatchPanelCommand( UIEditorDockHostTabStripInteractionEntry& FindOrCreateTabStripInteractionEntry( UIEditorDockHostInteractionState& state, std::string_view nodeId); -void PruneTabStripInteractionEntries( +bool PruneTabStripInteractionEntries( UIEditorDockHostInteractionState& state, const Widgets::UIEditorDockHostLayout& layout); void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state); diff --git a/new_editor/src/Docking/UIEditorDockHostInteraction.cpp b/new_editor/src/Docking/UIEditorDockHostInteraction.cpp index 90034286..3e6b3b72 100644 --- a/new_editor/src/Docking/UIEditorDockHostInteraction.cpp +++ b/new_editor/src/Docking/UIEditorDockHostInteraction.cpp @@ -41,15 +41,16 @@ Widgets::UIEditorDockHostLayout RebuildDockHostLayout( controller.GetSession(), state.dockHostState, metrics); - Internal::PruneTabStripInteractionEntries(state, layout); - Internal::SyncDockHostTabStripVisualStates(state); - layout = BuildUIEditorDockHostLayout( - bounds, - controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), - state.dockHostState, - metrics); + if (Internal::PruneTabStripInteractionEntries(state, layout)) { + Internal::SyncDockHostTabStripVisualStates(state); + layout = BuildUIEditorDockHostLayout( + bounds, + controller.GetPanelRegistry(), + controller.GetWorkspace(), + controller.GetSession(), + state.dockHostState, + metrics); + } Internal::SyncHoverTarget(state, layout); return layout; } @@ -126,6 +127,30 @@ bool HasMeaningfulDockHostResult( !result.detachedPanelId.empty(); } +bool ShouldRebuildDockHostLayoutForNextEvent( + const UIEditorDockHostInteractionResult& result) { + if (result.layoutChanged) { + return true; + } + + if (!result.commandExecuted) { + return false; + } + + switch (result.commandResult.kind) { + case UIEditorWorkspaceCommandKind::ActivatePanel: + return false; + case UIEditorWorkspaceCommandKind::OpenPanel: + case UIEditorWorkspaceCommandKind::ClosePanel: + case UIEditorWorkspaceCommandKind::ShowPanel: + case UIEditorWorkspaceCommandKind::HidePanel: + case UIEditorWorkspaceCommandKind::ResetWorkspace: + return true; + } + + return true; +} + UIEditorDockHostInteractionResult BeginEventResult( const DockHostEventContext& context) { UIEditorDockHostInteractionResult result = {}; @@ -538,7 +563,11 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction( break; } - layout = RebuildDockHostLayout(state, controller, bounds, metrics); + if (ShouldRebuildDockHostLayoutForNextEvent(eventResult)) { + layout = RebuildDockHostLayout(state, controller, bounds, metrics); + } else { + Internal::SyncHoverTarget(state, layout); + } if (eventResult.hitTarget.kind == UIEditorDockHostHitTargetKind::None) { eventResult.hitTarget = state.dockHostState.hoveredTarget; } diff --git a/new_editor/src/Panels/UIEditorPanelContentHost.cpp b/new_editor/src/Panels/UIEditorPanelContentHost.cpp index 66d01dc4..5cf874f4 100644 --- a/new_editor/src/Panels/UIEditorPanelContentHost.cpp +++ b/new_editor/src/Panels/UIEditorPanelContentHost.cpp @@ -34,10 +34,10 @@ bool SupportsExternalHosting(const UIEditorPanelDescriptor& descriptor) { return IsUIEditorPanelPresentationExternallyHosted(descriptor.presentationKind); } -UIEditorPanelContentHostPanelState* FindMutablePanelState( - UIEditorPanelContentHostState& state, +const UIEditorPanelContentHostPanelState* FindPanelState( + const std::vector& panelStates, std::string_view panelId) { - for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) { + for (const UIEditorPanelContentHostPanelState& panelState : panelStates) { if (panelState.panelId == panelId) { return &panelState; } @@ -46,21 +46,32 @@ UIEditorPanelContentHostPanelState* FindMutablePanelState( return nullptr; } -UIEditorPanelContentHostPanelState& EnsurePanelState( - UIEditorPanelContentHostState& state, - std::string_view panelId, - UIEditorPanelPresentationKind kind) { - if (UIEditorPanelContentHostPanelState* existing = - FindMutablePanelState(state, panelId)) { - existing->kind = kind; - return *existing; +std::vector BuildPanelStates( + const UIEditorPanelContentHostRequest& request, + const UIEditorPanelRegistry& panelRegistry) { + std::vector panelStates = {}; + panelStates.reserve(panelRegistry.panels.size()); + + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + if (!SupportsExternalHosting(descriptor)) { + continue; + } + + UIEditorPanelContentHostPanelState panelState = {}; + panelState.panelId = descriptor.panelId; + panelState.kind = descriptor.presentationKind; + if (const UIEditorPanelContentHostMountRequest* mountRequest = + FindUIEditorPanelContentHostMountRequest(request, descriptor.panelId); + mountRequest != nullptr) { + panelState.mounted = true; + panelState.kind = mountRequest->kind; + panelState.bounds = mountRequest->bounds; + } + + panelStates.push_back(std::move(panelState)); } - UIEditorPanelContentHostPanelState panelState = {}; - panelState.panelId = std::string(panelId); - panelState.kind = kind; - state.panelStates.push_back(std::move(panelState)); - return state.panelStates.back(); + return panelStates; } } // namespace @@ -145,42 +156,26 @@ UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( return request; } +UIEditorPanelContentHostFrame BuildUIEditorPanelContentHostFrame( + const UIEditorPanelContentHostRequest& request, + const UIEditorPanelRegistry& panelRegistry) { + UIEditorPanelContentHostFrame frame = {}; + frame.panelStates = BuildPanelStates(request, panelRegistry); + return frame; +} + UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( UIEditorPanelContentHostState& state, const UIEditorPanelContentHostRequest& request, const UIEditorPanelRegistry& panelRegistry) { UIEditorPanelContentHostFrame frame = {}; - std::unordered_set supportedPanelIds = {}; - for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { - if (!SupportsExternalHosting(descriptor)) { - continue; - } - - supportedPanelIds.insert(descriptor.panelId); - EnsurePanelState(state, descriptor.panelId, descriptor.presentationKind); - } - - state.panelStates.erase( - std::remove_if( - state.panelStates.begin(), - state.panelStates.end(), - [&](const UIEditorPanelContentHostPanelState& panelState) { - return !supportedPanelIds.contains(panelState.panelId); - }), - state.panelStates.end()); - - for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) { - const UIEditorPanelContentHostMountRequest* mountRequest = - FindUIEditorPanelContentHostMountRequest(request, panelState.panelId); - - const bool wasMounted = panelState.mounted; - const ::XCEngine::UI::UIRect previousBounds = panelState.bounds; - - panelState.mounted = mountRequest != nullptr; - panelState.bounds = mountRequest != nullptr ? mountRequest->bounds : ::XCEngine::UI::UIRect{}; - if (mountRequest != nullptr) { - panelState.kind = mountRequest->kind; - } + frame.panelStates = BuildPanelStates(request, panelRegistry); + for (const UIEditorPanelContentHostPanelState& panelState : frame.panelStates) { + const UIEditorPanelContentHostPanelState* previousState = + FindPanelState(state.panelStates, panelState.panelId); + const bool wasMounted = previousState != nullptr && previousState->mounted; + const ::XCEngine::UI::UIRect previousBounds = + previousState != nullptr ? previousState->bounds : ::XCEngine::UI::UIRect{}; if (!wasMounted && panelState.mounted) { frame.events.push_back({ @@ -207,7 +202,7 @@ UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( } } - frame.panelStates = state.panelStates; + state.panelStates = frame.panelStates; return frame; } diff --git a/new_editor/src/Shell/ShellInteractionInternal.h b/new_editor/src/Shell/ShellInteractionInternal.h index 15e2f630..16ad09d7 100644 --- a/new_editor/src/Shell/ShellInteractionInternal.h +++ b/new_editor/src/Shell/ShellInteractionInternal.h @@ -48,7 +48,6 @@ bool ShouldRefreshResolvedShellModel( const UIEditorShellInteractionResult& result); BuildRequestOutput BuildRequest( const ::XCEngine::UI::UIRect& bounds, - const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state, const UIEditorShellInteractionMetrics& metrics, diff --git a/new_editor/src/Shell/UIEditorShellCompose.cpp b/new_editor/src/Shell/UIEditorShellCompose.cpp index e58d0bc3..e9fd4c6b 100644 --- a/new_editor/src/Shell/UIEditorShellCompose.cpp +++ b/new_editor/src/Shell/UIEditorShellCompose.cpp @@ -119,7 +119,6 @@ void AppendUIEditorShellToolbar( return; } - const std::size_t buttonCount = (std::min)(buttons.size(), layout.buttonRects.size()); for (std::size_t index = 0; index < buttonCount; ++index) { const UIRect& buttonRect = layout.buttonRects[index]; @@ -242,12 +241,7 @@ UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( const UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, const UIEditorShellComposeModel& model, - const Widgets::UIEditorDockHostState& dockHostState, - const UIEditorShellComposeState& state, const UIEditorShellComposeMetrics& metrics) { UIEditorShellComposeRequest request = {}; request.layout = BuildUIEditorShellComposeLayout( @@ -256,28 +250,6 @@ UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( model.toolbarButtons, model.statusSegments, metrics); - request.workspaceRequest = ResolveUIEditorWorkspaceComposeRequest( - request.layout.workspaceRect, - panelRegistry, - workspace, - session, - model.workspacePresentations, - dockHostState, - metrics.dockHostMetrics, - metrics.viewportMetrics); - request.layout.menuBarLayout = BuildUIEditorMenuBarLayout( - request.layout.menuBarRect, - model.menuBarItems, - metrics.menuBarMetrics); - request.layout.toolbarLayout = BuildUIEditorShellToolbarLayout( - request.layout.toolbarRect, - model.toolbarButtons, - metrics.toolbarMetrics); - request.layout.statusBarLayout = BuildUIEditorStatusBarLayout( - request.layout.statusBarRect, - model.statusSegments, - metrics.statusBarMetrics); - (void)state; return request; } @@ -298,7 +270,7 @@ UIEditorShellComposeFrame UpdateUIEditorShellCompose( model.toolbarButtons, model.statusSegments, metrics); - frame.workspaceFrame = UpdateUIEditorWorkspaceCompose( + (void)UpdateUIEditorWorkspaceCompose( state.workspaceState, frame.layout.workspaceRect, panelRegistry, @@ -309,22 +281,46 @@ UIEditorShellComposeFrame UpdateUIEditorShellCompose( dockHostState, metrics.dockHostMetrics, metrics.viewportMetrics); - frame.layout.menuBarLayout = BuildUIEditorMenuBarLayout( - frame.layout.menuBarRect, - model.menuBarItems, - metrics.menuBarMetrics); - frame.layout.toolbarLayout = BuildUIEditorShellToolbarLayout( - frame.layout.toolbarRect, - model.toolbarButtons, - metrics.toolbarMetrics); - frame.layout.statusBarLayout = BuildUIEditorStatusBarLayout( - frame.layout.statusBarRect, - model.statusSegments, - metrics.statusBarMetrics); return frame; } void AppendUIEditorShellCompose( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellComposeFrame& frame, + const UIEditorWorkspaceComposeFrame& workspaceFrame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette, + const UIEditorShellComposeMetrics& metrics, + const App::BuiltInIcons* builtInIcons) { + AppendUIEditorShellComposeBase( + drawList, + frame, + model, + state, + palette, + metrics, + builtInIcons); + + AppendUIEditorWorkspaceCompose( + drawList, + workspaceFrame, + palette.dockHostPalette, + metrics.dockHostMetrics, + palette.viewportPalette, + metrics.viewportMetrics, + UIEditorWorkspaceComposeAppendOptions{ true }); + + AppendUIEditorShellComposeStatusBar( + drawList, + frame, + model, + state, + palette, + metrics); +} + +void AppendUIEditorShellComposeBase( ::XCEngine::UI::UIDrawList& drawList, const UIEditorShellComposeFrame& frame, const UIEditorShellComposeModel& model, @@ -364,16 +360,15 @@ void AppendUIEditorShellCompose( palette.toolbarPalette, metrics.toolbarMetrics, builtInIcons); +} - AppendUIEditorWorkspaceCompose( - drawList, - frame.workspaceFrame, - palette.dockHostPalette, - metrics.dockHostMetrics, - palette.viewportPalette, - metrics.viewportMetrics, - UIEditorWorkspaceComposeAppendOptions{ true }); - +void AppendUIEditorShellComposeStatusBar( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellComposeFrame& frame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette, + const UIEditorShellComposeMetrics& metrics) { AppendUIEditorStatusBarBackground( drawList, frame.layout.statusBarLayout, @@ -392,12 +387,12 @@ void AppendUIEditorShellCompose( void AppendUIEditorShellComposeOverlay( ::XCEngine::UI::UIDrawList& drawList, - const UIEditorShellComposeFrame& frame, + const UIEditorWorkspaceComposeFrame& workspaceFrame, const UIEditorShellComposePalette& palette, const UIEditorShellComposeMetrics& metrics) { AppendUIEditorWorkspaceComposeOverlay( drawList, - frame.workspaceFrame, + workspaceFrame, palette.dockHostPalette, metrics.dockHostMetrics); } diff --git a/new_editor/src/Shell/UIEditorShellInteraction.cpp b/new_editor/src/Shell/UIEditorShellInteraction.cpp index f6a7475b..5e83a517 100644 --- a/new_editor/src/Shell/UIEditorShellInteraction.cpp +++ b/new_editor/src/Shell/UIEditorShellInteraction.cpp @@ -248,7 +248,6 @@ bool ShouldRefreshResolvedShellModel( BuildRequestOutput BuildRequest( const UIRect& bounds, - const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state, const UIEditorShellInteractionMetrics& metrics, @@ -264,12 +263,7 @@ BuildRequestOutput BuildRequest( BuildShellComposeModel(model, request.menuBarItems); request.shellRequest = ResolveUIEditorShellComposeRequest( bounds, - controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), shellModel, - state.workspaceInteractionState.dockHostInteractionState.dockHostState, - state.composeState, metrics.shellMetrics); request.menuButtons.reserve(request.menuBarItems.size()); @@ -515,14 +509,12 @@ UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const UIRect& bounds, - const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state, const UIEditorShellInteractionMetrics& metrics, const UIEditorShellInteractionServices& services) { return Internal::BuildRequest( bounds, - controller, model, state, metrics, @@ -540,7 +532,6 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( ResolveUIEditorShellInteractionModel(controller, definition, services); return ResolveUIEditorShellInteractionRequest( bounds, - controller, model, state, metrics, @@ -560,7 +551,6 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( Internal::BuildRequestOutput requestBuild = Internal::BuildRequest( bounds, - controller, model, state, metrics, @@ -575,7 +565,6 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( requestBuild = Internal::BuildRequest( bounds, - controller, model, state, metrics, @@ -727,11 +716,12 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( interactionResult = std::move(eventResult); } - if (interactionResult.menuMutation.changed || state.menuSession.HasOpenMenu()) { + if (state.menuSession.HasOpenMenu()) { menuModalDuringFrame = true; + } + if (eventResult.menuMutation.changed) { request = Internal::BuildRequest( bounds, - controller, model, state, metrics, @@ -753,14 +743,6 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( metrics.shellMetrics.dockHostMetrics); state.composeState.workspaceState = state.workspaceInteractionState.composeState; - request = Internal::BuildRequest( - bounds, - controller, - model, - state, - metrics, - services).request; - const Internal::RequestHit finalHit = Internal::HitTestRequest( request, state.pointerPosition, @@ -770,7 +752,6 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionFrame frame = {}; frame.request = request; frame.shellFrame.layout = request.shellRequest.layout; - frame.shellFrame.workspaceFrame = workspaceInteractionFrame.composeFrame; frame.workspaceInteractionFrame = std::move(workspaceInteractionFrame); frame.popupFrames = Internal::BuildPopupFrames( frame.request, @@ -855,6 +836,7 @@ void AppendUIEditorShellInteraction( AppendUIEditorShellCompose( drawList, frame.shellFrame, + frame.workspaceInteractionFrame.composeFrame, shellModel, state.composeState, palette.shellPalette, diff --git a/new_editor/src/Viewport/UIEditorViewportShell.cpp b/new_editor/src/Viewport/UIEditorViewportShell.cpp index 41fcf793..a392ed2b 100644 --- a/new_editor/src/Viewport/UIEditorViewportShell.cpp +++ b/new_editor/src/Viewport/UIEditorViewportShell.cpp @@ -65,14 +65,13 @@ UIEditorViewportShellRequest ResolveUIEditorViewportShellRequest( UIEditorViewportShellFrame UpdateUIEditorViewportShell( UIEditorViewportShellState& state, - const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellRequest& request, const UIEditorViewportShellModel& model, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Widgets::UIEditorViewportSlotMetrics& metrics, const UIEditorViewportInputBridgeRequest& inputRequest) { UIEditorViewportShellFrame frame = {}; - frame.slotLayout = BuildViewportShellLayout(bounds, model.spec, model.frame, metrics); - frame.requestedViewportSize = frame.slotLayout.requestedSurfaceSize; + frame.slotLayout = request.slotLayout; + frame.requestedViewportSize = request.requestedViewportSize; frame.inputFrame = UpdateUIEditorViewportInputBridge( state.inputBridgeState, frame.slotLayout.bounds, @@ -88,4 +87,21 @@ UIEditorViewportShellFrame UpdateUIEditorViewportShell( return frame; } +UIEditorViewportShellFrame UpdateUIEditorViewportShell( + UIEditorViewportShellState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorViewportSlotMetrics& metrics, + const UIEditorViewportInputBridgeRequest& inputRequest) { + const UIEditorViewportShellRequest request = + ResolveUIEditorViewportShellRequest(bounds, model.spec, metrics); + return UpdateUIEditorViewportShell( + state, + request, + model, + inputEvents, + inputRequest); +} + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Viewport/UIEditorViewportSlot.cpp b/new_editor/src/Viewport/UIEditorViewportSlot.cpp index a11e10da..f9fb914f 100644 --- a/new_editor/src/Viewport/UIEditorViewportSlot.cpp +++ b/new_editor/src/Viewport/UIEditorViewportSlot.cpp @@ -372,7 +372,8 @@ void AppendUIEditorViewportSlotForeground( const std::vector& statusSegments, const UIEditorViewportSlotState& state, const UIEditorViewportSlotPalette& palette, - const UIEditorViewportSlotMetrics& metrics) { + const UIEditorViewportSlotMetrics& metrics, + const UIEditorViewportSlotForegroundAppendOptions& options) { if (layout.hasTopBar) { if (!chrome.title.empty()) { drawList.AddText( @@ -405,7 +406,9 @@ void AppendUIEditorViewportSlotForeground( } } - if (frame.hasTexture && frame.texture.IsValid()) { + if (options.includeSurfaceTexture && + frame.hasTexture && + frame.texture.IsValid()) { drawList.AddImage(layout.textureRect, frame.texture, palette.imageTint); } @@ -419,6 +422,18 @@ void AppendUIEditorViewportSlotForeground( } } +void AppendUIEditorViewportSlotSurfaceTexture( + UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const UIEditorViewportSlotFrame& frame, + const UIEditorViewportSlotPalette& palette) { + if (!frame.hasTexture || !frame.texture.IsValid()) { + return; + } + + drawList.AddImage(layout.textureRect, frame.texture, palette.imageTint); +} + void AppendUIEditorViewportSlot( UIDrawList& drawList, const UIRect& bounds, @@ -454,7 +469,8 @@ void AppendUIEditorViewportSlot( statusSegments, state, palette, - metrics); + metrics, + {}); } } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp b/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp index bcb1402a..f178be37 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp @@ -144,25 +144,16 @@ const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresen } UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( - const ::XCEngine::UI::UIRect& bounds, + const Widgets::UIEditorDockHostLayout& dockHostLayout, const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, const std::vector& presentations, - const Widgets::UIEditorDockHostState& dockHostState, - const Widgets::UIEditorDockHostMetrics& dockHostMetrics, const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) { UIEditorWorkspaceComposeRequest request = {}; - request.dockHostLayout = BuildUIEditorDockHostLayout( - bounds, - panelRegistry, - workspace, - session, - dockHostState, - dockHostMetrics); + request.dockHostLayout = dockHostLayout; request.contentHostRequest = ResolveUIEditorPanelContentHostRequest( request.dockHostLayout, panelRegistry); + request.viewportRequests.reserve(request.contentHostRequest.mountRequests.size()); for (const UIEditorPanelContentHostMountRequest& mountRequest : request.contentHostRequest.mountRequests) { @@ -190,6 +181,82 @@ UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( return request; } +UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const std::vector& presentations, + const Widgets::UIEditorDockHostState& dockHostState, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics, + const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) { + const Widgets::UIEditorDockHostLayout dockHostLayout = BuildUIEditorDockHostLayout( + bounds, + panelRegistry, + workspace, + session, + dockHostState, + dockHostMetrics); + return ResolveUIEditorWorkspaceComposeRequest( + dockHostLayout, + panelRegistry, + presentations, + viewportMetrics); +} + +UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( + UIEditorWorkspaceComposeState& state, + const UIEditorWorkspaceComposeRequest& request, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& presentations, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const UIEditorWorkspaceInputOwner* inputOwner) { + UIEditorWorkspaceComposeFrame frame = {}; + frame.dockHostLayout = request.dockHostLayout; + frame.contentHostFrame = UpdateUIEditorPanelContentHost( + state.contentHostState, + request.contentHostRequest, + panelRegistry); + TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations); + frame.viewportFrames.reserve(request.viewportRequests.size()); + + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) { + continue; + } + + const UIEditorWorkspaceViewportComposeRequest* viewportRequest = + FindUIEditorWorkspaceViewportPresentationRequest(request, presentation.panelId); + if (viewportRequest == nullptr) { + ResetHiddenViewportPresentationState(state, presentation.panelId); + continue; + } + + UIEditorWorkspacePanelPresentationState& panelState = + EnsurePanelState(state, presentation.panelId); + + UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; + viewportFrame.panelId = presentation.panelId; + viewportFrame.bounds = viewportRequest->bounds; + viewportFrame.viewportShellModel = presentation.viewportShellModel; + UIEditorViewportInputBridgeRequest inputRequest = {}; + if (inputOwner != nullptr) { + inputRequest.focusMode = UIEditorViewportInputBridgeFocusMode::External; + inputRequest.focused = + IsUIEditorWorkspaceViewportInputOwner(*inputOwner, presentation.panelId); + } + viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell( + panelState.viewportShellState, + viewportRequest->viewportShellRequest, + presentation.viewportShellModel, + inputEvents, + inputRequest); + frame.viewportFrames.push_back(std::move(viewportFrame)); + } + + return frame; +} + UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( UIEditorWorkspaceComposeState& state, const ::XCEngine::UI::UIRect& bounds, @@ -202,60 +269,22 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( const Widgets::UIEditorDockHostMetrics& dockHostMetrics, const Widgets::UIEditorViewportSlotMetrics& viewportMetrics, const UIEditorWorkspaceInputOwner* inputOwner) { - UIEditorWorkspaceComposeFrame frame = {}; - frame.dockHostLayout = BuildUIEditorDockHostLayout( + const UIEditorWorkspaceComposeRequest request = ResolveUIEditorWorkspaceComposeRequest( bounds, panelRegistry, workspace, session, + presentations, dockHostState, - dockHostMetrics); - const UIEditorPanelContentHostRequest contentHostRequest = - ResolveUIEditorPanelContentHostRequest( - frame.dockHostLayout, - panelRegistry); - frame.contentHostFrame = UpdateUIEditorPanelContentHost( - state.contentHostState, - contentHostRequest, - panelRegistry); - TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations); - - for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { - if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) { - continue; - } - - const UIEditorPanelContentHostPanelState* contentHostPanelState = - FindUIEditorPanelContentHostPanelState(frame.contentHostFrame, presentation.panelId); - if (contentHostPanelState == nullptr || !contentHostPanelState->mounted) { - ResetHiddenViewportPresentationState(state, presentation.panelId); - continue; - } - - UIEditorWorkspacePanelPresentationState& panelState = - EnsurePanelState(state, presentation.panelId); - - UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; - viewportFrame.panelId = presentation.panelId; - viewportFrame.bounds = contentHostPanelState->bounds; - viewportFrame.viewportShellModel = presentation.viewportShellModel; - UIEditorViewportInputBridgeRequest inputRequest = {}; - if (inputOwner != nullptr) { - inputRequest.focusMode = UIEditorViewportInputBridgeFocusMode::External; - inputRequest.focused = - IsUIEditorWorkspaceViewportInputOwner(*inputOwner, presentation.panelId); - } - viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell( - panelState.viewportShellState, - contentHostPanelState->bounds, - presentation.viewportShellModel, - inputEvents, - viewportMetrics, - inputRequest); - frame.viewportFrames.push_back(std::move(viewportFrame)); - } - - return frame; + dockHostMetrics, + viewportMetrics); + return UpdateUIEditorWorkspaceCompose( + state, + request, + panelRegistry, + presentations, + inputEvents, + inputOwner); } std::vector CollectUIEditorWorkspaceComposeExternalBodyPanelIds( @@ -295,7 +324,10 @@ void AppendUIEditorWorkspaceCompose( viewportFrame.viewportShellModel.spec.statusSegments, viewportFrame.viewportShellFrame.slotState, viewportPalette, - viewportMetrics); + viewportMetrics, + Widgets::UIEditorViewportSlotForegroundAppendOptions{ + options.includeViewportTextures + }); } UIEditorDockHostForegroundOptions foregroundOptions = {}; @@ -322,4 +354,17 @@ void AppendUIEditorWorkspaceComposeOverlay( dockHostMetrics); } +void AppendUIEditorWorkspaceComposeViewportTextures( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorWorkspaceComposeFrame& frame, + const Widgets::UIEditorViewportSlotPalette& viewportPalette) { + for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) { + Widgets::AppendUIEditorViewportSlotSurfaceTexture( + drawList, + viewportFrame.viewportShellFrame.slotLayout, + viewportFrame.viewportShellModel.frame, + viewportPalette); + } +} + } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp b/new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp index 44f5dc2b..9932292e 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp @@ -74,47 +74,53 @@ UIEditorWorkspaceInteractionFrame UpdateUIEditorWorkspaceInteraction( inputEvents, dockHostMetrics); - const UIEditorWorkspaceComposeFrame previewComposeFrame = UpdateUIEditorWorkspaceCompose( - state.composeState, - bounds, - controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), - model.workspacePresentations, - {}, - state.dockHostInteractionState.dockHostState, - dockHostMetrics, - viewportMetrics); + const UIEditorWorkspaceComposeRequest previewRequest = + ResolveUIEditorWorkspaceComposeRequest( + frame.dockHostFrame.layout, + controller.GetPanelRegistry(), + model.workspacePresentations, + viewportMetrics); + const UIEditorPanelContentHostFrame previewContentHostFrame = + BuildUIEditorPanelContentHostFrame( + previewRequest.contentHostRequest, + controller.GetPanelRegistry()); state.inputOwner = ResolveNextInputOwner( state.inputOwner, - previewComposeFrame.dockHostLayout, - previewComposeFrame.contentHostFrame, + previewRequest.dockHostLayout, + previewContentHostFrame, inputEvents); frame.inputOwner = state.inputOwner; frame.inputOwnerChanged = !AreUIEditorWorkspaceInputOwnersEquivalent( frame.previousInputOwner, frame.inputOwner); - state.dockHostInteractionState.dockHostState.focused = + + const bool dockFocused = IsUIEditorWorkspaceDockHostInputOwner(frame.inputOwner); - frame.dockHostFrame.layout = Widgets::BuildUIEditorDockHostLayout( - bounds, - controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), - state.dockHostInteractionState.dockHostState, - dockHostMetrics); - frame.dockHostFrame.focused = state.dockHostInteractionState.dockHostState.focused; + const bool dockFocusChanged = + state.dockHostInteractionState.dockHostState.focused != dockFocused; + state.dockHostInteractionState.dockHostState.focused = dockFocused; + + UIEditorWorkspaceComposeRequest composeRequest = previewRequest; + // Dock focus only changes visual state; hosted content mounts and viewport + // bounds remain stable for this frame. + if (dockFocusChanged) { + composeRequest.dockHostLayout = Widgets::BuildUIEditorDockHostLayout( + bounds, + controller.GetPanelRegistry(), + controller.GetWorkspace(), + controller.GetSession(), + state.dockHostInteractionState.dockHostState, + dockHostMetrics); + } + + frame.dockHostFrame.layout = composeRequest.dockHostLayout; + frame.dockHostFrame.focused = dockFocused; frame.composeFrame = UpdateUIEditorWorkspaceCompose( state.composeState, - bounds, + composeRequest, controller.GetPanelRegistry(), - controller.GetWorkspace(), - controller.GetSession(), model.workspacePresentations, inputEvents, - state.dockHostInteractionState.dockHostState, - dockHostMetrics, - viewportMetrics, &frame.inputOwner); frame.result.dockHostResult = frame.dockHostFrame.result;