Files
XCEngine/docs/plan/NewEditor_LiveResizeJitterRootCleanupPlan_2026-04-22.md

6.0 KiB
Raw Blame History

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