new_editor: stabilize resize lifecycle groundwork

This commit is contained in:
2026-04-23 00:36:28 +08:00
parent c10367a42e
commit 03e0b362f7
19 changed files with 439 additions and 161 deletions

View File

@@ -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 层补丁

View File

@@ -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<std::uintptr_t>(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);

View File

@@ -5,6 +5,7 @@
#endif
#include "Platform/Win32/EditorWindowPointerCapture.h"
#include "Platform/Win32/EditorWindowState.h"
#include "Platform/Win32/EditorWindowTransferRequests.h"
#include <windows.h>
@@ -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<EditorWindowFrameOrchestrator> m_frameOrchestrator = {};
std::unique_ptr<EditorWindowInputController> m_inputController = {};
std::unique_ptr<EditorWindowRuntimeController> m_runtime = {};
EditorWindowFrameTransferRequests m_queuedImmediateFrameTransferRequests = {};
bool m_hasQueuedCompletedImmediateFrame = false;
};
} // namespace XCEngine::UI::Editor::App

View File

@@ -11,7 +11,9 @@
#include <XCEngine/UI/Layout/UITabStripLayout.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <sstream>
#include <string_view>
#include <windowsx.h>
@@ -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<UINT>(width),
static_cast<UINT>(height));
const auto applyResizeBegin = std::chrono::steady_clock::now();
if (window.ApplyWindowResize(static_cast<UINT>(width), static_cast<UINT>(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<std::chrono::milliseconds>(
immediateFrameBegin - applyResizeBegin).count();
const auto immediateFrameMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
immediateFrameEnd - immediateFrameBegin).count();
const auto totalMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
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<std::chrono::milliseconds>(
totalEnd - setWindowPosBegin).count();
const auto totalMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
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<UINT>(width), static_cast<UINT>(height));
if (window.ApplyWindowResize(static_cast<UINT>(width), static_cast<UINT>(height))) {
(void)EditorWindowFrameDriver::DriveImmediateFrame(
window,
editorContext,
globalTabDragActive);
window.QueueCompletedImmediateFrame(
EditorWindowFrameDriver::DriveImmediateFrame(
window,
editorContext,
globalTabDragActive));
MarkPredictedClientPixelSizePresented();
}
SetWindowPos(

View File

@@ -12,7 +12,7 @@ EditorWindowFrameTransferRequests EditorWindowFrameDriver::DriveFrameInternal(
bool requestSkipNextSteadyStateFrame) {
if (!window.IsRenderReady() ||
window.GetHwnd() == nullptr ||
window.IsClosing()) {
window.GetLifecycleState() != EditorWindowLifecycleState::Running) {
return {};
}

View File

@@ -6,17 +6,48 @@
#include <windows.h>
#include <cstdint>
#include <string>
#include <string_view>
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 {

View File

@@ -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;
}

View File

@@ -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<EditorWindow>& 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;
}

View File

@@ -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<EditorWindow>& 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 {

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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<EditorWindow>& 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;
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -2,6 +2,7 @@
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
#include <chrono>
#include <cstdint>
#include <sstream>
@@ -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<DWORD>(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<std::uint32_t>(width),
static_cast<std::uint32_t>(height));
const auto resizeBuffersEnd = std::chrono::steady_clock::now();
const HRESULT resizeHr = d3d12SwapChain->GetLastResizeResult();
if (IsVerboseResizeTraceEnabled()) {
const auto waitMs =
std::chrono::duration_cast<std::chrono::milliseconds>(waitEnd - waitBegin).count();
const auto resizeBuffersMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
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<unsigned long>(resizeHr);
AppendUIEditorRuntimeTrace("resize", trace.str());
}
if (FAILED(resizeHr)) {
if (RecreateSwapChain(width, height)) {
m_lastError.clear();