From 5b6c46d38277564e7dd2575e7ff041c3b4341d04 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 26 Apr 2026 00:19:58 +0800 Subject: [PATCH] Refactor editor window synchronization flow --- Rendering/AGENTS.md | 63 +++ docs/plan/rendering_srp_urp_closeout_plan.md | 214 ++++++++++ ...xcui_editor_windowing_architecture_plan.md | 184 +++++++++ editor/CMakeLists.txt | 25 +- .../EditorWindowWorkspaceStore.cpp | 66 --- .../Composition/EditorWindowWorkspaceStore.h | 8 +- .../EditorWindowLifecycleCoordinator.cpp | 6 +- .../Win32/Windowing/EditorWindowSession.cpp | 4 + .../EditorWindowWorkspaceCoordinator.cpp | 336 ++++++++------- .../EditorWindowWorkspaceCoordinator.h | 12 +- ...EditorWorkspaceWindowContentController.cpp | 19 +- .../Frame/EditorWindowTransferRequests.h | 14 +- .../System/EditorWindowPresentationPolicy.cpp | 47 +++ .../System/EditorWindowPresentationPolicy.h | 21 + .../System/EditorWindowSynchronizationPlan.h | 84 ++++ .../EditorWindowSynchronizationPlanner.cpp | 143 +++++++ .../EditorWindowSynchronizationPlanner.h | 13 + .../Windowing/System/EditorWindowSystem.cpp | 195 ++++++++- .../app/Windowing/System/EditorWindowSystem.h | 37 +- .../ManagedScriptableRenderPipelineAsset.h | 18 +- .../ManagedScriptableRenderPipelineAsset.cpp | 17 +- .../src/Scripting/Mono/MonoScriptRuntime.cpp | 17 +- .../Universal/ScriptableRendererFeature.cs | 4 +- .../Universal/UniversalPostProcessBlock.cs | 12 + .../Rendering/Universal/UniversalRenderer.cs | 5 + tests/CMakeLists.txt | 17 +- tests/UI/Editor/CMakeLists.txt | 3 +- .../Editor/manual_validation/CMakeLists.txt | 10 +- tests/UI/Editor/unit/CMakeLists.txt | 44 ++ ..._editor_window_synchronization_planner.cpp | 391 ++++++++++++++++++ tests/editor/CMakeLists.txt | 6 + tests/scripting/test_mono_script_runtime.cpp | 72 ++-- 32 files changed, 1787 insertions(+), 320 deletions(-) create mode 100644 Rendering/AGENTS.md create mode 100644 docs/plan/rendering_srp_urp_closeout_plan.md create mode 100644 docs/plan/xcui_editor_windowing_architecture_plan.md create mode 100644 editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp create mode 100644 editor/app/Windowing/System/EditorWindowPresentationPolicy.h create mode 100644 editor/app/Windowing/System/EditorWindowSynchronizationPlan.h create mode 100644 editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp create mode 100644 editor/app/Windowing/System/EditorWindowSynchronizationPlanner.h create mode 100644 tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp diff --git a/Rendering/AGENTS.md b/Rendering/AGENTS.md new file mode 100644 index 00000000..1c415039 --- /dev/null +++ b/Rendering/AGENTS.md @@ -0,0 +1,63 @@ +# Rendering 模块说明(在建) + +## 1. 目的 +- 本文只描述当前已落地的渲染主线和后续收口方向。 +- 这里优先记录真实代码职责,不按理想状态脑补。 +- 如果实现与本文冲突,以代码为准,再更新本文。 + +## 2. 当前主链 +```text +SceneRenderer + -> SceneRenderRequestPlanner + -> RenderPipelineHost + -> CameraFramePlanBuilder + -> CameraRenderer + -> DirectionalShadowRuntime + -> RenderSceneExtractor + -> CameraFrameGraph + -> RenderGraph Record / Compile / Execute +``` + +## 3. 当前架构判断 +- 这套系统已经不是“一个 forward pipeline 直接画完整帧”。 +- 当前主模型是:`SRP host + renderer backend + per-camera plan + render graph execution`。 +- `ScriptableRenderPipelineHost` 是 native 侧最重要的 SRP 接缝。 +- `CameraRenderRequest` 和 `CameraFramePlan` 已经承担了每相机规划职责。 +- `RenderSceneExtractor` 负责从 `Scene` 生成 `RenderSceneData`。 +- `CameraFrameGraph` 和 `RenderGraph` 已经是执行内核的一部分,不是附属实验层。 + +## 4. 和 Unity SRP / URP 的关系 +- `RenderPipelineAsset` 对应 Unity 的 `RenderPipelineAsset`。 +- `ScriptableRenderPipelineHost` 接近 Unity 的 `RenderPipeline` 宿主。 +- managed 侧已经有 `ScriptableRenderPipelineAsset -> ScriptableRendererData -> ScriptableRenderer -> ScriptableRendererFeature/Pass` 这条产品层骨架。 +- 因此当前问题不是“SRP 没打通”,而是“URP 风格上层还没彻底收口”。 + +## 5. 当前未收口点 +- managed runtime 目前仍复用默认 native forward backend 作为共享绘制后端。 +- 这意味着 `BuiltinForwardPipeline` 还同时带着“默认产品层”和“native 执行后端”两种身份。 +- 一些本该属于 URP asset / renderer / feature 的组织策略,仍可能滑回 native backend。 +- 所以现在更准确的名字是“SRP 已通、URP 主产品线未收口”。 + +## 6. 在这个模块里应怎样继续演进 +- 把 camera request policy、renderer 选择、stage planning、scene setup、shadow execution config、pass/feature 排序继续上移到 managed `URP`。 +- 把 scene extraction、renderer list 绘制、native scene feature、fullscreen primitive 执行保留在 native。 +- 非通用策略不要继续直接塞进 `BuiltinForwardPipeline`。 +- 如果新增的是“绘制能力”或“native backend contract”,可以放 C++。 +- 如果新增的是“渲染组织策略”或“URP 行为差异”,优先放 managed。 + +## 7. 关键文件 +- `engine/src/Rendering/Execution/SceneRenderer.cpp` +- `engine/src/Rendering/Execution/RenderPipelineHost.cpp` +- `engine/src/Rendering/Execution/CameraRenderer.cpp` +- `engine/src/Rendering/Extraction/RenderSceneExtractor.cpp` +- `engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp` +- `engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp` +- `managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline*.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/*.cs` + +## 8. 长期目标 +- 形成唯一默认主线:`UniversalRenderPipelineAsset -> UniversalRendererData -> UniversalRenderer -> ScriptableRendererFeature/Pass`。 +- `ShadowCaster / DepthOnly / MainScene / PostProcess / FinalOutput` 的组织权由 managed `URP` 持有。 +- `BuiltinForwardPipeline` 或其后继应退回为 native scene draw backend,而不是继续充当上层产品语义。 +- native 侧最终保留“执行内核”和“通用后端 contract”,managed 侧负责“渲染策略”和“产品层组织”。 +- 简单说:目标不是继续堆更多 URP 外形,而是把 URP 真正收口成唯一上层组织者。 diff --git a/docs/plan/rendering_srp_urp_closeout_plan.md b/docs/plan/rendering_srp_urp_closeout_plan.md new file mode 100644 index 00000000..2a3c90a7 --- /dev/null +++ b/docs/plan/rendering_srp_urp_closeout_plan.md @@ -0,0 +1,214 @@ +# Rendering SRP / URP Closeout Plan + +更新日期: `2026-04-26` + +状态: `Draft, Phase 1 queued` + +## 1. Plan 管理规则 + +- `docs/plan` 只保留总 plan。 +- 已完成 subplan 的结论直接并回总 plan,不再长期保留独立阶段文档。 +- 如果实现与本计划冲突,以代码现状为准,再回写本计划。 +- 本计划关注“收口顺序”和“职责边界”,不把所有未来渲染特性都混进当前路线。 + +## 2. 当前状态判断 + +- native 主链已经比较清楚: `SceneRenderer -> SceneRenderRequestPlanner -> RenderPipelineHost -> CameraRenderer -> RenderSceneExtractor -> CameraFrameGraph -> RenderGraph`。 +- managed 侧已经存在 `ScriptableRenderPipelineAsset -> ScriptableRendererData -> ScriptableRenderer -> ScriptableRendererFeature/Pass` 这条产品层骨架。 +- `ScriptableRenderPipelineHost` 已经能优先把 stage graph、scene setup、directional shadow execution state 交给 managed runtime。 +- 因此当前问题不是“SRP 没打通”,而是“URP 风格上层还没有彻底收口成唯一主线”。 +- 目前 managed runtime 仍复用默认 native forward backend 作为共享绘制后端,这让 `BuiltinForwardPipeline` 同时带着“产品层语义”和“native 执行后端”两种身份。 + +## 3. 顶层架构问题 + +- `BuiltinForwardPipeline` 身份过重,既像默认产品管线,又像通用 scene draw backend。 +- managed `URP` 还不是默认主线的唯一组织者,部分策略仍可能滑回 native backend。 +- backend 解析目前偏隐式,default fallback 的存在让“谁在拥有组织权”不够明确。 +- `PostProcess / FinalOutput / Shadow / DepthOnly / MainScene` 虽然都有 managed 接缝,但还没有被定义成统一、稳定、单一路径的产品闭环。 +- 现有测试覆盖了很多局部契约,但“默认 URP 主路径”的端到端产品闭环还不够突出。 + +## 4. 长期目标 + +长期目标不是继续堆更多 URP 外形,而是把 `URP` 真正收口成唯一上层组织者。 + +目标架构应收敛为: + +```text +GraphicsSettings.renderPipelineAsset + -> UniversalRenderPipelineAsset + -> selected ScriptableRendererData + -> UniversalRenderer + -> ScriptableRendererFeature / ScriptableRenderPass + -> ScriptableRenderPipelineHost + -> native scene draw backend contract + -> RenderSceneExtractor / CameraFrameGraph / RenderGraph / RHI +``` + +## 5. 长期职责边界 + +- managed `URP` 负责: + - camera request policy + - renderer 选择 + - per-camera stage planning + - render scene setup + - shadow execution policy + - pass / feature 排序与产品层组织 +- native Rendering 负责: + - `Scene` 到 `RenderSceneData` 的提取 + - renderer list 绘制 + - native scene feature 执行 + - fullscreen primitive 与 graph 执行支持 + - RenderGraph / RHI 执行内核 +- `BuiltinForwardPipeline` 或其后继最终只应保留 backend 语义,不再承载默认产品层策略。 +- 新增“绘制能力”优先放 native;新增“渲染组织策略”优先放 managed。 + +## 6. 长期阶段路线 + +## Phase 1: Renderer-backed URP v1 收口 + +- 目标: 让 `UniversalRenderPipelineAsset -> UniversalRendererData -> UniversalRenderer` 成为默认主线的明确组织者。 +- 重点: 不大改底层执行内核,先收口产品层 ownership。 +- 交付: + - 把共享 native backend 的解析变成显式 contract,而不是隐藏 fallback。 + - 明确 `ShadowCaster / DepthOnly / MainScene / PostProcess / FinalOutput` 的 managed ownership。 + - 固化默认 renderer data、default feature lineup、camera override 和 final color 策略。 + - 补一组面向默认 URP 主线的端到端验证。 +- 验收: + - 默认 `UniversalRenderPipelineAsset` 的主路径不再依赖“读代码才知道”的隐式 backend fallback 语义。 + - managed 层成为五类默认 stage 的唯一组织入口。 + - 本阶段不再把新的产品策略塞进 `BuiltinForwardPipeline`。 + +## Phase 2: Native backend contract 抽取 + +- 目标: 把 `BuiltinForwardPipeline` 退回为 backend-only 角色,抽出更稳定的 native scene draw contract。 +- 重点: 从“具体默认管线类”过渡到“可被 managed 调用的 native backend 能力集合”。 +- 交付: + - 抽取明确的 backend asset / backend key / backend contract。 + - 明确 scene draw、native feature pass、fullscreen support 的 contract surface。 + - 收窄 `ScriptableRenderPipelineHost` 和 managed bridge 对 `BuiltinForwardPipeline` 具体类型的隐式依赖。 +- 验收: + - managed bridge 暴露的是 backend contract,而不是继续暴露一个带产品语义的默认 forward pipeline 资产。 + - host fallback 只保留兼容路径,不再是默认产品路径的一部分。 + +## Phase 3: Feature ownership 迁移 + +- 目标: 把上层 feature 组织从“native 顺带拥有”进一步迁到 managed `URP`。 +- 重点: native 只保留叶子能力,feature 编排放回 `ScriptableRendererFeature` 和 renderer block。 +- 交付: + - 明确哪些能力保留为 native leaf feature,例如 volumetric、gaussian splat、tooling feature pass。 + - 把这些能力的启用、排序、插入点和开关逻辑统一交给 managed feature/controller。 + - 清理仍然散落在 C++ 中的 URP-like 组织策略。 +- 验收: + - feature 是否执行、何时执行、插入在哪个 stage/block,应由 managed 层决定。 + - native 只回答“怎么执行”,不再回答“应不应该插入”。 + +## Phase 4: Asset / Editor / Runtime 产品化 + +- 目标: 让 renderer data、renderer feature、camera override、graphics settings 形成稳定产品闭环。 +- 重点: 让默认主线不只是能跑,而是能配置、能失效重建、能被 editor 正常消费。 +- 交付: + - 稳定 `UniversalRendererData`、default renderer composition、renderer override、camera additional data。 + - 梳理 runtime invalidation、renderer rebuild、feature cache release 路径。 + - 把默认 asset、renderer data、feature 的 authoring 体验稳定下来。 +- 验收: + - graphics settings、camera override、renderer invalidation 都能沿主路径稳定工作。 + - editor / runtime 不再依赖 probe 级行为去维持默认主线。 + +## Phase 5: 收尾与清理 + +- 目标: 删除临时兼容层,统一命名和文档,把“在建中的双重语义”收尾。 +- 重点: 只在前面阶段稳定后做命名与删除,不和 ownership 迁移并行进行。 +- 交付: + - 删除过时 fallback。 + - 统一 host / backend / renderer / feature 的命名。 + - 更新模块文档、测试说明和 blueprint。 +- 验收: + - 默认主线对新人是可解释的。 + - “哪个层负责什么”可以不靠历史背景直接从命名和代码结构看出来。 + +## 7. 跨阶段约束 + +- 不把 deferred、clustered lighting、完整 volume framework、更多 Unity compatibility shim 混入当前收口路线。 +- 不在收口阶段同时做大规模 rename 和 ownership 迁移。 +- 不新增新的“临时 fallback 语义”来掩盖边界问题。 +- 每推进一个阶段,都必须补足默认主线路径的 focused test,而不是只补 probe。 +- 任何新增策略逻辑,都要先回答“它属于 managed 组织层还是 native 执行层”。 + +## 8. 非目标 + +- 这份计划当前不直接追求完整 HDRP / 延迟渲染路线。 +- 不直接重写 `RenderGraph`。 +- 不直接重做 `RenderSceneExtractor` 和相机规划主链。 +- 不要求短期内删掉所有 builtin pass。 +- 不把 editor tooling pass、object id、selection outline 等工具路径强行并入默认 URP 主线。 + +## 9. 下一个阶段 subplan + +下一个阶段对应 `Phase 1: Renderer-backed URP v1 收口`。 + +### 9.1 阶段目标 + +- 让默认 `UniversalRenderPipelineAsset` 成为“可解释、可验证、可扩展”的唯一上层主线。 +- 保持底层执行内核稳定,不在本阶段直接做 backend 大拆分。 +- 先把 ownership 和 contract 说清楚,再进入更大的 backend 抽取。 + +### 9.2 本阶段明确要解决的问题 + +1. managed bridge 目前对共享 native backend 的解析太隐式。 +2. `UniversalRenderer` 对默认 stage 的 ownership 还不够集中,`PostProcess` 路径尤其需要拍板。 +3. `BuiltinForwardPipeline` 仍容易继续吸收本该属于产品层的策略。 +4. 默认 URP 主线缺一组更醒目的 end-to-end 验证。 + +### 9.3 建议改动范围 + +- `engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h` +- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` +- `engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderPipelineAsset.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderer.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalPostProcessBlock.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalFinalOutputBlock.cs` +- `tests/scripting/test_mono_script_runtime.cpp` +- `tests/Rendering/unit/test_camera_scene_renderer.cpp` +- 如有必要,再补 `project/Assets/Scripts/ProjectRenderPipelineProbe.cs` + +### 9.4 执行顺序 + +1. 先把 shared native backend contract 显式化。 + - 明确 managed asset runtime 返回的到底是“默认 fallback backend”还是“显式指定 backend”。 + - 推荐把默认 URP 使用的 native backend 变成显式选择,而不是 `CreateDefaultPipelineBackendAsset()` 兜底。 + - 兼容 fallback 可以保留,但必须退到 guarded compatibility path。 +2. 再收口默认 stage ownership。 + - `ShadowCaster / DepthOnly / MainScene / PostProcess / FinalOutput` 的默认规划和调度都要从 `UniversalRenderer` 主线解释得通。 + - `PostProcess` 建议正式成为 `UniversalRenderer` 的显式 stage 分支,而不是只靠 feature 侧“顺带成立”。 + - `UniversalPostProcessBlock` 与 `UniversalFinalOutputBlock` 的关系要稳定成单一路径。 +3. 冻结 `BuiltinForwardPipeline` 的产品层职责继续外溢。 + - 本阶段起不再向它添加新的 camera policy、renderer 选择、stage 编排逻辑。 + - 若新增能力确实必须放 native,也要以 backend contract 的形式暴露,而不是直接塞产品策略。 +4. 补默认主线 focused validation。 + - 覆盖默认 `UniversalRenderPipelineAsset`。 + - 覆盖 custom renderer data / renderer override。 + - 覆盖 feature 注入对 post-process / main-scene 路径的影响。 + - 覆盖 renderer invalidation 和 runtime rebuild。 + +### 9.5 本阶段验收标准 + +- 默认 `UniversalRenderPipelineAsset` 的 backend 来源是显式可追踪的。 +- 默认五类 stage 都能从 managed `URP` 主线解释清楚 ownership。 +- `BuiltinForwardPipeline` 在本阶段没有继续吸收新的产品层策略。 +- 默认 URP 主路径至少具备一组比 probe 更贴近产品语义的 focused 测试。 +- 文档和代码中的默认主线表述一致。 + +### 9.6 本阶段非目标 + +- 不在本阶段直接重命名 `BuiltinForwardPipeline`。 +- 不在本阶段抽出最终形态的 backend-only 类层次。 +- 不引入 deferred、renderer graph 大改或新的大体量渲染特性。 +- 不把 editor tooling 路径强行并进默认 URP 产品线。 + +### 9.7 阶段完成后的输出 + +- 一条更清晰的默认 `URP` 主线。 +- 一组更明确的 backend contract 下一步抽取前提。 +- 一个可以继续进入 `Phase 2` 的更稳定起点。 diff --git a/docs/plan/xcui_editor_windowing_architecture_plan.md b/docs/plan/xcui_editor_windowing_architecture_plan.md new file mode 100644 index 00000000..c53f12cd --- /dev/null +++ b/docs/plan/xcui_editor_windowing_architecture_plan.md @@ -0,0 +1,184 @@ +# XCUIEditor Windowing Architecture Plan + +更新日期: `2026-04-26` + +状态: `Phase 1 completed, Phase 2 completed, Subplan 3A planned` + +## 1. Plan 管理规则 + +- `docs/plan` 现在只保留总 plan。 +- 已完成 subplan 的结论直接并回总 plan,不再保留阶段性执行文档。 +- 新的 subplan 必须保持小切口;一次只收口一条主链,不把 projection、destroy、drag/drop 等多条链路捆在同一阶段。 + +## 2. 顶层架构问题 + +当前 `XCUIEditorApp` 最严重的架构问题仍然是: 多窗口工作区状态存在双真相源。 + +这两个真相源目前分别落在: + +- `editor/app/Windowing/System/EditorWindowSystem.*` +- `editor/app/Composition/EditorWindowWorkspaceStore.*` +- live `EditorWindow` +- `editor/app/Windowing/Content/EditorWorkspaceWindowContentController.*` +- `editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.*` + +虽然已经把“同步 diff 规划”和“标题策略”从 `Win32` 层切回了 app 层,但当前仍然存在以下事实: + +- live `EditorWindow` / `EditorWorkspaceWindowContentController` 里的 `UIEditorWorkspaceController` 仍然是可变状态容器。 +- `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 已经产出显式 `workspaceMutation` request,但 request 仍然来自 live controller 的本地副本。 +- `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 收到 `workspaceMutation` 后,仍通过 `CommitLiveWindowMutation(sourceWindow)` 回读 `window.GetWorkspaceController()`,而不是直接消费 request payload。 +- `EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan(...)` 仍同时承担 host 执行与 authoritative commit 的时序编排。 + +这会带来四个直接后果: + +1. app 层和 host 层都能改 `UIEditorWindowWorkspaceSet`,单一真相源名义存在、实际不存在。 +2. frame 内部交互修改依赖平台回写兜底,边界方向不稳定。 +3. 关闭、拖拽、分离、primary 切换这些跨窗口时序很难纯粹按领域规则推导。 +4. `editor/app/Platform/Win32` 继续承担了不属于 host adapter 的领域编排责任。 + +## 3. 已完成收口 + +当前已完成两阶段收口,结果如下: + +- 新增 app 层同步模型: + - `EditorWindowSynchronizationPlan` + - `EditorWindowSynchronizationPlanner` + - `EditorWindowPresentationPolicy` +- `EditorWindowSystem` 新增 `BuildSynchronizationPlan(...)`,开始由 app 层产出 authoritative sync plan。 +- `EditorWindowWorkspaceCoordinator` 不再持有窗口集 diff 算法和标题策略定义,只保留 host snapshot 采集与 plan 执行。 +- `EditorWindowTransferRequests` 新增显式 workspace mutation request,frame 内工作区变化不再依赖平台隐式回写 authority。 +- `EditorWindowSystem` 收口 live window mutation / native destroy observation / authoritative commit 语义入口。 +- `EditorWindowLifecycleCoordinator::HandleNativeWindowDestroyed(...)` 不再直接删除 authority store entry,而是回到 app 层做 destroy reconciliation。 +- `editor/app/Platform/Win32/**` 已移除对 authority raw write 接口的直接依赖。 +- 新增聚焦验证: + - `tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp` + - `editor_windowing_phase1_tests` +- editor 本体已完成构建与 `12s` 启动冒烟验证。 +- editor 测试入口回到 `tests/UI/Editor`,legacy `tests/editor` 已从当前活动构建图移除。 + +这两阶段的目标分别是: + +- Phase 1: 先把“领域决策”和“Win32 执行”切开。 +- Phase 2: 删除“平台回写 authority”链路,把 authority mutation 收口回 `editor/app/Windowing`。 + +这两个目标都已经达成。 + +## 4. 当前剩余缺口 + +原计划中针对 `authority writeback` 的核心缺口已经完成收口,但还有一段“半收口”链路仍然存在: + +- `EditorWindowTransferRequests` 已有 `EditorWindowWorkspaceMutationRequest`,但 `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 目前只填 `workspace/session`,没有把稳定的 `windowId` 一并带出。 +- `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 目前拿到 request 后并不直接消费它,而是再次回读 `sourceWindow` 上的 live projection。 +- 这意味着 frame 内交互虽然已经显式化为 request,但 request 还没有真正成为这条链路的唯一语义输入。 + +这一段正好适合作为下一个小 subplan;在它完成之前,不适合继续展开更大的 live projection 去状态化。 + +## 5. 目标架构 + +目标状态下,窗口系统的数据流应当是单向的: + +```text +authoritative window set (app/windowing) + -> mutation validation + -> synchronization plan + -> Win32 execution + -> host observation / lifecycle event + -> app-layer semantic callback +``` + +这里的关键约束是: + +- authority 只在 `editor/app/Windowing` 侧维护。 +- `Win32` 层只能消费 plan、上报 observation/event,不能直接写 authority store。 +- live content controller 只能是 projection 或受控 mutation producer,不能再充当隐式 authority 副本。 + +## 6. 后续路线 + +后续按“小 subplan”推进,每个 subplan 只解决一条主链。下一个 subplan 只收口 frame 内显式 `workspaceMutation` request,不同时处理 live projection 去状态化、native destroy reconciliation 或 global tab drag。 + +### 6.1 Subplan 3A: 显式 workspace mutation request 收口 + +#### 6.1.1 阶段目标 + +- 让 frame 内 workspace 变更通过 `EditorWindowWorkspaceMutationRequest` 进入 app 层,而不是由 coordinator 再回读 live `UIEditorWorkspaceController`。 +- 把“显式 request 已存在但未真正成为主输入”的半收口状态补完整。 +- 保持本阶段足够小,只处理 live workspace mutation commit 这条链。 + +#### 6.1.2 建议改动范围 + +- `editor/app/Windowing/Frame/EditorWindowTransferRequests.h` +- `editor/app/Windowing/Content/EditorWorkspaceWindowContentController.*` +- `editor/app/Windowing/System/EditorWindowSystem.*` +- `editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.*` +- `tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp` +- 必要时补 `tests/UI/Editor/unit` 下新的 focused test;若必须经过 host 协调器,再补 `editor_app_feature_tests` + +#### 6.1.3 详细执行步骤 + +1. 先把 `workspaceMutation` request payload 补完整。 + - `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 在生成 `EditorWindowWorkspaceMutationRequest` 时补齐 `windowState.windowId`。 + - request 的有效性以“`windowId` 非空且 workspace state 可用”为准,避免继续依赖 source window 隐式补信息。 +2. 再把 app 层计划入口改成显式 request 驱动。 + - 在 `EditorWindowSystem` 中新增或收口一个直接消费 `EditorWindowWorkspaceMutationRequest` 的 plan builder。 + - 该入口只做三件事: 校验 `windowId`、把 request 中的 `workspace/session` 合并进当前 authoritative window set、复用 `BuildPlanForWindowSet(...)`。 + - `BuildPlanForLiveWindowMutation(...)` 如果还有旧调用方,可暂时降级成薄封装;本阶段不再新增新的调用点依赖它。 +3. 最后切换 `EditorWindowWorkspaceCoordinator` 的 live commit 路径。 + - `HandleWindowFrameTransferRequests(...)` 直接把 `transferRequests.workspace.workspaceMutation` 传给 commit 路径。 + - `CommitLiveWindowMutation(...)` 改成基于 request 的实现,或者删除旧的 `EditorWindow&` 回读版本。 + - 本阶段完成后,live workspace mutation commit 不再依赖 `window.GetWorkspaceController()`;该接口只允许继续服务 projection / title / UI 查询。 +4. 补 focused 验证,保证这是小收口而不是行为漂移。 + - planner/system 侧至少覆盖“显式 request 改 active panel 后可生成并提交有效 plan”。 + - 增加“unknown windowId request 被拒绝”的负向验证。 + - 如果 unit 无法覆盖 coordinator 消费 request 的行为,再补一条最小 host-side focused test,而不是直接上重型运行时验证。 + +#### 6.1.4 本阶段验收标准 + +- `workspaceMutation` request 自身携带稳定 `windowId`,不再需要 source window 兜底补齐。 +- `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 不再忽略 request payload。 +- live frame mutation 的 authoritative commit 路径不再回读 `sourceWindow.GetWorkspaceController()`。 +- 现有 detach / dock / close / primary switch 行为不因本阶段发生语义漂移。 +- `editor_windowing_phase1_tests` 继续通过;若新增 host-side focused test,对应测试目标也应通过。 + +#### 6.1.5 本阶段非目标 + +- 不在本阶段删除 `EditorWindow` / content controller 上的 `TryGetWorkspaceController()` 或 `ReplaceWorkspaceController()`。 +- 不在本阶段处理 native destroy reconciliation。 +- 不在本阶段重做 global tab drag / detach 相关编排。 +- 不在本阶段把 live content controller 改成完全只读 projection。 + +## 7. 验收标准 + +整个计划完成后,应至少满足: + +1. `editor/app/Platform/Win32/**` 不再直接调用 authority store 原始写接口。 +2. `EditorWindowSystem` 成为窗口工作区 authoritative mutation 的唯一 app 层入口。 +3. 任意多窗口变化都先形成 app 层语义上的 mutation / transition,再形成 sync plan,再由 host 执行。 +4. native close / destroy 不再通过平台层“直接删 store entry”完成状态收口。 +5. `EditorWorkspaceWindowContentController` 不再被当作 authority 副本,而只是 projection 或受控 mutation source。 +6. detach / dock / close / primary switch 至少具备稳定 unit coverage,并对关键 host 时序具备 focused integration coverage。 + +## 8. 验证策略 + +验证入口仍然以 `tests/UI/Editor` 为主,不重新启用旧 `tests/editor`。 + +优先级如下: + +- `tests/UI/Editor/unit` +- 必要时补 `tests/UI/Editor/smoke` +- 只有在 unit / focused integration 无法覆盖的 host 时序下,才考虑更重的运行时验证 + +当前已建立的验证基线: + +- `cmake --build build --config Debug --target editor_windowing_phase1_tests` +- `build/tests/UI/Editor/unit/Debug/editor_windowing_phase1_tests.exe` + +## 9. 非目标 + +本计划当前不直接解决以下事项: + +- 把 `XCUIEditorApp` 立即改造成跨平台窗口系统 +- 全量重写 shell / viewport / message dispatcher +- 顺手重构所有 inspector、panel、runtime 业务逻辑 +- 在 authority 收口前提前合并全部 Win32 coordinator + +这些都必须排在“窗口领域 authority 真正收口”之后。 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 6922e5b9..17aa3b36 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -149,6 +149,26 @@ set(XCUI_EDITOR_SHARED_SOURCES ${XCUI_EDITOR_WIDGET_SUPPORT_SOURCES} ) +add_library(XCUIEditorLib STATIC + ${XCUI_EDITOR_SHARED_SOURCES} +) + +target_include_directories(XCUIEditorLib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_include_directories(XCUIEditorLib PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/app + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_link_libraries(XCUIEditorLib PUBLIC + XCEngine +) + +xcui_editor_apply_common_target_settings(XCUIEditorLib PUBLIC) + set(XCUI_EDITOR_HOST_PLATFORM_SOURCES app/Platform/Win32/Chrome/BorderlessWindowChrome.cpp app/Platform/Win32/Chrome/BorderlessWindowFrame.cpp @@ -192,7 +212,6 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) app/Composition/EditorShellInteractionEngine.cpp app/Composition/EditorShellRuntime.cpp app/Composition/EditorShellSessionCoordinator.cpp - app/Composition/EditorWindowWorkspaceStore.cpp app/Composition/WorkspaceEventSync.cpp ) @@ -240,10 +259,13 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) ) set(XCUI_EDITOR_APP_WINDOWING_SOURCES + app/Composition/EditorWindowWorkspaceStore.cpp app/Windowing/Content/EditorWindowContentFactory.cpp app/Windowing/Content/EditorUtilityWindowContentController.cpp app/Windowing/Content/EditorWorkspaceWindowContentController.cpp app/Windowing/Frame/EditorWindowFrameOrchestrator.cpp + app/Windowing/System/EditorWindowPresentationPolicy.cpp + app/Windowing/System/EditorWindowSynchronizationPlanner.cpp app/Windowing/System/EditorWindowSystem.cpp ) @@ -303,6 +325,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) target_link_libraries(XCUIEditorApp PRIVATE XCEngine + XCUIEditorLib XCEngineRenderingEditorSupport d3d12.lib d3dcompiler.lib diff --git a/editor/app/Composition/EditorWindowWorkspaceStore.cpp b/editor/app/Composition/EditorWindowWorkspaceStore.cpp index 8e0fe9c7..701b2a16 100644 --- a/editor/app/Composition/EditorWindowWorkspaceStore.cpp +++ b/editor/app/Composition/EditorWindowWorkspaceStore.cpp @@ -34,72 +34,6 @@ bool EditorWindowWorkspaceStore::TrySetWindowSet( return true; } -bool EditorWindowWorkspaceStore::UpsertWindowState( - const UIEditorWindowWorkspaceState& windowState, - bool primary, - std::string& outError) { - UIEditorWindowWorkspaceSet nextWindowSet = m_windowSet; - if (UIEditorWindowWorkspaceState* existingState = - FindMutableUIEditorWindowWorkspaceState(nextWindowSet, windowState.windowId); - existingState != nullptr) { - *existingState = windowState; - } else { - nextWindowSet.windows.push_back(windowState); - } - - if (primary || nextWindowSet.primaryWindowId.empty()) { - nextWindowSet.primaryWindowId = windowState.windowId; - } - if (nextWindowSet.activeWindowId.empty() || - FindUIEditorWindowWorkspaceState(nextWindowSet, nextWindowSet.activeWindowId) == nullptr) { - nextWindowSet.activeWindowId = windowState.windowId; - } - - if (!ValidateWindowSet(nextWindowSet, outError)) { - return false; - } - - m_windowSet = std::move(nextWindowSet); - outError.clear(); - return true; -} - -void EditorWindowWorkspaceStore::RemoveWindowState(std::string_view windowId, bool primary) { - if (primary) { - m_windowSet = {}; - return; - } - - UIEditorWindowWorkspaceSet nextWindowSet = m_windowSet; - nextWindowSet.windows.erase( - std::remove_if( - nextWindowSet.windows.begin(), - nextWindowSet.windows.end(), - [windowId](const UIEditorWindowWorkspaceState& state) { - return state.windowId == windowId; - }), - nextWindowSet.windows.end()); - - if (nextWindowSet.windows.empty()) { - m_windowSet = {}; - return; - } - - if (nextWindowSet.primaryWindowId == windowId || - FindUIEditorWindowWorkspaceState(nextWindowSet, nextWindowSet.primaryWindowId) == nullptr) { - nextWindowSet.primaryWindowId = nextWindowSet.windows.front().windowId; - } - if (nextWindowSet.activeWindowId == windowId || - FindUIEditorWindowWorkspaceState(nextWindowSet, nextWindowSet.activeWindowId) == nullptr) { - nextWindowSet.activeWindowId = nextWindowSet.primaryWindowId; - } - - std::string ignoredError = {}; - if (ValidateWindowSet(nextWindowSet, ignoredError)) { - m_windowSet = std::move(nextWindowSet); - } -} - bool EditorWindowWorkspaceStore::IsPrimaryWindowId(std::string_view windowId) const { return !windowId.empty() && m_windowSet.primaryWindowId == windowId; } diff --git a/editor/app/Composition/EditorWindowWorkspaceStore.h b/editor/app/Composition/EditorWindowWorkspaceStore.h index 236e580d..c08cae57 100644 --- a/editor/app/Composition/EditorWindowWorkspaceStore.h +++ b/editor/app/Composition/EditorWindowWorkspaceStore.h @@ -20,11 +20,9 @@ public: bool TrySetWindowSet( UIEditorWindowWorkspaceSet windowSet, std::string& outError); - bool UpsertWindowState( - const UIEditorWindowWorkspaceState& windowState, - bool primary, - std::string& outError); - void RemoveWindowState(std::string_view windowId, bool primary); + void ClearWindowSet() { + m_windowSet = {}; + } bool IsPrimaryWindowId(std::string_view windowId) const; const UIEditorWindowWorkspaceSet& GetWindowSet() const { return m_windowSet; diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.cpp b/editor/app/Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.cpp index aaef013c..6e4ac1b8 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.cpp @@ -135,10 +135,10 @@ void EditorWindowLifecycleCoordinator::HandleNativeWindowDestroyed(EditorWindow& } ShutdownRuntimeIfNeeded(window); - if (window.IsWorkspaceWindow()) { - m_workspaceCoordinator.RemoveWindowProjection(window.GetWindowId(), destroyedPrimary); - } window.MarkDestroyed(); + if (window.IsWorkspaceWindow()) { + m_workspaceCoordinator.HandleNativeWindowDestroyed(window.GetWindowId()); + } if (destroyedPrimary) { std::vector closeTargets = {}; diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowSession.cpp b/editor/app/Platform/Win32/Windowing/EditorWindowSession.cpp index 58d8f8a0..812e04f4 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowSession.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindowSession.cpp @@ -112,6 +112,10 @@ void EditorWindowSession::SetTitle(std::wstring title) { void EditorWindowSession::QueueCompletedImmediateFrame( EditorWindowFrameTransferRequests transferRequests) { m_hasQueuedCompletedImmediateFrame = true; + if (transferRequests.workspace.workspaceMutation.has_value()) { + m_queuedImmediateFrameTransferRequests.workspace.workspaceMutation = + std::move(transferRequests.workspace.workspaceMutation); + } if (transferRequests.workspace.beginGlobalTabDrag.has_value()) { m_queuedImmediateFrameTransferRequests.workspace.beginGlobalTabDrag = std::move(transferRequests.workspace.beginGlobalTabDrag); diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp index f971bf00..ec53cbfb 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp @@ -5,11 +5,11 @@ #include "Platform/Win32/Windowing/EditorFloatingWindowPlacement.h" #include "Platform/Win32/Windowing/EditorWindowHostRuntime.h" #include "Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.h" +#include "Windowing/System/EditorWindowPresentationPolicy.h" #include "Windowing/System/EditorWindowSystem.h" #include "Windowing/Content/EditorWindowContentController.h" #include -#include #include #include #include @@ -24,7 +24,7 @@ namespace XCEngine::UI::Editor::App { namespace { struct ExistingWindowSnapshot { - EditorWindow* window = nullptr; + std::string windowId = {}; UIEditorWorkspaceController workspaceController = {}; std::wstring title = {}; bool primary = false; @@ -106,7 +106,6 @@ void EditorWindowWorkspaceCoordinator::BindLifecycleCoordinator( } void EditorWindowWorkspaceCoordinator::RegisterExistingWindow(EditorWindow& window) { - UpdateWindowProjection(window); RefreshWindowTitle(window); } @@ -114,10 +113,36 @@ void EditorWindowWorkspaceCoordinator::RefreshWindowPresentation(EditorWindow& w if (!window.IsWorkspaceWindow()) { return; } - UpdateWindowProjection(window); RefreshWindowTitle(window); } +void EditorWindowWorkspaceCoordinator::HandleNativeWindowDestroyed(std::string_view windowId) { + const std::wstring_view primaryWindowTitle = + m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr + ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) + : std::wstring_view{}; + std::string error = {}; + EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForDestroyedWindow( + windowId, + CaptureHostSnapshots(), + primaryWindowTitle, + error); + if (!plan.valid) { + LogRuntimeTrace( + "window-close", + "workspace destroy reconciliation rejected for window '" + + std::string(windowId) + "': " + error); + return; + } + + if (!ApplySynchronizationPlan(plan)) { + LogRuntimeTrace( + "window-close", + "workspace destroy reconciliation execution failed for window '" + + std::string(windowId) + "'"); + } +} + bool EditorWindowWorkspaceCoordinator::IsPrimaryWindowId(std::string_view windowId) const { return m_windowSystem.IsPrimaryWindowId(windowId); } @@ -126,22 +151,6 @@ std::string EditorWindowWorkspaceCoordinator::DescribeWindowSet() const { return DescribeWindowSetState(m_windowSystem.GetWindowSet()); } -void EditorWindowWorkspaceCoordinator::RemoveWindowProjection( - std::string_view windowId, - bool primary) { - LogRuntimeTrace( - "window-close", - "workspace remove begin windowId='" + std::string(windowId) + - "' primaryArg=" + (primary ? "1" : "0") + - " stateBefore=" + DescribeWindowSet()); - - m_windowSystem.RemoveWindowState(windowId, primary); - LogRuntimeTrace( - "window-close", - "workspace remove end windowId='" + std::string(windowId) + - "' stateAfter=" + DescribeWindowSet()); -} - UIEditorWindowWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController() const { return m_windowSystem.BuildWorkspaceMutationController(); @@ -160,55 +169,49 @@ UIEditorWindowWorkspaceState EditorWindowWorkspaceCoordinator::BuildWindowStateF return state; } -void EditorWindowWorkspaceCoordinator::UpdateWindowProjection(EditorWindow& window) { +bool EditorWindowWorkspaceCoordinator::CommitLiveWindowMutation(EditorWindow& window) { if (!window.IsWorkspaceWindow() || window.GetHwnd() == nullptr || window.TryGetWorkspaceController() == nullptr) { - return; + return true; } + const std::wstring_view primaryWindowTitle = + m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr + ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) + : std::wstring_view{}; std::string error = {}; - if (!m_windowSystem.UpsertWindowState( - BuildWindowStateForWindow(window), - window.IsPrimary(), - error)) { + EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForLiveWindowMutation( + window.GetWindowId(), + window.GetWorkspaceController(), + CaptureHostSnapshots(), + primaryWindowTitle, + error); + if (!plan.valid) { LogRuntimeTrace( "window", - "workspace projection update rejected for window '" + + "workspace live mutation rejected for window '" + std::string(window.GetWindowId()) + "': " + error); - } -} - -UIEditorWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceControllerForWindow( - const UIEditorWindowWorkspaceState& windowState) const { - return UIEditorWorkspaceController( - m_windowSystem.GetPanelRegistry(), - windowState.workspace, - windowState.session); -} - -std::wstring EditorWindowWorkspaceCoordinator::BuildWindowTitle( - const UIEditorWorkspaceController& workspaceController) const { - const std::string titleText = - ResolveUIEditorDetachedWorkspaceTitle(workspaceController); - if (!titleText.empty()) { - const std::string decoratedTitle = titleText + " - XCEngine Editor"; - return std::wstring(decoratedTitle.begin(), decoratedTitle.end()); + return false; } - return std::wstring(L"XCEngine Editor"); + return ApplySynchronizationPlan(plan); } void EditorWindowWorkspaceCoordinator::RefreshWindowTitle(EditorWindow& window) const { - if (window.IsPrimary()) { - return; - } - if (window.TryGetWorkspaceController() == nullptr) { return; } - const std::wstring title = BuildWindowTitle(window.GetWorkspaceController()); + const std::wstring_view primaryWindowTitle = + m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr + ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) + : std::wstring_view{}; + const std::wstring title = ResolveEditorWindowPresentationTitle( + primaryWindowTitle, + m_windowSystem.GetPanelRegistry(), + BuildWindowStateForWindow(window), + window.IsPrimary()); if (title == window.GetTitle()) { return; } @@ -225,17 +228,79 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( const POINT& preferredScreenPoint, LONG preferredWidth, LONG preferredHeight) { - const auto restoreWindowSnapshot = [](const ExistingWindowSnapshot& snapshot) { - if (snapshot.window == nullptr) { + EditorWindowSynchronizationPlacement preferredPlacement = {}; + if (!preferredNewWindowId.empty()) { + const RECT detachedRect = BuildEditorFloatingWindowRect( + preferredScreenPoint, + preferredWidth, + preferredHeight); + preferredPlacement.enabled = true; + preferredPlacement.initialX = detachedRect.left; + preferredPlacement.initialY = detachedRect.top; + preferredPlacement.initialWidth = detachedRect.right - detachedRect.left; + preferredPlacement.initialHeight = detachedRect.bottom - detachedRect.top; + } + + std::string error = {}; + EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForWindowSet( + windowSet, + CaptureHostSnapshots(), + m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr + ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) + : std::wstring_view{}, + preferredNewWindowId, + preferredPlacement, + error); + if (!plan.valid) { + LogRuntimeTrace("window", "workspace synchronization rejected: " + error); + return false; + } + + return ApplySynchronizationPlan(plan); +} + +std::vector EditorWindowWorkspaceCoordinator::CaptureHostSnapshots() const { + std::vector snapshots = {}; + snapshots.reserve(m_hostRuntime.GetWindows().size()); + + for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { + if (window == nullptr) { + continue; + } + + EditorWindowHostSnapshot snapshot = {}; + snapshot.windowId = std::string(window->GetWindowId()); + snapshot.workspaceWindow = window->IsWorkspaceWindow(); + snapshot.utilityWindow = window->IsUtilityWindow(); + snapshot.primary = window->IsPrimary(); + snapshot.running = window->GetLifecycleState() == EditorWindowLifecycleState::Running; + snapshot.destroyed = window->IsDestroyed(); + snapshot.hasNativeWindow = window->GetHwnd() != nullptr; + snapshot.title = window->GetTitle(); + if (window->TryGetWorkspaceController() != nullptr) { + snapshot.hasWorkspaceProjection = true; + snapshot.workspaceState = BuildWindowStateForWindow(*window); + } + snapshots.push_back(std::move(snapshot)); + } + + return snapshots; +} + +bool EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan( + const EditorWindowSynchronizationPlan& plan) { + const auto restoreWindowSnapshot = [this](const ExistingWindowSnapshot& snapshot) { + EditorWindow* const window = m_hostRuntime.FindWindow(snapshot.windowId); + if (window == nullptr) { return; } - snapshot.window->ReplaceWorkspaceController(snapshot.workspaceController); - snapshot.window->SetPrimary(snapshot.primary); - snapshot.window->SetTitle(snapshot.title); - snapshot.window->ResetInteractionState(); - if (snapshot.window->GetHwnd() != nullptr) { - SetWindowTextW(snapshot.window->GetHwnd(), snapshot.window->GetTitle().c_str()); + window->ReplaceWorkspaceController(snapshot.workspaceController); + window->SetPrimary(snapshot.primary); + window->SetTitle(snapshot.title); + window->ResetInteractionState(); + if (window->GetHwnd() != nullptr) { + SetWindowTextW(window->GetHwnd(), window->GetTitle().c_str()); } }; @@ -249,127 +314,81 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( return true; }; - std::vector windowIdsInSet = {}; - windowIdsInSet.reserve(windowSet.windows.size()); std::vector existingWindowSnapshots = {}; - existingWindowSnapshots.reserve(windowSet.windows.size()); std::vector createdWindowIds = {}; - for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { - windowIdsInSet.push_back(entry.windowId); - const bool isPrimaryWindow = entry.windowId == windowSet.primaryWindowId; - 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->IsWorkspaceWindow()) { - LogRuntimeTrace( - "window", - "workspace synchronization rejected: window '" + entry.windowId + - "' is not a workspace window"); - return false; + for (const EditorWindowSynchronizationAction& action : plan.actions) { + switch (action.kind) { + case EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow: { + EditorWindow* existingWindow = + m_hostRuntime.FindWindow(action.update.windowState.windowId); + if (existingWindow == nullptr && m_lifecycleCoordinator != nullptr) { + m_lifecycleCoordinator->ReapDestroyedWindows(); + existingWindow = m_hostRuntime.FindWindow(action.update.windowState.windowId); } - - if (existingWindow->GetLifecycleState() != EditorWindowLifecycleState::Running) { - LogRuntimeTrace( - "window", - "workspace synchronization rejected: window '" + entry.windowId + - "' is in lifecycle state '" + - std::string( - GetEditorWindowLifecycleStateName( - existingWindow->GetLifecycleState())) + - "'"); + if (existingWindow == nullptr) { return false; } existingWindowSnapshots.push_back(ExistingWindowSnapshot{ - existingWindow, + std::string(existingWindow->GetWindowId()), existingWindow->GetWorkspaceController(), existingWindow->GetTitle(), existingWindow->IsPrimary(), }); - existingWindow->SetPrimary(isPrimaryWindow); - existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindow(entry)); + existingWindow->SetPrimary(action.update.primary); + existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindowState( + m_windowSystem.GetPanelRegistry(), + action.update.windowState)); existingWindow->ResetInteractionState(); - existingWindow->SetTitle( - isPrimaryWindow - ? std::wstring( - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr && - m_hostRuntime.GetHostConfig().primaryWindowTitle[0] != L'\0' - ? m_hostRuntime.GetHostConfig().primaryWindowTitle - : L"XCEngine Editor") - : BuildWindowTitle(existingWindow->GetWorkspaceController())); + existingWindow->SetTitle(action.update.title); if (existingWindow->GetHwnd() != nullptr) { SetWindowTextW(existingWindow->GetHwnd(), existingWindow->GetTitle().c_str()); } - continue; + break; } - - EditorWindowHostRuntime::CreateParams createParams = {}; - createParams.windowId = entry.windowId; - createParams.category = EditorWindowCategory::Workspace; - createParams.primary = isPrimaryWindow; - createParams.title = - createParams.primary - ? std::wstring( - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr && - m_hostRuntime.GetHostConfig().primaryWindowTitle[0] != L'\0' - ? m_hostRuntime.GetHostConfig().primaryWindowTitle - : L"XCEngine Editor") - : BuildWindowTitle(BuildWorkspaceControllerForWindow(entry)); - if (entry.windowId == preferredNewWindowId) { - const RECT detachedRect = BuildEditorFloatingWindowRect( - preferredScreenPoint, - preferredWidth, - preferredHeight); - createParams.initialX = detachedRect.left; - createParams.initialY = detachedRect.top; - createParams.initialWidth = detachedRect.right - detachedRect.left; - createParams.initialHeight = detachedRect.bottom - detachedRect.top; - } - - if (m_hostRuntime.CreateWorkspaceWindow( - BuildWorkspaceControllerForWindow(entry), - createParams) == nullptr) { - for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { - restoreWindowSnapshot(snapshot); + case EditorWindowSynchronizationActionKind::CreateWorkspaceWindow: { + EditorWindowHostRuntime::CreateParams createParams = {}; + createParams.windowId = action.create.windowState.windowId; + createParams.category = EditorWindowCategory::Workspace; + createParams.primary = action.create.primary; + createParams.title = action.create.title; + if (action.create.placement.enabled) { + createParams.initialX = action.create.placement.initialX; + createParams.initialY = action.create.placement.initialY; + createParams.initialWidth = action.create.placement.initialWidth; + createParams.initialHeight = action.create.placement.initialHeight; } - for (auto it = createdWindowIds.rbegin(); it != createdWindowIds.rend(); ++it) { - destroyAndEraseWindowById(*it); + + if (m_hostRuntime.CreateWorkspaceWindow( + BuildWorkspaceControllerForWindowState( + m_windowSystem.GetPanelRegistry(), + action.create.windowState), + createParams) == nullptr) { + for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { + restoreWindowSnapshot(snapshot); + } + for (auto it = createdWindowIds.rbegin(); it != createdWindowIds.rend(); ++it) { + destroyAndEraseWindowById(*it); + } + return false; } - return false; + createdWindowIds.push_back(action.create.windowState.windowId); + break; } - createdWindowIds.push_back(entry.windowId); - } - - for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { - if (window == nullptr || - window->GetHwnd() == nullptr || - !window->IsWorkspaceWindow() || - window->IsPrimary() || - window->GetLifecycleState() != EditorWindowLifecycleState::Running) { - continue; - } - - const bool existsInWindowSet = - std::find( - windowIdsInSet.begin(), - windowIdsInSet.end(), - window->GetWindowId()) != windowIdsInSet.end(); - if (!existsInWindowSet) { + case EditorWindowSynchronizationActionKind::CloseWorkspaceWindow: if (m_lifecycleCoordinator != nullptr) { - m_lifecycleCoordinator->PostCloseRequest(*window); + if (EditorWindow* window = m_hostRuntime.FindWindow(action.close.windowId); + window != nullptr) { + m_lifecycleCoordinator->PostCloseRequest(*window); + } } + break; } } std::string storeError = {}; - if (!m_windowSystem.TrySetWindowSet(windowSet, storeError)) { + if (!m_windowSystem.CommitSynchronizationPlan(plan, storeError)) { LogRuntimeTrace( "window", "workspace synchronization produced invalid stored state: " + storeError); @@ -805,8 +824,13 @@ bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( void EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests( EditorWindow& sourceWindow, const EditorWindowFrameTransferRequests& transferRequests) { - if (sourceWindow.IsWorkspaceWindow()) { - UpdateWindowProjection(sourceWindow); + if (transferRequests.workspace.workspaceMutation.has_value()) { + if (!CommitLiveWindowMutation(sourceWindow)) { + LogRuntimeTrace( + "window", + "failed to commit live workspace mutation for window '" + + std::string(sourceWindow.GetWindowId()) + "'"); + } } if (!m_globalTabDragSession.active && diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h index d35111e3..d5a26098 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h +++ b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h @@ -5,6 +5,7 @@ #endif #include "Windowing/Frame/EditorWindowTransferRequests.h" +#include "Windowing/System/EditorWindowSynchronizationPlan.h" #include #include @@ -14,6 +15,7 @@ #include #include +#include namespace XCEngine::UI::Editor::App { @@ -32,9 +34,9 @@ public: void BindLifecycleCoordinator(EditorWindowLifecycleCoordinator& lifecycleCoordinator); void RegisterExistingWindow(EditorWindow& window); void RefreshWindowPresentation(EditorWindow& window); + void HandleNativeWindowDestroyed(std::string_view windowId); bool IsPrimaryWindowId(std::string_view windowId) const; std::string DescribeWindowSet() const; - void RemoveWindowProjection(std::string_view windowId, bool primary); bool IsGlobalTabDragActive() const; bool OwnsActiveGlobalTabDrag(std::string_view windowId) const; @@ -63,6 +65,8 @@ private: const POINT& preferredScreenPoint, LONG preferredWidth = 0, LONG preferredHeight = 0); + bool ApplySynchronizationPlan(const EditorWindowSynchronizationPlan& plan); + std::vector CaptureHostSnapshots() const; bool CommitWindowWorkspaceMutation( const UIEditorWindowWorkspaceController& windowWorkspaceController, std::string_view preferredNewWindowId, @@ -70,11 +74,7 @@ private: LONG preferredWidth = 0, LONG preferredHeight = 0); UIEditorWindowWorkspaceState BuildWindowStateForWindow(const EditorWindow& window) const; - void UpdateWindowProjection(EditorWindow& window); - UIEditorWorkspaceController BuildWorkspaceControllerForWindow( - const UIEditorWindowWorkspaceState& windowState) const; - std::wstring BuildWindowTitle( - const UIEditorWorkspaceController& workspaceController) const; + bool CommitLiveWindowMutation(EditorWindow& window); void RefreshWindowTitle(EditorWindow& window) const; void BeginGlobalTabDragSession( std::string_view panelWindowId, diff --git a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp index 2b8af5dd..cc2cbbca 100644 --- a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp +++ b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp @@ -1,6 +1,7 @@ #include "Windowing/Content/EditorWorkspaceWindowContentController.h" #include +#include namespace XCEngine::UI::Editor::App { @@ -108,7 +109,10 @@ void EditorWorkspaceWindowContentController::SetViewportSurfacePresentationEnabl EditorWindowFrameTransferRequests EditorWorkspaceWindowContentController::UpdateAndAppend( const EditorWindowContentFrameContext& context, ::XCEngine::UI::UIDrawData& drawData) { - return m_frameOrchestrator.UpdateAndAppend( + const auto beforeSnapshot = BuildUIEditorWorkspaceLayoutSnapshot( + m_workspaceController.GetWorkspace(), + m_workspaceController.GetSession()); + EditorWindowFrameTransferRequests transferRequests = m_frameOrchestrator.UpdateAndAppend( context.editorContext, m_workspaceController, m_shellRuntime, @@ -120,6 +124,19 @@ EditorWindowFrameTransferRequests EditorWorkspaceWindowContentController::Update context.globalTabDragActive, context.useDetachedTitleBarTabStrip, drawData); + const auto afterSnapshot = BuildUIEditorWorkspaceLayoutSnapshot( + m_workspaceController.GetWorkspace(), + m_workspaceController.GetSession()); + if (!AreUIEditorWorkspaceLayoutSnapshotsEquivalent(beforeSnapshot, afterSnapshot)) { + transferRequests.workspace.workspaceMutation = EditorWindowWorkspaceMutationRequest{ + .windowState = + UIEditorWindowWorkspaceState{ + .workspace = m_workspaceController.GetWorkspace(), + .session = m_workspaceController.GetSession(), + }, + }; + } + return transferRequests; } void EditorWorkspaceWindowContentController::RenderRequestedViewports( diff --git a/editor/app/Windowing/Frame/EditorWindowTransferRequests.h b/editor/app/Windowing/Frame/EditorWindowTransferRequests.h index 8afa0cce..2c807743 100644 --- a/editor/app/Windowing/Frame/EditorWindowTransferRequests.h +++ b/editor/app/Windowing/Frame/EditorWindowTransferRequests.h @@ -7,6 +7,8 @@ #include "Windowing/EditorWindowShared.h" #include "UtilityWindows/EditorUtilityWindowKind.h" +#include + #include #include @@ -22,6 +24,14 @@ struct EditorWindowPanelTransferRequest { } }; +struct EditorWindowWorkspaceMutationRequest { + XCEngine::UI::Editor::UIEditorWindowWorkspaceState windowState = {}; + + bool IsValid() const { + return !windowState.windowId.empty(); + } +}; + struct EditorWindowOpenUtilityWindowRequest { EditorUtilityWindowKind kind = EditorUtilityWindowKind::None; EditorWindowScreenPoint screenPoint = {}; @@ -33,11 +43,13 @@ struct EditorWindowOpenUtilityWindowRequest { }; struct EditorWorkspaceWindowFrameTransferRequests { + std::optional workspaceMutation = {}; std::optional beginGlobalTabDrag = {}; std::optional detachPanel = {}; bool HasPendingRequests() const { - return beginGlobalTabDrag.has_value() || + return workspaceMutation.has_value() || + beginGlobalTabDrag.has_value() || detachPanel.has_value(); } }; diff --git a/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp b/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp new file mode 100644 index 00000000..607ff6c8 --- /dev/null +++ b/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp @@ -0,0 +1,47 @@ +#include "Windowing/System/EditorWindowPresentationPolicy.h" + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +std::wstring ResolvePrimaryTitle(std::wstring_view primaryWindowTitle) { + return primaryWindowTitle.empty() + ? std::wstring(L"XCEngine Editor") + : std::wstring(primaryWindowTitle); +} + +} // namespace + +UIEditorWorkspaceController BuildWorkspaceControllerForWindowState( + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState) { + return UIEditorWorkspaceController( + panelRegistry, + windowState.workspace, + windowState.session); +} + +std::wstring ResolveEditorWindowPresentationTitle( + std::wstring_view primaryWindowTitle, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState, + bool primary) { + if (primary) { + return ResolvePrimaryTitle(primaryWindowTitle); + } + + const UIEditorWorkspaceController workspaceController = + BuildWorkspaceControllerForWindowState(panelRegistry, windowState); + const std::string detachedTitle = + ResolveUIEditorDetachedWorkspaceTitle(workspaceController); + if (detachedTitle.empty()) { + return std::wstring(L"XCEngine Editor"); + } + + const std::string decoratedTitle = detachedTitle + " - XCEngine Editor"; + return std::wstring(decoratedTitle.begin(), decoratedTitle.end()); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowPresentationPolicy.h b/editor/app/Windowing/System/EditorWindowPresentationPolicy.h new file mode 100644 index 00000000..a63cbb38 --- /dev/null +++ b/editor/app/Windowing/System/EditorWindowPresentationPolicy.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +UIEditorWorkspaceController BuildWorkspaceControllerForWindowState( + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState); + +std::wstring ResolveEditorWindowPresentationTitle( + std::wstring_view primaryWindowTitle, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState, + bool primary); + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowSynchronizationPlan.h b/editor/app/Windowing/System/EditorWindowSynchronizationPlan.h new file mode 100644 index 00000000..eedc4a6c --- /dev/null +++ b/editor/app/Windowing/System/EditorWindowSynchronizationPlan.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct EditorWindowHostSnapshot { + std::string windowId = {}; + bool workspaceWindow = false; + bool utilityWindow = false; + bool primary = false; + bool running = false; + bool destroyed = false; + bool hasNativeWindow = false; + bool hasWorkspaceProjection = false; + UIEditorWindowWorkspaceState workspaceState = {}; + std::wstring title = {}; +}; + +struct EditorWindowSynchronizationPlacement { + bool enabled = false; + int initialX = 0; + int initialY = 0; + int initialWidth = 0; + int initialHeight = 0; +}; + +struct EditorWindowSynchronizationUpdate { + UIEditorWindowWorkspaceState windowState = {}; + bool primary = false; + std::wstring title = {}; +}; + +struct EditorWindowSynchronizationCreate { + UIEditorWindowWorkspaceState windowState = {}; + bool primary = false; + std::wstring title = {}; + EditorWindowSynchronizationPlacement placement = {}; +}; + +struct EditorWindowSynchronizationClose { + std::string windowId = {}; +}; + +enum class EditorWindowSynchronizationActionKind : std::uint8_t { + CreateWorkspaceWindow = 0, + UpdateWorkspaceWindow, + CloseWorkspaceWindow, +}; + +struct EditorWindowSynchronizationAction { + EditorWindowSynchronizationActionKind kind = + EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow; + EditorWindowSynchronizationCreate create = {}; + EditorWindowSynchronizationUpdate update = {}; + EditorWindowSynchronizationClose close = {}; +}; + +struct EditorWindowSynchronizationPlan { + bool valid = false; + std::string errorMessage = {}; + UIEditorWindowWorkspaceSet targetWindowSet = {}; + std::vector actions = {}; + + [[nodiscard]] bool HasActions() const { + return !actions.empty(); + } +}; + +struct EditorWindowSynchronizationPlannerInput { + const UIEditorPanelRegistry* panelRegistry = nullptr; + const UIEditorWindowWorkspaceSet* targetWindowSet = nullptr; + std::wstring_view primaryWindowTitle = {}; + std::string_view preferredNewWindowId = {}; + EditorWindowSynchronizationPlacement preferredPlacement = {}; + std::vector hostWindows = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp b/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp new file mode 100644 index 00000000..1ce7f52e --- /dev/null +++ b/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp @@ -0,0 +1,143 @@ +#include "Windowing/System/EditorWindowSynchronizationPlanner.h" + +#include "Windowing/System/EditorWindowPresentationPolicy.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +const EditorWindowHostSnapshot* FindHostWindowSnapshot( + const std::vector& hostWindows, + std::string_view windowId) { + const auto it = std::find_if( + hostWindows.begin(), + hostWindows.end(), + [windowId](const EditorWindowHostSnapshot& snapshot) { + return snapshot.windowId == windowId; + }); + return it != hostWindows.end() ? &(*it) : nullptr; +} + +bool AreWindowWorkspaceStatesEquivalent( + const UIEditorWindowWorkspaceState& lhs, + const UIEditorWindowWorkspaceState& rhs) { + if (lhs.windowId != rhs.windowId) { + return false; + } + + return AreUIEditorWorkspaceLayoutSnapshotsEquivalent( + BuildUIEditorWorkspaceLayoutSnapshot(lhs.workspace, lhs.session), + BuildUIEditorWorkspaceLayoutSnapshot(rhs.workspace, rhs.session)); +} + +} // namespace + +EditorWindowSynchronizationPlan EditorWindowSynchronizationPlanner::Build( + const EditorWindowSynchronizationPlannerInput& input) { + EditorWindowSynchronizationPlan plan = {}; + + if (input.panelRegistry == nullptr) { + plan.errorMessage = "window synchronization planner missing panel registry"; + return plan; + } + if (input.targetWindowSet == nullptr) { + plan.errorMessage = "window synchronization planner missing target window set"; + return plan; + } + + plan.valid = true; + plan.targetWindowSet = *input.targetWindowSet; + plan.actions.reserve( + input.targetWindowSet->windows.size() + input.hostWindows.size()); + + for (const UIEditorWindowWorkspaceState& targetWindowState : + input.targetWindowSet->windows) { + const bool primary = targetWindowState.windowId == + input.targetWindowSet->primaryWindowId; + const std::wstring title = ResolveEditorWindowPresentationTitle( + input.primaryWindowTitle, + *input.panelRegistry, + targetWindowState, + primary); + + const EditorWindowHostSnapshot* const existingSnapshot = + FindHostWindowSnapshot(input.hostWindows, targetWindowState.windowId); + if (existingSnapshot == nullptr) { + EditorWindowSynchronizationAction action = {}; + action.kind = EditorWindowSynchronizationActionKind::CreateWorkspaceWindow; + action.create.windowState = targetWindowState; + action.create.primary = primary; + action.create.title = title; + if (!input.preferredNewWindowId.empty() && + targetWindowState.windowId == input.preferredNewWindowId) { + action.create.placement = input.preferredPlacement; + } + plan.actions.push_back(std::move(action)); + continue; + } + + if (!existingSnapshot->workspaceWindow) { + plan.valid = false; + plan.errorMessage = + "workspace synchronization rejected: window '" + + targetWindowState.windowId + "' is not a workspace window"; + plan.actions.clear(); + return plan; + } + + if (!existingSnapshot->running) { + plan.valid = false; + plan.errorMessage = + "workspace synchronization rejected: window '" + + targetWindowState.windowId + "' is not running"; + plan.actions.clear(); + return plan; + } + + const bool needsUpdate = + !existingSnapshot->hasWorkspaceProjection || + !AreWindowWorkspaceStatesEquivalent( + existingSnapshot->workspaceState, + targetWindowState) || + existingSnapshot->primary != primary || + existingSnapshot->title != title; + if (!needsUpdate) { + continue; + } + + EditorWindowSynchronizationAction action = {}; + action.kind = EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow; + action.update.windowState = targetWindowState; + action.update.primary = primary; + action.update.title = title; + plan.actions.push_back(std::move(action)); + } + + for (const EditorWindowHostSnapshot& snapshot : input.hostWindows) { + if (!snapshot.workspaceWindow || + !snapshot.hasNativeWindow || + !snapshot.running || + snapshot.primary) { + continue; + } + + const bool existsInTarget = + FindUIEditorWindowWorkspaceState(*input.targetWindowSet, snapshot.windowId) != nullptr; + if (existsInTarget) { + continue; + } + + EditorWindowSynchronizationAction action = {}; + action.kind = EditorWindowSynchronizationActionKind::CloseWorkspaceWindow; + action.close.windowId = snapshot.windowId; + plan.actions.push_back(std::move(action)); + } + + return plan; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.h b/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.h new file mode 100644 index 00000000..9489d308 --- /dev/null +++ b/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Windowing/System/EditorWindowSynchronizationPlan.h" + +namespace XCEngine::UI::Editor::App { + +class EditorWindowSynchronizationPlanner final { +public: + static EditorWindowSynchronizationPlan Build( + const EditorWindowSynchronizationPlannerInput& input); +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowSystem.cpp b/editor/app/Windowing/System/EditorWindowSystem.cpp index d41b52e9..11b9d7a2 100644 --- a/editor/app/Windowing/System/EditorWindowSystem.cpp +++ b/editor/app/Windowing/System/EditorWindowSystem.cpp @@ -1,5 +1,10 @@ #include "Windowing/System/EditorWindowSystem.h" +#include "Windowing/System/EditorWindowSynchronizationPlanner.h" + +#include + +#include #include namespace XCEngine::UI::Editor::App { @@ -37,23 +42,6 @@ bool EditorWindowSystem::ValidateWindowSet( return m_workspaceStore.ValidateWindowSet(windowSet, outError); } -bool EditorWindowSystem::TrySetWindowSet( - UIEditorWindowWorkspaceSet windowSet, - std::string& outError) { - return m_workspaceStore.TrySetWindowSet(std::move(windowSet), outError); -} - -bool EditorWindowSystem::UpsertWindowState( - const UIEditorWindowWorkspaceState& windowState, - bool primary, - std::string& outError) { - return m_workspaceStore.UpsertWindowState(windowState, primary, outError); -} - -void EditorWindowSystem::RemoveWindowState(std::string_view windowId, bool primary) { - m_workspaceStore.RemoveWindowState(windowId, primary); -} - bool EditorWindowSystem::IsPrimaryWindowId(std::string_view windowId) const { return m_workspaceStore.IsPrimaryWindowId(windowId); } @@ -66,6 +54,146 @@ UIEditorWindowWorkspaceController EditorWindowSystem::BuildWorkspaceMutationCont return UIEditorWindowWorkspaceController(GetPanelRegistry(), GetWindowSet()); } +EditorWindowSynchronizationPlan EditorWindowSystem::BuildPlanForWindowSet( + const UIEditorWindowWorkspaceSet& targetWindowSet, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string_view preferredNewWindowId, + const EditorWindowSynchronizationPlacement& preferredPlacement, + std::string& outError) const { + return BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &targetWindowSet, + .primaryWindowTitle = primaryWindowTitle, + .preferredNewWindowId = preferredNewWindowId, + .preferredPlacement = preferredPlacement, + .hostWindows = hostWindows, + }, + outError); +} + +EditorWindowSynchronizationPlan EditorWindowSystem::BuildPlanForLiveWindowMutation( + std::string_view windowId, + const UIEditorWorkspaceController& workspaceController, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string& outError) const { + if (windowId.empty()) { + outError = "live window mutation missing window id"; + return {}; + } + + UIEditorWindowWorkspaceSet nextWindowSet = GetWindowSet(); + UIEditorWindowWorkspaceState* existingState = + FindMutableUIEditorWindowWorkspaceState(nextWindowSet, windowId); + if (existingState == nullptr) { + outError = "live window mutation references unknown window '" + std::string(windowId) + "'"; + return {}; + } + + existingState->workspace = workspaceController.GetWorkspace(); + existingState->session = workspaceController.GetSession(); + return BuildPlanForWindowSet( + nextWindowSet, + hostWindows, + primaryWindowTitle, + {}, + {}, + outError); +} + +EditorWindowSynchronizationPlan EditorWindowSystem::BuildPlanForDestroyedWindow( + std::string_view windowId, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string& outError) const { + if (windowId.empty()) { + outError = "window destroy event missing window id"; + return {}; + } + + UIEditorWindowWorkspaceSet nextWindowSet = GetWindowSet(); + const bool destroyedPrimary = nextWindowSet.primaryWindowId == windowId; + if (!RemoveWindowStateFromSet(nextWindowSet, windowId)) { + outError = + "window destroy event references unknown authoritative window '" + + std::string(windowId) + "'"; + return {}; + } + if (destroyedPrimary) { + EditorWindowSynchronizationPlan plan = {}; + plan.valid = true; + plan.targetWindowSet = {}; + for (const EditorWindowHostSnapshot& snapshot : hostWindows) { + if (!snapshot.workspaceWindow || + !snapshot.hasNativeWindow || + !snapshot.running || + snapshot.windowId == windowId) { + continue; + } + + EditorWindowSynchronizationAction action = {}; + action.kind = EditorWindowSynchronizationActionKind::CloseWorkspaceWindow; + action.close.windowId = snapshot.windowId; + plan.actions.push_back(std::move(action)); + } + outError.clear(); + return plan; + } + + return BuildPlanForWindowSet( + nextWindowSet, + hostWindows, + primaryWindowTitle, + {}, + {}, + outError); +} + +EditorWindowSynchronizationPlan EditorWindowSystem::BuildSynchronizationPlan( + const EditorWindowSynchronizationPlannerInput& input, + std::string& outError) const { + if (input.targetWindowSet == nullptr) { + outError = "window synchronization planner missing target window set"; + return {}; + } + + if (!ValidateWindowSet(*input.targetWindowSet, outError)) { + return {}; + } + + EditorWindowSynchronizationPlannerInput resolvedInput = input; + resolvedInput.panelRegistry = &GetPanelRegistry(); + EditorWindowSynchronizationPlan plan = + EditorWindowSynchronizationPlanner::Build(resolvedInput); + if (!plan.valid) { + outError = plan.errorMessage; + return {}; + } + + outError.clear(); + return plan; +} + +bool EditorWindowSystem::CommitSynchronizationPlan( + const EditorWindowSynchronizationPlan& plan, + std::string& outError) { + if (!plan.valid) { + outError = !plan.errorMessage.empty() + ? plan.errorMessage + : "cannot commit invalid synchronization plan"; + return false; + } + + if (plan.targetWindowSet.windows.empty()) { + m_workspaceStore.ClearWindowSet(); + outError.clear(); + return true; + } + + return m_workspaceStore.TrySetWindowSet(plan.targetWindowSet, outError); +} + UIEditorWindowWorkspaceOperationResult EditorWindowSystem::EvaluateDetachPanelToNewWindow( std::string_view sourceWindowId, std::string_view sourceNodeId, @@ -75,4 +203,37 @@ UIEditorWindowWorkspaceOperationResult EditorWindowSystem::EvaluateDetachPanelTo return outController.DetachPanelToNewWindow(sourceWindowId, sourceNodeId, panelId); } +bool EditorWindowSystem::RemoveWindowStateFromSet( + UIEditorWindowWorkspaceSet& windowSet, + std::string_view windowId) { + const auto originalSize = windowSet.windows.size(); + windowSet.windows.erase( + std::remove_if( + windowSet.windows.begin(), + windowSet.windows.end(), + [windowId](const UIEditorWindowWorkspaceState& state) { + return state.windowId == windowId; + }), + windowSet.windows.end()); + if (windowSet.windows.size() == originalSize) { + return false; + } + + if (windowSet.windows.empty()) { + windowSet = {}; + return true; + } + + if (windowSet.primaryWindowId == windowId || + FindUIEditorWindowWorkspaceState(windowSet, windowSet.primaryWindowId) == nullptr) { + windowSet.primaryWindowId = windowSet.windows.front().windowId; + } + if (windowSet.activeWindowId == windowId || + FindUIEditorWindowWorkspaceState(windowSet, windowSet.activeWindowId) == nullptr) { + windowSet.activeWindowId = windowSet.primaryWindowId; + } + + return true; +} + } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowSystem.h b/editor/app/Windowing/System/EditorWindowSystem.h index 9cd944c7..4596daf3 100644 --- a/editor/app/Windowing/System/EditorWindowSystem.h +++ b/editor/app/Windowing/System/EditorWindowSystem.h @@ -1,6 +1,7 @@ #pragma once #include "Composition/EditorWindowWorkspaceStore.h" +#include "Windowing/System/EditorWindowSynchronizationPlan.h" #include @@ -23,18 +24,34 @@ public: bool ValidateWindowSet( const UIEditorWindowWorkspaceSet& windowSet, std::string& outError) const; - bool TrySetWindowSet( - UIEditorWindowWorkspaceSet windowSet, - std::string& outError); - bool UpsertWindowState( - const UIEditorWindowWorkspaceState& windowState, - bool primary, - std::string& outError); - void RemoveWindowState(std::string_view windowId, bool primary); bool IsPrimaryWindowId(std::string_view windowId) const; const UIEditorWindowWorkspaceSet& GetWindowSet() const; UIEditorWindowWorkspaceController BuildWorkspaceMutationController() const; + EditorWindowSynchronizationPlan BuildPlanForWindowSet( + const UIEditorWindowWorkspaceSet& targetWindowSet, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string_view preferredNewWindowId, + const EditorWindowSynchronizationPlacement& preferredPlacement, + std::string& outError) const; + EditorWindowSynchronizationPlan BuildPlanForLiveWindowMutation( + std::string_view windowId, + const UIEditorWorkspaceController& workspaceController, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string& outError) const; + EditorWindowSynchronizationPlan BuildPlanForDestroyedWindow( + std::string_view windowId, + const std::vector& hostWindows, + std::wstring_view primaryWindowTitle, + std::string& outError) const; + EditorWindowSynchronizationPlan BuildSynchronizationPlan( + const EditorWindowSynchronizationPlannerInput& input, + std::string& outError) const; + bool CommitSynchronizationPlan( + const EditorWindowSynchronizationPlan& plan, + std::string& outError); UIEditorWindowWorkspaceOperationResult EvaluateDetachPanelToNewWindow( std::string_view sourceWindowId, @@ -43,6 +60,10 @@ public: UIEditorWindowWorkspaceController& outController) const; private: + static bool RemoveWindowStateFromSet( + UIEditorWindowWorkspaceSet& windowSet, + std::string_view windowId); + EditorWindowWorkspaceStore m_workspaceStore; }; diff --git a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h index 9b1fc4ed..23c9282b 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h @@ -18,6 +18,12 @@ struct DirectionalShadowSurfaceAllocation; namespace Pipelines { +enum class ManagedPipelineRendererAssetPolicy { + Unspecified = 0, + ExplicitAsset = 1, + DefaultNativeBackend = 2, +}; + struct ManagedRenderPipelineAssetDescriptor { std::string assemblyName; std::string namespaceName; @@ -101,9 +107,19 @@ public: } virtual std::shared_ptr - GetSharedPipelineBackendAsset() const { + GetPipelineRendererAsset() const { return nullptr; } + virtual std::shared_ptr + GetSharedPipelineBackendAsset() const { + return GetPipelineRendererAsset(); + } + virtual ManagedPipelineRendererAssetPolicy + GetPipelineRendererAssetPolicy() const { + return GetPipelineRendererAsset() != nullptr + ? ManagedPipelineRendererAssetPolicy::ExplicitAsset + : ManagedPipelineRendererAssetPolicy::Unspecified; + } virtual bool TryGetDefaultFinalColorSettings(FinalColorSettings&) const { return false; } diff --git a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp index a1f24ef6..05095323 100644 --- a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp +++ b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp @@ -1,6 +1,7 @@ #include "Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h" #include "Rendering/GraphicsSettingsState.h" +#include "Rendering/Internal/RenderPipelineFactory.h" #include @@ -42,7 +43,21 @@ ManagedScriptableRenderPipelineAsset::ResolveSharedPipelineBackendAsset() const if (const std::shared_ptr runtime = ResolveManagedAssetRuntime(); runtime != nullptr) { - return runtime->GetSharedPipelineBackendAsset(); + if (const std::shared_ptr rendererAsset = + runtime->GetPipelineRendererAsset(); + rendererAsset != nullptr) { + return rendererAsset; + } + + switch (runtime->GetPipelineRendererAssetPolicy()) { + case ManagedPipelineRendererAssetPolicy::ExplicitAsset: + return runtime->GetSharedPipelineBackendAsset(); + case ManagedPipelineRendererAssetPolicy::DefaultNativeBackend: + return Internal::CreateDefaultPipelineBackendAsset(); + case ManagedPipelineRendererAssetPolicy::Unspecified: + default: + return nullptr; + } } return nullptr; diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 795c94ef..97b3aa52 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -1641,7 +1641,9 @@ public: bool TryGetDefaultFinalColorSettings( Rendering::FinalColorSettings& settings) const override; std::shared_ptr - GetSharedPipelineBackendAsset() const override; + GetPipelineRendererAsset() const override; + Rendering::Pipelines::ManagedPipelineRendererAssetPolicy + GetPipelineRendererAssetPolicy() const override; MonoScriptRuntime* GetRuntime() const { return m_runtime; @@ -2044,7 +2046,7 @@ private: const std::shared_ptr sharedPipelineBackendAsset = m_assetRuntime != nullptr - ? m_assetRuntime->GetSharedPipelineBackendAsset() + ? m_assetRuntime->GetPipelineRendererAsset() : nullptr; if (sharedPipelineBackendAsset == nullptr) { return nullptr; @@ -2318,7 +2320,7 @@ bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings( } std::shared_ptr -MonoManagedRenderPipelineAssetRuntime::GetSharedPipelineBackendAsset() const { +MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const { if (!SyncManagedAssetRuntimeState()) { return nullptr; } @@ -2332,11 +2334,16 @@ MonoManagedRenderPipelineAssetRuntime::GetSharedPipelineBackendAsset() const { } m_sharedPipelineBackendAssetResolved = true; - m_sharedPipelineBackendAsset = - Rendering::Internal::CreateDefaultPipelineBackendAsset(); return m_sharedPipelineBackendAsset; } +Rendering::Pipelines::ManagedPipelineRendererAssetPolicy +MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAssetPolicy() const { + return EnsureManagedAsset() + ? Rendering::Pipelines::ManagedPipelineRendererAssetPolicy::DefaultNativeBackend + : Rendering::Pipelines::ManagedPipelineRendererAssetPolicy::Unspecified; +} + bool MonoManagedRenderPipelineAssetRuntime::AcquireManagedPipelineHandle( uint32_t& outPipelineHandle) const { if (!SyncManagedAssetRuntimeState()) { diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs index 34fced5e..70319b6e 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs @@ -40,7 +40,7 @@ namespace XCEngine.Rendering.Universal return m_runtimeStateVersion; } - internal void CreateInstance() + protected internal void CreateInstance() { if (m_runtimeCreated) { @@ -243,7 +243,7 @@ namespace XCEngine.Rendering.Universal } } - internal static class RuntimeStateHashUtility + public static class RuntimeStateHashUtility { public static int Combine( int hash, diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalPostProcessBlock.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalPostProcessBlock.cs index 7c263c37..3c70af27 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalPostProcessBlock.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalPostProcessBlock.cs @@ -5,6 +5,18 @@ namespace XCEngine.Rendering.Universal { internal sealed class UniversalPostProcessBlock { + public void EnqueueRenderPasses( + ScriptableRenderer renderer, + RenderingData renderingData) + { + if (renderer == null || + renderingData == null || + !renderingData.isPostProcessStage) + { + return; + } + } + public void ConfigureCameraFramePlan( ScriptableRenderPipelinePlanningContext context, CameraFrameColorSource sourceColor) diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderer.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderer.cs index 4004fd20..8a63445f 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderer.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderer.cs @@ -108,6 +108,11 @@ namespace XCEngine.Rendering.Universal this, m_rendererData.GetMainSceneInstance()); break; + case CameraFrameStage.PostProcess: + m_postProcessBlock.EnqueueRenderPasses( + this, + renderingData); + break; case CameraFrameStage.FinalOutput: m_finalOutputBlock.EnqueueRenderPasses( this, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ab2b9299..c6bb2a4a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,16 +61,23 @@ add_subdirectory(Rendering) add_subdirectory(RHI) add_subdirectory(Resources) add_subdirectory(Input) -add_subdirectory(Editor) - if(WIN32) find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED) + set(XCENGINE_RENDERING_PHASE_REGRESSION_DEPENDENCIES + rendering_all_tests + XCEditor + ) + + if(TARGET editor_tests) + list(APPEND XCENGINE_RENDERING_PHASE_REGRESSION_DEPENDENCIES + editor_tests + ) + endif() + add_custom_target(rendering_phase_regression_build DEPENDS - rendering_all_tests - editor_tests - XCEditor + ${XCENGINE_RENDERING_PHASE_REGRESSION_DEPENDENCIES} ) add_custom_target(rendering_phase_regression diff --git a/tests/UI/Editor/CMakeLists.txt b/tests/UI/Editor/CMakeLists.txt index cc1a7976..5c9064e4 100644 --- a/tests/UI/Editor/CMakeLists.txt +++ b/tests/UI/Editor/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) project(XCEngine_EditorUITests) set(XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT - "${CMAKE_SOURCE_DIR}/new_editor") + "${CMAKE_SOURCE_DIR}/editor") add_subdirectory(unit) add_subdirectory(smoke) @@ -11,6 +11,7 @@ add_subdirectory(manual_validation) set(EDITOR_UI_UNIT_TEST_TARGETS editor_ui_tests + editor_windowing_phase1_tests ) if(TARGET editor_app_feature_tests) list(APPEND EDITOR_UI_UNIT_TEST_TARGETS diff --git a/tests/UI/Editor/manual_validation/CMakeLists.txt b/tests/UI/Editor/manual_validation/CMakeLists.txt index 3e93f189..cae4b30c 100644 --- a/tests/UI/Editor/manual_validation/CMakeLists.txt +++ b/tests/UI/Editor/manual_validation/CMakeLists.txt @@ -1,11 +1,11 @@ file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH) -if(NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Rendering/Native/AutoScreenshot.h" - OR NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Platform/Win32/InputModifierTracker.h" - OR NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Rendering/Native/NativeRenderer.h") +if(NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app" + OR NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include" + OR NOT EXISTS "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Platform/Win32/Runtime/InputModifierTracker.h") message(FATAL_ERROR - "Editor UI manual validation expects rendering/platform headers under " - "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app.") + "Editor UI manual validation expects the current editor tree under " + "${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}.") endif() function(xcengine_configure_editor_ui_integration_validation_target target) diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 0ea3c31e..f8019c34 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -98,6 +98,50 @@ gtest_discover_tests(editor_ui_tests DISCOVERY_MODE PRE_TEST ) +add_executable(editor_windowing_phase1_tests + test_editor_window_synchronization_planner.cpp +) + +target_link_libraries(editor_windowing_phase1_tests + PRIVATE + XCUIEditorLib + GTest::gtest_main +) + +target_sources(editor_windowing_phase1_tests PRIVATE + ${CMAKE_SOURCE_DIR}/editor/app/Composition/EditorWindowWorkspaceStore.cpp + ${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp + ${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp + ${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowSystem.cpp +) + +target_include_directories(editor_windowing_phase1_tests + PRIVATE + ${CMAKE_SOURCE_DIR}/editor/app + ${CMAKE_SOURCE_DIR}/editor/include + ${CMAKE_SOURCE_DIR}/editor/src + ${CMAKE_SOURCE_DIR}/engine/include +) + +if(MSVC) + target_compile_options(editor_windowing_phase1_tests PRIVATE /utf-8 /FS) + set_target_properties(editor_windowing_phase1_tests PROPERTIES + MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" + COMPILE_PDB_NAME "editor_windowing_phase1_tests-compile" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb" + COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug" + COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release" + COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel" + COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo" + ) + set_property(TARGET editor_windowing_phase1_tests PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +gtest_discover_tests(editor_windowing_phase1_tests + DISCOVERY_MODE PRE_TEST +) + if(TARGET XCUIEditorAppLib) set(EDITOR_APP_FEATURE_TEST_SOURCES test_editor_project_runtime.cpp diff --git a/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp b/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp new file mode 100644 index 00000000..d182f87d --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp @@ -0,0 +1,391 @@ +#include + +#include "Windowing/System/EditorWindowSystem.h" + +#include +#include +#include +#include + +namespace { + +using XCEngine::UI::Editor::App::EditorWindowHostSnapshot; +using XCEngine::UI::Editor::App::EditorWindowSynchronizationActionKind; +using XCEngine::UI::Editor::App::EditorWindowSynchronizationPlannerInput; +using XCEngine::UI::Editor::App::EditorWindowSystem; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; +using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState; +using XCEngine::UI::Editor::UIEditorWorkspaceCommand; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorWindowWorkspaceController; +using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", {}, true, true, true }, + { "doc-b", "Document B", {}, true, true, true }, + { "inspector", "Inspector", {}, true, true, true }, + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root-split", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.7f, + BuildUIEditorWorkspaceTabStack( + "document-tabs", + { + BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true), + BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true), + }, + 0u), + BuildUIEditorWorkspaceSingleTabStack( + "inspector-panel", + "inspector", + "Inspector", + true)); + workspace.activePanelId = "doc-a"; + return workspace; +} + +EditorWindowHostSnapshot BuildWorkspaceSnapshot( + std::string windowId, + bool primary, + const XCEngine::UI::Editor::UIEditorWorkspaceController& workspaceController, + std::wstring title) { + EditorWindowHostSnapshot snapshot = {}; + snapshot.windowId = std::move(windowId); + snapshot.workspaceWindow = true; + snapshot.primary = primary; + snapshot.running = true; + snapshot.hasNativeWindow = true; + snapshot.hasWorkspaceProjection = true; + snapshot.workspaceState.windowId = snapshot.windowId; + snapshot.workspaceState.workspace = workspaceController.GetWorkspace(); + snapshot.workspaceState.session = workspaceController.GetSession(); + snapshot.title = std::move(title); + return snapshot; +} + +EditorWindowSystem BuildSystem() { + return EditorWindowSystem(BuildPanelRegistry()); +} + +} // namespace + +TEST(EditorWindowSynchronizationPlannerTest, ProducesNoActionsWhenHostAlreadyMatchesAuthoritativeWindowSet) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + auto plan = system.BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &system.GetWindowSet(), + .primaryWindowTitle = L"Main Scene - XCEngine Editor", + .hostWindows = { + BuildWorkspaceSnapshot( + "main", + true, + workspaceController, + L"Main Scene - XCEngine Editor"), + }, + }, + error); + + ASSERT_TRUE(plan.valid) << error; + EXPECT_TRUE(plan.actions.empty()); +} + +TEST(EditorWindowSynchronizationPlannerTest, ProducesCreateActionForDetachedWindowMutation) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + UIEditorWindowWorkspaceController mutationController = + system.BuildWorkspaceMutationController(); + const auto detachResult = mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window"); + ASSERT_EQ(detachResult.status, UIEditorWindowWorkspaceOperationStatus::Changed); + + auto plan = system.BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &mutationController.GetWindowSet(), + .primaryWindowTitle = L"Main Scene - XCEngine Editor", + .preferredNewWindowId = "doc-b-window", + .hostWindows = { + BuildWorkspaceSnapshot( + "main", + true, + workspaceController, + L"Main Scene - XCEngine Editor"), + }, + }, + error); + + ASSERT_TRUE(plan.valid) << error; + ASSERT_EQ(plan.actions.size(), 2u); + EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow); + EXPECT_EQ(plan.actions[1].kind, EditorWindowSynchronizationActionKind::CreateWorkspaceWindow); + EXPECT_EQ(plan.actions[1].create.windowState.windowId, "doc-b-window"); +} + +TEST(EditorWindowSynchronizationPlannerTest, ProducesCloseActionForRemovedDetachedWindow) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + UIEditorWindowWorkspaceController mutationController = + system.BuildWorkspaceMutationController(); + ASSERT_EQ( + mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window").status, + UIEditorWindowWorkspaceOperationStatus::Changed); + const auto* detachedWindow = + FindUIEditorWindowWorkspaceState(mutationController.GetWindowSet(), "doc-b-window"); + ASSERT_NE(detachedWindow, nullptr); + + auto plan = system.BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &system.GetWindowSet(), + .primaryWindowTitle = L"Main Scene - XCEngine Editor", + .hostWindows = { + BuildWorkspaceSnapshot( + "main", + true, + workspaceController, + L"Main Scene - XCEngine Editor"), + EditorWindowHostSnapshot{ + .windowId = "doc-b-window", + .workspaceWindow = true, + .primary = false, + .running = true, + .hasNativeWindow = true, + .hasWorkspaceProjection = true, + .workspaceState = *detachedWindow, + .title = L"Document B - XCEngine Editor", + }, + }, + }, + error); + + ASSERT_TRUE(plan.valid) << error; + ASSERT_EQ(plan.actions.size(), 1u); + EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::CloseWorkspaceWindow); + EXPECT_EQ(plan.actions[0].close.windowId, "doc-b-window"); +} + +TEST(EditorWindowSynchronizationPlannerTest, ProducesUpdateActionWhenPrimaryWindowChanges) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + UIEditorWindowWorkspaceController mutationController = + system.BuildWorkspaceMutationController(); + ASSERT_EQ( + mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window").status, + UIEditorWindowWorkspaceOperationStatus::Changed); + auto nextWindowSet = mutationController.GetWindowSet(); + nextWindowSet.primaryWindowId = "doc-b-window"; + nextWindowSet.activeWindowId = "doc-b-window"; + + const auto* mainWindow = FindUIEditorWindowWorkspaceState(nextWindowSet, "main"); + const auto* detachedWindow = FindUIEditorWindowWorkspaceState(nextWindowSet, "doc-b-window"); + ASSERT_NE(mainWindow, nullptr); + ASSERT_NE(detachedWindow, nullptr); + + auto plan = system.BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &nextWindowSet, + .primaryWindowTitle = L"Main Scene - XCEngine Editor", + .hostWindows = { + EditorWindowHostSnapshot{ + .windowId = "main", + .workspaceWindow = true, + .primary = true, + .running = true, + .hasNativeWindow = true, + .hasWorkspaceProjection = true, + .workspaceState = *mainWindow, + .title = L"Main Scene - XCEngine Editor", + }, + EditorWindowHostSnapshot{ + .windowId = "doc-b-window", + .workspaceWindow = true, + .primary = false, + .running = true, + .hasNativeWindow = true, + .hasWorkspaceProjection = true, + .workspaceState = *detachedWindow, + .title = L"Document B - XCEngine Editor", + }, + }, + }, + error); + + ASSERT_TRUE(plan.valid) << error; + ASSERT_EQ(plan.actions.size(), 2u); + EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow); + EXPECT_EQ(plan.actions[0].update.windowState.windowId, "main"); + EXPECT_FALSE(plan.actions[0].update.primary); + EXPECT_EQ(plan.actions[1].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow); + EXPECT_EQ(plan.actions[1].update.windowState.windowId, "doc-b-window"); + EXPECT_TRUE(plan.actions[1].update.primary); + EXPECT_EQ(plan.actions[1].update.title, L"Main Scene - XCEngine Editor"); +} + +TEST(EditorWindowSynchronizationPlannerTest, RejectsInvalidTargetWindowSet) { + EditorWindowSystem system = BuildSystem(); + XCEngine::UI::Editor::UIEditorWindowWorkspaceSet invalidWindowSet = {}; + invalidWindowSet.primaryWindowId = "main"; + invalidWindowSet.activeWindowId = "main"; + + std::string error = {}; + auto plan = system.BuildSynchronizationPlan( + EditorWindowSynchronizationPlannerInput{ + .targetWindowSet = &invalidWindowSet, + .primaryWindowTitle = L"Main Scene - XCEngine Editor", + }, + error); + + EXPECT_FALSE(plan.valid); + EXPECT_FALSE(error.empty()); +} + +TEST(EditorWindowSynchronizationPlannerTest, LiveWindowMutationBuildsCommitPlanWithoutPlatformWriteback) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + auto liveController = workspaceController; + const auto commandResult = liveController.Dispatch( + UIEditorWorkspaceCommand{ + .kind = UIEditorWorkspaceCommandKind::ActivatePanel, + .panelId = "doc-b", + }); + ASSERT_EQ(commandResult.status, UIEditorWorkspaceCommandStatus::Changed); + + auto plan = system.BuildPlanForLiveWindowMutation( + "main", + liveController, + { + BuildWorkspaceSnapshot( + "main", + true, + liveController, + L"Main Scene - XCEngine Editor"), + }, + L"Main Scene - XCEngine Editor", + error); + + ASSERT_TRUE(plan.valid) << error; + EXPECT_TRUE(plan.actions.empty()); + const auto* mutatedState = FindUIEditorWindowWorkspaceState(plan.targetWindowSet, "main"); + ASSERT_NE(mutatedState, nullptr); + EXPECT_EQ(mutatedState->workspace.activePanelId, "doc-b"); + + ASSERT_TRUE(system.CommitSynchronizationPlan(plan, error)) << error; + EXPECT_EQ(system.GetWindowSet().windows.front().workspace.activePanelId, "doc-b"); +} + +TEST(EditorWindowSynchronizationPlannerTest, DestroyedPrimaryWindowProducesCloseActionsForRemainingDetachedWindows) { + EditorWindowSystem system = BuildSystem(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error; + + UIEditorWindowWorkspaceController mutationController = + system.BuildWorkspaceMutationController(); + ASSERT_EQ( + mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window").status, + UIEditorWindowWorkspaceOperationStatus::Changed); + auto detachedPlan = system.BuildPlanForWindowSet( + mutationController.GetWindowSet(), + { + BuildWorkspaceSnapshot( + "main", + true, + workspaceController, + L"Main Scene - XCEngine Editor"), + }, + L"Main Scene - XCEngine Editor", + "doc-b-window", + {}, + error); + ASSERT_TRUE(detachedPlan.valid) << error; + ASSERT_TRUE(system.CommitSynchronizationPlan(detachedPlan, error)) << error; + + const auto* detachedState = + FindUIEditorWindowWorkspaceState(system.GetWindowSet(), "doc-b-window"); + ASSERT_NE(detachedState, nullptr); + + auto plan = system.BuildPlanForDestroyedWindow( + "main", + { + EditorWindowHostSnapshot{ + .windowId = "main", + .workspaceWindow = true, + .primary = true, + .running = false, + .destroyed = true, + .hasNativeWindow = false, + }, + EditorWindowHostSnapshot{ + .windowId = "doc-b-window", + .workspaceWindow = true, + .primary = false, + .running = true, + .destroyed = false, + .hasNativeWindow = true, + .hasWorkspaceProjection = true, + .workspaceState = *detachedState, + .title = L"Document B - XCEngine Editor", + }, + }, + L"Main Scene - XCEngine Editor", + error); + + ASSERT_TRUE(plan.valid) << error; + ASSERT_EQ(plan.actions.size(), 1u); + EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::CloseWorkspaceWindow); + EXPECT_EQ(plan.actions[0].close.windowId, "doc-b-window"); + EXPECT_TRUE(plan.targetWindowSet.windows.empty()); + ASSERT_TRUE(system.CommitSynchronizationPlan(plan, error)) << error; + EXPECT_TRUE(system.GetWindowSet().windows.empty()); +} diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index ec004a80..0a5e8341 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -14,6 +14,12 @@ function(xcengine_attach_recursive_build_target target dependency_target) COMMENT "Building ${dependency_target} before ${target}") endfunction() +if(NOT EXISTS "${CMAKE_SOURCE_DIR}/editor/src/Core/EditorConsoleSink.cpp" + OR NOT EXISTS "${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp") + message(STATUS "Skipping legacy tests/editor targets because required legacy editor/src/Core sources are absent.") + return() +endif() + set(EDITOR_TEST_SOURCES test_action_routing.cpp test_application_asset_cache_stub.cpp diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 186478a9..92600483 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -2928,7 +2928,7 @@ TEST_F( TEST_F( MonoScriptRuntimeTest, - ManagedRenderPipelineBridgeRuntimeExposesBuiltinForwardRendererAsset) { + ManagedRenderPipelineBridgeRuntimeExposesDefaultNativeBackendPolicy) { const auto bridge = XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); ASSERT_NE(bridge, nullptr); @@ -2945,20 +2945,16 @@ TEST_F( const std::shared_ptr rendererAsset = assetRuntime->GetPipelineRendererAsset(); - ASSERT_NE(rendererAsset, nullptr); - - std::unique_ptr pipeline = - rendererAsset->CreatePipeline(); - ASSERT_NE(pipeline, nullptr); - EXPECT_NE( - dynamic_cast( - pipeline.get()), - nullptr); + EXPECT_EQ(rendererAsset, nullptr); + EXPECT_EQ( + assetRuntime->GetPipelineRendererAssetPolicy(), + XCEngine::Rendering::Pipelines::ManagedPipelineRendererAssetPolicy:: + DefaultNativeBackend); } TEST_F( MonoScriptRuntimeTest, - ScriptCoreUniversalRenderPipelineAssetExposesBuiltinForwardRendererAsset) { + ScriptCoreUniversalRenderPipelineAssetExposesDefaultNativeBackendPolicy) { const auto bridge = XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); ASSERT_NE(bridge, nullptr); @@ -2975,15 +2971,11 @@ TEST_F( const std::shared_ptr rendererAsset = assetRuntime->GetPipelineRendererAsset(); - ASSERT_NE(rendererAsset, nullptr); - - std::unique_ptr pipeline = - rendererAsset->CreatePipeline(); - ASSERT_NE(pipeline, nullptr); - EXPECT_NE( - dynamic_cast( - pipeline.get()), - nullptr); + EXPECT_EQ(rendererAsset, nullptr); + EXPECT_EQ( + assetRuntime->GetPipelineRendererAssetPolicy(), + XCEngine::Rendering::Pipelines::ManagedPipelineRendererAssetPolicy:: + DefaultNativeBackend); } TEST_F( @@ -3024,7 +3016,7 @@ TEST_F( TEST_F( MonoScriptRuntimeTest, - ManagedRenderPipelineBridgeUsesDefaultRendererSelectionForNativeBackendAsset) { + ManagedRenderPipelineBridgeUsesDefaultRendererSelectionForNativeBackendPolicy) { const auto bridge = XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); ASSERT_NE(bridge, nullptr); @@ -3041,13 +3033,19 @@ TEST_F( const std::shared_ptr rendererAsset = assetRuntime->GetPipelineRendererAsset(); - ASSERT_NE(rendererAsset, nullptr); + EXPECT_EQ(rendererAsset, nullptr); + EXPECT_EQ( + assetRuntime->GetPipelineRendererAssetPolicy(), + XCEngine::Rendering::Pipelines::ManagedPipelineRendererAssetPolicy:: + DefaultNativeBackend); + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset + asset(descriptor); std::unique_ptr pipeline = - rendererAsset->CreatePipeline(); + asset.CreatePipeline(); ASSERT_NE(pipeline, nullptr); EXPECT_NE( - dynamic_cast( + dynamic_cast( pipeline.get()), nullptr); } @@ -3071,13 +3069,19 @@ TEST_F( const std::shared_ptr rendererAsset = assetRuntime->GetPipelineRendererAsset(); - ASSERT_NE(rendererAsset, nullptr); + EXPECT_EQ(rendererAsset, nullptr); + EXPECT_EQ( + assetRuntime->GetPipelineRendererAssetPolicy(), + XCEngine::Rendering::Pipelines::ManagedPipelineRendererAssetPolicy:: + DefaultNativeBackend); + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset + asset(descriptor); std::unique_ptr pipeline = - rendererAsset->CreatePipeline(); + asset.CreatePipeline(); ASSERT_NE(pipeline, nullptr); EXPECT_NE( - dynamic_cast( + dynamic_cast( pipeline.get()), nullptr); } @@ -3101,15 +3105,11 @@ TEST_F( const std::shared_ptr rendererAsset = assetRuntime->GetPipelineRendererAsset(); - ASSERT_NE(rendererAsset, nullptr); - - std::unique_ptr rendererPipeline = - rendererAsset->CreatePipeline(); - ASSERT_NE(rendererPipeline, nullptr); - EXPECT_NE( - dynamic_cast( - rendererPipeline.get()), - nullptr); + EXPECT_EQ(rendererAsset, nullptr); + EXPECT_EQ( + assetRuntime->GetPipelineRendererAssetPolicy(), + XCEngine::Rendering::Pipelines::ManagedPipelineRendererAssetPolicy:: + DefaultNativeBackend); Scene* runtimeScene = CreateScene("ManagedFallbackRendererSelectionConsistencyScene");