docs(plan): add new_editor d3d12 ui root refactor plan
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user