docs(plan): add new_editor d3d12 ui root refactor plan

This commit is contained in:
2026-04-21 16:37:58 +08:00
parent 1a6aacb636
commit eb5b51ddb1
2 changed files with 1362 additions and 0 deletions

View File

@@ -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

View File

@@ -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 代码被删除,而不是隐藏保留。
如果只做到其中前两三项,那只是“部分切换”,不算这次重构真正结束。