Files
XCEngine/docs/plan/NewEditor_D3D12_LegacyD2DClosurePlan_2026-04-22.md

8.1 KiB

NewEditor D3D12 Legacy D2D Closure Plan

Date: 2026-04-22

1. Problem Statement

new_editor main-window realtime presentation has already moved onto the native D3D12 path, but the codebase still retains a legacy D2D / D3D11On12 bridge in the host layer.

That residual path is no longer the main realtime hot path, but it is still architecturally serious because it keeps a second rendering/composition implementation alive for the same UI draw model.

Current residual seam:

  • app/Rendering/Native/NativeRenderer.*
  • app/Rendering/D3D12/D3D12WindowInteropContext.*
  • app/Rendering/D3D12/D3D12WindowInteropHelpers.h
  • host/build references that still compile and link the old bridge

2. Confirmed Current Facts

2.1 Main window realtime presentation no longer depends on D2D

Main realtime presentation flows through:

  • D3D12WindowRenderer
  • D3D12UiRenderer
  • D3D12WindowRenderLoop

The active main path does not call NativeRenderer::Render(...).

2.2 Residual D2D bridge is concentrated, not scattered

The old bridge is now effectively concentrated in:

  • screenshot/export flow
  • dead hwnd-native D2D window rendering code that is no longer wired into the main frame loop

2.3 NativeRenderer is no longer the main texture host

TexturePort for the active editor path is already D3D12UiTextureHost.

This means the remaining NativeRenderer responsibilities can be removed instead of preserved as an active subsystem.

3. Root Architectural Issue

The root issue is not “some D2D symbols still exist”.

The root issue is that new_editor still retains two incompatible host-side ways to turn UI output into pixels:

  1. Active path: native D3D12 rendering into the swapchain backbuffer.
  2. Residual path: UIDrawData -> D2D replay, with optional D3D11On12 resource interop.

As long as both remain alive:

  • draw semantics can drift between live output and capture/export output
  • future UI/rendering changes must be kept compatible with two host backends
  • dead-window code remains available to be accidentally reused
  • build/link dependencies preserve obsolete platform coupling

So the correct goal is full single-path closure, not incremental patching.

4. Target End State

After this refactor:

  1. Main-window realtime rendering remains native D3D12.
  2. Main-window screenshot capture also uses native D3D12 backbuffer capture.
  3. new_editor no longer contains any host-side D3D11On12 or D2D bridge code for the main window.
  4. Old hwnd D2D renderer code is deleted, not merely disconnected.
  5. XCUIEditorHost no longer compiles obsolete interop units.
  6. Host link dependencies only retain what is still genuinely required.

5. Non-Goals

  • Do not roll the editor back to D2D.
  • Do not reintroduce a fallback composition backend.
  • Do not change the active D3D12UiRenderer architecture back into draw-data replay through another API.
  • Do not remove DirectWrite from D3D12UiTextSystem; text shaping/raster generation remains valid there.

6. Refactor Strategy

Phase A. Replace screenshot bridge with native D3D12 capture

Introduce a native D3D12 capture path that reads the final swapchain backbuffer and writes PNG through CPU-side WIC encoding.

This capture path must:

  • capture the final composited editor frame
  • avoid UIDrawData replay
  • avoid D3D11On12
  • avoid D2D
  • operate on the same swapchain backbuffer the user actually sees

Preferred integration point:

  • after UI rendering has finished for the active backbuffer
  • after the frame has been submitted to the queue
  • before PresentFrame()

Reason:

  • the frame is already fully rendered
  • the current backbuffer is still the one about to be presented
  • capture remains aligned with the user-visible final image

Phase B. Collapse screenshot orchestration around the active D3D12 path

Rework AutoScreenshotController so it only:

  • tracks pending capture requests
  • resolves output/history paths
  • finalizes success/error summaries

It must no longer own the rendering backend choice.

Phase C. Delete obsolete NativeRenderer window-render path

Delete the old hwnd D2D window renderer behavior:

  • Initialize(HWND)
  • Resize(...)
  • Render(...)
  • hwnd render-target management
  • D2D draw command replay entrypoint for window presentation

If a remaining utility is still needed after this phase, it must live in a narrowly-scoped replacement component, not inside a zombie renderer class.

Phase D. Delete D3D11On12 interop bridge

Once native D3D12 capture is live:

  • delete D3D12WindowInteropContext.*
  • delete D3D12WindowInteropHelpers.h
  • delete all NativeRenderer interop code

Update host build definitions so obsolete units and libraries are removed.

Expected removals if no remaining consumer exists:

  • app/Rendering/Native/NativeRenderer.cpp
  • app/Rendering/D3D12/D3D12WindowInteropContext.cpp
  • d2d1.lib
  • d3d11.lib

Expected retained dependencies:

  • d3d12.lib
  • d3dcompiler.lib
  • dwrite.lib
  • dxgi.lib
  • windowscodecs.lib

7. Concrete File-Level Work

7.1 Add new native capture unit

Add new host-side unit(s), likely under app/Rendering/D3D12/:

  • D3D12WindowCapture.h
  • D3D12WindowCapture.cpp

Responsibilities:

  • copy current swapchain backbuffer to readback
  • map CPU pixels
  • encode PNG via WIC
  • return rich error text

7.2 Extend window renderer / render loop

Modify:

  • app/Rendering/D3D12/D3D12WindowRenderer.h
  • app/Rendering/D3D12/D3D12WindowRenderer.cpp
  • app/Rendering/D3D12/D3D12WindowRenderLoop.h
  • app/Rendering/D3D12/D3D12WindowRenderLoop.cpp

Needed changes:

  • expose a native capture operation on the active backbuffer
  • extend present result with capture outcome
  • allow present flow to execute capture before PresentFrame()

7.3 Rework screenshot controller boundary

Modify:

  • app/Rendering/Native/AutoScreenshot.h
  • app/Rendering/Native/AutoScreenshot.cpp
  • app/Platform/Win32/EditorWindowRuntimeController.h
  • app/Platform/Win32/EditorWindowRuntimeController.cpp
  • app/Platform/Win32/EditorWindow.cpp

Needed changes:

  • stop passing UIDrawData into screenshot rendering
  • stop depending on NativeRenderer
  • move screenshot triggering into the D3D12 present path

7.4 Delete obsolete renderer/interop code

Delete if fully unreferenced after the above:

  • app/Rendering/Native/NativeRenderer.h
  • app/Rendering/Native/NativeRenderer.cpp
  • app/Rendering/Native/NativeRendererHelpers.h
  • app/Rendering/D3D12/D3D12WindowInteropContext.h
  • app/Rendering/D3D12/D3D12WindowInteropContext.cpp
  • app/Rendering/D3D12/D3D12WindowInteropHelpers.h

7.5 Build-system cleanup

Modify:

  • new_editor/CMakeLists.txt

Remove:

  • old source units
  • dead link libraries

8. Validation Requirements

8.1 Build validation

Must build:

  • XCUIEditorHost
  • XCUIEditorAppLib
  • XCUIEditorApp

8.2 Runtime validation

Must validate:

  1. editor launches normally
  2. main window still presents correctly
  3. manual screenshot still succeeds
  4. startup auto-capture still succeeds if enabled
  5. no screenshot path closes or stalls the main window unexpectedly

8.3 Structural validation

Searches after completion must show:

  • no NativeRenderer references in new_editor/app
  • no D3D12WindowInteropContext references in new_editor
  • no D3D11On12 references in new_editor
  • no ID2D / D2D1 references in new_editor/app

Exception:

  • DirectWrite references inside D3D12UiTextSystem remain allowed

9. Execution Order

  1. Add native D3D12 backbuffer capture path.
  2. Route AutoScreenshotController through that new path.
  3. Remove runtime-controller dependence on NativeRenderer.
  4. Delete NativeRenderer and D3D12WindowInteropContext.
  5. Clean CMake/link dependencies.
  6. Build and smoke-test.
  7. Run structural grep validation.

10. Success Criteria

This plan is complete only if all of the following are true:

  • screenshots come from the native D3D12 backbuffer path
  • no main-window host rendering code path can fall back to D2D
  • no D3D11On12 bridge remains in new_editor
  • old dead window-render code is physically removed
  • build and smoke validation pass