Files
XCEngine/docs/used/NewEditor_RHI_PresentPolicy_RootRefactorPlan_2026-04-21.md
2026-04-21 13:57:08 +08:00

9.4 KiB

NewEditor RHI Present Policy Root Refactor Plan

Background

The current new_editor empty-window frame rate is limited by the windowed presentation path rather than by panel content cost. The deeper problem is not a single Present(0, 0) call, but that the RHI swapchain abstraction does not model presentation policy in a backend-correct way:

  • D3D12 expresses most presentation policy at swapchain creation time plus DXGI present flags.
  • Vulkan expresses it mainly through swapchain present mode selection.
  • OpenGL expresses it through swap interval state, not per-present flags.

The current RHISwapChain::Present(syncInterval, flags) interface mixes these semantics and leaves each backend to interpret them differently.

Goals

  • Fix the problem at the RHI root instead of patching new_editor locally.
  • Introduce a formal, cross-backend presentation policy model.
  • Make D3D12 support unlocked windowed presentation when the platform allows it.
  • Preserve compatibility for existing call sites during the first refactor stage.
  • Keep backend fallback behavior explicit and queryable.

Non-Goals

  • Do not optimize panel/widget content costs in this refactor.
  • Do not redesign the entire window manager or render loop.
  • Do not remove legacy Present(syncInterval, flags) calls in one step if doing so would cause broad compile breakage.

Root Cause

  1. SwapChainDesc does not carry a formal presentation policy.
  2. RHISwapChain::Present(syncInterval, flags) is not a sound cross-backend abstraction.
  3. D3D12 swapchain creation is still using a conservative DXGI path and does not expose tearing/unlocked presentation as a first-class capability.
  4. new_editor cannot request a high-FPS presentation policy through the RHI because the RHI does not represent that choice.

Refactor Strategy

Stage 1: Introduce formal presentation policy in the RHI

  • Add SwapChainPresentMode to RHITypes.h.
  • Add SwapChainPresentPolicy to RHITypes.h.
  • Extend SwapChainDesc with presentPolicy.
  • Extend RHISwapChain with read-only introspection for the active presentation policy/mode.
  • Keep the old Present(syncInterval, flags) signature temporarily for compatibility, but move backend behavior to the stored policy instead of the ad hoc parameters.

Stage 2: Rework D3D12 swapchain creation around policy

  • Change D3D12 swapchain initialization to consume the full SwapChainDesc.
  • Detect tearing support through DXGI.
  • Move D3D12 creation to a modern CreateSwapChainForHwnd path with explicit flags.
  • Map policy to actual D3D12 behavior:
    • Fifo -> vsync path
    • Immediate -> tearing/unlocked path when supported, otherwise explicit fallback
    • Mailbox -> explicit fallback because DXGI has no direct mailbox equivalent
  • Store both requested policy and active mode on the swapchain instance.

Stage 3: Align Vulkan and OpenGL to the same policy model

  • Vulkan:
    • choose present mode from policy
    • store active mode after fallback resolution
  • OpenGL:
    • apply swap interval from policy
    • store active mode after fallback resolution

Stage 4: Rewire new_editor to request the intended policy

  • new_editor D3D12 presenter should request Immediate presentation for editor responsiveness.
  • Keep the choice explicit in the presenter instead of hard-coding magic present flags.

Stage 5: Compatibility cleanup

  • Update direct window renderer usages that create swapchains.
  • Keep old Present(syncInterval, flags) call sites compiling during this stage.
  • After the new policy path is proven, consider a later cleanup pass to remove the legacy arguments entirely.

Files Expected To Change

  • engine/include/XCEngine/RHI/RHITypes.h
  • engine/include/XCEngine/RHI/RHISwapChain.h
  • engine/include/XCEngine/RHI/D3D12/D3D12SwapChain.h
  • engine/src/RHI/D3D12/D3D12SwapChain.cpp
  • engine/src/RHI/D3D12/D3D12Device.cpp
  • engine/include/XCEngine/RHI/Vulkan/VulkanSwapChain.h
  • engine/src/RHI/Vulkan/VulkanSwapChain.cpp
  • engine/include/XCEngine/RHI/OpenGL/OpenGLSwapChain.h
  • engine/src/RHI/OpenGL/OpenGLSwapChain.cpp
  • new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp
  • editor/src/Platform/D3D12WindowRenderer.h
  • editor/src/Platform/D3D12WindowRendererImGuiInterop.h

Validation Plan

  • Build XCUIEditorApp
  • Build XCUIEditorAppLib
  • Build any engine targets directly consuming the modified swapchain interfaces
  • Verify that:
    • RHI compiles across D3D12/Vulkan/OpenGL backends
    • new_editor requests Immediate
    • D3D12 reports a sane fallback when tearing is unavailable
    • no existing call site is left using removed API

Risks

  • Old editor code may still depend on the legacy present arguments.
  • OpenGL swap interval extension availability may differ by platform/driver.
  • D3D12 fallback semantics must be explicit to avoid silent behavior changes.

Immediate Execution Order

  1. Add RHI types for present policy.
  2. Update RHISwapChain interface.
  3. Port D3D12 swapchain internals to the new model.
  4. Update new_editor to request Immediate.
  5. Port Vulkan/OpenGL to the same policy model.
  6. Build and fix fallout.

Detailed Execution Checklist

Phase 0: Stabilize the in-progress refactor so the tree is buildable again

  • Remove or re-declare any stale D3D12SwapChain::Initialize(...) overloads so header/source signatures match.
  • Stop hard-coding Format::R8G8B8A8_UNorm inside D3D12 swapchain creation and consume SwapChainDesc.format.
  • Ensure D3D12 swapchain state resets are complete:
    • m_presentPolicy
    • m_activePresentMode
    • m_tearingSupported
    • m_bufferCount
  • Preserve legacy Present(syncInterval, flags) call sites for now, but make backend behavior come from stored policy.

Phase 1: Move present semantics into backend-owned swapchain state

  • D3D12:
    • carry full SwapChainDesc through initialization
    • resolve requested mode to actual mode once, during creation
    • derive DXGI creation flags and present flags from resolved mode
    • apply maximum frame latency from SwapChainPresentPolicy in the backend, not in the editor wrapper
  • Vulkan:
    • carry requested SwapChainPresentPolicy
    • pick a Vulkan present mode from supported modes with explicit fallback order
    • expose both requested policy and resolved active mode
  • OpenGL:
    • carry requested SwapChainPresentPolicy
    • resolve swap interval once from policy
    • explicitly report fallback when immediate swap interval is unavailable

Phase 2: Rewire caller intent explicitly

  • new_editor main window presenter:
    • request SwapChainPresentMode::Immediate
    • request low frame latency explicitly through SwapChainDesc.presentPolicy
    • stop passing magic Present(0, 0) semantics as the source of truth
  • legacy editor D3D12 window renderer:
    • request the same policy so both editors exercise the same RHI path

Phase 3: Validation and fallout cleanup

  • Build XCUIEditorAppLib
  • Build XCUIEditorApp
  • Fix any pure-virtual fallout in non-D3D12 backends
  • Verify these invariants in code:
    • SwapChainDesc.presentPolicy is the only place callers express intent
    • RHISwapChain::GetActivePresentMode() reports the real backend-resolved mode
    • D3D12 resize preserves tearing-capable flags when immediate mode is active
    • no editor path still depends on ad hoc present flags for behavior

File-Level Work Breakdown

RHI contract

  • engine/include/XCEngine/RHI/RHITypes.h
  • engine/include/XCEngine/RHI/RHISwapChain.h

Work:

  • keep policy types centralized in the public RHI contract
  • make swapchain introspection part of the base interface

D3D12 backend

  • engine/include/XCEngine/RHI/D3D12/D3D12SwapChain.h
  • engine/src/RHI/D3D12/D3D12SwapChain.cpp
  • engine/src/RHI/D3D12/D3D12Device.cpp

Work:

  • consume full SwapChainDesc
  • resolve present mode once
  • create flip-model swapchain with policy-derived flags
  • apply frame latency in the backend
  • preserve compatibility surface for existing callers during this pass

Vulkan backend

  • engine/include/XCEngine/RHI/Vulkan/VulkanSwapChain.h
  • engine/src/RHI/Vulkan/VulkanSwapChain.cpp
  • engine/src/RHI/Vulkan/VulkanDevice.cpp

Work:

  • route policy through initialization
  • map policy to Vulkan present modes with explicit fallback
  • publish resolved mode through the common interface

OpenGL backend

  • engine/include/XCEngine/RHI/OpenGL/OpenGLSwapChain.h
  • engine/src/RHI/OpenGL/OpenGLSwapChain.cpp
  • engine/src/RHI/OpenGL/OpenGLDevice.cpp

Work:

  • store requested policy
  • map policy to swap interval behavior
  • publish resolved mode through the common interface

Editor call sites

  • new_editor/app/Rendering/D3D12/D3D12WindowSwapChainPresenter.cpp
  • editor/src/Platform/D3D12WindowRenderer.h
  • editor/src/Platform/D3D12WindowRendererImGuiInterop.h

Work:

  • make intent explicit through SwapChainDesc.presentPolicy
  • keep presentation calls compatible while backend semantics migrate

Acceptance Criteria For This Refactor Pass

  • The codebase builds for the edited targets without interface drift.
  • new_editor and legacy editor both request immediate presentation through the RHI contract.
  • D3D12 no longer depends on caller-supplied per-present sync arguments to decide whether it runs unlocked.
  • Vulkan/OpenGL no longer fail to compile because of the new RHISwapChain pure virtual interface.
  • Any remaining performance limit after this pass can be attributed to another subsystem with the present path already architecturally corrected.