444 lines
15 KiB
Markdown
444 lines
15 KiB
Markdown
|
|
# 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
|