Files
XCEngine/docs/plan/NewEditor_D3D12_UI_RootArchitectureRefactorPlan_2026-04-21.md

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

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