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 层补丁
|
||||
|
||||
Reference in New Issue
Block a user