# NewEditor Live Resize Jitter Root Cleanup Plan Date: 2026-04-23 Status: Planned ## 1. Objective 彻底清理 `new_editor` 自定义顶栏窗口在实时拖拽 resize 时的抖动问题,并且继续保持当前已经恢复的“内容不变形”契约。 这次修复只接受下面这个结果: 1. 右侧和下侧,尤其右下角,拖拽时不再出现明显抖动 2. `Scene` 内容继续实时跟随,不允许用冻结、延迟、复用旧帧拉伸来掩盖问题 3. 自定义顶栏和 borderless resize 机制继续保留 4. 代码结构保持主线化,不能靠零散补丁维持 ## 2. Confirmed Root Cause 当前问题的根源不是 `Scene` 视口本身慢,也不是自定义顶栏设计有问题。 已经确认的根因是窗口层的 resize / present 时序错误: 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` 填到仍处于旧几何的窗口表面 因此抖动的本质不是“重绘慢”,而是: 1. 可见 swapchain 被拿来展示了一帧与当前 HWND 几何不一致的 future-size 内容 2. DXGI stretch 误差在归一化坐标接近 `1.0` 的位置最大,所以右侧和下侧最明显 3. `Scene` 只是把这段错误状态维持得更久,因此把问题放大了 直接调整 `Scene` 视口 splitter 之所以平滑,是因为那条路径没有“future-size frame 先进入旧 HWND 可见 swapchain”这个错误。 ## 3. Architectural Decision 根修复必须把两个尺寸彻底分开: 1. `predicted size` 用于 borderless resize 期间的下一步布局推演和离屏整窗合成 2. `committed visible size` 只对应已经被 Win32 / DXGI 真正确认的 HWND client size 和可见 swapchain backbuffer 最终原则: 1. 预测尺寸可以驱动布局和离屏合成 2. 可见 swapchain 只能 present committed size 的内容 3. HWND 几何提交前,future-size frame 不能再直接进入可见 swapchain ## 4. Red Lines 本次修复不能做下面这些事: 1. 不能取消自定义顶栏 2. 不能把 resize 责任下沉到 UI Editor 层 3. 不能冻结 `Scene` 4. 不能复用旧纹理做拉伸/延迟展示来冒充实时 resize 5. 不能在各处消息处理里堆条件分支补丁 6. 不能破坏 tab 拖拽拆窗、utility window、正常 `WM_SIZE`、viewport splitter resize ## 5. Target End State 修完之后,窗口层行为必须变成下面这样: 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 ## 6. Implementation Plan ### Phase A. Separate predicted composition from visible swapchain state 1. 在 `D3D12WindowSwapChainPresenter` / `D3D12WindowRenderer` 引入窗口级离屏 composition target 2. 为它建立独立的 color texture、RTV、SRV 和 `RenderSurface` 3. 把“当前可见 swapchain 尺寸”和“最近预测 composition 尺寸”分成两套状态 ### Phase B. Route immediate resize frames into the offscreen composition target 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 合成,不做冻结 ### Phase C. Commit HWND geometry first, then publish only committed-size content 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 层补丁