From eb5b51ddb19d452c4e5f65a0fff9f2796c03eb35 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 21 Apr 2026 16:37:58 +0800 Subject: [PATCH] docs(plan): add new_editor d3d12 ui root refactor plan --- ...RootArchitectureRefactorPlan_2026-04-21.md | 443 +++++++++ ...I_Pass_RefactorPlan_Archived_2026-04-21.md | 919 ++++++++++++++++++ 2 files changed, 1362 insertions(+) create mode 100644 docs/plan/NewEditor_D3D12_UI_RootArchitectureRefactorPlan_2026-04-21.md create mode 100644 docs/used/NewEditor_D3D12_UI_Pass_RefactorPlan_Archived_2026-04-21.md diff --git a/docs/plan/NewEditor_D3D12_UI_RootArchitectureRefactorPlan_2026-04-21.md b/docs/plan/NewEditor_D3D12_UI_RootArchitectureRefactorPlan_2026-04-21.md new file mode 100644 index 00000000..b7d63c76 --- /dev/null +++ b/docs/plan/NewEditor_D3D12_UI_RootArchitectureRefactorPlan_2026-04-21.md @@ -0,0 +1,443 @@ +# NewEditor D3D12 UI Root Architecture Refactor Plan + +Date: `2026-04-21` + +Supersedes: +- `docs/used/NewEditor_D3D12_UI_Pass_RefactorPlan_Archived_2026-04-21.md` + +## Goal + +Keep `new_editor` on the native `D3D12` main-window path and refactor the UI stack from the root so the renderer can actually reach native-backend performance ceilings. + +This plan is not a patch plan. It is a structural replacement plan. + +## Hard Constraints + +- Do not roll the main editor window back to `D2D` or `D3D11On12`. +- Do not keep long-term fallback-heavy mixed rendering for the main window. +- Do not treat scene rendering optimization as part of this task. +- Do not optimize around symptoms while preserving the current multi-rebuild architecture. +- Do not touch unrelated dirty worktree changes outside the scoped editor/UI files. + +## Confirmed Root Problems + +### 1. The editor frame graph rebuilds shell/workspace layout too many times per frame + +Confirmed call-chain facts: + +- `new_editor/app/Platform/Win32/EditorWindow.cpp` +- `new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp` +- `new_editor/app/Composition/EditorShellRuntime.cpp` +- `new_editor/src/Shell/UIEditorShellInteraction.cpp` +- `new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp` +- `new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp` +- `new_editor/src/Docking/UIEditorDockHostInteraction.cpp` +- `new_editor/src/Docking/UIEditorDockHost.cpp` + +Current behavior: + +- the shell request is rebuilt multiple times inside one frame +- the workspace compose path is rebuilt again for input ownership preview and again for final compose +- dock host interaction rebuilds dock layout twice inside one rebuild +- the renderer then consumes a fresh immediate `UIDrawData` again instead of a retained render product + +Result: + +- CPU cost scales with repeated full-tree work before rendering even begins +- a fast renderer still receives expensive, redundant input + +### 2. The current native D3D12 UI renderer is not GPU-native in architecture + +Confirmed files: + +- `new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp` +- `new_editor/app/Rendering/Native/NativeRenderer.cpp` + +Current behavior: + +- rounded rects, circles, outlines and fills are tessellated on the CPU every frame +- text is cached as `string -> texture`, not `glyph -> atlas` +- rasterized text misses go through `DirectWrite + WIC + upload` +- the renderer rebuilds vertices, indices and batches from high-level draw commands every frame + +Result: + +- `D3D12` is only the final API, not the actual rendering architecture +- the current implementation replaces a mature high-level renderer with a naive CPU front-end + +### 3. The main editor output is still immediate-command oriented instead of retained and dirty-driven + +Confirmed files: + +- `new_editor/app/Composition/EditorShellRuntime.cpp` +- `new_editor/app/Features/Project/ProjectPanel.cpp` +- `new_editor/app/Features/Hierarchy/HierarchyPanel.cpp` +- `new_editor/app/Features/Inspector/InspectorPanel.cpp` +- `new_editor/src/Shell/UIEditorShellCompose.cpp` + +Current behavior: + +- panels already keep interaction and layout state +- but final rendering still emits immediate `UIDrawList` commands every frame +- there is no long-lived UI render scene, panel packet cache, or subtree dirty invalidation on the final render path + +Result: + +- stable UI still pays repeated build and translation costs + +## End State + +The final main-window rendering chain must be: + +```text +single authoritative shell/workspace frame +-> retained UI scene / panel render packets +-> native D3D12 primitive pass + native D3D12 text pass +-> same command list +-> same direct queue +-> explicit backbuffer state transitions +-> present +``` + +The final main-window chain must not contain: + +- `D3D11On12` +- `D2D` draw submission +- per-frame CPU polygon generation for common UI primitives +- per-string text textures as the primary text path +- repeated full compose/layout rebuilds inside one frame + +## Non-Goals + +- scene renderer optimization +- viewport render graph redesign +- Vulkan/OpenGL editor-host parity during this task +- cosmetic UI redesign + +## Refactor Principles + +- Freeze the already validated present/swapchain/backbuffer path first. +- Refactor top-down ownership and bottom-up rendering architecture together. +- Make one frame produce one authoritative layout/compose snapshot. +- Move from immediate draw commands to retained render packets. +- Move from CPU geometry generation to GPU-friendly primitive evaluation. +- Move from string texture caching to glyph atlas rendering. +- Remove dead compatibility paths after replacement is validated. + +## Workstream A: Freeze The Validated D3D12 Present Path + +Purpose: + +- protect the working `D3D12` present path from further accidental churn while higher layers are rebuilt + +Scope: + +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.*` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.*` +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.*` +- `new_editor/app/Rendering/D3D12/D3D12HostDevice.*` + +Required outcome: + +- preserve current explicit `Present -> RenderTarget -> Present` flow +- preserve current `Immediate + maxFrameLatency=1` policy unless later measurement proves a different policy is better +- do not mix this workstream with UI-pass redesign logic + +## Workstream B: Collapse Shell And Workspace To One Authoritative Frame Build + +Purpose: + +- remove repeated layout/compose/rebuild work inside a single frame + +Primary files: + +- `new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp` +- `new_editor/app/Composition/EditorShellRuntime.cpp` +- `new_editor/src/Shell/UIEditorShellInteraction.cpp` +- `new_editor/src/Shell/UIEditorShellCompose.cpp` +- `new_editor/src/Workspace/UIEditorWorkspaceInteraction.cpp` +- `new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp` +- `new_editor/src/Docking/UIEditorDockHostInteraction.cpp` +- `new_editor/src/Docking/UIEditorDockHost.cpp` +- `new_editor/src/Panels/UIEditorPanelContentHost.cpp` + +Required structural changes: + +- introduce one authoritative frame object for shell/workspace layout, hit-test inputs, mounted panel bounds and viewport slots +- stop rebuilding shell request multiple times unless a real state mutation invalidates the frame +- remove preview compose + final compose duplication where the same layout data is recomputed from scratch +- remove the double `BuildUIEditorDockHostLayout()` pattern inside dock-host interaction rebuilds +- make input ownership resolution consume the authoritative layout snapshot instead of forcing an extra full compose +- ensure panel mount bounds, viewport slot bounds and dock hit-test data all come from the same frame snapshot + +Deliverables: + +- one frame build entrypoint +- one dock-host layout build per frame in the steady state +- explicit invalidation points when menu state, dock mutation, panel visibility or viewport mounts actually change + +Acceptance criteria: + +- no steady-state frame should perform repeated full shell/workspace compose passes without a state mutation that requires recomputation + +## Workstream C: Introduce A Retained UI Scene Instead Of Per-Frame Immediate UIDrawData + +Purpose: + +- stop rebuilding the final render payload from scratch for stable UI + +Primary files: + +- `new_editor/app/Composition/EditorShellRuntime.cpp` +- `new_editor/app/Features/Project/ProjectPanel.*` +- `new_editor/app/Features/Hierarchy/HierarchyPanel.*` +- `new_editor/app/Features/Inspector/InspectorPanel.*` +- `new_editor/app/Features/Console/ConsolePanel.*` +- `new_editor/app/Features/ColorPicker/ColorPickerPanel.*` +- `new_editor/app/Features/Scene/SceneViewportFeature.*` +- `new_editor/app/Platform/Win32/EditorWindow.cpp` + +Required structural changes: + +- define a retained `UiScene` or equivalent render-product layer for the main editor window +- each major shell region and hosted panel produces a stable render packet instead of immediate draw commands +- packets carry primitive instances, image instances, text runs, clip rectangles and z/order information +- packets are rebuilt only when their owning state is dirty +- stable frame append becomes packet aggregation, not command regeneration + +Recommended ownership model: + +- shell chrome packet +- menu bar packet +- toolbar packet +- status bar packet +- dock host packet +- per-panel packet +- overlay packet + +Acceptance criteria: + +- a steady-state empty panel must not rebuild full command streams each frame +- unchanged panels must be reusable across frames + +## Workstream D: Replace The Current D3D12UiRenderer With A Real Native Primitive Pass + +Purpose: + +- stop treating `D3D12` as a thin API wrapper around CPU-generated meshes + +Primary files: + +- `new_editor/app/Rendering/D3D12/D3D12UiRenderer.*` +- potentially new files under `new_editor/app/Rendering/D3D12/` + +Current path to retire: + +- CPU polygon generation for rounded rects and circles +- CPU-generated outline rings for common primitives +- direct translation of every immediate command to transient vertices and indices + +Target design: + +- fixed quad or minimal base geometry +- instance buffers for: + - solid rect + - rounded rect + - border rect + - line + - circle + - image quad +- shader-side shape evaluation for fill, border and clipping +- batch grouping by pipeline, texture set, clip state and blend behavior + +Required renderer modules: + +- `D3D12UiPrimitivePass` +- `D3D12UiFrameResources` +- `D3D12UiClipState` or equivalent scissor/clip stream +- `D3D12UiImagePass` or merged image primitive path + +Acceptance criteria: + +- no CPU tessellation for common editor primitives in the steady state +- vertex/index growth no longer scales with roundness or circle segment counts + +## Workstream E: Replace String Texture Text Rendering With Glyph Atlas Text Rendering + +Purpose: + +- remove the largest text-side architectural bottleneck + +Primary files: + +- `new_editor/app/Rendering/D3D12/D3D12UiRenderer.*` +- `new_editor/app/Rendering/D3D12/D3D12UiTextureHost.*` +- `new_editor/app/Rendering/Native/NativeRenderer.*` +- new text-system files under `new_editor/app/Rendering/D3D12/` or a dedicated text namespace + +Current path to retire: + +- `text + font size + dpi -> whole string texture` +- `RasterizeTextMask()` +- transient WIC bitmap creation for display text misses + +Target design: + +- `DirectWrite` remains for shaping, metrics and glyph raster generation +- per-glyph atlas pages on the GPU +- glyph run cache keyed by text content, font, size, DPI and shaping result +- render text as glyph instances or glyph quads, not per-string textures +- share text metrics cache with render cache so measure and render do not do separate work + +Required renderer modules: + +- `D3D12UiTextSystem` +- `D3D12UiGlyphAtlas` +- `D3D12UiGlyphRunCache` +- `D3D12UiTextPass` + +Acceptance criteria: + +- no main-window display text path uses per-string textures as the primary render mechanism +- repeated labels reuse shaped runs and glyph atlas entries + +## Workstream F: Establish Dirty/Invalidation-Driven Packet Rebuild Rules + +Purpose: + +- make retained rendering actually effective + +Primary files: + +- shell/workspace interaction and compose files +- panel state files +- new retained-scene cache files + +Required invalidation classes: + +- layout dirty +- visual dirty +- text content dirty +- texture binding dirty +- clip hierarchy dirty +- panel visibility/mount dirty +- DPI dirty + +Rules: + +- layout dirty rebuilds bounds and dependent packets +- visual dirty only rebuilds affected render packet contents +- text dirty only rebuilds affected text runs +- viewport texture changes do not invalidate unrelated shell packets +- panel-local changes do not invalidate the full editor scene + +Acceptance criteria: + +- dirty propagation is explicit and local +- unchanged subtrees survive frame-to-frame without packet regeneration + +## Workstream G: Remove Obsolete Main-Window Compatibility Paths + +Purpose: + +- finish the refactor cleanly instead of leaving dead bridges in the hot path + +Primary files: + +- `new_editor/app/Rendering/Native/NativeRenderer.*` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.*` +- `new_editor/app/Rendering/Native/AutoScreenshot.*` +- any remaining main-window D3D11On12 bridge code + +Required cleanup: + +- remove old main-window `D3D11On12/D2D` composition entrypoints +- keep screenshot/offscreen interop only if it still serves a non-main-window path +- remove unused string-texture caches once glyph atlas path is validated +- remove dead UI texture semantics only needed by the old D2D bridge + +Acceptance criteria: + +- no main-window render code path references D3D11On12 wrapped resources +- screenshot/offscreen helpers are clearly isolated from the main editor hot path + +## Workstream H: Instrumentation And Verification + +Purpose: + +- prevent another round of “wrong bottleneck, wrong fix” + +Required measurements before and during each phase: + +- shell/workspace frame build count per presented frame +- dock-host layout rebuild count per presented frame +- panel packet rebuild count per frame +- primitive instance counts by type +- text run cache hit/miss counts +- glyph atlas upload count and uploaded pixel count +- batch count +- total draw call count +- CPU time for: + - shell/workspace frame build + - retained scene rebuild + - primitive pass build + - text shaping/raster/atlas upload + - final render submission + +Instrumentation targets: + +- `EditorWindowRuntimeController` +- `EditorWindowFrameOrchestrator` +- shell/workspace interaction and compose files +- `D3D12UiRenderer` +- future primitive/text pass modules + +Acceptance criteria: + +- each major architectural replacement can be validated against measured cost shifts + +## Recommended Execution Order + +1. Freeze present path and add instrumentation. +2. Collapse shell/workspace to one authoritative frame build. +3. Introduce retained UI scene and per-panel render packets. +4. Replace primitive rendering with GPU-native instance-based primitive pass. +5. Replace text rendering with glyph atlas rendering. +6. Add explicit dirty propagation and cache invalidation rules. +7. Remove obsolete main-window D2D/D3D11On12 paths. +8. Tighten screenshot/offscreen ownership boundaries. +9. Final performance verification and cleanup pass. + +## Commit Strategy + +Recommended submission boundaries: + +1. docs and instrumentation only +2. shell/workspace authoritative frame collapse +3. retained UI scene and panel packet introduction +4. primitive pass replacement +5. text system replacement +6. cleanup and dead-path removal +7. final verification and documentation + +## Risks To Manage Explicitly + +- input hit-testing drift if layout ownership changes without updating all consumers +- stale packet reuse if dirty propagation is incomplete +- glyph atlas eviction bugs causing missing text +- clip/scissor mismatches after primitive-pass conversion +- hidden dependencies on old `UIDrawData` immediate ordering +- viewport texture lifetime issues when packet caching crosses frame boundaries + +## Final Acceptance Standard + +This refactor is only considered complete when all of the following are true: + +- the main editor window stays on the native `D3D12` path only +- one steady-state frame does not repeatedly rebuild shell/workspace layout +- unchanged panels reuse retained render products +- common UI primitives are not CPU-tessellated per frame +- main-window display text is rendered from a glyph atlas path +- old D3D11On12/D2D main-window composition code is removed or fully isolated from the hot path +- performance analysis shows the dominant steady-state cost moved away from repeated CPU rebuild work and naive UI translation diff --git a/docs/used/NewEditor_D3D12_UI_Pass_RefactorPlan_Archived_2026-04-21.md b/docs/used/NewEditor_D3D12_UI_Pass_RefactorPlan_Archived_2026-04-21.md new file mode 100644 index 00000000..9fdf90f3 --- /dev/null +++ b/docs/used/NewEditor_D3D12_UI_Pass_RefactorPlan_Archived_2026-04-21.md @@ -0,0 +1,919 @@ +# new_editor 主窗口 UI 原生 D3D12 UI Pass 重构计划 +日期: `2026-04-21` + +## 1. 文档定位 + +这份计划只解决一件事: + +把 `new_editor` 主窗口 UI 从当前这条 + +`D3D12 -> D3D11On12 -> D2D/DirectWrite -> D3D11 Flush -> Present` + +的每帧跨 API 合成链里彻底拿出来,改成同一条 `D3D12 command list / direct queue` 内完成的原生 `UI pass`。 + +这不是“补一个优化点”,而是一次热路径架构替换。目标不是在现有 `D2D` 链上继续打补丁,而是把主窗口 UI 的显示方案改对。 + +## 2. 范围与边界 + +本次重构纳入范围: + +- `new_editor` 主窗口和 detached editor window 的 UI 热路径。 +- 主窗口 swapchain backbuffer 的准备、UI 录制、提交、显式回到 `Present`。 +- `UIDrawData` 到 GPU draw call 的整条路径。 +- editor 自己的图片纹理、asset preview 缩略图、embedded PNG、scene/game viewport 纹理在 UI pass 中的采样。 +- 文本的测量、排版、字形缓存、GPU 绘制。 +- 自动截图/手动截图,要求最终走和屏幕一致的渲染路径。 +- 旧的 `D3D11On12/D2D` 热路径删除和收口。 + +本次重构明确不纳入范围: + +- scene 本身的渲染优化。 +- shell/workspace/dock 交互层的 CPU 端重复构建问题。 +- 非 `new_editor` 的游戏运行时渲染路径。 +- Vulkan/OpenGL 后端的 editor host 实现。 + +## 3. 代码事实与当前根问题 + +当前主窗口 UI 热路径由下列代码组成: + +- `new_editor/app/Rendering/Native/NativeRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp` + +当前每帧主窗口 UI 的实际流程是: + +```text +BeginFrame +-> scene viewport 等内容先在 D3D12 command list 上录制 +-> PreparePresentSurface() +-> SubmitFrame(false) +-> D3D11On12 AcquireWrappedResources(backbuffer + source textures) +-> D2D/DirectWrite 逐命令绘制 UIDrawData +-> D3D11On12 ReleaseWrappedResources(...) +-> ID3D11DeviceContext::Flush() +-> SignalFrameCompletion() +-> PresentFrame() +``` + +这里有几个必须正视的事实: + +1. `NativeRenderer` 现在不是单一职责类。 + 它同时承担了: + - D2D 主窗口绘制 + - DWrite 文本测量 + - WIC 图片解码 + - D3D11On12 interop + - 截图输出 + +2. `D3D12WindowInteropContext` 把 backbuffer 和 viewport source texture 每帧包成 `ID3D11Resource/ID2D1Bitmap`。 + 这意味着 UI 最终不是在原生 D3D12 backbuffer 上完成,而是在跨 API 包装后再画一遍。 + +3. `D3D12WindowSwapChainPresenter::PreparePresentSurface()` 只做了 `Present -> RenderTarget`。 + 当前 `RenderTarget -> Present` 的回归是依赖 `D3D11On12 ReleaseWrappedResources(...)` 隐式完成的。 + 一旦移除 interop,这个状态回归必须由 D3D12 显式补齐。 + +4. `UITextureHandle` 现在有两套旧语义: + - `DescriptorHandle` 用于 `NativeRenderer` 自己的 CPU/WIC/D2D 纹理。 + - `ShaderResourceView` 用于 viewport 纹理,再经 `D3D11On12/D2D` 包装成可绘制 bitmap。 + 这套语义是被旧显示方案绑出来的,不适合原生 D3D12 UI pass。 + +5. 当前图片解码路径输出的是 `32bppPBGRA`,而 engine RHI 公共格式里只有 `R8G8B8A8_UNorm`,没有 `BGRA` 公共格式。 + 这意味着 texture contract 也必须一起收口,不能直接把旧 `D2D` 像素契约搬过来。 + +## 4. 重构完成后的目标架构 + +重构后的目标架构是: + +```text +UIDrawData +-> CPU batch/build +-> same D3D12 command list +-> same direct queue +-> current swapchain backbuffer +-> explicit RenderTarget -> Present +-> Present +``` + +最终形态必须满足以下硬约束: + +1. 正常运行一帧时,主窗口 UI 热路径里不再创建或使用: + - `ID3D11Device` + - `ID3D11On12Device` + - `ID2D1DeviceContext` + - `ID2D1Bitmap` + - `AcquireWrappedResources` + - `ReleaseWrappedResources` + - `ID3D11DeviceContext::Flush` + +2. UI 必须和 viewport 内容共用同一个 frame command list。 + +3. 当前 backbuffer 的状态切换必须是显式对称的: + - `Present -> RenderTarget` + - UI 绘制 + - `RenderTarget -> Present` + +4. viewport 纹理不再经过 `D3D11On12 + D2D bitmap` 中转,而是直接作为 GPU SRV 采样。 + +5. 主窗口 path 不允许再保留 “D3D12 失败就回退 D2D hwnd render target” 这种长期热路径 fallback。 + 调试阶段可以临时保留兼容开关,但最终收口版本必须删掉。 + +## 5. 核心设计决策 + +### 5.1 不再保留一个“大而杂”的 NativeRenderer + +`NativeRenderer` 当前把五种职责揉在一起,这会直接导致重构不彻底。 + +最终必须拆成以下职责层: + +- `D3D12UiRenderer` + 负责把 `UIDrawData` 变成 D3D12 draw calls。 + +- `D3D12UiTextureHost` + 实现 `TexturePort`,负责 editor 自己的图片纹理加载、上传、SRV 分配与释放。 + +- `DirectWriteTextSystem` + 负责文本测量、排版、glyph cache、atlas 更新。 + 它可以继续依赖 `DirectWrite`,但不能再依赖 `D2D` 作为每帧显示后端。 + +- `D3D12UiCaptureRenderer` + 负责截图,把同一套 UI pass 录制到离屏纹理,再读回 PNG。 + +是否保留 `NativeRenderer` 这个类型名不是重点,重点是最终不能再有一个类同时握着: + +- D2D render target +- D3D11On12 interop +- 纹理加载 +- 文本测量 +- 主窗口 present 逻辑 + +### 5.2 UI pass 使用“CPU 批处理三角形 + 单一 alpha blend pipeline”路线 + +主窗口 UI 的 draw command 类型并不复杂: + +- FilledRect +- FilledRectLinearGradient +- RectOutline +- Line +- FilledTriangle +- FilledCircle +- CircleOutline +- Text +- Image +- PushClipRect / PopClipRect + +因此最稳妥的路线不是搞多套 D2D 等价抽象,而是: + +把全部 UI 命令转换为 GPU triangle batches。 + +最终统一成一套基本顶点格式: + +- `position` +- `uv` +- `color` + +最终统一成一条主图形 pipeline: + +- alpha blend 打开 +- 深度关闭 +- cull 关闭 +- scissor 打开 +- 正交投影 + +具体策略: + +- 纯色/描边/线段/圆/圆角矩形: + 由 CPU 直接三角化,采样一个 `1x1 white texture`。 + +- 线性渐变矩形: + 仍然三角化,但颜色写到顶点颜色里,shader 只做 `texture * vertexColor`。 + +- 图片: + 采样对应 SRV。 + +- 文本: + 采样 glyph atlas 对应 SRV。 + +这样做的好处是: + +- 主窗口 UI pass 可以收口成一条真正原生的 GPU 渲染路径。 +- 不需要在 GPU 侧再保留 D2D 风格图元接口。 +- draw order 与现有 `UIDrawData` 顺序一致,行为最稳定。 +- clip 直接映射成 scissor,简单明确。 + +### 5.3 文本继续使用 DirectWrite 做“CPU 端排版/字形分析”,但不再用 D2D 绘制 + +成熟编辑器不是每帧把 swapchain 包成 D2D bitmap 再 `DrawTextW`。 +更合理的做法是: + +- 用 `DirectWrite` 做字体测量和文本 shaping。 +- 把 glyph coverage 缓存进 atlas。 +- 真正显示时在 GPU 上绘制 glyph quads。 + +因此本次文本路线定为: + +- 保留 `DirectWrite` 作为文本系统。 +- 删除 `D2D::DrawTextW` 作为主窗口显示后端。 +- 新建 glyph atlas。 +- 文本 draw command 最终落成普通 textured quads。 + +### 5.4 viewport 纹理直接采样,不再每帧 CreateWrappedResource/CreateBitmapFromDxgiSurface + +scene/game viewport 纹理当前已经有 `UITextureHandle.nativeHandle = GPU descriptor handle`。 + +这意味着在原生 D3D12 UI pass 里,viewport 图片本质上已经具备直接采样条件。 + +因此: + +- `D3D12WindowInteropContext::PrepareSourceTextures()` 整条链直接删除。 +- UI pass 直接读取 `UITextureHandle.nativeHandle` 对应的 SRV。 +- `resourceHandle` 只保留给释放路径或调试路径使用,不再参与渲染期查表。 + +### 5.5 截图也必须切到同一套 GPU 路径 + +如果主窗口显示已经换成原生 D3D12 UI pass,但截图仍然走旧的 `D2D offscreen render target`,那验证链路会分叉,后续会一直出现: + +- 屏幕上看见的是一套 +- 自动截图导出的是另一套 + +所以截图也必须纳入重构,而不是留作旧系统残留。 + +## 6. 模块拆分计划 + +### 6.1 D3D12UiRenderer + +职责: + +- 接收 `UIDrawData` +- 维护 per-frame vertex/index/upload ring +- 维护 pipeline / root signature / sampler / white texture +- 维护 clip stack -> scissor +- 将 batches 录制到当前 `D3D12CommandList` + +必须覆盖的命令: + +- `FilledRect` +- `FilledRectLinearGradient` +- `RectOutline` +- `Line` +- `FilledTriangle` +- `FilledCircle` +- `CircleOutline` +- `Text` +- `Image` +- `PushClipRect` +- `PopClipRect` + +必须支持的渲染状态: + +- alpha blend +- cull none +- depth off +- scissor on +- swapchain RT 格式自适应 + +### 6.2 D3D12UiTextureHost + +职责: + +- 实现 `Ports::TexturePort` +- 加载文件/内存/RGBA 纹理 +- 创建 `R8G8B8A8_UNorm` GPU texture +- 在 shader-visible descriptor heap 中分配 SRV +- 为 `UITextureHandle` 填入: + - `nativeHandle = GPU descriptor handle` + - `width` + - `height` + - `kind` + - 必要时 `resourceHandle` +- 管理纹理释放和 descriptor 回收 + +这里必须明确: + +- 旧的 `NativeTextureResource::cachedBitmap` 模型要彻底删除。 +- 图片像素契约要改成适合 D3D12 的 `RGBA straight alpha` 或统一后的 GPU 格式。 +- 不能再保留 `D2D bitmap lazy-create`。 + +### 6.3 DirectWriteTextSystem + +职责: + +- 实现 `UIEditorTextMeasurer` +- 复用/管理 `IDWriteFactory` +- 缓存 `IDWriteTextFormat` +- 缓存文本 layout 结果 +- 把 glyph rasterize 到 atlas +- 为文本 draw 生成 glyph quads + +必须考虑的点: + +- 现有 editor 既有英文,也必须允许中文文本正确显示。 +- 需要支持 DPI scale。 +- 需要支持重复标签的缓存复用,避免每帧重新 shape。 +- `MeasureTextWidth()` 和真正渲染应共享同一套文本 layout cache,避免两套逻辑各算一遍。 + +### 6.4 D3D12UiCaptureRenderer + +职责: + +- 创建离屏 color texture +- 用同一套 `D3D12UiRenderer` 逻辑把 `UIDrawData` 画进去 +- GPU readback +- WIC 编码 PNG + +必须保证: + +- 截图结果与真实主窗口 UI pass 使用同一套 shader / batch / text atlas / texture contract。 + +## 7. 关键数据契约调整 + +### 7.1 UITextureHandle 语义收口 + +受影响文件: + +- `engine/include/XCEngine/UI/Types.h` +- `new_editor/app/Rendering/Native/NativeRenderer.cpp` +- `new_editor/app/Rendering/Native/NativeRendererHelpers.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp` + +需要做的事: + +1. 去掉“D2D bitmap handle”和“viewport SRV handle”这两套历史含义。 +2. 统一主窗口 UI 渲染期对 `nativeHandle` 的解释: + `nativeHandle` 就是 GPU 可采样 descriptor handle。 +3. `resourceHandle` 不再用于每帧渲染查找 `D3D11On12 wrapped resource`。 +4. `kind` 要么重命名为更准确的 GPU 语义,要么保留枚举名但重新定义 contract,并把所有调用点一起收口。 + +### 7.2 图片像素格式收口 + +当前 `DecodeTextureFrame()` 输出 `32bppPBGRA`,这是为 D2D 服务的。 + +原生 D3D12 UI pass 必须改成统一的 GPU 纹理契约: + +- 统一使用 `R8G8B8A8_UNorm` +- 明确使用 straight alpha 还是 premultiplied alpha +- blend state 和 shader 按这一个约定实现 + +这个点必须全链路一致: + +- 文件纹理加载 +- 内存纹理加载 +- embedded PNG +- asset preview 缩略图 +- glyph atlas +- white texture + +## 8. D3D12 渲染链改造计划 + +### 8.1 D3D12WindowSwapChainPresenter + +受影响文件: + +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp` + +必须改造: + +1. `PreparePresentSurface()` 要改成更明确的 backbuffer render begin 语义。 +2. 新增对称的 backbuffer render end / present finalize 语义。 +3. 不再依赖 interop 帮忙把 backbuffer 从 `RenderTarget` 回到 `Present`。 +4. `RenderSurface` 元数据要和真实 backbuffer 状态一致。 + +建议改成两段式接口: + +- `PrepareCurrentBackBufferForUiRender(renderContext)` +- `FinalizeCurrentBackBufferForPresent(renderContext)` + +### 8.2 D3D12WindowRenderer + +受影响文件: + +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp` + +必须改造: + +1. 保留 `BeginFrame()`。 +2. 不再让 `SubmitFrame(false)` 提前提交,再由别的 API 补 UI。 +3. 提供当前 frame 的: + - `RenderContext` + - `D3D12CommandList` + - 当前 backbuffer surface +4. `PresentFrame()` 前必须确保本 frame command list 已经包含 UI pass。 + +### 8.3 D3D12WindowRenderLoop + +受影响文件: + +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp` + +这是主窗口热路径的 orchestrator,必须重写逻辑。 + +当前职责里最需要删除的是: + +- interop attach/detach +- “present failure -> 回退 hwnd D2D renderer” + +最终流程应当是: + +```text +BeginFrame() +-> EditorShellRuntime 更新并生成 UIDrawData +-> RenderRequestedViewports(renderContext) +-> PrepareCurrentBackBufferForUiRender(renderContext) +-> D3D12UiRenderer::Render(drawData, renderContext, backbufferSurface) +-> FinalizeCurrentBackBufferForPresent(renderContext) +-> SubmitFrame() +-> SignalFrameCompletion() +-> PresentFrame() +``` + +也就是说,`D3D12WindowRenderLoop::Present()` 不能再只是“调用某个 UI renderer 再兜底”,而必须变成真正的原生 D3D12 submit/present 收口点。 + +## 9. UI pass 具体实现计划 + +### 9.1 Batch 构建 + +建议新增一层 CPU batch builder。 + +输入: + +- `UIDrawData` + +输出: + +- 顶点流 +- 索引流 +- batch 列表 + +每个 batch 至少包含: + +- `firstIndex` +- `indexCount` +- `scissorRect` +- `textureDescriptor` + +batch 切分条件: + +- clip/scissor 变化 +- texture 变化 +- pipeline 变化 + +禁止的做法: + +- 继续一条命令调用一次 D3D API +- 继续在渲染期动态创建图形对象 +- 继续依赖 D2D 帮忙做 clip/gradient/text + +### 9.2 几何生成 + +必须明确每种命令的几何策略: + +- `FilledRect` + 两三角形,圆角时用 CPU 扇形近似。 + +- `RectOutline` + 生成为四条边的条带几何,或拆成若干三角形。 + +- `Line` + 生成厚线四边形。 + +- `FilledTriangle` + 直接三角形。 + +- `FilledCircle` + CPU 分段近似生成扇形。 + +- `CircleOutline` + CPU 分段生成环带。 + +- `FilledRectLinearGradient` + 与普通矩形相同,但顶点颜色按方向插值。 + +- `Image` + 生成标准 textured quad。 + +- `Text` + 生成 glyph textured quads。 + +### 9.3 Clip 栈 + +现有 `PushClipRect/PopClipRect` 必须映射成真正的 scissor 逻辑。 + +具体要求: + +- CPU 维护 clip stack。 +- `intersectWithCurrentClip` 必须保留语义。 +- clip 变化时强制 flush 当前 batch。 +- D3D12 command list 在每个 batch 提交前设置 scissor。 + +### 9.4 Per-frame 上传资源 + +不能每帧创建/销毁 vertex/index buffer。 + +必须改成: + +- 以 `frame slot` 为单位分配 upload buffer ring +- 每个 frame slot 至少持有: + - vertex upload buffer + - index upload buffer + - 必要时 frame constants buffer +- 重用现有 `D3D12HostDevice::kFrameContextCount` + +这里要注意: + +- 现有 `D3D12HostDevice` 已经按 frame slot 做 fence 跟踪。 +- UI pass 的动态上传资源也必须跟这个 frame slot 生命周期绑定,不能另起一套未受 fence 保护的资源轮换。 + +### 9.5 Root signature / pipeline / sampler + +推荐把 UI pass 收敛为一套固定资源布局: + +- root constants 或 frame CBV: + 正交投影、屏幕尺寸等 frame 常量 +- descriptor table: + 当前 batch 使用的 SRV +- static sampler: + 至少一个 `linear clamp` + +需要明确: + +- 是否保留 `point clamp` 给像素风图标或 object-id 类资源 +- 是否通过 `white texture` 统一纯色图元路径 + +## 10. 文本系统重构计划 + +### 10.1 测量与渲染共享同一套文本缓存 + +受影响文件: + +- `new_editor/include/XCEditor/Foundation/UIEditorTextMeasurement.h` +- `new_editor/app/Rendering/Native/NativeRenderer.h` +- `new_editor/app/Rendering/Native/NativeRenderer.cpp` +- `new_editor/src/Shell/UIEditorShellInteraction.cpp` +- `new_editor/app/Features/Project/ProjectPanel.cpp` +- `new_editor/app/Platform/Win32/EditorWindowChromeController.cpp` + +必须改造: + +1. `MeasureTextWidth()` 不再挂在一个 D2D renderer 身上。 +2. 文本测量与渲染共享一套 `DirectWriteTextSystem`。 +3. 同一个 `(font size, string)` 的 layout 结果可以同时服务: + - 宽度测量 + - glyph quad 生成 + +### 10.2 Glyph atlas + +必须解决的问题: + +- atlas 尺寸策略 +- glyph key 设计 +- atlas 满了之后的增长或淘汰 +- atlas dirty 区域上传 +- 多 DPI / 多字号支持 + +推荐 key: + +- font family +- font size +- weight/style +- glyph index +- antialias mode + +推荐策略: + +- 初始 atlas 固定尺寸 +- dirty rect 子区域上传 +- 如 atlas 不够则扩容并重建 descriptor,不做每帧整图重建 + +### 10.3 文字行为一致性 + +必须验证: + +- 标题栏 FPS 文本 +- 面板标题 +- 菜单栏 +- tab 文本 +- ProjectPanel 路径 breadcrumb +- Inspector / Hierarchy / StatusBar +- 中文字符串显示 +- DPI 切换后文字大小与定位 + +## 11. 纹理与图片路径重构计划 + +### 11.1 现有图片调用点 + +必须覆盖以下来源: + +- `EmbeddedPngLoader` +- `BuiltInIcons` +- `SceneViewportToolOverlay` +- asset preview 缩略图 +- viewport surface texture handle + +对应文件: + +- `new_editor/app/Support/EmbeddedPngLoader.cpp` +- `new_editor/app/Rendering/Assets/BuiltInIcons.cpp` +- `new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp` +- `new_editor/app/Rendering/Viewport/ViewportRenderTargets.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp` + +### 11.2 descriptor 分配器 + +`D3D12ShaderResourceDescriptorAllocator` 现在只明确服务 viewport texture。 + +重构后它要么: + +- 直接升级为 editor 全局 UI SRV 分配器 + +要么: + +- viewport texture allocator 和 editor texture allocator 分离 + +但最终必须解决三个问题: + +1. editor 自己的图片纹理和 viewport 纹理都能在 UI pass 中按同一方式绑定。 +2. descriptor 不泄漏。 +3. resize / window shutdown / texture release 时 descriptor 生命周期正确。 + +## 12. 截图链路重构计划 + +受影响文件: + +- `new_editor/app/Rendering/Native/AutoScreenshot.cpp` +- `new_editor/app/Rendering/Native/AutoScreenshot.h` +- 以及新的 GPU capture 模块 + +重构后截图流程: + +```text +UIDrawData +-> D3D12 offscreen color texture +-> same UI pipeline render +-> readback buffer +-> WIC encode PNG +``` + +必须删除: + +- `CreateWicBitmapRenderTarget` +- `ID2D1RenderTarget::EndDraw` 截图路径 + +## 13. 旧代码删除与收口计划 + +### 13.1 必删模块 + +以下旧模块在最终收口时不应继续存在于主窗口热路径: + +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h` + +以下旧模块要么删除,要么被拆空成兼容壳,最终不能继续承担主窗口 UI 显示: + +- `new_editor/app/Rendering/Native/NativeRenderer.h` +- `new_editor/app/Rendering/Native/NativeRenderer.cpp` +- `new_editor/app/Rendering/Native/NativeRendererHelpers.h` + +### 13.2 构建系统收口 + +受影响文件: + +- `new_editor/CMakeLists.txt` + +需要做的事: + +1. 移除 interop 相关源文件。 +2. 新增 `D3D12UiRenderer`、text system、texture host、capture renderer 源文件。 +3. 最终主窗口热路径不再依赖 `d3d11.lib`。 +4. 如果截图也已完全切走,则 `d2d1.lib` 也应移除。 + +`dwrite.lib` 和 `windowscodecs.lib` 可以继续保留,因为它们分别服务: + +- 文本测量/字形分析 +- PNG/WIC 编码解码 + +## 14. 受影响文件总表 + +### 14.1 必改文件 + +- `new_editor/CMakeLists.txt` +- `engine/include/XCEngine/UI/Types.h` +- `new_editor/app/Platform/Win32/EditorWindowRuntimeController.h` +- `new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp` +- `new_editor/app/Rendering/D3D12/D3D12ShaderResourceDescriptorAllocator.h` +- `new_editor/app/Rendering/D3D12/D3D12ShaderResourceDescriptorAllocator.cpp` +- `new_editor/app/Rendering/Assets/BuiltInIcons.cpp` +- `new_editor/app/Support/EmbeddedPngLoader.cpp` +- `new_editor/app/Rendering/Native/AutoScreenshot.cpp` +- `new_editor/app/Rendering/Native/AutoScreenshot.h` + +### 14.2 高概率新增文件 + +- `new_editor/app/Rendering/D3D12/D3D12UiRenderer.h` +- `new_editor/app/Rendering/D3D12/D3D12UiRenderer.cpp` +- `new_editor/app/Rendering/D3D12/D3D12UiBatchBuilder.h` +- `new_editor/app/Rendering/D3D12/D3D12UiBatchBuilder.cpp` +- `new_editor/app/Rendering/D3D12/D3D12UiTextureHost.h` +- `new_editor/app/Rendering/D3D12/D3D12UiTextureHost.cpp` +- `new_editor/app/Rendering/D3D12/DirectWriteTextSystem.h` +- `new_editor/app/Rendering/D3D12/DirectWriteTextSystem.cpp` +- `new_editor/app/Rendering/D3D12/D3D12UiCaptureRenderer.h` +- `new_editor/app/Rendering/D3D12/D3D12UiCaptureRenderer.cpp` +- `new_editor/resources/shaders/ui/...` + +### 14.3 必删文件 + +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.h` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp` +- `new_editor/app/Rendering/D3D12/D3D12WindowInteropHelpers.h` + +## 15. 分阶段实施顺序 + +### 阶段 A: 先把显示热路径切正 + +目标: + +- 主窗口不再走 `D3D11On12/D2D` +- viewport 纹理直接采样 +- backbuffer 状态显式闭环 + +执行顺序: + +1. 引入新的 `D3D12UiRenderer` 骨架。 +2. 改 `D3D12WindowSwapChainPresenter`,补齐 backbuffer begin/end。 +3. 改 `D3D12WindowRenderLoop`,把 UI pass 放进同一 command list。 +4. 先支持: + - FilledRect + - RectOutline + - Line + - Image + - PushClipRect / PopClipRect +5. 让空面板、普通图标 UI、viewport 纹理直采先跑通。 + +阶段 A 完成标准: + +- 主窗口帧内不再出现 interop acquire/release/flush。 +- 关闭 scene 只留空面板时,仍然走原生 D3D12 UI pass。 + +### 阶段 B: 补齐全部 UI 命令 + +目标: + +- 所有现有 `UIDrawCommandType` 行为可用 + +执行顺序: + +1. 补三角化命令: + - gradient rect + - triangle + - circle + - outline circle +2. 补圆角矩形/描边近似策略。 +3. 调整 batch 合并与 scissor flush。 + +### 阶段 C: 文本系统切换 + +目标: + +- 删除 `D2D::DrawTextW` 作为显示手段 + +执行顺序: + +1. 引入 `DirectWriteTextSystem` +2. 先完成文本测量迁移 +3. 再完成 glyph atlas 与文本 quad 输出 +4. 替换主窗口文本绘制 + +阶段 C 完成标准: + +- FPS 文本、菜单、tab、ProjectPanel、StatusBar 文本都走 atlas 路径。 + +### 阶段 D: 截图链路切换 + +目标: + +- 屏幕和截图使用同一 UI 渲染路径 + +执行顺序: + +1. 新建离屏 UI capture renderer +2. 接到 `AutoScreenshot` +3. 删除 D2D screenshot path + +### 阶段 E: 删除旧代码与链接依赖 + +目标: + +- 真正收口,而不是新旧并存 + +执行顺序: + +1. 删除 `D3D12WindowInteropContext` +2. 删除 interop helpers +3. 删除旧 `NativeRenderer` 中的 D2D/D3D11On12 代码 +4. 清理 `CMake` + +## 16. 风险清单 + +### 16.1 状态回归风险 + +如果只删 interop、不补 `RenderTarget -> Present`,就会直接出现: + +- present 失败 +- 黑屏 +- DXGI 报错 + +这是第一优先级风险。 + +### 16.2 文本外观差异风险 + +从 D2D 文本切到 atlas 后,最容易变化的是: + +- baseline +- 字距 +- 灰度抗锯齿观感 +- 中文字形边缘 + +这需要单独做 UI 对比验证。 + +### 16.3 像素格式风险 + +当前图片链路是 `PBGRA`,新链路如果切成 `RGBA` 但 blend/shader 没一起改,会出现: + +- 图标发黑 +- 半透明边缘发灰 +- 预乘错误 + +### 16.4 descriptor 生命周期风险 + +如果 SRV 分配与释放没有彻底梳理,会出现: + +- viewport 纹理释放后 descriptor 悬挂 +- asset preview 刷新泄漏 descriptor +- 多窗口关闭后 descriptor 未回收 + +### 16.5 截图路径分叉风险 + +如果截图仍走旧 D2D 路径,后续任何 UI 行为验证都会变得不可信。 + +## 17. 验证标准 + +### 17.1 代码级验证 + +最终热路径代码里不应再存在以下调用: + +- `D3D11On12CreateDevice` +- `AcquireWrappedResources` +- `ReleaseWrappedResources` +- `CreateBitmapFromDxgiSurface` +- `ID3D11DeviceContext::Flush` +- `ID2D1DeviceContext::SetTarget(backbuffer)` +- `DrawTextW` 用于主窗口 present + +### 17.2 运行时验证 + +必须覆盖以下场景: + +- 只留一个空白面板 +- 只留 scene 面板 +- scene 为空场景 +- 多面板正常布局 +- tab 拖出独立窗口 +- DPI 改变 +- 窗口 resize +- asset preview 缩略图出现与回收 +- scene/game viewport 纹理显示 +- 自动截图 +- 标题栏 FPS 持续刷新 + +### 17.3 图形调试验证 + +建议用 PIX 或 RenderDoc 验证: + +- 一帧内主窗口 UI 是否确实录制在同一条 direct command list 上 +- swapchain backbuffer 状态切换是否完整 +- 是否已不存在 D3D11 packet / interop 提交 + +## 18. 收口判定 + +只有同时满足以下条件,这次重构才算收口彻底: + +1. 主窗口 UI 每帧显示热路径完全不再依赖 `D3D11On12/D2D`。 +2. viewport 纹理改为 GPU 直接采样。 +3. backbuffer 显式状态闭环完成。 +4. 文本显示改为 atlas + GPU quad,而不是 `DrawTextW`。 +5. 截图与屏幕使用同一套 UI pass。 +6. 旧 interop 代码被删除,而不是隐藏保留。 + +如果只做到其中前两三项,那只是“部分切换”,不算这次重构真正结束。