From b05e76de0c707aaf9bf391d9100f19a2fe559778 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 14:36:02 +0800 Subject: [PATCH] Publish native hosted preview textures through XCUI compositor --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 21 +- new_editor/src/Application.cpp | 170 +++++++++++-- new_editor/src/Application.h | 83 ++++++- .../XCUIBackend/NativeWindowUICompositor.cpp | 70 +++++- .../src/XCUIBackend/UITextureRegistration.h | 10 +- ...est_application_shell_command_bindings.cpp | 54 ++++ .../test_native_window_ui_compositor.cpp | 233 ++++++++++++++++++ 7 files changed, 605 insertions(+), 36 deletions(-) diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index fdb41989..9b19db55 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -20,6 +20,8 @@ Old `editor` replacement is explicitly out of scope for this phase. - `NativeWindowUICompositor` is now buildable alongside the legacy ImGui compositor - `Application` now defaults to a native XCUI host path instead of creating the ImGui shell by default - the native default path now drives `XCUIDemoRuntime` and `XCUILayoutLabRuntime` directly, composes their `UIDrawData`, and submits one swapchain packet through the native compositor + - the native compositor now publishes hosted-preview textures as SRV-backed `UITextureRegistration` / `UITextureHandle` values instead of relying on ImGui-only descriptor semantics + - the native shell now begins the hosted-preview queue/registry lifecycle each frame, queues native preview frames, drains them during the native render pass, and consumes published hosted-surface images directly in panel cards with live/warming placeholder states - the old ImGui shell path remains present as an explicit compatibility host instead of the default host - `XCUIInputBridge.h` no longer drags `imgui.h` through the public XCUI input seam - Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`. @@ -70,7 +72,7 @@ Current gap: ### 3. Editor Layer - `new_editor` remains the isolated XCUI sandbox. -- Native hosted preview is working as `RHI offscreen surface -> hosted surface image present` through the current shell adapter path. +- Native hosted preview is now working end-to-end as `RHI offscreen surface -> SRV-backed publication -> hosted surface image present` through the native shell path. - Hosted preview surface descriptors now stay on XCUI-owned value types (`UITextureHandle`, `UIPoint`, `UIRect`) instead of exposing ImGui texture/UV types through the generic preview contract. - Shared `UI::UIDrawData` image commands now carry explicit `uvMin` / `uvMax` source-rect semantics, and the native panel canvas host preserves those UVs when it records hosted surface-image preview commands. - `XCUI Demo` remains the long-lived effect and behavior testbed. @@ -108,13 +110,14 @@ Current gap: - drives both sandbox runtimes directly from Win32/XCUI input snapshots instead of routing through ImGui panel hosts - composes one native `UIDrawData` packet and submits it through `NativeWindowUICompositor` - `NativeXCUIPanelCanvasHost` now backs that direct shell path as an externally driven canvas/session host for native cards instead of assuming an ImGui child-window model, and it now emits native `Image` draw commands for hosted surface-image previews while preserving per-surface UVs. +- `NativeWindowUICompositor` now creates and frees SRV-backed texture registrations for hosted preview surfaces, so native publication no longer depends on ImGui descriptor handles. +- `Application` now runs the hosted-preview lifecycle in both legacy and native frame paths, treats published textures as XCUI-owned `UITextureHandle` state, queues native preview frames from `BuildNativeShellDrawData(...)`, and drains them during native rendering before shell chrome overlays. - `XCUIInputBridge.h` no longer includes `imgui.h`, so the public XCUI input bridge seam is now host-neutral at the header boundary. - `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`. Current gap: - The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`. -- Hosted-preview offscreen surface publication is still not wired for the native compositor path because descriptor-backed texture registration is still ImGui-host-centric today. - The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document. - Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules. - The default native path still depends on ImGui font-atlas internals through `XCUIStandaloneTextAtlasProvider`, so text rendering is not yet cleanly de-ImGuiized even though the shell/panel composition path is native by default. @@ -130,9 +133,9 @@ Current gap: - `new_editor_xcui_rhi_render_backend_tests`: `5/5` - `new_editor_xcui_hosted_preview_presenter_tests`: `20/20` - `new_editor_imgui_window_ui_compositor_tests`: `7/7` -- `new_editor_native_window_ui_compositor_tests`: `5/5` +- `new_editor_native_window_ui_compositor_tests`: `8/8` - `new_editor_xcui_editor_command_router_tests`: `5/5` -- `new_editor_application_shell_command_bindings_tests`: `6/6` +- `new_editor_application_shell_command_bindings_tests`: `8/8` - `new_editor_xcui_shell_chrome_state_tests`: `11/11` - `new_editor_xcui_panel_canvas_host_tests`: `4/4` - `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1` @@ -250,6 +253,12 @@ Current gap: - generic preview frame submission no longer carries an ImGui draw-list pointer - the ImGui presenter now resolves inline draw targets through an explicit ImGui-only binding seam - panel/runtime callers still preserve the same legacy and native-preview behavior +- Native hosted-preview publication is now wired through the default shell path: + - `NativeWindowUICompositor` publishes hosted preview textures as SRV-backed XCUI registrations and frees them through the compositor seam + - `Application::BeginHostedPreviewFrameLifecycle(...)` now resets queue/registry state for both legacy and native frame paths + - hosted-preview surface readiness now keys on published texture availability instead of ImGui-style descriptor validity + - `BuildNativeShellDrawData(...)` now queues native preview frames for `XCUI Demo` / `LayoutLab`, while shell cards consume the previously published hosted-surface image with warming/live placeholder text + - native compositor and shell-command tests now cover the new publication / lifecycle guards - `LayoutLab` now resolves editor collection widget taxonomy and metrics through shared `UIEditorCollectionPrimitives` helpers instead of duplicating the same tag and metric rules inside the sandbox runtime. - `LayoutLab` runtime now consumes shared keyboard-navigation semantics for list/tree/property traversal, while the remaining panel-level key mapping is tracked as an editor-host integration gap rather than a runtime gap. - `LayoutLab` panel now maps concrete arrow/home/end keys into the shared navigation model, with dedicated panel-level coverage proving that the sandbox UI can actually drive the runtime navigation seam end-to-end. @@ -270,6 +279,6 @@ Current gap: 1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies. 2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu chrome, shell-state adoption, virtualization, and broader focus/multi-selection behavior. -3. Finish native compositor texture registration and hosted-preview surface publication so the native path no longer depends on ImGui-centric descriptor registration. -4. Strip the remaining default-path ImGui seams down to compatibility-only code, starting with the standalone text/font-atlas path and then the remaining hosted-preview fallback layers. +3. Strip the remaining default-path ImGui seams down to compatibility-only code, starting with the standalone text/font-atlas path and then the remaining hosted-preview fallback layers. +4. Promote the native shell chrome and card layout out of bespoke `Application` code into a shared XCUI/editor-layer shell model or authored shell document. 5. Continue phased validation, commit, push, and plan refresh after each stable batch. diff --git a/new_editor/src/Application.cpp b/new_editor/src/Application.cpp index eb471f38..2775487d 100644 --- a/new_editor/src/Application.cpp +++ b/new_editor/src/Application.cpp @@ -591,7 +591,7 @@ void Application::ShutdownRenderer() { void Application::DestroyHostedPreviewSurfaces() { for (HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { - if (previewSurface.textureRegistration.cpuHandle.ptr != 0) { + if (Application::HasHostedPreviewTextureRegistration(previewSurface.textureRegistration)) { if (m_windowCompositor != nullptr) { m_windowCompositor->FreeTextureDescriptor(previewSurface.textureRegistration); } @@ -698,7 +698,7 @@ bool Application::EnsureHostedPreviewSurface( return true; } - if (previewSurface.textureRegistration.cpuHandle.ptr != 0) { + if (Application::HasHostedPreviewTextureRegistration(previewSurface.textureRegistration)) { if (m_windowCompositor != nullptr) { m_windowCompositor->FreeTextureDescriptor(previewSurface.textureRegistration); } @@ -980,6 +980,35 @@ bool Application::RenderHostedPreviewOffscreenSurface( for (const NativeShellPanelLayout& panelLayout : panelLayouts) { if (panelLayout.panelId == ShellPanelId::XCUIDemo) { + const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUIDemo); + const bool nativeHostedPreview = + panelState != nullptr && + panelState->hostedPreviewEnabled && + IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {}; + const bool hasHostedSurfaceDescriptor = + nativeHostedPreview && + panelState != nullptr && + !panelState->previewDebugName.empty() && + m_hostedPreviewSurfaceRegistry.TryGetSurfaceDescriptor( + panelState->previewDebugName.data(), + hostedSurfaceDescriptor); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {}; + const bool showHostedSurfaceImage = + nativeHostedPreview && + panelState != nullptr && + !panelState->previewDebugName.empty() && + m_hostedPreviewSurfaceRegistry.TryGetSurfaceImage( + panelState->previewDebugName.data(), + hostedSurfaceImage); + const NativeHostedPreviewConsumption previewConsumption = + Application::ResolveNativeHostedPreviewConsumption( + nativeHostedPreview, + hasHostedSurfaceDescriptor, + showHostedSurfaceImage, + "Native XCUI preview pending", + "Waiting for queued native preview output to publish into the shell card."); + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession canvasSession = {}; canvasSession.hostRect = panelLayout.panelRect; canvasSession.canvasRect = panelLayout.canvasRect; @@ -994,6 +1023,12 @@ bool Application::RenderHostedPreviewOffscreenSurface( canvasRequest.height = panelLayout.panelRect.height; canvasRequest.topInset = panelLayout.canvasRect.y - panelLayout.panelRect.y; canvasRequest.drawPreviewFrame = false; + canvasRequest.showSurfaceImage = previewConsumption.showSurfaceImage; + canvasRequest.surfaceImage = hostedSurfaceImage; + canvasRequest.placeholderTitle = + previewConsumption.placeholderTitle.empty() ? nullptr : previewConsumption.placeholderTitle.data(); + canvasRequest.placeholderSubtitle = + previewConsumption.placeholderSubtitle.empty() ? nullptr : previewConsumption.placeholderSubtitle.data(); const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession resolvedSession = m_nativeDemoCanvasHost.BeginCanvas(canvasRequest); @@ -1018,7 +1053,21 @@ bool Application::RenderHostedPreviewOffscreenSurface( input.events = panelFrameDelta.events; const auto& frame = m_nativeDemoRuntime.Update(input); - AppendDrawData(composedDrawData, frame.drawData); + if (previewConsumption.queueRuntimeFrame) { + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {}; + previewFrame.drawData = &frame.drawData; + previewFrame.canvasRect = resolvedSession.canvasRect; + previewFrame.logicalSize = UI::UISize( + resolvedSession.canvasRect.width, + resolvedSession.canvasRect.height); + previewFrame.debugName = + panelState != nullptr ? panelState->previewDebugName.data() : nullptr; + previewFrame.debugSource = + panelState != nullptr ? panelState->previewDebugSource.data() : nullptr; + m_hostedPreviewQueue.Submit(previewFrame); + } else if (previewConsumption.appendRuntimeDrawDataToShell) { + AppendDrawData(composedDrawData, frame.drawData); + } if (IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud)) { const auto& stats = frame.stats; @@ -1049,22 +1098,33 @@ bool Application::RenderHostedPreviewOffscreenSurface( textMuted); } - const auto drawDebugRect = - [this, &stats](const std::string& elementId, const UI::UIColor& color, const char* label) { - if (elementId.empty()) { - return; - } - UI::UIRect rect = {}; - if (!m_nativeDemoRuntime.TryGetElementRect(elementId, rect)) { - return; - } - m_nativeDemoCanvasHost.DrawOutlineRect(rect, color, 2.0f, 6.0f); - if (label != nullptr && label[0] != '\0') { - m_nativeDemoCanvasHost.DrawText(UI::UIPoint(rect.x + 4.0f, rect.y + 4.0f), label, color); - } - }; - drawDebugRect(stats.hoveredElementId, UI::UIColor(1.0f, 195.0f / 255.0f, 64.0f / 255.0f, 1.0f), "hover"); - drawDebugRect(stats.focusedElementId, UI::UIColor(64.0f / 255.0f, 214.0f / 255.0f, 1.0f, 1.0f), "focus"); + if (previewConsumption.drawRuntimeDebugRects) { + const auto drawDebugRect = + [this](const std::string& elementId, const UI::UIColor& color, const char* label) { + if (elementId.empty()) { + return; + } + UI::UIRect rect = {}; + if (!m_nativeDemoRuntime.TryGetElementRect(elementId, rect)) { + return; + } + m_nativeDemoCanvasHost.DrawOutlineRect(rect, color, 2.0f, 6.0f); + if (label != nullptr && label[0] != '\0') { + m_nativeDemoCanvasHost.DrawText( + UI::UIPoint(rect.x + 4.0f, rect.y + 4.0f), + label, + color); + } + }; + drawDebugRect( + stats.hoveredElementId, + UI::UIColor(1.0f, 195.0f / 255.0f, 64.0f / 255.0f, 1.0f), + "hover"); + drawDebugRect( + stats.focusedElementId, + UI::UIColor(64.0f / 255.0f, 214.0f / 255.0f, 1.0f, 1.0f), + "focus"); + } } m_nativeDemoCanvasHost.EndCanvas(); @@ -1072,7 +1132,9 @@ bool Application::RenderHostedPreviewOffscreenSurface( NativePanelFrameSummary summary = {}; summary.layout = panelLayout; summary.lineA = m_nativeDemoReloadSucceeded - ? frame.stats.statusMessage + ? Application::ComposeNativeHostedPreviewStatusLine( + previewConsumption, + frame.stats.statusMessage) : "Document reload failed; showing last retained runtime state."; summary.lineB = std::string(panelLayout.active ? "Active" : "Passive") + @@ -1085,6 +1147,35 @@ bool Application::RenderHostedPreviewOffscreenSurface( continue; } + const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab); + const bool nativeHostedPreview = + panelState != nullptr && + panelState->hostedPreviewEnabled && + IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {}; + const bool hasHostedSurfaceDescriptor = + nativeHostedPreview && + panelState != nullptr && + !panelState->previewDebugName.empty() && + m_hostedPreviewSurfaceRegistry.TryGetSurfaceDescriptor( + panelState->previewDebugName.data(), + hostedSurfaceDescriptor); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {}; + const bool showHostedSurfaceImage = + nativeHostedPreview && + panelState != nullptr && + !panelState->previewDebugName.empty() && + m_hostedPreviewSurfaceRegistry.TryGetSurfaceImage( + panelState->previewDebugName.data(), + hostedSurfaceImage); + const NativeHostedPreviewConsumption previewConsumption = + Application::ResolveNativeHostedPreviewConsumption( + nativeHostedPreview, + hasHostedSurfaceDescriptor, + showHostedSurfaceImage, + "Native layout preview pending", + "Waiting for queued native preview output to publish into the layout card."); + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession canvasSession = {}; canvasSession.hostRect = panelLayout.panelRect; canvasSession.canvasRect = panelLayout.canvasRect; @@ -1099,6 +1190,12 @@ bool Application::RenderHostedPreviewOffscreenSurface( canvasRequest.height = panelLayout.panelRect.height; canvasRequest.topInset = panelLayout.canvasRect.y - panelLayout.panelRect.y; canvasRequest.drawPreviewFrame = false; + canvasRequest.showSurfaceImage = previewConsumption.showSurfaceImage; + canvasRequest.surfaceImage = hostedSurfaceImage; + canvasRequest.placeholderTitle = + previewConsumption.placeholderTitle.empty() ? nullptr : previewConsumption.placeholderTitle.data(); + canvasRequest.placeholderSubtitle = + previewConsumption.placeholderSubtitle.empty() ? nullptr : previewConsumption.placeholderSubtitle.data(); const auto resolvedSession = m_nativeLayoutCanvasHost.BeginCanvas(canvasRequest); const bool wantsKeyboard = panelLayout.active; @@ -1119,13 +1216,29 @@ bool Application::RenderHostedPreviewOffscreenSurface( panelLayout.active && ShouldCaptureKeyboardNavigation(resolvedSession, m_nativeLayoutRuntime.GetFrameResult())); const auto& frame = m_nativeLayoutRuntime.Update(input); - AppendDrawData(composedDrawData, frame.drawData); + if (previewConsumption.queueRuntimeFrame) { + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {}; + previewFrame.drawData = &frame.drawData; + previewFrame.canvasRect = resolvedSession.canvasRect; + previewFrame.logicalSize = UI::UISize( + resolvedSession.canvasRect.width, + resolvedSession.canvasRect.height); + previewFrame.debugName = + panelState != nullptr ? panelState->previewDebugName.data() : nullptr; + previewFrame.debugSource = + panelState != nullptr ? panelState->previewDebugSource.data() : nullptr; + m_hostedPreviewQueue.Submit(previewFrame); + } else if (previewConsumption.appendRuntimeDrawDataToShell) { + AppendDrawData(composedDrawData, frame.drawData); + } m_nativeLayoutCanvasHost.EndCanvas(); NativePanelFrameSummary summary = {}; summary.layout = panelLayout; summary.lineA = m_nativeLayoutReloadSucceeded - ? frame.stats.statusMessage + ? Application::ComposeNativeHostedPreviewStatusLine( + previewConsumption, + frame.stats.statusMessage) : "Layout lab reload failed; showing last retained runtime state."; summary.lineB = std::to_string(frame.stats.rowCount) + " rows | " + @@ -1197,8 +1310,9 @@ bool Application::RenderHostedPreviewOffscreenSurface( } void Application::FrameLegacyImGuiHost() { - m_hostedPreviewQueue.BeginFrame(); - m_hostedPreviewSurfaceRegistry.BeginFrame(); + Application::BeginHostedPreviewFrameLifecycle( + m_hostedPreviewQueue, + m_hostedPreviewSurfaceRegistry); SyncHostedPreviewSurfaces(); if (m_windowCompositor == nullptr) { m_xcuiInputSource.ClearFrameTransients(); @@ -1273,6 +1387,11 @@ void Application::FrameLegacyImGuiHost() { } void Application::FrameNativeXCUIHost() { + Application::BeginHostedPreviewFrameLifecycle( + m_hostedPreviewQueue, + m_hostedPreviewSurfaceRegistry); + SyncHostedPreviewSurfaces(); + auto* nativeCompositor = dynamic_cast<::XCEngine::Editor::XCUIBackend::NativeWindowUICompositor*>(m_windowCompositor.get()); if (nativeCompositor == nullptr) { @@ -1287,6 +1406,7 @@ void Application::FrameNativeXCUIHost() { const auto shellFrameDelta = DispatchShellShortcuts(shellSnapshot); ::XCEngine::UI::UIDrawData nativeShellDrawData = BuildNativeShellDrawData(shellSnapshot, shellFrameDelta); nativeCompositor->SubmitRenderPacket(nativeShellDrawData, &m_hostedPreviewTextAtlasProvider); + SyncHostedPreviewSurfaces(); m_windowCompositor->RenderFrame( kClearColor, @@ -1294,6 +1414,8 @@ void Application::FrameNativeXCUIHost() { [this]( const ::XCEngine::Rendering::RenderContext& renderContext, const ::XCEngine::Rendering::RenderSurface& surface) { + RenderQueuedHostedPreviews(renderContext, surface); + MainWindowNativeBackdropRenderer::FrameState frameState = {}; frameState.elapsedSeconds = static_cast( std::chrono::duration(std::chrono::steady_clock::now() - m_startTime).count()); diff --git a/new_editor/src/Application.h b/new_editor/src/Application.h index b2f3417b..551b5e34 100644 --- a/new_editor/src/Application.h +++ b/new_editor/src/Application.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -86,6 +87,23 @@ public: std::function onHostedPreviewModeChanged = {}; }; + enum class NativeHostedPreviewSurfaceState : std::uint8_t { + Disabled = 0, + AwaitingSubmit, + Warming, + Live + }; + + struct NativeHostedPreviewConsumption { + NativeHostedPreviewSurfaceState surfaceState = NativeHostedPreviewSurfaceState::Disabled; + bool queueRuntimeFrame = false; + bool appendRuntimeDrawDataToShell = true; + bool showSurfaceImage = false; + bool drawRuntimeDebugRects = true; + std::string_view placeholderTitle = {}; + std::string_view placeholderSubtitle = {}; + }; + static ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot BuildShellShortcutSnapshot( const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& frameDelta) { ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot snapshot = {}; @@ -251,6 +269,69 @@ public: bindings.onHostedPreviewModeChanged); } + static void BeginHostedPreviewFrameLifecycle( + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue& previewQueue, + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) { + previewQueue.BeginFrame(); + surfaceRegistry.BeginFrame(); + } + + static bool HasHostedPreviewTextureRegistration( + const ::XCEngine::Editor::XCUIBackend::UITextureRegistration& registration) { + return registration.texture.IsValid() || + registration.cpuHandle.ptr != 0u || + registration.gpuHandle.ptr != 0u; + } + + static bool HasHostedPreviewPublishedTexture( + const ::XCEngine::Editor::XCUIBackend::UITextureRegistration& registration) { + return registration.texture.IsValid(); + } + + static NativeHostedPreviewConsumption ResolveNativeHostedPreviewConsumption( + bool nativeHostedPreview, + bool hasHostedSurfaceDescriptor, + bool showHostedSurfaceImage, + std::string_view pendingTitle = {}, + std::string_view pendingSubtitle = {}) { + NativeHostedPreviewConsumption consumption = {}; + if (!nativeHostedPreview) { + return consumption; + } + + consumption.queueRuntimeFrame = true; + consumption.appendRuntimeDrawDataToShell = false; + consumption.showSurfaceImage = showHostedSurfaceImage; + consumption.drawRuntimeDebugRects = showHostedSurfaceImage; + consumption.surfaceState = showHostedSurfaceImage + ? NativeHostedPreviewSurfaceState::Live + : (hasHostedSurfaceDescriptor + ? NativeHostedPreviewSurfaceState::Warming + : NativeHostedPreviewSurfaceState::AwaitingSubmit); + if (!showHostedSurfaceImage) { + consumption.placeholderTitle = pendingTitle; + consumption.placeholderSubtitle = pendingSubtitle; + } + + return consumption; + } + + static std::string ComposeNativeHostedPreviewStatusLine( + const NativeHostedPreviewConsumption& consumption, + std::string_view status) { + switch (consumption.surfaceState) { + case NativeHostedPreviewSurfaceState::AwaitingSubmit: + return std::string("Native surface awaiting submit | ") + std::string(status); + case NativeHostedPreviewSurfaceState::Warming: + return std::string("Native surface warming | ") + std::string(status); + case NativeHostedPreviewSurfaceState::Live: + return std::string("Native surface live | ") + std::string(status); + case NativeHostedPreviewSurfaceState::Disabled: + default: + return std::string(status); + } + } + int Run(HINSTANCE instance, int nCmdShow); private: @@ -291,7 +372,7 @@ private: return !debugName.empty() && colorTexture != nullptr && colorView != nullptr && - textureRegistration.IsValid() && + Application::HasHostedPreviewPublishedTexture(textureRegistration) && width > 0u && height > 0u; } diff --git a/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp b/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp index ba6fbf52..439b74b7 100644 --- a/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp +++ b/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp @@ -13,6 +13,26 @@ namespace XCUIBackend { namespace { +::XCEngine::RHI::ResourceViewDimension ResolveShaderResourceDimension( + ::XCEngine::RHI::TextureType textureType) { + switch (textureType) { + case ::XCEngine::RHI::TextureType::Texture1D: + return ::XCEngine::RHI::ResourceViewDimension::Texture1D; + case ::XCEngine::RHI::TextureType::Texture2D: + return ::XCEngine::RHI::ResourceViewDimension::Texture2D; + case ::XCEngine::RHI::TextureType::Texture2DArray: + return ::XCEngine::RHI::ResourceViewDimension::Texture2DArray; + case ::XCEngine::RHI::TextureType::Texture3D: + return ::XCEngine::RHI::ResourceViewDimension::Texture3D; + case ::XCEngine::RHI::TextureType::TextureCube: + return ::XCEngine::RHI::ResourceViewDimension::TextureCube; + case ::XCEngine::RHI::TextureType::TextureCubeArray: + return ::XCEngine::RHI::ResourceViewDimension::TextureCubeArray; + default: + return ::XCEngine::RHI::ResourceViewDimension::Texture2D; + } +} + bool PrepareSwapChainRender( ::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer, const float clearColor[4], @@ -172,14 +192,56 @@ bool NativeWindowUICompositor::CreateTextureDescriptor( ::XCEngine::RHI::RHIDevice* device, ::XCEngine::RHI::RHITexture* texture, UITextureRegistration& outRegistration) { - (void)device; - (void)texture; outRegistration = {}; - return false; + if (device == nullptr || texture == nullptr) { + return false; + } + + ::XCEngine::RHI::ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(texture->GetFormat()); + viewDesc.dimension = ResolveShaderResourceDimension(texture->GetTextureType()); + viewDesc.mipLevel = 0u; + + ::XCEngine::RHI::RHIResourceView* shaderResourceView = device->CreateShaderResourceView(texture, viewDesc); + if (shaderResourceView == nullptr) { + return false; + } + + if (!shaderResourceView->IsValid() || + shaderResourceView->GetViewType() != ::XCEngine::RHI::ResourceViewType::ShaderResource) { + shaderResourceView->Shutdown(); + delete shaderResourceView; + return false; + } + + outRegistration.cpuHandle.ptr = + reinterpret_cast(shaderResourceView->GetNativeHandle()); + outRegistration.texture.nativeHandle = + reinterpret_cast(shaderResourceView); + outRegistration.texture.width = texture->GetWidth(); + outRegistration.texture.height = texture->GetHeight(); + outRegistration.texture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView; + + if (!outRegistration.IsValid()) { + shaderResourceView->Shutdown(); + delete shaderResourceView; + outRegistration = {}; + return false; + } + + return true; } void NativeWindowUICompositor::FreeTextureDescriptor(const UITextureRegistration& registration) { - (void)registration; + if (registration.texture.kind != ::XCEngine::UI::UITextureHandleKind::ShaderResourceView || + registration.texture.nativeHandle == 0u) { + return; + } + + auto* shaderResourceView = + reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(registration.texture.nativeHandle); + shaderResourceView->Shutdown(); + delete shaderResourceView; } void NativeWindowUICompositor::SubmitRenderPacket(const XCUINativeWindowRenderPacket& packet) { diff --git a/new_editor/src/XCUIBackend/UITextureRegistration.h b/new_editor/src/XCUIBackend/UITextureRegistration.h index 392f285c..78ff1ec7 100644 --- a/new_editor/src/XCUIBackend/UITextureRegistration.h +++ b/new_editor/src/XCUIBackend/UITextureRegistration.h @@ -14,7 +14,15 @@ struct UITextureRegistration { ::XCEngine::UI::UITextureHandle texture = {}; bool IsValid() const { - return cpuHandle.ptr != 0u && gpuHandle.ptr != 0u && texture.IsValid(); + if (!texture.IsValid()) { + return false; + } + + if (texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) { + return cpuHandle.ptr != 0u; + } + + return cpuHandle.ptr != 0u && gpuHandle.ptr != 0u; } }; diff --git a/tests/NewEditor/test_application_shell_command_bindings.cpp b/tests/NewEditor/test_application_shell_command_bindings.cpp index 284fd706..2c20aa02 100644 --- a/tests/NewEditor/test_application_shell_command_bindings.cpp +++ b/tests/NewEditor/test_application_shell_command_bindings.cpp @@ -7,9 +7,20 @@ namespace { using XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry; +using XCEngine::Editor::XCUIBackend::UITextureRegistration; using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta; using XCEngine::Input::KeyCode; using XCEngine::NewEditor::Application; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +using XCEngine::UI::UITextureHandle; +using XCEngine::UI::UITextureHandleKind; constexpr std::size_t ToPanelIndex(Application::ShellPanelId panelId) { return static_cast(panelId); @@ -265,4 +276,47 @@ TEST(ApplicationShellCommandBindingsTest, PreviewShortcutInvokesCommandHandlerAn EXPECT_EQ(harness.hostedPreviewReconfigureCount, 1); } +TEST(ApplicationShellCommandBindingsTest, BeginHostedPreviewFrameLifecycleClearsQueueAndResetsRegistryFlags) { + XCUIHostedPreviewQueue queue = {}; + XCUIHostedPreviewSurfaceRegistry registry = {}; + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreview"); + drawList.AddFilledRect(UIRect(4.0f, 6.0f, 24.0f, 16.0f), UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + XCUIHostedPreviewFrame frame = {}; + frame.drawData = &drawData; + frame.canvasRect = UIRect(0.0f, 0.0f, 160.0f, 90.0f); + frame.logicalSize = UISize(160.0f, 90.0f); + frame.debugName = "HostedPreview"; + frame.debugSource = "tests.application"; + ASSERT_TRUE(queue.Submit(frame)); + ASSERT_EQ(queue.GetQueuedFrames().size(), 1u); + + registry.RecordQueuedFrame(queue.GetQueuedFrames().front(), 0u); + ASSERT_EQ(registry.GetDescriptors().size(), 1u); + EXPECT_TRUE(registry.GetDescriptors().front().queuedThisFrame); + + Application::BeginHostedPreviewFrameLifecycle(queue, registry); + + EXPECT_TRUE(queue.GetQueuedFrames().empty()); + ASSERT_EQ(registry.GetDescriptors().size(), 1u); + EXPECT_FALSE(registry.GetDescriptors().front().queuedThisFrame); +} + +TEST(ApplicationShellCommandBindingsTest, HostedPreviewRegistrationGuardsAllowTextureOnlyNativePublication) { + UITextureRegistration registration = {}; + registration.texture = UITextureHandle{ 99u, 256u, 128u, UITextureHandleKind::ShaderResourceView }; + + EXPECT_TRUE(Application::HasHostedPreviewTextureRegistration(registration)); + EXPECT_TRUE(Application::HasHostedPreviewPublishedTexture(registration)); + + registration = {}; + registration.cpuHandle.ptr = 11u; + registration.gpuHandle.ptr = 29u; + + EXPECT_TRUE(Application::HasHostedPreviewTextureRegistration(registration)); + EXPECT_FALSE(Application::HasHostedPreviewPublishedTexture(registration)); +} + } // namespace diff --git a/tests/NewEditor/test_native_window_ui_compositor.cpp b/tests/NewEditor/test_native_window_ui_compositor.cpp index d45a9965..6bbe0d37 100644 --- a/tests/NewEditor/test_native_window_ui_compositor.cpp +++ b/tests/NewEditor/test_native_window_ui_compositor.cpp @@ -4,11 +4,16 @@ #include "XCUIBackend/NativeWindowUICompositor.h" #include "XCUIBackend/UITextureRegistration.h" +#include +#include +#include +#include #include #include #include #include +#include namespace { @@ -20,6 +25,16 @@ using XCEngine::Editor::XCUIBackend::NativeWindowUICompositor; using XCEngine::Editor::XCUIBackend::UITextureRegistration; using XCEngine::Editor::XCUIBackend::XCUINativeWindowPresentStats; using XCEngine::Editor::XCUIBackend::XCUINativeWindowRenderPacket; +using XCEngine::RHI::Format; +using XCEngine::RHI::ResourceStates; +using XCEngine::RHI::ResourceViewDesc; +using XCEngine::RHI::ResourceViewDimension; +using XCEngine::RHI::ResourceViewType; +using XCEngine::RHI::RHIDevice; +using XCEngine::RHI::RHIResourceView; +using XCEngine::RHI::RHITexture; +using XCEngine::RHI::TextureType; +using XCEngine::UI::UITextureHandleKind; HWND MakeFakeHwnd() { return reinterpret_cast(static_cast(0x2345u)); @@ -76,6 +91,162 @@ XCEngine::UI::UIDrawData MakeDrawData() { return drawData; } +struct TrackingShaderViewState { + int shutdownCount = 0; + int destructorCount = 0; +}; + +class TrackingShaderResourceView final : public XCEngine::RHI::RHIShaderResourceView { +public: + TrackingShaderResourceView( + TrackingShaderViewState& state, + bool valid, + ResourceViewDimension dimension = ResourceViewDimension::Texture2D, + Format format = Format::R8G8B8A8_UNorm) + : m_state(state) + , m_valid(valid) + , m_dimension(dimension) + , m_format(format) { + } + + ~TrackingShaderResourceView() override { + ++m_state.destructorCount; + } + + void Shutdown() override { + ++m_state.shutdownCount; + m_valid = false; + } + + void* GetNativeHandle() override { + return this; + } + + bool IsValid() const override { + return m_valid; + } + + ResourceViewType GetViewType() const override { + return ResourceViewType::ShaderResource; + } + + ResourceViewDimension GetDimension() const override { + return m_dimension; + } + + Format GetFormat() const override { + return m_format; + } + +private: + TrackingShaderViewState& m_state; + bool m_valid = true; + ResourceViewDimension m_dimension = ResourceViewDimension::Texture2D; + Format m_format = Format::R8G8B8A8_UNorm; +}; + +class FakeTexture final : public RHITexture { +public: + FakeTexture( + std::uint32_t width, + std::uint32_t height, + Format format = Format::R8G8B8A8_UNorm, + TextureType textureType = TextureType::Texture2D) + : m_width(width) + , m_height(height) + , m_format(format) + , m_textureType(textureType) { + } + + std::uint32_t GetWidth() const override { return m_width; } + std::uint32_t GetHeight() const override { return m_height; } + std::uint32_t GetDepth() const override { return 1u; } + std::uint32_t GetMipLevels() const override { return 1u; } + Format GetFormat() const override { return m_format; } + TextureType GetTextureType() const override { return m_textureType; } + ResourceStates GetState() const override { return m_state; } + void SetState(ResourceStates state) override { m_state = state; } + void* GetNativeHandle() override { return this; } + const std::string& GetName() const override { return m_name; } + void SetName(const std::string& name) override { m_name = name; } + void Shutdown() override { m_shutdownCalled = true; } + + bool shutdownCalled() const { return m_shutdownCalled; } + +private: + std::uint32_t m_width = 0u; + std::uint32_t m_height = 0u; + Format m_format = Format::Unknown; + TextureType m_textureType = TextureType::Texture2D; + ResourceStates m_state = ResourceStates::Common; + std::string m_name = {}; + bool m_shutdownCalled = false; +}; + +class RecordingDevice final : public RHIDevice { +public: + TrackingShaderViewState* nextShaderViewState = nullptr; + bool createShaderViewValid = true; + int createShaderViewCount = 0; + RHITexture* lastShaderViewTexture = nullptr; + ResourceViewDesc lastShaderViewDesc = {}; + + bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } + void Shutdown() override {} + XCEngine::RHI::RHIBuffer* CreateBuffer(const XCEngine::RHI::BufferDesc&) override { return nullptr; } + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; } + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&, const void*, size_t, std::uint32_t) override { return nullptr; } + XCEngine::RHI::RHISwapChain* CreateSwapChain(const XCEngine::RHI::SwapChainDesc&, XCEngine::RHI::RHICommandQueue*) override { return nullptr; } + XCEngine::RHI::RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } + XCEngine::RHI::RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } + XCEngine::RHI::RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } + XCEngine::RHI::RHIPipelineState* CreatePipelineState(const XCEngine::RHI::GraphicsPipelineDesc&) override { return nullptr; } + XCEngine::RHI::RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return nullptr; } + XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } + XCEngine::RHI::RHISampler* CreateSampler(const XCEngine::RHI::SamplerDesc&) override { return nullptr; } + XCEngine::RHI::RHIRenderPass* CreateRenderPass( + std::uint32_t, + const XCEngine::RHI::AttachmentDesc*, + const XCEngine::RHI::AttachmentDesc*) override { return nullptr; } + XCEngine::RHI::RHIFramebuffer* CreateFramebuffer( + XCEngine::RHI::RHIRenderPass*, + std::uint32_t, + std::uint32_t, + std::uint32_t, + RHIResourceView**, + RHIResourceView*) override { return nullptr; } + XCEngine::RHI::RHIDescriptorPool* CreateDescriptorPool(const XCEngine::RHI::DescriptorPoolDesc&) override { return nullptr; } + XCEngine::RHI::RHIDescriptorSet* CreateDescriptorSet( + XCEngine::RHI::RHIDescriptorPool*, + const XCEngine::RHI::DescriptorSetLayoutDesc&) override { return nullptr; } + RHIResourceView* CreateVertexBufferView(XCEngine::RHI::RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateIndexBufferView(XCEngine::RHI::RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateRenderTargetView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateDepthStencilView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateShaderResourceView(XCEngine::RHI::RHITexture* texture, const ResourceViewDesc& desc) override { + ++createShaderViewCount; + lastShaderViewTexture = texture; + lastShaderViewDesc = desc; + if (nextShaderViewState == nullptr) { + return nullptr; + } + + return new TrackingShaderResourceView( + *nextShaderViewState, + createShaderViewValid, + desc.dimension != ResourceViewDimension::Unknown ? desc.dimension : ResourceViewDimension::Texture2D, + desc.format != 0u ? static_cast(desc.format) : Format::R8G8B8A8_UNorm); + } + RHIResourceView* CreateUnorderedAccessView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; } + const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; } + void* GetNativeDevice() override { return this; } + +private: + XCEngine::RHI::RHICapabilities m_capabilities = {}; + XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; +}; + TEST(NativeWindowUICompositorTest, RenderPacketReportsDrawDataPresenceAndClearResetsPayload) { XCUINativeWindowRenderPacket packet = {}; EXPECT_FALSE(packet.HasDrawData()); @@ -209,4 +380,66 @@ TEST(NativeWindowUICompositorTest, InterfaceFactoryReturnsSafeNativeCompositorDe compositor->Shutdown(); } +TEST(NativeWindowUICompositorTest, ShaderResourceViewRegistrationsStayValidWithoutGpuDescriptorHandle) { + UITextureRegistration registration = {}; + registration.cpuHandle.ptr = 17u; + registration.texture.nativeHandle = 33u; + registration.texture.width = 64u; + registration.texture.height = 32u; + registration.texture.kind = UITextureHandleKind::ShaderResourceView; + + EXPECT_TRUE(registration.IsValid()); + + registration.texture.kind = UITextureHandleKind::ImGuiDescriptor; + EXPECT_FALSE(registration.IsValid()); + + registration.gpuHandle.ptr = 19u; + EXPECT_TRUE(registration.IsValid()); +} + +TEST(NativeWindowUICompositorTest, CreateTextureDescriptorPublishesShaderResourceViewAndFreeReleasesIt) { + NativeWindowUICompositor compositor = {}; + RecordingDevice device = {}; + TrackingShaderViewState viewState = {}; + device.nextShaderViewState = &viewState; + + FakeTexture texture(256u, 128u, Format::R8G8B8A8_UNorm, TextureType::Texture2DArray); + UITextureRegistration registration = {}; + + ASSERT_TRUE(compositor.CreateTextureDescriptor(&device, &texture, registration)); + EXPECT_EQ(device.createShaderViewCount, 1); + EXPECT_EQ(device.lastShaderViewTexture, &texture); + EXPECT_EQ(device.lastShaderViewDesc.format, static_cast(Format::R8G8B8A8_UNorm)); + EXPECT_EQ(device.lastShaderViewDesc.dimension, ResourceViewDimension::Texture2DArray); + EXPECT_TRUE(registration.IsValid()); + EXPECT_NE(registration.cpuHandle.ptr, 0u); + EXPECT_EQ(registration.gpuHandle.ptr, 0u); + EXPECT_EQ(registration.texture.width, 256u); + EXPECT_EQ(registration.texture.height, 128u); + EXPECT_EQ(registration.texture.kind, UITextureHandleKind::ShaderResourceView); + EXPECT_EQ(registration.cpuHandle.ptr, registration.texture.nativeHandle); + + compositor.FreeTextureDescriptor(registration); + EXPECT_EQ(viewState.shutdownCount, 1); + EXPECT_EQ(viewState.destructorCount, 1); +} + +TEST(NativeWindowUICompositorTest, CreateTextureDescriptorRejectsInvalidShaderResourceViewAndCleansItUp) { + NativeWindowUICompositor compositor = {}; + RecordingDevice device = {}; + TrackingShaderViewState viewState = {}; + device.nextShaderViewState = &viewState; + device.createShaderViewValid = false; + + FakeTexture texture(96u, 64u); + UITextureRegistration registration = {}; + + EXPECT_FALSE(compositor.CreateTextureDescriptor(&device, &texture, registration)); + EXPECT_EQ(device.createShaderViewCount, 1); + EXPECT_FALSE(registration.IsValid()); + EXPECT_EQ(registration.texture.nativeHandle, 0u); + EXPECT_EQ(viewState.shutdownCount, 1); + EXPECT_EQ(viewState.destructorCount, 1); +} + } // namespace