new_editor: stabilize resize lifecycle groundwork
This commit is contained in:
@@ -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 层补丁
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -12,7 +12,7 @@ EditorWindowFrameTransferRequests EditorWindowFrameDriver::DriveFrameInternal(
|
||||
bool requestSkipNextSteadyStateFrame) {
|
||||
if (!window.IsRenderReady() ||
|
||||
window.GetHwnd() == nullptr ||
|
||||
window.IsClosing()) {
|
||||
window.GetLifecycleState() != EditorWindowLifecycleState::Running) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user