48 KiB
New Editor Win32 窗口架构重构总计划
1. 文档目的
本计划用于指导 new_editor/app/Platform/Win32 相关窗口系统的彻底重构。
这不是一次“把大类再拆小一点”的整理,而是一次底层架构重建。核心目标是把当前体系改造成:
状态机 + 单向事件流 + 平台投影
本计划覆盖:
new_editor/app/Platform/Win32/Windowingnew_editor/app/Platform/Win32/Runtimenew_editor/app/Platform/Win32/Chromenew_editor/app/Platform/Win32/Contentnew_editor/app/Platform/Win32/Systemnew_editor/app/Composition中与窗口、workspace、utility window、全局上下文相关的部分
2. 当前系统的根因
2.1 根因结论
当前最根本的问题只有一个:
EditorWindow 没有形成真正的状态边界,窗口相关状态被多个类共同持有、共同修改、相互穿透。
这带来的直接后果是:
- 单窗口状态没有唯一所有者。
- 跨窗口状态没有唯一所有者。
WndProc路径承担了过多业务编排职责。- 稳态渲染与即时渲染双轨并存,且存在重入。
- transfer request、binding 探测、兼容状态机大量出现,本质上都在给“边界失效”打补丁。
2.2 当前系统的派生问题
2.2.1 EditorWindow 变成了共享可变状态包
它同时挂着:
- lifecycle
- HWND / title / dpi / size
- input controller
- chrome controller
- runtime controller
- frame orchestrator
- queued transfer requests
但这些状态不由它自己独占修改,而是被 MessageDispatcher、ChromeController、HostRuntime、LifecycleCoordinator、WorkspaceCoordinator 等对象直接穿透读写。
2.2.2 Win32 消息分发承担了应用层职责
当前 WM_* 消息路径中,除了输入翻译,还直接做了:
- 生命周期推进
- resize / dpi 响应
- 立即帧渲染
- global tab drag 编排
- workspace 事务提交
- utility window 打开
- chrome 命令执行
这说明当前平台层已经不是适配层,而是业务主链的一部分。
2.2.3 渲染链路是双轨的
现状存在两条渲染主链:
- 稳态帧:主循环统一
RenderAllWindows - 即时帧:消息回调里直接 render / present
这会造成:
- 消息回调重入渲染。
- transfer request 需要额外缓冲。
- resize / dpi / paint 顺序变得脆弱。
2.2.4 WorkspaceCoordinator 已经偷偷变成状态源
当前 EditorWindowWorkspaceCoordinator 不只是协调器,而是同时承担:
- window set 构造
- workspace 同步
- 快照回滚
- 新窗口创建
- 窗口关闭编排
- global tab drag 会话
- 标题刷新
但它并没有被正式定义为“权威状态源”,所以职责和边界都不稳定。
2.2.5 全局 EditorContext 混入了窗口局部能力
当前每个窗口 runtime 都会把自己的局部能力挂到全局 context 上,例如:
- text measurer
- utility window request 消费
- capture 状态文本依赖
这让全局上下文和局部窗口能力耦在了一起。
2.3 代码层证据
下面这些不是推测,而是当前代码直接暴露出来的结构事实:
new_editor/app/Platform/Win32/Windowing/EditorWindowState.h中所谓EditorWindowState实际只保存了HWND、windowId、title、primary、lifecycle。输入、chrome、resize、dpi、capture、frame、content 相关真实状态都不在这个“状态对象”里,这说明系统从命名层面就在伪装一个并不存在的统一状态边界。new_editor/app/Platform/Win32/Windowing/EditorWindow.h通过friend class把EditorWindowChromeController、EditorWindowFrameDriver、EditorWindowHostRuntime、EditorWindowMessageDispatcher、EditorWindowLifecycleCoordinator、EditorWindowWorkspaceCoordinator全部放进EditorWindow内部。这意味着窗口对象没有封装,只是一个公开状态袋。new_editor/app/Platform/Win32/Windowing/EditorWindowMessageDispatcher.cpp在WM_DPICHANGED、WM_EXITSIZEMOVE、WM_SIZE、WM_PAINT中直接触发RenderAndHandleWindowFrame或OnPaintMessage。也就是说 Win32 消息回调直接参与渲染、事务完成和跨模块编排。new_editor/app/Platform/Win32/Windowing/EditorWindowHostRuntime.cpp中的RenderAllWindows不只是“遍历窗口”,它同时承担帧驱动、presentation 刷新、transfer request 聚合和后续分发。平台宿主已经兼任应用层调度器。new_editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp通过BuildLiveWindowWorkspaceSet(...)从 live windows 反推当前窗口集合,再基于这个集合做同步、提交和回滚。这说明跨窗口 topology 并没有显式权威状态,只能从现场对象拼出来。new_editor/app/Platform/Win32/Content/EditorWindowContentController.h暴露TryGetWorkspaceBinding、TryGetDockHostBinding、TryGetInputFeedbackBinding、TryGetTitleBarBinding。应用层必须去“探测内容层支持哪些能力”,再临时决定走哪条逻辑分支,本质上是拿 capability probing 代替正式协议。new_editor/app/Platform/Win32/Runtime/EditorWindowRuntimeController.h同时持有 D3D12 renderer、texture host、text system、window render loop、screenshot controller、content controller、frame timing、dpi scale。render host、content host、diagnostic host 被揉成了一个类。new_editor/app/Platform/Win32/Content/EditorWorkspaceWindowContentController.cpp里 workspace 内容控制器一边对外暴露 binding,一边直接驱动 shell runtime 和 frame orchestrator。说明“内容输出”不是一个结构化结果,而是一堆散落接口和副作用。
因此,当前问题不是“某几个类太大”,而是根本没有形成:
- 单窗口状态所有权
- 跨窗口拓扑状态所有权
- 事件到 effect 的正式管线
- 平台层与应用层的稳定边界
3. 重构目标
3.1 总目标
建立以下新的权责模型:
- 单窗口状态
只能由
WindowSession持有。 - 跨窗口拓扑状态
只能由
WindowTopologyService持有。 - 平台对象 由 Win32 / D3D12 backend 持有,但只作为投影结果,不作为业务状态源。
- 内容层
不再通过 binding 探测链暴露能力,而是统一返回
ContentOutput。 - 帧调度
统一由
FrameScheduler管理,消息回调只登记帧请求,不直接渲染。
3.2 完成后必须满足的硬约束
- 新窗口架构中禁止
friend穿透状态。 - domain / application 头文件中禁止出现
HWND、WPARAM、LPARAM、RECT、POINT。 WndProc路径中禁止直接 render / present。EditorWindowFrameTransferRequests必须删除。TryGetWorkspaceBinding/TryGetDockHostBinding/TryGetInputFeedbackBinding/TryGetTitleBarBinding必须退出主干架构。- 跨窗口状态必须显式保存在
WindowTopologyState中,禁止从 live windows 反推。 EditorContext中禁止挂载窗口私有能力对象。
4. 目标架构
4.1 分层模型
Domain
状态、事件、命令、意图、effect 定义
Application
WindowSession / WindowTopologyService / FrameScheduler / Projector
Content
WorkspaceContentHost / UtilityContentHost
Platform
Win32 事件适配、native window backend、D3D12 render host
4.2 状态所有权
单窗口状态:WindowState
由 WindowSession 独占持有,建议至少包含:
- identity
windowIdwindowKind
- lifecycle
PendingCreateNativeAttachedInitializingRunningClosingDestroyed
- presentation
titledpidpiScalefocusedvisibleminimized
- size
clientPixelWidthclientPixelHeightpredictedClientPixelWidthpredictedClientPixelHeight
- input
pointerCaptureOwnertrackingMouseLeavemodifierSnapshotpendingEventspendingDoubleClickMask
- chrome
hoveredChromeTargetpressedChromeTargethoveredResizeEdgeactiveResizeEdgemaximizedrestoreRectdragRestoreState
- frame
needsFrameframePriorityframeSerialresizeEpoch
- content
cursorKindcaptureDemandtitleBarModecontentStatus
跨窗口状态:WindowTopologyState
由 WindowTopologyService 独占持有,至少包含:
primaryWindowIdactiveWindowIdorderedWindowIdsworkspaceWindowIdsutilityWindowIdswindowProjectionsworkspaceProjectionSnapshotsworkspaceSessionSnapshotsglobalTabDragStatependingUtilityRequestspendingDetachTransactionspendingDropTransactions
说明:
WindowTopologyState不要求吞掉整个 workspace runtime 对象,但必须显式持有“窗口到 workspace 的权威投影真相”。- 至少要能按
windowId解释:window kind / role、workspace projection snapshot、workspace session snapshot、utility window kind、primary / active 标记、正在进行的 detach / drop / reuse 事务。 - 禁止再通过 live window、content controller、workspace controller 临时拼装当前业务 window set。
平台状态
平台状态由 platform backend 持有,但不作为业务真状态:
HWND- Win32 class registration
- OS capture / cursor
- D3D12 device / swapchain / renderer
5. 核心抽象
5.1 Domain 抽象
建议新增目录:
new_editor/app/Windowing/Domain/
建议新增类型:
WindowId.hWindowState.hWindowTopologyState.hWindowEvent.hWindowIntent.hWindowCommand.hWindowEffect.hWindowCursorKind.hWindowCaptureOwner.hFramePriority.h
WindowEvent
平台层只负责把 WM_* 转成这些事件:
- 生命周期
NativeAttachedNativeDestroyedCloseRequested
- 输入
PointerMovedPointerLeftPointerButtonDownPointerButtonUpPointerWheelKeyDownKeyUpCharacterFocusGainedFocusLostCaptureChanged
- 尺寸
ClientResizedInteractiveResizeStartedInteractiveResizeEndedDpiChanged
- 帧
FrameTickPaintRequestedFramePresented
- 内容
ContentOutputReady
WindowIntent
内容层不再返回 transfer request,而是返回正式意图:
StartGlobalTabDragDetachPanelOpenUtilityWindowRequestCursorRequestCaptureReleaseCaptureRequestImmediateFrameRequestInvalidateProposeWorkspaceMutation
WindowEffect
应用层最终向平台层发 effect:
CreateNativeWindowDestroyNativeWindowShowWindowFocusWindowMoveWindowResizeWindowSetWindowTitleSetCursorAcquireCaptureReleaseCaptureTrackMouseLeaveScheduleFrameApplyRenderResize
6. 目标模块设计
6.1 WindowSession
职责:
- 持有
WindowState - 接收
WindowEvent - 演进单窗口状态
- 触发内容更新
- 生成窗口级命令和 effect
禁止:
- 直接调用 Win32 API
- 直接调用 D3D12 present
- 直接修改其它窗口状态
- 直接创建 utility window
6.2 WindowTopologyService
职责:
- 持有
WindowTopologyState - 处理跨窗口事务
- 统一管理:
- primary / active window
- panel detach
- cross-window drop
- global tab drag
- utility window 去重与重用
它必须替代:
EditorWindowWorkspaceCoordinatorEditorUtilityWindowCoordinator
6.3 FrameScheduler
职责:
- 统一登记所有窗口帧请求
- 合并稳态帧与优先帧
- 对 resize / dpi / paint 恢复设定显式优先级
关键规则:
- 消息回调只登记帧请求。
- 所有 render / present 集中在调度点执行。
- 不允许出现新的“隐式即时帧”路径。
6.4 IWindowContentHost
内容层改成统一宿主接口,而不是多 binding 探测。
输入:WindowContentInput
- workspace bounds
- input events
- frame context
- capture status text
- topology snapshot
- window role
输出:WindowContentOutput
- draw data
- cursor kind
- capture demand
- title bar mode
- workspace mutation intent
- utility window intent
- focus hints
- status text
6.5 WorkspaceContentHost
替代:
EditorWorkspaceWindowContentController
职责:
- 持有
EditorShellRuntime - 更新 shell
- 输出统一
WindowContentOutput
6.6 UtilityContentHost
替代:
EditorUtilityWindowContentController
职责:
- 承载 utility panel
- 输出聚焦态、绘制结果、最小尺寸和内容状态
6.7 Win32WindowProcAdapter
替代:
EditorWindowMessageDispatcher
职责:
WM_* -> WindowEvent- 不做业务决策
- 不持有业务状态
- 不 render / present
6.8 Win32WindowBackend
职责:
CreateWindowExWDestroyWindowSetWindowPosSetWindowTextWShowWindowSetForegroundWindowTrackMouseEventSetCapture / ReleaseCapture
原则:
- 只执行 effect
- 不知道 workspace
- 不知道 topology
- 不知道 content host
6.9 D3D12WindowRenderHost
替代旧 EditorWindowRuntimeController 中的 D3D12 部分。
职责:
- 初始化窗口 renderer
- resize
- begin frame
- present
- viewport surface presentation capability
它是渲染宿主,不是业务控制器。
6.10 Chrome 子层
当前 stateful EditorWindowChromeController 需要拆为:
ChromeGeometry- 纯几何、纯 hit test
ChromeReducer- hover / press / resize / drag restore 状态机
ChromeEffectsBuilder- 根据状态变化生成 effect
纯 helper 如 BorderlessWindowChrome.*、BorderlessWindowFrame.* 可以保留或收缩。
6.11 关键接口草案
下面给的是目标结构草案,不要求首阶段一次到位,但最终主干必须收敛到类似形态。
WindowSession
class WindowSession final {
public:
explicit WindowSession(WindowId id, WindowKind kind);
const WindowState& Snapshot() const;
WindowSessionResult ApplyEvent(const WindowEvent& event);
WindowSessionResult ApplyContentOutput(const WindowContentOutput& output);
WindowSessionResult ApplyTopologyFeedback(const WindowTopologyFeedback& feedback);
private:
WindowState m_state = {};
};
约束:
WindowSession是单窗口真状态唯一可写者。- 外部只能拿 snapshot,不能直接改字段。
- chrome、input、lifecycle、frame 都通过 reducer 更新,不允许 controller 私改。
WindowTopologyService
class WindowTopologyService final {
public:
const WindowTopologyState& Snapshot() const;
TopologyResult ApplyIntent(
WindowId sourceWindowId,
const WindowIntent& intent,
const WindowTopologySnapshot& snapshot);
void OnWindowClosed(WindowId windowId);
void OnWindowCreated(WindowId windowId, WindowRole role);
private:
WindowTopologyState m_state = {};
};
约束:
- 所有跨窗口事务统一进这里。
- 不允许在别处直接增删
workspaceWindowIds/utilityWindowIds。 - global tab drag 只能由它维护完整会话状态。
FrameScheduler
class FrameScheduler final {
public:
void RequestFrame(WindowId windowId, FramePriority priority, FrameReason reason);
bool HasPendingFrame() const;
FrameBatch DequeueReadyBatch(const WindowSessionStore& sessions);
void NotifyFramePresented(WindowId windowId, std::uint64_t frameSerial);
};
约束:
- 只有
FrameScheduler可以决定某一帧何时真正执行。 WM_PAINT、WM_SIZE、WM_DPICHANGED只产生请求,不直接 render。- steady frame / immediate frame 不再是两条链,而是一个调度器内的不同优先级。
FrameScheduler 帧事务模型
单帧事务顺序必须固定,不允许实现时自由发挥。标准顺序定义为:
- drain window events
- merge frame requests
- build
WindowContentInput - update content hosts
- submit topology intents / effect intents
- finalize render plan
- execute render subpasses / viewport work
- present
- publish
FramePresented/ frame feedback - validate paint / clear frame debt / decide requeue
必须同时定义以下语义:
WM_PAINT只产生FrameReason::PaintValidationWM_SIZE只产生FrameReason::ResizeWM_DPICHANGED只产生FrameReason::DpiChange- steady tick 只产生
FrameReason::SteadyTick - global tab drag / capture 恢复只产生显式 interaction continuation reason
优先级规则必须显式写死:
DpiChange=CriticalResize=HighPaintValidation=HighInteractionContinuation=HighSteadyTick=Normal
旧语义替代关系必须明确:
DriveFrame-> steady tick frame requestDriveImmediateFrame-> high/critical priority frame requestRequestSkipNextSteadyStateFrame-> scheduler suppression rule- queued completed immediate frame -> scheduler-owned post-present topology transaction queue
IWindowContentHost
class IWindowContentHost {
public:
virtual ~IWindowContentHost() = default;
virtual void Initialize(const WindowContentHostInitContext&) = 0;
virtual WindowContentOutput Update(const WindowContentInput&) = 0;
virtual void RenderSubpasses(const WindowRenderSubpassContext&) = 0;
virtual void Shutdown() = 0;
};
约束:
- 内容层统一返回
WindowContentOutput。 - 应用层不再
TryGet*Binding。 - 内容层如果需要表达 cursor、capture、detach、utility window、title bar mode,都在 output 中显式返回。
Win32WindowProcAdapter
class Win32WindowProcAdapter final {
public:
std::optional<WindowEvent> Translate(
HWND hwnd,
UINT message,
WPARAM wParam,
LPARAM lParam) const;
};
约束:
- 只做消息翻译,不做业务分支。
- 不调用 workspace coordinator。
- 不调用 utility coordinator。
- 不调用 render / present。
7. 旧模块到新模块的映射
| 现有模块 | 目标归宿 | 最终命运 |
|---|---|---|
EditorWindow |
WindowSession + 轻量 native facade |
大幅收缩或删除 |
EditorWindowManager |
WindowSessionStore + WindowTopologyService + FrameScheduler |
删除 |
EditorWindowHostRuntime |
WindowSessionStore + Win32WindowBackend |
删除 |
EditorWindowMessageDispatcher |
Win32WindowProcAdapter |
删除 |
EditorWindowLifecycleCoordinator |
WindowSession + topology service |
删除 |
EditorWindowWorkspaceCoordinator |
WindowTopologyService |
删除 |
EditorUtilityWindowCoordinator |
WindowTopologyService |
删除 |
EditorWindowFrameDriver |
FrameScheduler |
删除 |
EditorWindowRuntimeController |
D3D12WindowRenderHost + content host bridge |
拆分后删除旧形态 |
EditorWorkspaceWindowContentController |
WorkspaceContentHost |
替换 |
EditorUtilityWindowContentController |
UtilityContentHost |
替换 |
EditorWindowTransferRequests |
WindowIntent / WindowCommand / WindowEffect |
删除 |
TryGet*Binding 链 |
WindowContentOutput |
删除 |
7.1 最终顶层编排者
最终必须引入新的顶层运行时,建议命名:
WindowApplicationRuntime
它的职责只能是:
- 持有
WindowSessionStore - 持有
WindowTopologyService - 持有
FrameScheduler - 持有 content host factory
- 持有
Win32WindowBackend - 持有
Win32WindowProjector - 持有
D3D12WindowRenderHostregistry - 驱动主循环调度
它明确禁止做的事:
- 不新增业务可写状态真相
- 不直接执行 Win32 API
- 不直接做 workspace 事务
- 不把 topology / session / frame 规则塞回
Application
Application 最终只保留:
- 进程级初始化
- crash / log / DPI bootstrap
- 创建
WindowApplicationRuntime - 调用 runtime run / shutdown
8. 目录重组建议
建议新增目录:
new_editor/app/Windowing/Domain/
new_editor/app/Windowing/Application/
new_editor/app/Windowing/Content/
new_editor/app/Platform/Win32/Adapter/
new_editor/app/Platform/Win32/Backend/
new_editor/app/Platform/Win32/Projection/
new_editor/app/Platform/Win32/Rendering/
旧目录的处理策略:
Platform/Win32/Windowing先收缩为迁移桥,最终清空或仅保留少量 native gluePlatform/Win32/RuntimeD3D12 相关迁出后删除旧 driver / controllerPlatform/Win32/Content迁入Windowing/ContentPlatform/Win32/Chrome仅保留纯 helper,删除 stateful controller
8.1 依赖与边界规则
重构过程中必须同时执行“依赖收口”,否则类名换了,旧问题会原样复活。
允许的依赖方向
Domain <- Application <- Content
<- Platform Projection
Platform Backend 只被 Application/Projection 调用
Rendering Host 只被 FrameScheduler/Render Pipeline 调用
明确禁止的依赖
Windowing/Domain禁止包含windows.h、D3D12、swapchain、renderer 头文件。Windowing/Application禁止直接调用CreateWindowExW、SetWindowPos、SetCapture、Present。Windowing/Content禁止感知HWND、RECT、POINT、WPARAM、LPARAM。Platform/Win32/Adapter禁止包含 workspace 事务逻辑。Platform/Win32/Backend禁止读取 workspace/session/content 模型。WindowTopologyService之外禁止直接维护 global tab drag 会话。FrameScheduler之外禁止新增任何“临时立即帧”入口。EditorContext之外的全局单例禁止继续扩散;新增全局能力必须先证明不是 per-window。
include 审核规则
审核员在代码审查时必须额外检查:
- 新建 domain 头文件是否仍然引用 Win32 类型。
- 新建 content host 是否仍然暴露 binding 探测接口。
- 新建 platform 代码是否偷偷包含
UIEditorWorkspaceController。 - 任何新类是否再次通过
friend穿透WindowSession。
8.2 迁移期间的反腐层规则
迁移不是一口气删光旧代码,而是分期替换。但桥接层必须受限,否则过渡层会永久化。
允许存在的临时桥
EditorWindowManager只允许暂时作为启动入口和 legacy facade。EditorWindow过渡期允许保留,但只能逐步退化为:windowId- native handle facade
- 指向
WindowSession/ backend 的轻量桥
EditorWindowRuntimeController只允许短期作为content host + render host的组合桥,后续必须拆解。EditorWindowMessageDispatcher只允许短期 forward 到Win32WindowProcAdapter,不能继续长逻辑。
所有临时桥都必须同时满足以下硬约束:
- 只允许做机械转发或数据形状适配。
- 不允许持有业务可写状态。
- 不允许做条件分支决策。
- 不允许生成新的业务语义。
- 不允许吞掉 effect、intent、错误或反馈。
- 必须在计划中绑定删除阶段和删除条件。
- 做不到以上约束的桥,不允许“先留着”,必须在当前阶段直接拆掉。
迁移期明确禁止
- 禁止新增任何新的
friend class。 - 禁止给
EditorWindowFrameTransferRequests再加字段。 - 禁止新增新的
TryGet*Binding。 - 禁止在
WM_*路径里新增 render / present / workspace 事务代码。 - 禁止在旧 coordinator 中继续沉积新业务。
- 禁止从 live windows 再推导新的“临时全局状态”。
判断一个桥是否可以删除的标准
同时满足以下条件时,桥必须进入删除阶段:
- 新结构已经有等价状态对象。
- 新结构已经有等价命令或 effect。
- 旧桥只剩转发,不再做决策。
- 已有回归覆盖该路径。
9.0 所有阶段通用门禁
下面这些门禁不是某一阶段独有,而是从 Phase 1 开始到 Phase 8 都必须成立:
- 本阶段新增桥接层只能是机械转发 adapter,不能业务化。
- 任何单窗口状态在同一时刻只能有一个可写源。
- 任何跨窗口状态在同一时刻只能有一个可写源。
- 新增类型不得把 Win32 类型重新包装后带回 domain / application。
- 没有对应测试门禁和回归命令的结构,不允许宣称阶段完成。
9. 分阶段执行方案
Phase 0:基线冻结与护栏建立
目标
在动架构前冻结当前行为基线。
工作项
- 记录旧系统关键链路:
- 建窗
- 关闭
- steady frame
- immediate frame
- resize
- dpi change
- utility window open / reuse
- panel detach
- global tab drag
- 补最小回归清单。
- 建立 trace / log 基线。
- 标记本计划的阶段门禁。
退出条件
- 关键行为已文档化。
- 有最小回归检查项。
- 已定义测试 target 规划、fake 对象职责和阶段门禁命令。
- 评论区已登记 Phase 0 启动记录。
Phase 1:新协议落地
目标
先引入新的 domain 抽象,不替换旧主链。
工作项
- 新增
WindowState、WindowTopologyState、WindowEvent、WindowIntent、WindowEffect。 - 新增
WindowContentInput、WindowContentOutput。 - 新增纯 reducer 雏形:
- lifecycle
- input
- chrome
- 保持旧主链不动。
退出条件
- 新类型不依赖 Win32 类型。
- 新类型足以表达旧主链中的关键意图。
- 已把
window_session_tests、window_topology_tests、frame_scheduler_tests、win32_message_translator_tests纳入 CMake 目标规划与目录规划。
Phase 2:单窗口状态迁入 WindowSession
目标
解决最底层的状态 ownership 问题。
工作项
- 创建
WindowSession。 - 把单窗口状态迁入
WindowSession。 EditorWindow退化为兼容外壳。- 开始消灭外部对
EditorWindow内部状态的直接访问。
退出条件
- 单窗口真状态只在
WindowSession或其 reducer 中可写。 - 旧
EditorWindow/EditorWindowInputController/EditorWindowChromeController不能再持有独立业务状态真相。 - 不允许存在新旧双写路径。
EditorWindow不再是共享可变状态包。
Phase 3:消息路径改造
目标
把 WndProc 路径变成纯事件适配。
工作项
- 新建
Win32WindowProcAdapter。 - 所有
WM_*转成WindowEvent。 - 移出消息路径中的:
- render / present
- workspace 事务
- utility window 打开
- chrome 直接 effect
退出条件
WndProc中不再直接调用渲染入口。
Phase 4:统一帧调度
目标
消灭稳态帧与即时帧的双轨结构。
工作项
- 建立
FrameScheduler。 - 消息路径只登记帧请求。
- 稳态帧和优先帧统一调度。
- 定义完整 frame lifecycle、
FrameReason和FramePriority。 - 删除
DriveImmediateFrame思维模式。
退出条件
- 没有消息路径直接 render / present。
FrameScheduler生命周期顺序、reason 映射、priority 映射、失败/跳帧规则已落成文档并对应旧语义。- 不再需要 queued completed immediate frame 补偿逻辑。
Phase 5:内容层协议替换
目标
删除 binding 探测链。
工作项
- 建立
IWindowContentHost。 - 实现
WorkspaceContentHost。 - 实现
UtilityContentHost。 - 让内容层统一返回
WindowContentOutput。 - 删除
TryGet*Binding链。
退出条件
- 应用层不再感知内容层内部的 binding 形状。
Phase 6:拓扑服务落地
目标
建立显式 WindowTopologyState。
工作项
- 建立
WindowTopologyService。 - 迁移:
- primary / active window
- panel detach
- cross-window drop
- global tab drag
- utility window 管理
- 停止从 live windows 反推业务 window set。
退出条件
- 系统里只有一个跨窗口拓扑状态源。
WindowTopologyState已显式保存 window projection / workspace projection / session snapshot / 事务快照。BuildLiveWindowWorkspaceSet(...)不再承担业务真相恢复职责。
Phase 7:平台投影与后端收口
目标
把 Win32 / D3D12 执行逻辑从业务决策中彻底剥离。
工作项
- 新建
WindowApplicationRuntime。 - 新建
Win32WindowBackend。 - 新建
Win32WindowProjector。 - 新建
D3D12WindowRenderHost。 - 把
Application收缩为 bootstrap,主循环调度迁给WindowApplicationRuntime。 - 所有 Win32 effect 都通过 backend 执行。
退出条件
EditorWindowManager删除后的顶层调用链已经明确落到WindowApplicationRuntime。- 应用层不再直接调用 Win32 API。
- 会话层不再直接持有 D3D12 业务控制职责。
Phase 8:删除旧结构与最终清理
目标
删掉所有旧桥、旧接口、旧控制器。
工作项
- 删除旧 coordinator / driver / dispatcher / transfer request。
- 删除旧 binding 探测链。
- 收缩
EditorContext。 - 清理 include 方向和目录结构。
退出条件
- 本计划第 3 节的硬约束全部满足。
9.1 文件级实施矩阵
这一节是执行时的真正落地清单。阶段说明解决“做什么”,这一节解决“先动哪些文件、怎么桥接、删哪些旧件”。
Phase 0 文件级动作
目标:冻结现状,不改行为。
重点读取与建档文件:
new_editor/app/Platform/Win32/Windowing/EditorWindow.h/.cppnew_editor/app/Platform/Win32/Windowing/EditorWindowMessageDispatcher.cppnew_editor/app/Platform/Win32/Windowing/EditorWindowHostRuntime.cppnew_editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cppnew_editor/app/Platform/Win32/Windowing/EditorUtilityWindowCoordinator.cppnew_editor/app/Platform/Win32/Runtime/EditorWindowFrameDriver.cppnew_editor/app/Platform/Win32/Runtime/EditorWindowRuntimeController.h/.cppnew_editor/app/Platform/Win32/Chrome/EditorWindowChromeController.h/.cpp
必须沉淀的基线:
- 消息到渲染路径时序图
- steady frame 时序图
- immediate frame 时序图
- global tab drag 时序图
- detach panel -> 新窗口创建时序图
- utility window open / reuse 时序图
审核检查点:
- 文档是否覆盖所有
WM_*关键路径 - 是否识别了所有 direct Win32 API 调用点
- 是否列出当前 transfer request 生产者和消费者
Phase 1 文件级动作
目标:先落地新协议,不改主行为。
新增文件建议:
new_editor/app/Windowing/Domain/WindowId.hnew_editor/app/Windowing/Domain/WindowKind.hnew_editor/app/Windowing/Domain/WindowState.hnew_editor/app/Windowing/Domain/WindowTopologyState.hnew_editor/app/Windowing/Domain/WindowEvent.hnew_editor/app/Windowing/Domain/WindowIntent.hnew_editor/app/Windowing/Domain/WindowCommand.hnew_editor/app/Windowing/Domain/WindowEffect.hnew_editor/app/Windowing/Domain/WindowContentInput.hnew_editor/app/Windowing/Domain/WindowContentOutput.hnew_editor/app/Windowing/Domain/FramePriority.h
新增 reducer 建议:
new_editor/app/Windowing/Application/Reducers/WindowLifecycleReducer.*new_editor/app/Windowing/Application/Reducers/WindowInputReducer.*new_editor/app/Windowing/Application/Reducers/WindowChromeReducer.*
桥接策略:
- 旧
EditorWindow继续跑,但开始把内部可见状态映射到新的WindowState草案。 - 新 domain 类型只作为纯类型定义,不接管现网行为。
本阶段不能做的事:
- 不能让新 domain 类型依赖
windows.h - 不能在新协议里出现
HWND - 不能为了兼容旧代码把 Win32 类型偷偷包一层再塞回 domain
Phase 2 文件级动作
目标:把单窗口真状态收回到 WindowSession。
新增文件建议:
new_editor/app/Windowing/Application/WindowSession.hnew_editor/app/Windowing/Application/WindowSession.cppnew_editor/app/Windowing/Application/WindowSessionStore.hnew_editor/app/Windowing/Application/WindowSessionStore.cppnew_editor/app/Windowing/Application/WindowSessionResult.h
优先迁移状态来源:
EditorWindowState.hEditorWindowInputControllerEditorWindowChromeControllerEditorWindow中与 resize / dpi / queued immediate frame 相关的状态
具体做法:
- 先把当前 scattered state 建成
WindowState快照字段,不立即删旧控制器。 - 让
EditorWindow只通过WindowSession读写窗口状态。 - 外部模块改成读取
WindowSession::Snapshot(),不再读EditorWindow私有成员。 - 旧 controller 如果暂时还存在,只允许退化为无状态 helper,不允许继续做独立状态持有者。
必须优先清掉的穿透点:
EditorWindow.h中的friend class链EditorWindowMessageDispatcher对m_inputController/m_chromeController/m_runtime的直接访问EditorWindowFrameDriver对window.m_chromeController的直接访问
阶段验收:
- 所有单窗口状态写入都只经由
WindowSession或 reducer EditorWindow不再是唯一可写状态袋- 不存在旧 controller 与
WindowSession双写 friend class不再承担状态穿透写入入口
Phase 3 文件级动作
目标:把消息路径收口成纯适配层。
新增文件建议:
new_editor/app/Platform/Win32/Adapter/Win32WindowProcAdapter.hnew_editor/app/Platform/Win32/Adapter/Win32WindowProcAdapter.cppnew_editor/app/Platform/Win32/Adapter/Win32MessageTranslator.*
旧文件处理:
EditorWindowMessageDispatcher.cpp第一阶段变薄,只保留:HWND -> WindowId查找- 调用
Win32WindowProcAdapter - 把事件送给
WindowSession
- 后续删除
EditorWindowMessageDispatcher.*
需要迁出的逻辑:
WM_SIZE内直接 immediate frameWM_DPICHANGED内直接 renderWM_PAINT内直接OnPaintMessageWM_CLOSE中对生命周期的复杂编排WM_MOUSE*中直接 global tab drag / chrome action 决策
阶段验收:
WndProc中只剩翻译和转发- 业务层收到的是
WindowEvent,不是WM_*
Phase 4 文件级动作
目标:建立统一帧调度器。
新增文件建议:
new_editor/app/Windowing/Application/FrameScheduler.hnew_editor/app/Windowing/Application/FrameScheduler.cppnew_editor/app/Windowing/Application/FrameRequestQueue.hnew_editor/app/Windowing/Application/FrameRequestQueue.cpp
旧文件收缩:
Runtime/EditorWindowFrameDriver.*先转成 scheduler 内部 helper,再删除EditorWindowHostRuntime::RenderAllWindows(...)改为委托FrameScheduler
关键迁移动作:
- 把
DriveFrame/DriveImmediateFrame收束成统一请求模型 - 把
RequestSkipNextSteadyStateFrame这种补丁语义转为显式调度规则 - 把
HasQueuedCompletedImmediateFrame/ConsumeQueuedCompletedImmediateFrameTransferRequests转换为 scheduler 完成回调或拓扑事务队列 - 把 frame transaction 顺序固定到: event drain -> request merge -> content update -> topology submit -> render -> present -> feedback -> paint validation
- 为
WM_PAINT/WM_SIZE/WM_DPICHANGED建立明确的FrameReason/FramePriority映射
阶段验收:
- steady frame / immediate frame 共享同一套调度实体
- 新代码里不存在“为了某个消息直接 present 一帧”的入口
Phase 5 文件级动作
目标:把内容层从 binding 探测链改成正式输出协议。
新增文件建议:
new_editor/app/Windowing/Content/IWindowContentHost.hnew_editor/app/Windowing/Content/WorkspaceContentHost.hnew_editor/app/Windowing/Content/WorkspaceContentHost.cppnew_editor/app/Windowing/Content/UtilityContentHost.hnew_editor/app/Windowing/Content/UtilityContentHost.cpp
重点改造旧文件:
Platform/Win32/Content/EditorWindowContentController.hPlatform/Win32/Content/EditorWorkspaceWindowContentController.*Platform/Win32/Content/EditorUtilityWindowContentController.*
替换策略:
- 先让旧 content controller 内部构造
WindowContentOutput - 再让应用层只消费 output,不再
TryGet*Binding - 等应用层不再 probe binding 后,删除 binding 接口
必须退出主干的接口:
TryGetWorkspaceBindingTryGetDockHostBindingTryGetInputFeedbackBindingTryGetTitleBarBindingEditorWindowFrameTransferRequests
阶段验收:
- 内容层对外只暴露一个宿主接口
- cursor / capture / title / utility / detach 等信号都能通过 output 表达
Phase 6 文件级动作
目标:建立显式拓扑状态源。
新增文件建议:
new_editor/app/Windowing/Application/WindowTopologyService.hnew_editor/app/Windowing/Application/WindowTopologyService.cppnew_editor/app/Windowing/Application/WindowTopologyResult.hnew_editor/app/Windowing/Application/WindowTopologySnapshot.h
优先迁移旧文件职责:
Windowing/EditorWindowWorkspaceCoordinator.*Windowing/EditorUtilityWindowCoordinator.*
必须先抽离的事务:
- active / primary window 管理
- global tab drag 会话
- panel detach -> 新窗口创建
- cross-window drop
- utility window open / reuse / focus
核心要求:
BuildLiveWindowWorkspaceSet(...)只能作为短期兼容代码,最终删除- topology state 必须显式保存,而不是运行时临时拼装
- topology state 必须持有按
windowId建立的权威 workspace projection / session snapshot
阶段验收:
- 任何跨窗口行为都能从
WindowTopologyState解释 - 不再需要从 live windows 反推出唯一真相
Phase 7 文件级动作
目标:把平台执行层和业务决策彻底分开。
新增文件建议:
new_editor/app/Windowing/Application/WindowApplicationRuntime.hnew_editor/app/Windowing/Application/WindowApplicationRuntime.cppnew_editor/app/Platform/Win32/Backend/Win32WindowBackend.hnew_editor/app/Platform/Win32/Backend/Win32WindowBackend.cppnew_editor/app/Platform/Win32/Projection/Win32WindowProjector.hnew_editor/app/Platform/Win32/Projection/Win32WindowProjector.cppnew_editor/app/Platform/Win32/Rendering/D3D12WindowRenderHost.hnew_editor/app/Platform/Win32/Rendering/D3D12WindowRenderHost.cpp
迁移来源:
EditorWindowHostRuntime.cpp中的 Win32 建窗、ShowWindow、UpdateWindowEditorWindowWorkspaceCoordinator.cpp中的SetWindowTextWEditorWindowChromeController.cpp中的窗口移动/缩放/最大化相关 Win32 调用EditorWindowRuntimeController.*中的渲染宿主职责
投影规则:
WindowApplicationRuntime只做依赖组装和主循环调度WindowSession/WindowTopologyService只产生命令和 effectWin32WindowProjector把状态变化映射成 effect 列表Win32WindowBackend执行 effectD3D12WindowRenderHost只执行渲染宿主职责
阶段验收:
- 应用层不直接包含 Win32 行为代码
- render host 不直接承担内容决策
Phase 8 文件级动作
目标:删掉旧骨架,只保留新主干。
计划删除的旧文件范围:
new_editor/app/Platform/Win32/Windowing/EditorWindowManager.*new_editor/app/Platform/Win32/Windowing/EditorWindowHostRuntime.*new_editor/app/Platform/Win32/Windowing/EditorWindowMessageDispatcher.*new_editor/app/Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.*new_editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.*new_editor/app/Platform/Win32/Windowing/EditorUtilityWindowCoordinator.*new_editor/app/Platform/Win32/Runtime/EditorWindowFrameDriver.*new_editor/app/Platform/Win32/Windowing/EditorWindowTransferRequests.h
高风险清理项:
EditorContext中所有 per-window capability- 旧 chrome controller 中残留的状态机
- 所有
TryGet*Binding调用点 - 所有
friend class穿透
最终收口标准:
- 旧桥只剩零或极薄 facade
- include 方向符合第 8.1 节规则
- 代码库中查不到 transfer request 和 binding probe 主干调用
10. EditorContext 重构要求
当前 EditorContext 必须被收缩,只保留全局状态和全局服务。
可以继续保留在全局中的内容
- shell asset
- selection service
- command focus service
- project runtime
- scene runtime
- system interaction host
- session store
必须移出的内容
- per-window text measurer
- per-window render host
- per-window capture / presentation capability
- 任何依赖“当前窗口实例”的局部能力
建议拆分为:
EditorAppContextEditorSessionStore- per-window capability context
11. 测试策略
11.1 测试 target 与目录规划
必须落成以下 target 规划:
window_session_testswindow_topology_testsframe_scheduler_testswin32_message_translator_testsnew_editor_windowing_tests作为聚合 target
建议目录:
new_editor/tests/windowing/window_session/new_editor/tests/windowing/window_topology/new_editor/tests/windowing/frame_scheduler/new_editor/tests/platform/win32_message_translator/
11.2 fake 组件要求
为保证这些测试不是空壳,必须同时定义:
FakeWindowBackend- 记录 effect 执行顺序,不实际调用 Win32
FakeRenderHost- 记录 resize / render / present / feedback 顺序
FakeContentHost- 可配置输出
WindowContentOutput
- 可配置输出
FakeTopologyObserver或FakeTopologySink- 校验 topology transaction 提交顺序
FakeFrameClock- 控制 steady tick / priority frame 时序
11.3 阶段门禁命令
计划执行时必须收口到明确命令,不能只写“补测试”。最低门禁如下:
- 编译门禁:
cmake --build build --config Debug --target XCUIEditorApp
- 单元测试门禁:
cmake --build build --config Debug --target new_editor_windowing_testsctest --test-dir build --config Debug --output-on-failure -R "window_session|window_topology|frame_scheduler|win32_message_translator"
- 启动级冒烟:
- 启动
build/new_editor/Debug/XCUIEditor.exe,确认能稳定启动并进入主循环
- 启动
阶段要求:
- Phase 0 结束前必须把这些命令和 target 写进计划。
- Phase 1 结束前必须把 target scaffold 和 fake 组件接口纳入 CMake 与目录结构。
- Phase 2 及之后每个阶段结束时,未通过本阶段门禁不得进入下一阶段。
单元测试
必须补:
WindowSession生命周期测试- 输入 reducer 测试
- chrome reducer 测试
- topology service 拖拽事务测试
- frame scheduler 优先级测试
平台适配测试
Win32WindowProcAdapterWin32WindowProjectorWin32WindowBackendeffect 执行桩
集成测试
至少覆盖:
- 建窗 / 关窗
- 主窗口关闭触发全局回收
- resize / dpi change 后的帧调度
- panel detach -> 新窗口创建
- global tab drag -> cross-window drop
- utility window open / reuse / focus
高频回归项
每阶段后都要回归:
- capture 是否正确
- chrome hover / press / resize 是否正常
- resize 时是否抖动或错帧
- utility window 是否重复创建
- 跨窗口拖拽是否残留 preview
- DPI 切换后 cursor / title / size 是否一致
12. 风险与控制
风险 A:新旧状态双写
控制:
- 每阶段明确唯一可写状态源。
- 审核员重点检查是否存在双写。
风险 B:移除即时帧后交互变钝
控制:
FrameScheduler必须支持优先帧。- resize / paint 恢复链必须做延迟对比。
风险 C:拓扑迁移时语义漂移
控制:
- 先冻结旧行为。
- 每个事务都要有明确测试和日志。
风险 D:EditorContext 收缩影响外围模块
控制:
- 先加新接口,再迁移调用点。
- 兼容桥必须有明确删除阶段。
13. 完成定义
当且仅当以下条件同时满足时,重构完成:
- 新窗口体系不存在
friend穿透状态。 - domain / application 中没有 Win32 类型泄漏。
WndProc路径中不存在直接 render / present。EditorWindowFrameTransferRequests已删除。TryGet*Binding探测链已删除。WindowTopologyService是唯一跨窗口状态源。EditorContext不再挂载窗口局部能力。- 旧 coordinator / dispatcher / driver 已退出主干。
- blocker / major 级审核评论全部关闭。
14. 审核评论规则
这部分直接作为本计划的附录使用,规则保持简单。
规则 1:评论必须带阶段和严重度
严重度只允许:
BLOCKERMAJORMINORQUESTION
规则 2:评论必须有证据
证据至少包含一项:
- 文件路径
- 类名 / 函数名
- 可复现行为
- 测试或日志
没有证据的评论不进入正式台账。
规则 3:一条评论只打一个核心问题
不要把多个无关问题塞进同一条评论。
规则 4:BLOCKER 直接卡阶段
有未关闭的 BLOCKER 时,不允许进入下一阶段。
规则 5:MAJOR 必须在当前阶段关闭
MAJOR 不一定立即停工,但当前阶段结束前必须处理完。
规则 6:执行者必须逐条回复
回复至少包含:
- 处理动作
- 证据
- 当前剩余风险
规则 7:评论只能追加,不删除历史
即使评论被驳回,也要保留记录。
规则 8:计划变更必须先记评论区
如果实施过程中要改阶段目标、改边界、改顺序,必须先在评论区登记“计划变更请求”。
15. 评论区
15.1 使用说明
以下评论区用于登记:
- 阶段开始
- 阶段结束
- 审核员评论
- 执行者回复
- 审核结论
- 计划变更请求
15.2 状态总览
| Phase | Status | Start Date | End Date | Executor Summary | Reviewer Summary |
|---|---|---|---|---|---|
| Phase 0 | Not Started | ||||
| Phase 1 | Not Started | ||||
| Phase 2 | Not Started | ||||
| Phase 3 | Not Started | ||||
| Phase 4 | Not Started | ||||
| Phase 5 | Not Started | ||||
| Phase 6 | Not Started | ||||
| Phase 7 | Not Started | ||||
| Phase 8 | Not Started |
15.3 评论模板
Comment ID:
Phase:
Severity:
Status: Open
Summary:
Evidence:
-
Impact:
Required Action:
Close Condition:
Reviewer:
Date:
15.4 回复模板
Response To:
Status: Answered
Executor:
Date:
Action Taken:
-
Evidence:
-
Residual Risk:
Requested Status:
15.5 计划变更请求模板
Plan Change Request ID:
Affected Phase:
Date:
Executor:
Reason:
Requested Change:
Impact:
-
Rollback Condition:
16. 阶段评论记录区
Phase 0
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No open comments. 2026-04-23 的 REVIEW-P0-001 ~ REVIEW-P0-006 已吸收到计划正文。
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 1
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 2
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 3
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 4
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 5
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 6
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 7
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes:
Phase 8
Start
- Date:
- Executor:
- Goal:
- Exit Criteria:
Comments
No comments yet.
End
- Date:
- Executor:
- Requested Review Decision:
Reviewer Decision
- Decision:
- Reviewer:
- Notes: