15 KiB
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
D2DorD3D11On12. - 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.cppnew_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cppnew_editor/app/Composition/EditorShellRuntime.cppnew_editor/src/Shell/UIEditorShellInteraction.cppnew_editor/src/Workspace/UIEditorWorkspaceInteraction.cppnew_editor/src/Workspace/UIEditorWorkspaceCompose.cppnew_editor/src/Docking/UIEditorDockHostInteraction.cppnew_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
UIDrawDataagain 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.cppnew_editor/app/Rendering/D3D12/D3D12UiTextureHost.cppnew_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, notglyph -> atlas - rasterized text misses go through
DirectWrite + WIC + upload - the renderer rebuilds vertices, indices and batches from high-level draw commands every frame
Result:
D3D12is 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.cppnew_editor/app/Features/Project/ProjectPanel.cppnew_editor/app/Features/Hierarchy/HierarchyPanel.cppnew_editor/app/Features/Inspector/InspectorPanel.cppnew_editor/src/Shell/UIEditorShellCompose.cpp
Current behavior:
- panels already keep interaction and layout state
- but final rendering still emits immediate
UIDrawListcommands 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:
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:
D3D11On12D2Ddraw 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
D3D12present 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 -> Presentflow - preserve current
Immediate + maxFrameLatency=1policy 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.cppnew_editor/app/Composition/EditorShellRuntime.cppnew_editor/src/Shell/UIEditorShellInteraction.cppnew_editor/src/Shell/UIEditorShellCompose.cppnew_editor/src/Workspace/UIEditorWorkspaceInteraction.cppnew_editor/src/Workspace/UIEditorWorkspaceCompose.cppnew_editor/src/Docking/UIEditorDockHostInteraction.cppnew_editor/src/Docking/UIEditorDockHost.cppnew_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.cppnew_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
UiSceneor 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
D3D12as 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:
D3D12UiPrimitivePassD3D12UiFrameResourcesD3D12UiClipStateor equivalent scissor/clip streamD3D12UiImagePassor 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 textureRasterizeTextMask()- transient WIC bitmap creation for display text misses
Target design:
DirectWriteremains 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:
D3D12UiTextSystemD3D12UiGlyphAtlasD3D12UiGlyphRunCacheD3D12UiTextPass
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/D2Dcomposition 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:
EditorWindowRuntimeControllerEditorWindowFrameOrchestrator- 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
- Freeze present path and add instrumentation.
- Collapse shell/workspace to one authoritative frame build.
- Introduce retained UI scene and per-panel render packets.
- Replace primitive rendering with GPU-native instance-based primitive pass.
- Replace text rendering with glyph atlas rendering.
- Add explicit dirty propagation and cache invalidation rules.
- Remove obsolete main-window D2D/D3D11On12 paths.
- Tighten screenshot/offscreen ownership boundaries.
- Final performance verification and cleanup pass.
Commit Strategy
Recommended submission boundaries:
- docs and instrumentation only
- shell/workspace authoritative frame collapse
- retained UI scene and panel packet introduction
- primitive pass replacement
- text system replacement
- cleanup and dead-path removal
- 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
UIDrawDataimmediate 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
D3D12path 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