From 03e0b362f7ca54785ffa6f68051e262bbf82b2b8 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 23 Apr 2026 00:36:28 +0800 Subject: [PATCH] new_editor: stabilize resize lifecycle groundwork --- ...eResizeJitterRootCleanupPlan_2026-04-22.md | 151 +++++++++++------- .../app/Platform/Win32/EditorWindow.cpp | 76 +++++++-- new_editor/app/Platform/Win32/EditorWindow.h | 12 +- .../Win32/EditorWindowChromeController.cpp | 67 ++++++-- .../Win32/EditorWindowFrameDriver.cpp | 2 +- .../app/Platform/Win32/EditorWindowState.h | 33 +++- .../EditorUtilityWindowCoordinator.cpp | 10 +- .../WindowManager/EditorWindowHostRuntime.cpp | 11 +- .../EditorWindowLifecycleCoordinator.cpp | 43 +++-- .../EditorWindowLifecycleCoordinator.h | 1 - .../EditorWindowMessageDispatcher.cpp | 64 ++++++-- .../EditorWindowMessageDispatcher.h | 4 + .../EditorWindowWorkspaceCoordinator.cpp | 52 +++--- .../app/Rendering/D3D12/D3D12HostDevice.cpp | 2 +- .../app/Rendering/D3D12/D3D12HostDevice.h | 2 +- .../Rendering/D3D12/D3D12WindowRenderLoop.cpp | 22 +-- .../Rendering/D3D12/D3D12WindowRenderer.cpp | 14 ++ .../app/Rendering/D3D12/D3D12WindowRenderer.h | 2 + .../D3D12/D3D12WindowSwapChainPresenter.cpp | 32 +++- 19 files changed, 439 insertions(+), 161 deletions(-) diff --git a/docs/plan/NewEditor_LiveResizeJitterRootCleanupPlan_2026-04-22.md b/docs/plan/NewEditor_LiveResizeJitterRootCleanupPlan_2026-04-22.md index 7f1b0cd8..126f349e 100644 --- a/docs/plan/NewEditor_LiveResizeJitterRootCleanupPlan_2026-04-22.md +++ b/docs/plan/NewEditor_LiveResizeJitterRootCleanupPlan_2026-04-22.md @@ -1,88 +1,129 @@ # NewEditor Live Resize Jitter Root Cleanup Plan -Date: 2026-04-22 -Status: In Progress +Date: 2026-04-23 +Status: Planned ## 1. Objective -Clean up the remaining `new_editor` live-resize jitter without weakening the already-restored anti-deformation contract. +彻底清理 `new_editor` 自定义顶栏窗口在实时拖拽 resize 时的抖动问题,并且继续保持当前已经恢复的“内容不变形”契约。 -The target contract remains: +这次修复只接受下面这个结果: -1. host-driven borderless resize must still be able to present target-size content before the native window visibly adopts that size -2. Win32 lifecycle handling must not reintroduce stretched stale content -3. resize-time rendering policy must stay centralized and structurally clean instead of being spread across message handlers and chrome code +1. 右侧和下侧,尤其右下角,拖拽时不再出现明显抖动 +2. `Scene` 内容继续实时跟随,不允许用冻结、延迟、复用旧帧拉伸来掩盖问题 +3. 自定义顶栏和 borderless resize 机制继续保留 +4. 代码结构保持主线化,不能靠零散补丁维持 ## 2. Confirmed Root Cause -The current jitter is caused by two structural problems acting together: +当前问题的根源不是 `Scene` 视口本身慢,也不是自定义顶栏设计有问题。 -1. live borderless resize still drives a real swap-chain resize on each pointer step -2. that resize path still performs a hard D3D12 synchronization before touching swap-chain buffers -3. the same resize step can then be rendered again from `WM_SIZE`, `WM_PAINT`, and the steady-state main loop +已经确认的根因是窗口层的 resize / present 时序错误: -So the root issue is not the custom title bar design. The custom title bar is the mechanism that preserves anti-deformation. The remaining problem is the host-side resize / present execution strategy under it. +1. `EditorWindowChromeController::HandleResizePointerMove(...)` 在 borderless live resize 中先把目标尺寸写进 predicted client size +2. `EditorWindow::ResolveRenderClientPixelSize(...)` 会优先使用这个 predicted size +3. 于是这一步立即帧会按“未来尺寸”完成整窗布局、viewport 请求和 UI 合成 +4. 但是此时 HWND/client rect 还没有通过 `SetWindowPos(...)` 真正确认到这个尺寸 +5. 这帧随后直接进入可见 swapchain,再由 DXGI `DXGI_SCALING_STRETCH` 填到仍处于旧几何的窗口表面 -## 3. Red Lines +因此抖动的本质不是“重绘慢”,而是: -This cleanup must not: +1. 可见 swapchain 被拿来展示了一帧与当前 HWND 几何不一致的 future-size 内容 +2. DXGI stretch 误差在归一化坐标接近 `1.0` 的位置最大,所以右侧和下侧最明显 +3. `Scene` 只是把这段错误状态维持得更久,因此把问题放大了 -1. remove the custom title-bar-driven anti-deformation flow -2. move resize / present ownership into the Editor UI content layer -3. add scattered `if (interactiveResize)` patches across unrelated code -4. create a second parallel frame executor beside the existing frame driver -5. weaken D3D12 lifetime safety by blindly deleting resize synchronization without replacing it with a narrower correct rule +直接调整 `Scene` 视口 splitter 之所以平滑,是因为那条路径没有“future-size frame 先进入旧 HWND 可见 swapchain”这个错误。 -## 4. Target End State +## 3. Architectural Decision -After the cleanup: +根修复必须把两个尺寸彻底分开: -1. immediate resize-time frames still go through one shared frame-driving entry point -2. predicted borderless resize transitions present at most one intentional immediate frame per interaction step -3. `WM_SIZE` only acknowledges a predicted resize that was already presented and does not redundantly render it again -4. the next steady-state app-loop frame is skipped once after an already-presented immediate resize frame -5. live resize waits only for tracked in-flight frame retirement instead of forcing a fresh queue-wide idle synchronization each step +1. `predicted size` + 用于 borderless resize 期间的下一步布局推演和离屏整窗合成 +2. `committed visible size` + 只对应已经被 Win32 / DXGI 真正确认的 HWND client size 和可见 swapchain backbuffer -## 5. Implementation Plan +最终原则: -### Phase A. Re-centralize immediate resize-time frame execution +1. 预测尺寸可以驱动布局和离屏合成 +2. 可见 swapchain 只能 present committed size 的内容 +3. HWND 几何提交前,future-size frame 不能再直接进入可见 swapchain -1. add an explicit immediate-frame path on top of `EditorWindowFrameDriver` -2. route borderless predicted transitions through that shared driver instead of calling `RenderFrame(...)` directly -3. keep transfer-request handling ownership unchanged +## 4. Red Lines -### Phase B. Add predicted-presentation acknowledgement state +本次修复不能做下面这些事: -1. extend host runtime state so predicted client size can remember whether it has already been presented -2. let borderless predicted transitions mark the prediction as presented only after the immediate frame path runs -3. teach `WM_SIZE` handling to consume that acknowledgement and skip its redundant render when the incoming size is only the native echo of an already-presented prediction +1. 不能取消自定义顶栏 +2. 不能把 resize 责任下沉到 UI Editor 层 +3. 不能冻结 `Scene` +4. 不能复用旧纹理做拉伸/延迟展示来冒充实时 resize +5. 不能在各处消息处理里堆条件分支补丁 +6. 不能破坏 tab 拖拽拆窗、utility window、正常 `WM_SIZE`、viewport splitter resize -### Phase C. Eliminate the immediate-frame / steady-state double render +## 5. Target End State -1. add one-shot state that skips the next steady-state frame after an immediate synchronous frame already presented the same window -2. consume that skip only inside `EditorWindowHostRuntime::RenderAllWindows(...)` -3. keep normal steady-state rendering unchanged for all other cases +修完之后,窗口层行为必须变成下面这样: -### Phase D. Narrow D3D12 live-resize synchronization +1. pointer move 到来时,先计算 predicted rect +2. predicted rect 只驱动离屏整窗 composition target 的大小和内容 +3. `SetWindowPos(...)` 提交 HWND 几何 +4. 真正收到 committed client size 后,再 resize 可见 swapchain +5. 可见 swapchain 只展示 committed size backbuffer +6. 如果 predicted size 和 committed size 一致,则直接把最近一次预测合成结果拷到 committed backbuffer,再 present +7. 如果 committed size 变化继续推进,则按新的 predicted size 重新离屏合成,但仍不提前污染可见 swapchain -1. add a host-device helper that waits only for tracked submitted frame slots to retire -2. use that narrower helper from live swap-chain resize -3. keep full `WaitForGpuIdle()` for shutdown / destruction paths where full teardown semantics are still required +## 6. Implementation Plan -## 6. Validation Requirements +### Phase A. Separate predicted composition from visible swapchain state -Before completion: +1. 在 `D3D12WindowSwapChainPresenter` / `D3D12WindowRenderer` 引入窗口级离屏 composition target +2. 为它建立独立的 color texture、RTV、SRV 和 `RenderSurface` +3. 把“当前可见 swapchain 尺寸”和“最近预测 composition 尺寸”分成两套状态 -1. `XCUIEditorApp` must build successfully after the cleanup -2. borderless live resize must still avoid content deformation -3. redundant `WM_SIZE` + steady-state resize-time frame duplication must be structurally removed from the new code path -4. no unrelated docking / detach / utility-window behavior may regress +### Phase B. Route immediate resize frames into the offscreen composition target -## 7. Completion Criteria +1. 给窗口 render loop 增加 present target policy +2. 正常 steady-state 帧仍然走 committed swapchain backbuffer +3. borderless resize pointer-move immediate frame 改为绘制到 predicted composition target +4. 这一步仍然执行真实 `Scene` 渲染和整窗 UI 合成,不做冻结 -This work is complete only when: +### Phase C. Commit HWND geometry first, then publish only committed-size content -1. live resize keeps the anti-deformation contract -2. one resize interaction no longer fans out into multiple avoidable frame executions -3. D3D12 live-resize synchronization is reduced from queue-wide idle to tracked frame retirement -4. the resulting code remains a mainline host/render lifecycle cleanup instead of a symptom patch +1. 清理 `HandleResizePointerMove(...)` 的顺序,让未来尺寸帧不再直接 present 到旧 swapchain +2. `SetWindowPos(...)` 提交后,由实际 committed client size 驱动 swapchain resize +3. 只有 committed size backbuffer 才允许进入 `Present()` +4. 如果最近的 predicted composition 与 committed size 匹配,则把该离屏结果拷贝到 committed backbuffer 后 present + +### Phase D. Narrow WM_SIZE / prediction acknowledgement semantics + +1. 预测尺寸状态不再表达“这帧已经进入可见 swapchain” +2. 改为表达“这份 predicted composition 已经生成,等待 committed publish” +3. `WM_SIZE` 只消费 committed size,并决定是否复用最近一次 composition 结果 +4. 保留已经正确收窄的 frame retirement / lifecycle 安全逻辑 + +### Phase E. Clean up right/bottom asymmetry at the actual source + +1. 彻底移除“future-size frame displayed in old HWND”这件事 +2. 让 DXGI stretch 不再承担预测阶段的显示职责 +3. 右侧、下侧、右下角的额外抖动应当因此一起消失 + +## 7. Validation Requirements + +完成前必须验证: + +1. `Scene` 激活时拖右侧、下侧、右下角,不再出现现在这种严重抖动 +2. `Scene` 内容继续实时响应 resize,而不是冻结或延后 +3. `Game`、空视口、普通面板 resize 行为不回退 +4. 直接拖 `Scene` 视口 splitter 仍然保持原来的平滑度 +5. tab 拖出独立窗口、utility window、maximize/restore、DPI change 不回退 +6. 不再出现 resize 期间 future-size frame 被直接 present 到旧窗口的问题 + +## 8. Completion Criteria + +只有同时满足下面几点,这个问题才算真正收拾干净: + +1. 不变形契约保留 +2. `Scene` 保持实时 resize +3. 右/下/右下 resize 抖动被从源头消除 +4. 可见 swapchain 与 HWND committed 几何重新建立一一对应 +5. 实现落在窗口平台层和渲染主线上,而不是 UI 层补丁 diff --git a/new_editor/app/Platform/Win32/EditorWindow.cpp b/new_editor/app/Platform/Win32/EditorWindow.cpp index 6807f60b..bdb3fc49 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.cpp +++ b/new_editor/app/Platform/Win32/EditorWindow.cpp @@ -2,6 +2,7 @@ #include "Bootstrap/EditorResources.h" #include "Platform/Win32/EditorWindowChromeController.h" #include "Platform/Win32/EditorWindowContentController.h" +#include "Platform/Win32/EditorWindowFrameDriver.h" #include "Platform/Win32/EditorWindowSupport.h" #include "Platform/Win32/EditorWindowFrameOrchestrator.h" #include "Platform/Win32/EditorWindowInputController.h" @@ -103,12 +104,20 @@ bool EditorWindow::HasHwnd() const { return m_state->window.hwnd != nullptr; } +EditorWindowLifecycleState EditorWindow::GetLifecycleState() const { + return m_state->window.lifecycle; +} + bool EditorWindow::IsPrimary() const { return m_state->window.primary; } bool EditorWindow::IsClosing() const { - return m_state->window.closing; + return m_state->window.lifecycle == EditorWindowLifecycleState::Closing; +} + +bool EditorWindow::IsDestroyed() const { + return m_state->window.lifecycle == EditorWindowLifecycleState::Destroyed; } bool EditorWindow::IsRenderReady() const { @@ -159,21 +168,25 @@ void EditorWindow::ClearExternalDockHostDropPreview() { void EditorWindow::AttachHwnd(HWND hwnd) { m_state->window.hwnd = hwnd; - m_state->window.closing = false; + m_state->window.lifecycle = EditorWindowLifecycleState::NativeAttached; +} + +void EditorWindow::MarkInitializing() { + m_state->window.lifecycle = EditorWindowLifecycleState::Initializing; +} + +void EditorWindow::MarkRunning() { + m_state->window.lifecycle = EditorWindowLifecycleState::Running; } void EditorWindow::MarkDestroyed() { m_state->window.hwnd = nullptr; - m_state->window.closing = false; + m_state->window.lifecycle = EditorWindowLifecycleState::Destroyed; m_inputController->ResetWindowState(); } void EditorWindow::MarkClosing() { - m_state->window.closing = true; -} - -void EditorWindow::ClearClosing() { - m_state->window.closing = false; + m_state->window.lifecycle = EditorWindowLifecycleState::Closing; } void EditorWindow::SetPrimary(bool primary) { @@ -219,12 +232,19 @@ bool EditorWindow::Initialize( << " scale=" << GetDpiScale(); LogRuntimeTrace("window", dpiTrace.str()); - return m_runtime->Initialize( + MarkInitializing(); + const bool initialized = m_runtime->Initialize( m_state->window.hwnd, repoRoot, editorContext, captureRoot, autoCaptureOnStartup); + if (initialized) { + MarkRunning(); + } else { + m_state->window.lifecycle = EditorWindowLifecycleState::NativeAttached; + } + return initialized; } void EditorWindow::Shutdown() { @@ -234,7 +254,7 @@ void EditorWindow::Shutdown() { << reinterpret_cast(GetHwnd()) << std::dec << " primary=" << (IsPrimary() ? 1 : 0) - << " closing=" << (IsClosing() ? 1 : 0) + << " lifecycle=" << GetEditorWindowLifecycleStateName(GetLifecycleState()) << " runtimeReady=" << (m_runtime->IsReady() ? 1 : 0); LogRuntimeTrace("window-close", trace.str()); ForceReleasePointerCapture(); @@ -655,12 +675,44 @@ EditorWindowFrameTransferRequests EditorWindow::OnPaintMessage( PAINTSTRUCT paintStruct = {}; BeginPaint(m_state->window.hwnd, &paintStruct); const EditorWindowFrameTransferRequests transferRequests = - RenderFrame(editorContext, globalTabDragActive); - m_chromeController->RequestSkipNextSteadyStateFrame(); + EditorWindowFrameDriver::DriveImmediateFrame( + *this, + editorContext, + globalTabDragActive); EndPaint(m_state->window.hwnd, &paintStruct); return transferRequests; } +void EditorWindow::QueueCompletedImmediateFrame( + EditorWindowFrameTransferRequests transferRequests) { + m_hasQueuedCompletedImmediateFrame = true; + if (transferRequests.beginGlobalTabDrag.has_value()) { + m_queuedImmediateFrameTransferRequests.beginGlobalTabDrag = + std::move(transferRequests.beginGlobalTabDrag); + } + if (transferRequests.detachPanel.has_value()) { + m_queuedImmediateFrameTransferRequests.detachPanel = + std::move(transferRequests.detachPanel); + } + if (transferRequests.openUtilityWindow.has_value()) { + m_queuedImmediateFrameTransferRequests.openUtilityWindow = + std::move(transferRequests.openUtilityWindow); + } +} + +bool EditorWindow::HasQueuedCompletedImmediateFrame() const { + return m_hasQueuedCompletedImmediateFrame; +} + +EditorWindowFrameTransferRequests +EditorWindow::ConsumeQueuedCompletedImmediateFrameTransferRequests() { + m_hasQueuedCompletedImmediateFrame = false; + EditorWindowFrameTransferRequests transferRequests = + std::move(m_queuedImmediateFrameTransferRequests); + m_queuedImmediateFrameTransferRequests = {}; + return transferRequests; +} + UIRect EditorWindow::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { if (!IsBorderlessWindowEnabled()) { return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index 09fadac8..c5928272 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -5,6 +5,7 @@ #endif #include "Platform/Win32/EditorWindowPointerCapture.h" +#include "Platform/Win32/EditorWindowState.h" #include "Platform/Win32/EditorWindowTransferRequests.h" #include @@ -88,8 +89,10 @@ public: std::string_view GetWindowId() const; HWND GetHwnd() const; bool HasHwnd() const; + EditorWindowLifecycleState GetLifecycleState() const; bool IsPrimary() const; bool IsClosing() const; + bool IsDestroyed() const; const std::wstring& GetTitle() const; const UIEditorWorkspaceController* TryGetWorkspaceController() const; const UIEditorWorkspaceController& GetWorkspaceController() const; @@ -122,9 +125,10 @@ private: void ClearExternalDockHostDropPreview(); void AttachHwnd(HWND hwnd); + void MarkInitializing(); + void MarkRunning(); void MarkDestroyed(); void MarkClosing(); - void ClearClosing(); void SetPrimary(bool primary); void SetTrackingMouseLeave(bool trackingMouseLeave); void SetTitle(std::wstring title); @@ -145,6 +149,10 @@ private: EditorWindowFrameTransferRequests OnPaintMessage( EditorContext& editorContext, bool globalTabDragActive); + void QueueCompletedImmediateFrame( + EditorWindowFrameTransferRequests transferRequests); + bool HasQueuedCompletedImmediateFrame() const; + EditorWindowFrameTransferRequests ConsumeQueuedCompletedImmediateFrameTransferRequests(); bool OnResize(UINT width, UINT height); void OnEnterSizeMove(); bool OnExitSizeMove(); @@ -256,6 +264,8 @@ private: std::unique_ptr m_frameOrchestrator = {}; std::unique_ptr m_inputController = {}; std::unique_ptr m_runtime = {}; + EditorWindowFrameTransferRequests m_queuedImmediateFrameTransferRequests = {}; + bool m_hasQueuedCompletedImmediateFrame = false; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp index 0f739b99..3e44d9f6 100644 --- a/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowChromeController.cpp @@ -11,7 +11,9 @@ #include #include +#include #include +#include #include #include @@ -32,6 +34,11 @@ constexpr float kTitleBarLogoInsetLeft = 8.0f; constexpr float kTitleBarLogoTextGap = 8.0f; constexpr float kTitleBarFrameStatsInsetRight = 12.0f; +bool IsVerboseResizeTraceEnabled() { + static const bool s_enabled = ResolveVerboseRuntimeTraceEnabled(); + return s_enabled; +} + float ResolveDetachedTabWidth( std::string_view text, const UIEditorTextMeasurer* textMeasurer) { @@ -340,17 +347,42 @@ bool EditorWindowChromeController::HandleResizePointerMove( return true; } + const auto resizeStepBegin = std::chrono::steady_clock::now(); SetPredictedClientPixelSize( static_cast(width), static_cast(height)); + const auto applyResizeBegin = std::chrono::steady_clock::now(); if (window.ApplyWindowResize(static_cast(width), static_cast(height))) { - (void)EditorWindowFrameDriver::DriveImmediateFrame( - window, - editorContext, - globalTabDragActive); + const auto immediateFrameBegin = std::chrono::steady_clock::now(); + window.QueueCompletedImmediateFrame( + EditorWindowFrameDriver::DriveImmediateFrame( + window, + editorContext, + globalTabDragActive)); + const auto immediateFrameEnd = std::chrono::steady_clock::now(); MarkPredictedClientPixelSizePresented(); + if (IsVerboseResizeTraceEnabled()) { + const auto totalEnd = std::chrono::steady_clock::now(); + const auto applyResizeMs = + std::chrono::duration_cast( + immediateFrameBegin - applyResizeBegin).count(); + const auto immediateFrameMs = + std::chrono::duration_cast( + immediateFrameEnd - immediateFrameBegin).count(); + const auto totalMs = + std::chrono::duration_cast( + totalEnd - resizeStepBegin).count(); + std::ostringstream trace = {}; + trace << "borderless live resize step predicted=" + << width << 'x' << height + << " applyResizeMs=" << applyResizeMs + << " immediateFrameMs=" << immediateFrameMs + << " totalBeforeSetWindowPosMs=" << totalMs; + LogRuntimeTrace("resize", trace.str()); + } } + const auto setWindowPosBegin = std::chrono::steady_clock::now(); SetWindowPos( window.m_state->window.hwnd, nullptr, @@ -358,7 +390,23 @@ bool EditorWindowChromeController::HandleResizePointerMove( targetRect.top, width, height, - SWP_NOZORDER | SWP_NOACTIVATE); + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOREDRAW); + ValidateRect(window.m_state->window.hwnd, nullptr); + if (IsVerboseResizeTraceEnabled()) { + const auto totalEnd = std::chrono::steady_clock::now(); + const auto setWindowPosMs = + std::chrono::duration_cast( + totalEnd - setWindowPosBegin).count(); + const auto totalMs = + std::chrono::duration_cast( + totalEnd - resizeStepBegin).count(); + std::ostringstream trace = {}; + trace << "borderless live resize present+apply complete predicted=" + << width << 'x' << height + << " setWindowPosMs=" << setWindowPosMs + << " totalStepMs=" << totalMs; + LogRuntimeTrace("resize", trace.str()); + } return true; } @@ -828,10 +876,11 @@ bool EditorWindowChromeController::ApplyPredictedWindowRectTransition( SetPredictedClientPixelSize(static_cast(width), static_cast(height)); if (window.ApplyWindowResize(static_cast(width), static_cast(height))) { - (void)EditorWindowFrameDriver::DriveImmediateFrame( - window, - editorContext, - globalTabDragActive); + window.QueueCompletedImmediateFrame( + EditorWindowFrameDriver::DriveImmediateFrame( + window, + editorContext, + globalTabDragActive)); MarkPredictedClientPixelSizePresented(); } SetWindowPos( diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameDriver.cpp b/new_editor/app/Platform/Win32/EditorWindowFrameDriver.cpp index bae00f60..e67bf73a 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameDriver.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrameDriver.cpp @@ -12,7 +12,7 @@ EditorWindowFrameTransferRequests EditorWindowFrameDriver::DriveFrameInternal( bool requestSkipNextSteadyStateFrame) { if (!window.IsRenderReady() || window.GetHwnd() == nullptr || - window.IsClosing()) { + window.GetLifecycleState() != EditorWindowLifecycleState::Running) { return {}; } diff --git a/new_editor/app/Platform/Win32/EditorWindowState.h b/new_editor/app/Platform/Win32/EditorWindowState.h index 25260936..5b17b15c 100644 --- a/new_editor/app/Platform/Win32/EditorWindowState.h +++ b/new_editor/app/Platform/Win32/EditorWindowState.h @@ -6,17 +6,48 @@ #include +#include #include +#include namespace XCEngine::UI::Editor::App { +enum class EditorWindowLifecycleState : std::uint8_t { + PendingNativeCreate = 0, + NativeAttached, + Initializing, + Running, + Closing, + Destroyed, +}; + +inline std::string_view GetEditorWindowLifecycleStateName( + EditorWindowLifecycleState state) { + switch (state) { + case EditorWindowLifecycleState::PendingNativeCreate: + return "PendingNativeCreate"; + case EditorWindowLifecycleState::NativeAttached: + return "NativeAttached"; + case EditorWindowLifecycleState::Initializing: + return "Initializing"; + case EditorWindowLifecycleState::Running: + return "Running"; + case EditorWindowLifecycleState::Closing: + return "Closing"; + case EditorWindowLifecycleState::Destroyed: + return "Destroyed"; + } + + return "Unknown"; +} + struct EditorWindowWindowState { HWND hwnd = nullptr; std::string windowId = {}; std::wstring title = {}; std::string titleText = {}; bool primary = false; - bool closing = false; + EditorWindowLifecycleState lifecycle = EditorWindowLifecycleState::PendingNativeCreate; }; struct EditorWindowState { diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorUtilityWindowCoordinator.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorUtilityWindowCoordinator.cpp index 715e0e4b..040be134 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorUtilityWindowCoordinator.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorUtilityWindowCoordinator.cpp @@ -16,7 +16,7 @@ namespace { bool IsLiveWindow(const EditorWindow* window) { return window != nullptr && window->GetHwnd() != nullptr && - !window->IsClosing(); + window->GetLifecycleState() == EditorWindowLifecycleState::Running; } LONG ResolveOuterDimension(float value, LONG fallback) { @@ -54,10 +54,10 @@ void EditorUtilityWindowCoordinator::HandleWindowFrameTransferRequests( bool EditorUtilityWindowCoordinator::TryProcessOpenUtilityWindowRequest( EditorWindow& sourceWindow, const EditorWindowOpenUtilityWindowRequest& request) { - if (sourceWindow.IsClosing()) { + if (!IsLiveWindow(&sourceWindow)) { LogRuntimeTrace( "utility", - "open utility window request rejected: source window is closing"); + "open utility window request rejected: source window is not running"); return false; } @@ -69,7 +69,7 @@ bool EditorUtilityWindowCoordinator::TryProcessOpenUtilityWindowRequest( } if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(descriptor->windowId); - existingWindow != nullptr && existingWindow->GetHwnd() == nullptr && + existingWindow != nullptr && existingWindow->IsDestroyed() && m_lifecycleCoordinator != nullptr) { m_lifecycleCoordinator->ReapDestroyedWindows(); } @@ -87,7 +87,7 @@ bool EditorUtilityWindowCoordinator::TryProcessOpenUtilityWindowRequest( existingWindow != nullptr) { LogRuntimeTrace( "utility", - "open utility window request rejected: existing utility window is still closing"); + "open utility window request rejected: existing utility window is not reusable"); return false; } diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp index 63070fc8..06438794 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp @@ -45,7 +45,7 @@ std::string DescribeHostWindows( stream << window->GetWindowId() << "{hwnd=" << DescribeHwnd(window->GetHwnd()) << ",primary=" << (window->IsPrimary() ? '1' : '0') - << ",closing=" << (window->IsClosing() ? '1' : '0') + << ",state=" << GetEditorWindowLifecycleStateName(window->GetLifecycleState()) << '}'; } stream << ']'; @@ -159,7 +159,10 @@ void EditorWindowHostRuntime::BindLifecycleCoordinator( } void EditorWindowHostRuntime::HandlePendingNativeWindowCreated(HWND hwnd) { - if (m_pendingCreateWindow != nullptr && !m_pendingCreateWindow->HasHwnd()) { + if (m_pendingCreateWindow != nullptr && + m_pendingCreateWindow->GetLifecycleState() == + EditorWindowLifecycleState::PendingNativeCreate && + !m_pendingCreateWindow->HasHwnd()) { m_pendingCreateWindow->AttachHwnd(hwnd); } } @@ -183,7 +186,7 @@ void EditorWindowHostRuntime::RenderAllWindows( for (const std::unique_ptr& window : m_windows) { if (window == nullptr || window->GetHwnd() == nullptr || - window->IsClosing()) { + window->GetLifecycleState() != EditorWindowLifecycleState::Running) { continue; } @@ -213,7 +216,7 @@ void EditorWindowHostRuntime::RenderAllWindows( for (WindowFrameTransferBatch& batch : transferBatches) { if (batch.sourceWindow == nullptr || batch.sourceWindow->GetHwnd() == nullptr || - batch.sourceWindow->IsClosing()) { + batch.sourceWindow->GetLifecycleState() != EditorWindowLifecycleState::Running) { continue; } diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.cpp index 3e2e43ba..a74b86c7 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.cpp @@ -38,7 +38,7 @@ std::string DescribeHostWindows( stream << window->GetWindowId() << "{hwnd=" << DescribeHwnd(window->GetHwnd()) << ",primary=" << (window->IsPrimary() ? '1' : '0') - << ",closing=" << (window->IsClosing() ? '1' : '0') + << ",state=" << GetEditorWindowLifecycleStateName(window->GetLifecycleState()) << '}'; } stream << ']'; @@ -56,6 +56,10 @@ EditorWindowLifecycleCoordinator::EditorWindowLifecycleCoordinator( EditorWindowLifecycleCoordinator::~EditorWindowLifecycleCoordinator() = default; void EditorWindowLifecycleCoordinator::PostCloseRequest(EditorWindow& window) { + if (window.IsDestroyed()) { + return; + } + const HWND hwnd = window.GetHwnd(); if (!window.IsClosing()) { window.MarkClosing(); @@ -77,13 +81,18 @@ void EditorWindowLifecycleCoordinator::PostCloseRequest(EditorWindow& window) { } void EditorWindowLifecycleCoordinator::ExecuteCloseRequest(EditorWindow& window) { + if (window.IsDestroyed()) { + return; + } + const HWND hwnd = window.GetHwnd(); LogRuntimeTrace( "window-close", "ExecuteCloseRequest begin windowId='" + std::string(window.GetWindowId()) + "' hwnd=" + DescribeHwnd(hwnd) + " primary=" + (window.IsPrimary() ? "1" : "0") + - " closingBefore=" + (window.IsClosing() ? "1" : "0") + + " lifecycleBefore=" + + std::string(GetEditorWindowLifecycleStateName(window.GetLifecycleState())) + " workspace=" + m_workspaceCoordinator.DescribeWindowSet() + " host=" + DescribeHostWindows(m_hostRuntime.GetWindows())); @@ -106,6 +115,10 @@ void EditorWindowLifecycleCoordinator::ExecuteCloseRequest(EditorWindow& window) } void EditorWindowLifecycleCoordinator::HandleNativeWindowDestroyed(EditorWindow& window) { + if (window.IsDestroyed()) { + return; + } + const bool destroyedPrimary = m_workspaceCoordinator.IsPrimaryWindowId(window.GetWindowId()); LogRuntimeTrace( @@ -165,11 +178,12 @@ void EditorWindowLifecycleCoordinator::AbortUnregisteredWindow(EditorWindow& win } ShutdownRuntimeIfNeeded(window); - window.MarkDestroyed(); if (hwnd != nullptr && IsWindow(hwnd)) { DestroyWindow(hwnd); + } else { + window.MarkDestroyed(); } - EraseWindow(window); + ReapDestroyedWindows(); LogRuntimeTrace( "window-close", @@ -199,7 +213,7 @@ void EditorWindowLifecycleCoordinator::ReapDestroyedWindows() { auto& windows = m_hostRuntime.GetWindows(); for (auto it = windows.begin(); it != windows.end();) { EditorWindow* const window = it->get(); - if (window == nullptr || window->GetHwnd() != nullptr) { + if (window == nullptr || !window->IsDestroyed()) { ++it; continue; } @@ -225,25 +239,6 @@ void EditorWindowLifecycleCoordinator::ShutdownRuntimeIfNeeded(EditorWindow& win } } -void EditorWindowLifecycleCoordinator::EraseWindow(EditorWindow& window) { - auto& windows = m_hostRuntime.GetWindows(); - const auto it = std::find_if( - windows.begin(), - windows.end(), - [&window](const std::unique_ptr& candidate) { - return candidate.get() == &window; - }); - if (it == windows.end()) { - return; - } - - if (m_hostRuntime.m_pendingCreateWindow == &window) { - m_hostRuntime.m_pendingCreateWindow = nullptr; - } - - windows.erase(it); -} - void EditorWindowLifecycleCoordinator::LogRuntimeTrace( std::string_view channel, std::string_view message) const { diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.h index 972f9da2..64c7c5ba 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.h +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowLifecycleCoordinator.h @@ -24,7 +24,6 @@ public: private: void ShutdownRuntimeIfNeeded(EditorWindow& window); - void EraseWindow(EditorWindow& window); void LogRuntimeTrace(std::string_view channel, std::string_view message) const; EditorWindowHostRuntime& m_hostRuntime; diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp index fb3f4292..8765eb0f 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp @@ -45,7 +45,7 @@ std::string DescribeHostWindows(const EditorWindowHostRuntime& hostRuntime) { stream << window->GetWindowId() << "{hwnd=" << DescribeHwnd(window->GetHwnd()) << ",primary=" << (window->IsPrimary() ? '1' : '0') - << ",closing=" << (window->IsClosing() ? '1' : '0') + << ",state=" << GetEditorWindowLifecycleStateName(window->GetLifecycleState()) << '}'; } stream << ']'; @@ -74,14 +74,36 @@ void EditorWindowMessageDispatcher::DispatchWindowFrameTransferRequests( transferRequests); } -void EditorWindowMessageDispatcher::RenderAndHandleWindowFrame(const DispatchContext& context) { - EditorWindowFrameTransferRequests transferRequests = EditorWindowFrameDriver::DriveImmediateFrame( - context.window, - context.hostRuntime.GetEditorContext(), - context.workspaceCoordinator.IsGlobalTabDragActive()); - if (transferRequests.HasPendingRequests()) { - DispatchWindowFrameTransferRequests(context, transferRequests); +void EditorWindowMessageDispatcher::FinalizeImmediateFrame( + const DispatchContext& context, + const EditorWindowFrameTransferRequests& transferRequests) { + context.workspaceCoordinator.RefreshWindowPresentation(context.window); + context.utilityCoordinator.RefreshWindowPresentation(context.window); + if (!transferRequests.HasPendingRequests()) { + return; } + + DispatchWindowFrameTransferRequests(context, transferRequests); +} + +void EditorWindowMessageDispatcher::FlushQueuedCompletedImmediateFrame( + const DispatchContext& context) { + if (!context.window.HasQueuedCompletedImmediateFrame()) { + return; + } + + FinalizeImmediateFrame( + context, + context.window.ConsumeQueuedCompletedImmediateFrameTransferRequests()); +} + +void EditorWindowMessageDispatcher::RenderAndHandleWindowFrame(const DispatchContext& context) { + FinalizeImmediateFrame( + context, + EditorWindowFrameDriver::DriveImmediateFrame( + context.window, + context.hostRuntime.GetEditorContext(), + context.workspaceCoordinator.IsGlobalTabDragActive())); } bool EditorWindowMessageDispatcher::EnsureTrackingMouseLeave(const DispatchContext& context) { @@ -435,12 +457,11 @@ bool EditorWindowMessageDispatcher::TryDispatchWindowLifecycleMessage( outResult = 0; return true; case WM_PAINT: - if (EditorWindowFrameTransferRequests transferRequests = context.window.OnPaintMessage( + FinalizeImmediateFrame( + context, + context.window.OnPaintMessage( context.hostRuntime.GetEditorContext(), - context.workspaceCoordinator.IsGlobalTabDragActive()); - transferRequests.HasPendingRequests()) { - DispatchWindowFrameTransferRequests(context, transferRequests); - } + context.workspaceCoordinator.IsGlobalTabDragActive())); outResult = 0; return true; case WM_ERASEBKGND: @@ -535,9 +556,20 @@ bool EditorWindowMessageDispatcher::TryDispatch( .window = window, }; - return TryDispatchWindowChromeMessage(context, message, wParam, lParam, outResult) || - TryDispatchWindowLifecycleMessage(context, message, wParam, lParam, outResult) || - TryDispatchWindowInputMessage(context, message, wParam, lParam, outResult); + bool handled = false; + if (TryDispatchWindowChromeMessage(context, message, wParam, lParam, outResult)) { + handled = true; + } else if (TryDispatchWindowLifecycleMessage(context, message, wParam, lParam, outResult)) { + handled = true; + } else if (TryDispatchWindowInputMessage(context, message, wParam, lParam, outResult)) { + handled = true; + } + + if (handled) { + FlushQueuedCompletedImmediateFrame(context); + } + + return handled; } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h index c7f005fa..2bf60349 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.h @@ -36,6 +36,10 @@ public: private: struct DispatchContext; + static void FinalizeImmediateFrame( + const DispatchContext& context, + const EditorWindowFrameTransferRequests& transferRequests); + static void FlushQueuedCompletedImmediateFrame(const DispatchContext& context); static void RenderAndHandleWindowFrame(const DispatchContext& context); static void DispatchWindowFrameTransferRequests( const DispatchContext& context, diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp index 4280b65d..31872b58 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceCoordinator.cpp @@ -61,7 +61,7 @@ bool CanStartGlobalTabDragFromWindow( bool IsLiveInteractiveWindow(const EditorWindow* window) { return window != nullptr && window->GetHwnd() != nullptr && - !window->IsClosing(); + window->GetLifecycleState() == EditorWindowLifecycleState::Running; } std::string DescribeWindowSetState(const UIEditorWindowWorkspaceSet& windowSet) { @@ -92,7 +92,8 @@ UIEditorWindowWorkspaceSet BuildLiveWindowWorkspaceSet( if (window == nullptr || window->GetHwnd() == nullptr || window->GetWindowId() == excludedWindowId || - (!includeClosingWindows && window->IsClosing())) { + (!includeClosingWindows && + window->GetLifecycleState() != EditorWindowLifecycleState::Running)) { continue; } @@ -262,9 +263,27 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { windowIdsInSet.push_back(entry.windowId); const bool isPrimaryWindow = entry.windowId == windowSet.primaryWindowId; - if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(entry.windowId); - existingWindow != nullptr) { - existingWindow->ClearClosing(); + EditorWindow* existingWindow = m_hostRuntime.FindWindow(entry.windowId); + if (existingWindow != nullptr && + existingWindow->IsDestroyed() && + m_lifecycleCoordinator != nullptr) { + m_lifecycleCoordinator->ReapDestroyedWindows(); + existingWindow = m_hostRuntime.FindWindow(entry.windowId); + } + + if (existingWindow != nullptr) { + if (existingWindow->GetLifecycleState() != EditorWindowLifecycleState::Running) { + LogRuntimeTrace( + "window", + "workspace synchronization rejected: window '" + entry.windowId + + "' is in lifecycle state '" + + std::string( + GetEditorWindowLifecycleStateName( + existingWindow->GetLifecycleState())) + + "'"); + return false; + } + existingWindowSnapshots.push_back(ExistingWindowSnapshot{ existingWindow, existingWindow->GetWorkspaceController(), @@ -328,7 +347,8 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { if (window == nullptr || window->GetHwnd() == nullptr || - window->IsPrimary()) { + window->IsPrimary() || + window->GetLifecycleState() != EditorWindowLifecycleState::Running) { continue; } @@ -503,7 +523,7 @@ void EditorWindowWorkspaceCoordinator::EndGlobalTabDragSession() { if (EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); ownerWindow != nullptr) { ownerWindow->ReleasePointerCapture(EditorWindowPointerCaptureOwner::GlobalTabDrag); - if (!ownerWindow->IsClosing()) { + if (IsLiveInteractiveWindow(ownerWindow)) { ownerWindow->ResetInteractionState(); } } @@ -598,9 +618,7 @@ bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND h } if (EditorWindow* updatedTargetWindow = m_hostRuntime.FindWindow(targetWindow->GetWindowId()); - updatedTargetWindow != nullptr && - updatedTargetWindow->GetHwnd() != nullptr && - !updatedTargetWindow->IsClosing()) { + IsLiveInteractiveWindow(updatedTargetWindow)) { SetForegroundWindow(updatedTargetWindow->GetHwnd()); } LogRuntimeTrace( @@ -613,7 +631,7 @@ bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND h bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( EditorWindow& sourceWindow, const EditorWindowPanelTransferRequest& request) { - if (sourceWindow.IsClosing()) { + if (!IsLiveInteractiveWindow(&sourceWindow)) { LogRuntimeTrace("drag", "failed to start global tab drag: source window is closing"); return false; } @@ -712,7 +730,7 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( EditorWindow& sourceWindow, const EditorWindowPanelTransferRequest& request) { - if (sourceWindow.IsClosing()) { + if (!IsLiveInteractiveWindow(&sourceWindow)) { LogRuntimeTrace("detach", "detach request rejected: source window is closing"); return false; } @@ -739,9 +757,7 @@ bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( } if (EditorWindow* detachedWindow = m_hostRuntime.FindWindow(result.targetWindowId); - detachedWindow != nullptr && - detachedWindow->GetHwnd() != nullptr && - !detachedWindow->IsClosing()) { + IsLiveInteractiveWindow(detachedWindow)) { SetForegroundWindow(detachedWindow->GetHwnd()); } @@ -774,8 +790,7 @@ EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( if (const HWND hitWindow = WindowFromPoint(screenPoint); hitWindow != nullptr) { const HWND rootWindow = GetAncestor(hitWindow, GA_ROOT); if (EditorWindow* window = m_hostRuntime.FindWindow(rootWindow); - window != nullptr && - !window->IsClosing() && + IsLiveInteractiveWindow(window) && window->GetWindowId() != excludedWindowId) { return window; } @@ -784,8 +799,7 @@ EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( for (auto it = m_hostRuntime.GetWindows().rbegin(); it != m_hostRuntime.GetWindows().rend(); ++it) { EditorWindow* const window = it->get(); if (window == nullptr || - window->GetHwnd() == nullptr || - window->IsClosing() || + !IsLiveInteractiveWindow(window) || window->GetWindowId() == excludedWindowId) { continue; } diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp index 3036fa56..90a262ab 100644 --- a/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp @@ -229,7 +229,7 @@ void D3D12HostDevice::WaitForFrame(std::uint32_t frameIndex) { } } -void D3D12HostDevice::WaitForSubmittedFrames() { +void D3D12HostDevice::WaitForTrackedFrameRetirement() { for (std::uint32_t frameIndex = 0u; frameIndex < m_frameFenceValues.size(); ++frameIndex) { diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDevice.h b/new_editor/app/Rendering/D3D12/D3D12HostDevice.h index 017f3e5e..8458cc47 100644 --- a/new_editor/app/Rendering/D3D12/D3D12HostDevice.h +++ b/new_editor/app/Rendering/D3D12/D3D12HostDevice.h @@ -33,7 +33,7 @@ public: bool SubmitFrame(std::uint32_t frameIndex); bool SignalFrameCompletion(std::uint32_t frameIndex); void WaitForFrame(std::uint32_t frameIndex); - void WaitForSubmittedFrames(); + void WaitForTrackedFrameRetirement(); void WaitForGpuIdle(); void ResetFrameTracking(); diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp index c7f2fe0f..2d56183e 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp @@ -120,14 +120,6 @@ D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( 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 (captureOutputPath != nullptr) { std::string captureError = {}; if (!m_windowRenderer->CaptureCurrentBackBufferToPng(*captureOutputPath, captureError)) { @@ -139,14 +131,24 @@ D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( } } - if (!m_windowRenderer->PresentFrame()) { + const bool presented = m_windowRenderer->PresentFrame(); + const std::string presentError = m_windowRenderer->GetLastError(); + + if (!m_windowRenderer->SignalFrameCompletion()) { const std::string& error = m_windowRenderer->GetLastError(); result.warning = error.empty() - ? "failed to present the swap chain." + ? "failed to signal current frame completion." : error; return result; } + if (!presented) { + result.warning = presentError.empty() + ? "failed to present the swap chain." + : presentError; + return result; + } + result.framePresented = true; return result; } diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp index ab32b078..32a1c3b8 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp @@ -186,6 +186,20 @@ RHIDevice* D3D12WindowRenderer::GetRHIDevice() const { return m_hostDevice.GetRHIDevice(); } +std::uint32_t D3D12WindowRenderer::GetViewportResourceRetirementSlotCount() const { + return kFrameContextCount; +} + +bool D3D12WindowRenderer::TryGetActiveViewportResourceRetirementSlot(std::uint32_t& outSlot) const { + outSlot = 0u; + if (GetRHIDevice() == nullptr) { + return false; + } + + outSlot = m_activeFrameSlot; + return true; +} + bool D3D12WindowRenderer::CreateViewportTextureHandle( ::XCEngine::RHI::RHITexture& texture, std::uint32_t width, diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h index a59733a9..530c88f6 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h @@ -48,6 +48,8 @@ public: ID3D12CommandQueue* GetCommandQueue() const; const std::string& GetLastError() const; ::XCEngine::RHI::RHIDevice* GetRHIDevice() const override; + std::uint32_t GetViewportResourceRetirementSlotCount() const override; + bool TryGetActiveViewportResourceRetirementSlot(std::uint32_t& outSlot) const override; bool CreateViewportTextureHandle( ::XCEngine::RHI::RHITexture& texture, std::uint32_t width, diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp index 6bf79b59..efe7eebd 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -17,6 +18,15 @@ using ::XCEngine::RHI::ResourceViewDimension; using ::XCEngine::RHI::SwapChainDesc; using ::XCEngine::RHI::SwapChainPresentMode; +bool IsVerboseResizeTraceEnabled() { + wchar_t buffer[8] = {}; + const DWORD length = GetEnvironmentVariableW( + L"XCUIEDITOR_VERBOSE_TRACE", + buffer, + static_cast(sizeof(buffer) / sizeof(buffer[0]))); + return length > 0u && buffer[0] != L'0'; +} + bool D3D12WindowSwapChainPresenter::Initialize( D3D12HostDevice& hostDevice, HWND hwnd, @@ -339,7 +349,9 @@ bool D3D12WindowSwapChainPresenter::Resize(int width, int height) { return true; } - m_hostDevice->WaitForSubmittedFrames(); + const auto waitBegin = std::chrono::steady_clock::now(); + m_hostDevice->WaitForTrackedFrameRetirement(); + const auto waitEnd = std::chrono::steady_clock::now(); ReleaseBackBufferCommandReferences(); ReleaseBackBufferViews(); @@ -349,10 +361,28 @@ bool D3D12WindowSwapChainPresenter::Resize(int width, int height) { return false; } + const auto resizeBuffersBegin = std::chrono::steady_clock::now(); d3d12SwapChain->Resize( static_cast(width), static_cast(height)); + const auto resizeBuffersEnd = std::chrono::steady_clock::now(); const HRESULT resizeHr = d3d12SwapChain->GetLastResizeResult(); + if (IsVerboseResizeTraceEnabled()) { + const auto waitMs = + std::chrono::duration_cast(waitEnd - waitBegin).count(); + const auto resizeBuffersMs = + std::chrono::duration_cast( + resizeBuffersEnd - resizeBuffersBegin).count(); + std::ostringstream trace = {}; + trace << "swapchain resize requested=" + << width << 'x' << height + << " waitForTrackedRetirementMs=" << waitMs + << " resizeBuffersMs=" << resizeBuffersMs + << " hr=0x" + << std::hex << std::uppercase + << static_cast(resizeHr); + AppendUIEditorRuntimeTrace("resize", trace.str()); + } if (FAILED(resizeHr)) { if (RecreateSwapChain(width, height)) { m_lastError.clear();