# 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