Files
XCEngine/docs/plan/editor-core-refactor-plan.md

25 KiB

Editor Core Refactor Plan

Goal

This refactor turns the current editor/app directory map from a documented convention into a build-checked architecture.

The desired production shape is:

XCUIEditor
  reusable editor UI framework:
  widgets, docking, shell, workspace, menu, viewport slots, field controls

XCEditorCore
  editor product core:
  composition, commands, state, project and scene services, feature panels,
  window/workspace core, and host-facing contracts

XCEditor
  thin executable host:
  main, Win32 process/window host, D3D12 runtime host, resources, startup,
  final object wiring

XCEditor may keep the output name XCEngine.exe; the target name should remain XCEditor.

Current Problem

The reusable layer is mostly healthy:

  • editor/include/XCEditor/** and editor/src/** form XCUIEditor.
  • XCUIEditor is testable and mostly free of app, Win32, and D3D12 concerns.

The app layer has a good directory vocabulary but enforcement is still incomplete:

  • XCEditorCore now limits its public app include surface to editor/app/Core and editor/app/Host/Interfaces, and app/core sources now include through explicit module roots instead of a private editor/app compatibility root. Concrete host code has converged under editor/app/Host/Win32 and editor/app/Host/D3D12.
  • Native editor resources now cross the app-core boundary through a neutral EditorHostResourceService contract under editor/app/Host/Interfaces. XCEditorCore no longer consumes app/Bootstrap or Win32 resource helper code to load built-in PNGs, title-bar branding, or executable-relative capture output paths.
  • Scene viewport shader paths now flow through runtime initialization. The viewport runtime service builds the paths from the runtime repo root and injects them into the render service/pass bundle instead of letting render passes infer source-tree paths from a compile-time repo-root macro.
  • Editor icon and viewport runtime contracts now live under editor/app/Core/Assets and editor/app/Core/Viewport. Concrete icon loading, viewport host services, scene viewport rendering, render targets, and object-id picking stay under editor/app/Rendering.
  • Feature panels no longer use Composition/EditorContext.h directly. The app-core and app feature/viewport test targets now exercise XCEditorCore outside the executable host.
  • Windowing now consumes composition through neutral contracts under editor/app/Core/Windowing: EditorFrameServices for per-frame composition/state access and EditorWorkspaceShellRuntime for per-window shell runtime behavior. EditorContext and EditorShellRuntime remain the concrete composition implementations, injected from Application.

Completed boundary cuts:

  • Project service ownership has moved to editor/app/Services/Project. EditorProjectRuntime depends on the service-owned ProjectBrowserModel, and Features/Project/ProjectPanel owns the widget-tree projection.
  • Panel IDs live under editor/app/Core/Panels, and workspace panel runtime contracts live under editor/app/Core/WorkspacePanels.
  • The generic workspace panel runtime-set implementation also lives under editor/app/Core/WorkspacePanels; Features/EditorWorkspacePanelRegistry.* owns only concrete panel adapters and the concrete factory.
  • Shared window category, lifecycle, chrome-policy, and native-host-policy contracts live under editor/app/Core/Windowing.
  • Utility-window kinds, descriptors, and panel contracts live under editor/app/Core/UtilityWindows; concrete Color Picker/Add Component panel creation lives under editor/app/Features/EditorUtilityWindowRegistry.* and is injected from Application.
  • Panel-facing app services live under editor/app/Core/Panels/EditorPanelServices.h. Workspace-panel and utility-window runtime contracts accept this service view instead of EditorContext, so concrete feature panels no longer depend on Composition/EditorContext.h.
  • Command routing and shared state contracts now live under editor/app/Core/Commands and editor/app/Core/State. XCEditorCore, XCEditor, and app-facing tests expose those contracts through the single editor/app/Core include surface instead of separate command/state roots.
  • Win32 now hands render startup a neutral Rendering::Host::EditorWindowRenderRuntimeSurface value, and D3D12 no longer includes Platform/Win32/** editor surface headers to obtain HWND.
  • Neutral host-facing contracts now live under editor/app/Host/Interfaces, including editor window host interfaces, render-runtime contracts, texture/viewport host contracts, pointer-capture contracts, and system interaction service interfaces.
  • Concrete Win32 and D3D12 host implementations now live under editor/app/Host/Win32 and editor/app/Host/D3D12.
  • Shared window screen geometry, chrome metrics, and frame transfer requests now live under editor/app/Core/Windowing, and XCEditorCore no longer exports the whole editor/app root as a public include directory.
  • app/Windowing/** no longer includes concrete Composition/EditorContext.h or Composition/EditorShellRuntime.h. Instead it consumes EditorFrameServices and EditorWorkspaceShellRuntime from editor/app/Core/Windowing.
  • The private editor/app compatibility include root is gone from XCEditorCore and XCEditor. App implementation files now include through explicit module roots such as app/Composition, app/Features, app/Windowing, app/Scene, app/Services, app/Support, app/Host/Interfaces, app/Host/Win32, and app/Host/D3D12; only executable-host code consumes app/Bootstrap.
  • Concrete app/Rendering/** files now build through the XCEditorCoreRendering object library. XCEditorCore consumes the object files but does not expose app/Rendering to Composition, Features, or Core sources.
  • editor_app_core_tests now links XCEditorCore directly and uses explicit app module include roots. Its initial suite covers host command routing, project runtime, shell asset validation, project browser model, hierarchy scene binding, and inspector presentation.
  • editor_app_feature_tests now links XCEditorCore directly and uses explicit app module include roots. It restores project panel, scene viewport, viewport render-plan, viewport object-id picker, and app input-routing tests without widening the include surface.
  • XCEditorCore is now controlled by XCENGINE_BUILD_XCUI_EDITOR_CORE instead of the executable-host switch. XCENGINE_BUILD_XCUI_EDITOR_APP=OFF can still build the product core and the app-facing test targets when renderer editor support is enabled.
  • XCEditorCore and XCEditor no longer receive a compile-time XCUIEDITOR_REPO_ROOT define. Product behavior that needs repo/resource roots must receive runtime paths through startup wiring, shell/runtime context, or host/resource services.
  • The old Win32 tab-drag-drop target test now covers the current reusable XCEditor/Docking/UIEditorDockHostTransfer.h API through editor_windowing_phase1_tests.

The root issue is not the existence of a single executable target by itself. The root issue was that shared app contracts were stored inside concrete layer directories, and CMake exposed the whole editor/app include root through XCEditorCore usage requirements. The public include surface has now been narrowed, and internal source compatibility with the private app root has now been removed. App-core and app feature/viewport tests now exercise the narrowed surface without using the whole editor/app include root.

Target Directory Shape

The refactor should converge on this shape:

editor/app/
  Core/
    Assets/
    Commands/
    Panels/
    Scene/
    State/
    UtilityWindows/
    Viewport/
    Windowing/
    WorkspacePanels/

  Services/
    Project/
    Scene/

  Features/
    Console/
    ColorPicker/
    Hierarchy/
    Inspector/
    Project/
    Scene/

  Windowing/
    Content/
    Coordinator/
    Frame/
    Host/
    Runtime/

  Host/
    Interfaces/
    Win32/
    D3D12/

  Rendering/
    Assets/
    Viewport/

  Bootstrap/
  Support/

This is a convergence target, not a requirement to move every file at once. The first cut should only move contracts that are already acting as global app interfaces.

Same domain names under different layers are acceptable only when the layer meaning is explicit and build-checked: Core/Viewport is the shared contract surface, while Rendering/Viewport is the concrete implementation. If a Composition or Feature file needs a viewport concept, it should include the Core contract, not the concrete Rendering path.

Dependency Rules

The final direction should be:

XCEditor -> XCEditorCore -> XCUIEditor -> XCEngine

Within XCEditorCore:

  • Composition may depend on Core, Services, Windowing contracts, and XCUIEditor.
  • Composition must not include concrete feature panel headers.
  • Features may depend on Core, Services, and XCUIEditor.
  • Composition and Features must not include concrete app/Rendering/** headers. They should use Core/Assets/EditorIconService.h and Core/Viewport/** contracts.
  • app/Rendering/** may depend on Core, Host/Interfaces, XCUIEditor, and engine renderer/editor support. It implements Core contracts and is wired from Application.
  • Services must not depend on Features.
  • State must not depend on Composition.
  • Windowing may depend on Core, Composition interfaces, and XCUIEditor, but must not include Win32 or D3D12 concrete host headers.
  • The current composition interfaces for windowing are Core/Windowing/EditorFrameServices.h and Core/Windowing/EditorWorkspaceShellRuntime.h.
  • Host contracts may live in Core or Host/Interfaces.
  • Win32 and D3D12 concrete implementations live outside XCEditorCore unless they are purely abstract host contracts.

Phase 1: Fix Shared Contract Placement

Move the most obviously misplaced shared contracts first.

1. Panel IDs

Move:

editor/app/Composition/EditorPanelIds.h

to:

editor/app/Core/Panels/EditorPanelIds.h

Then update all includes to use Core/Panels/EditorPanelIds.h.

Reason: panel IDs are global app model constants, not composition-private implementation details.

2. Workspace Panel Runtime Contract

Split the existing workspace panel registry header:

Current:

editor/app/Features/EditorWorkspacePanelRegistry.h

Target:

editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.h
editor/app/Features/EditorWorkspacePanelRegistry.h

The new core header owns:

  • EditorWorkspacePanelCursorKind
  • EditorWorkspacePanelUpdatePhase
  • EditorWorkspacePanelFrameEvent
  • EditorWorkspacePanelInitializationContext
  • EditorWorkspacePanelShutdownContext
  • EditorWorkspacePanelUpdateContext
  • EditorWorkspacePanel
  • EditorWorkspacePanelRuntimeSet

The feature registry header owns only:

  • CreateEditorWorkspacePanelRuntimeSet()

Reason: composition should depend on the panel runtime interface, not on the feature registry. Concrete feature construction belongs to Features. The generic EditorWorkspacePanelRuntimeSet implementation belongs with the Core workspace-panel runtime contract; the feature registry should only add concrete panel adapters to the set.

3. Keep the First Cut Buildable

This phase should not change behavior.

Expected behavioral diff: none.

Expected dependency improvement:

Composition -> Core/WorkspacePanels
Features    -> Core/WorkspacePanels
Features    -> Core/Panels

This does not fully solve all cycles, but it removes the worst misplaced public contract and creates the landing zone for the next cut.

Completed follow-up:

  • EditorWorkspacePanelRuntimeSet generic behavior now lives under Core/WorkspacePanels, leaving Features/EditorWorkspacePanelRegistry.* responsible only for concrete workspace-panel adapters and factory wiring.
  • EditorPanelServices now carries the panel-facing references to session, project runtime, scene runtime, command focus, color-picker state, system interaction, text measurement, and utility-window requests.
  • EditorWorkspacePanelRuntimeSet updates a single phase at a time; Composition owns the command-focus sync point between main panels and after-focus panels.
  • Concrete workspace and utility panels use EditorPanelServices and no longer include Composition/EditorContext.h.
  • app/Windowing/** no longer accepts or calls EditorWorkspacePanelRuntimeSetFactory. Application composes CreateEditorWorkspacePanelRuntimeSet() with CreateEditorWorkspaceShellRuntime(...), then injects a zero-argument EditorWorkspaceShellRuntimeFactory into windowing.

Phase 2: Introduce XCEditorCore

Status: completed and build-graph enforced.

Create:

add_library(XCEditorCore STATIC ...)

Initial contents:

  • app/Core/**
  • app/Composition/**
  • app/Features/**
  • app/Project/** or app/Services/Project/**
  • app/Scene/** or app/Services/Scene/**
  • app/Core/UtilityWindows/**
  • app/Core/Windowing/**
  • app/Windowing/** core files that do not require Win32 or D3D12
  • host-facing abstract interfaces

Keep in XCEditor executable:

  • app/main.cpp
  • app/Bootstrap/Application.*
  • app/Bootstrap/EditorApp.rc
  • app/Host/Win32/**
  • app/Host/D3D12/**
  • any concrete host glue that includes windows.h

XCEditor links XCEditorCore, XCUIEditor, and concrete platform/rendering libraries.

Important: do not hide Win32/D3D12 in XCEditorCore just to make the first CMake edit easier. If a source file needs windows.h, it belongs in the host side until a neutral interface exists.

Completed target-graph cut:

  • XCENGINE_BUILD_XCUI_EDITOR_CORE controls XCEditorCore and defaults to ON.
  • XCENGINE_BUILD_XCUI_EDITOR_APP controls only the concrete executable host target XCEditor.
  • XCEditor requires XCENGINE_BUILD_XCUI_EDITOR_CORE=ON.
  • When either the core library or executable host is enabled, the root build defaults XCENGINE_ENABLE_RENDERING_EDITOR_SUPPORT=ON because editor core owns viewport services that link renderer editor support.
  • The host-off validation shape is: -DXCENGINE_BUILD_XCUI_EDITOR_APP=OFF -DXCENGINE_BUILD_XCUI_EDITOR_CORE=ON -DXCENGINE_ENABLE_RENDERING_EDITOR_SUPPORT=ON.

Phase 3: Restore App-Core And App Feature Tests

Status: completed for the initial app-core and app feature/viewport suites.

Created:

editor_app_core_tests
editor_app_feature_tests

This target links:

XCEditorCore
GTest::gtest_main

The initial suite contains tests that should not need Win32/D3D12:

  • test_editor_host_command_bridge.cpp
  • test_editor_project_runtime.cpp
  • test_editor_shell_asset_validation.cpp
  • test_project_browser_model.cpp
  • test_hierarchy_scene_binding.cpp
  • test_inspector_presentation.cpp

The restored app feature/viewport suite contains:

  • test_editor_window_input_routing.cpp
  • test_project_panel.cpp
  • test_scene_viewport_render_plan.cpp
  • test_scene_viewport_runtime.cpp
  • test_viewport_object_id_picker.cpp

Completed stale-reference cleanup:

  • Ports/SystemInteractionPort.h test use was replaced with SystemInteractionService.h.
  • Rendering/Viewport/ViewportRenderTargetInternal.h test use was replaced with current ViewportRenderTargets and ViewportRenderTargetUtils headers.
  • app/Platform/Win32/WindowManager/TabDragDropTarget.h was not restored. The test now covers ResolveUIEditorDockHostTabDropTarget from the reusable XCEditor/Docking/UIEditorDockHostTransfer.h API through editor_windowing_phase1_tests.

These test targets are part of the architecture. If app core and app feature/viewport behavior cannot be tested without starting the executable host, the boundary is not real.

Phase 4: Service and Feature Boundary Cleanup

After XCEditorCore exists, remove remaining service-to-feature dependencies.

Project

Completed:

Services/Project/EditorProjectRuntime.h -> Services/Project/ProjectBrowserModel.h
Features/Project/ProjectPanel.h -> Services/Project/EditorProjectRuntime.h

Current shape:

Services/Project/ProjectBrowserModel.h
Services/Project/EditorProjectRuntime.h
Features/Project/ProjectPanel.h

ProjectBrowserModel is now service-owned domain/project state. It exposes folder, asset, and breadcrumb data, while ProjectPanel converts folders into UIEditorTreeViewItem presentation data. Keep future project filesystem and command-target behavior in Services/Project, not under Features/Project.

Scene

Keep scene runtime as a service layer. Scene viewport feature code may use scene services, but scene services should not depend on concrete feature UI or rendering implementation paths.

Completed:

Rendering/Viewport/SceneViewportRenderRequest.h
    -> Core/Scene/SceneViewportRenderRequest.h

Current shape:

Core/Scene/SceneViewportRenderRequest.h
Scene/EditorSceneRuntime.h
Rendering/Viewport/SceneViewportRenderService.h

SceneViewportRenderRequest is a shared scene-to-viewport request contract, not a rendering implementation detail. Keep future scene-to-viewport request values under Core/Scene so scene services can build requests without taking an app/Rendering/** include dependency.

  • Scene viewport shader paths are runtime configuration now. Build them from the runtime repo root in the viewport runtime service, inject them into SceneViewportRenderService, and keep grid/selection render passes from inferring source-tree paths with compile-time macros or __FILE__.

Utility Windows

Completed:

UtilityWindows/EditorUtilityWindowKind.h
UtilityWindows/EditorUtilityWindowPanel.h
UtilityWindows/EditorUtilityWindowRegistry.*
    -> Core/UtilityWindows/EditorUtilityWindowRuntime.h
    -> Core/UtilityWindows/EditorUtilityWindowRegistry.*
    -> Features/EditorUtilityWindowRegistry.*

Current shape:

Core/UtilityWindows/EditorUtilityWindowRuntime.h
Core/UtilityWindows/EditorUtilityWindowRegistry.*
Features/EditorUtilityWindowRegistry.*

Keep utility kinds, descriptors, host context, panel contract, and factory type in Core/UtilityWindows. Keep concrete utility panel construction in Features/EditorUtilityWindowRegistry.* and inject it from the application composition root. Windowing may resolve descriptors from Core, but must not include Color Picker, Add Component, or other concrete feature-panel headers.

Phase 5: Host Boundary Cleanup

Split neutral host contracts from concrete implementations.

Target:

app/Host/Interfaces/
  EditorWindowHostInterfaces.h
  EditorWindowRenderRuntime.h
  UiTextureHost.h
  ViewportRenderHost.h
  SystemInteractionService.h

app/Host/Win32/
  Win32SystemInteractionHost.*
  EditorWindow.*
  message dispatch, DPI, chrome, pointer capture

app/Host/D3D12/
  D3D12EditorWindowRenderRuntime.*
  D3D12 UI renderer, texture host, text system, swap chain presenter

Then replace concrete Win32/D3D12 cross-includes with a neutral surface or factory contract.

Completed cuts:

  • The old Win32EditorWindowRenderRuntimeSurface concrete adapter was removed.
  • EditorWindowRenderRuntimeSurface is now a value contract in Host/Interfaces/EditorWindowRenderRuntime.h.
  • Win32 fills that neutral contract during native surface capture; D3D12 reads the native handle from the contract instead of including a Win32 editor windowing header.
  • Neutral host-facing contracts now live under app/Host/Interfaces/.
  • EditorWindowTransferRequests, window screen geometry, and title-bar chrome metrics now live under app/Core/Windowing/.
  • XCEditorCore now exposes only app/Core and app/Host/Interfaces through its public usage requirements, and both XCEditorCore and XCEditor enumerate explicit module roots instead of using editor/app as a compatibility include directory.
  • Concrete Win32 and D3D12 host implementations now live under app/Host/Win32 and app/Host/D3D12; XCEditor consumes those roots privately, while XCEditorCore only sees app/Host/Interfaces.
  • Native PNG resource lookup and executable-directory discovery now go through Host/Interfaces/EditorHostResourceService.h. The concrete Host/Win32/Resources/Win32EditorResourceService.* owns EditorResources.h, Win32 resource APIs, and the mapping from product resource IDs to built-in editor icon requests, so XCEditorCore no longer needs app/Bootstrap as a private include root for resource loading.
  • Windowing now depends on composition through Core/Windowing/EditorFrameServices.h and Core/Windowing/EditorWorkspaceShellRuntime.h. Application injects the concrete EditorShellRuntime factory, and EditorContext remains the concrete EditorFrameServices implementation.
  • The shell-runtime construction hook visible to app/Windowing/** is now a zero-argument EditorWorkspaceShellRuntimeFactory. Workspace panel runtime set construction stays in Application, so windowing no longer sees EditorWorkspacePanelRuntimeSetFactory.
  • SceneViewportRenderRequest now lives under editor/app/Core/Scene instead of editor/app/Rendering/Viewport. EditorSceneRuntime consumes the shared scene viewport request contract without depending on app/Rendering/**, and editor_app_core_tests no longer needs the app rendering include root to validate scene runtime behavior.
  • Editor icon contracts now live under editor/app/Core/Assets/EditorIconService.h. BuiltInIcons remains the concrete editor/app/Rendering implementation, and panels/tool overlays resolve icons through the contract instead of loading resource files or naming concrete icon storage.
  • Viewport runtime contracts now live under editor/app/Core/Viewport/**. SceneViewportFeature consumes EditorSceneViewportRuntime, while ViewportHostService and SceneViewportRenderService stay concrete rendering implementations behind CreateEditorViewportRuntimeServices().
  • Application injects CreateEditorIconService() and CreateEditorViewportRuntimeServices() into EditorShellRuntime, making the executable startup path the only place that names those concrete rendering factories.
  • app/Rendering/** now builds as XCEditorCoreRendering, a dedicated object library consumed by XCEditorCore. This keeps rendering behavior in the product core while preventing Composition/Features/Core from regaining an app/Rendering include root.
  • Scene viewport shader/resource paths are now injected from the viewport runtime service into SceneViewportRenderService and SceneViewportRenderPassBundle. The concrete grid and selection-outline passes no longer depend on XCUIEDITOR_REPO_ROOT or __FILE__ source-tree fallback logic to find editor shaders.
  • Application now resolves the editor repo root at runtime by walking upward from the executable directory and checking for editor/project markers, rather than consuming a compile-time repo-root macro.

Phase 6: Documentation Update

Update editor/AGENTS.md after each completed boundary cut.

Required changes:

  • production target shape becomes XCUIEditor, XCEditorCore, XCEditor
  • Core owns shared app contracts
  • Features/EditorWorkspacePanelRegistry.* owns concrete feature factories
  • app tests link XCEditorCore
  • no new app code should add direct Composition <-> Features cycles

Validation

Run these after each phase where possible:

cmake --build build --config Debug --target XCUIEditor
cmake --build build --config Debug --target XCEditorCore
cmake --build build --config Debug --target XCEditor
cmake --build build --config Debug --target editor_ui_tests
cmake --build build --config Debug --target editor_app_core_tests
cmake --build build --config Debug --target editor_app_feature_tests
cmake --build build --config Debug --target editor_windowing_phase1_tests

To prove the product core boundary without the executable host, configure a separate build directory with:

cmake -S . -B build/editor_core_hostoff -DXCENGINE_BUILD_XCUI_EDITOR_APP=OFF -DXCENGINE_BUILD_XCUI_EDITOR_CORE=ON -DXCENGINE_ENABLE_RENDERING_EDITOR_SUPPORT=ON
cmake --build build/editor_core_hostoff --config Debug --target XCEditorCore
cmake --build build/editor_core_hostoff --config Debug --target editor_app_core_tests
cmake --build build/editor_core_hostoff --config Debug --target editor_app_feature_tests

When app smoke is available:

ctest -C Debug -R xceditor_smoke --output-on-failure

If XCEditorCore does not exist yet, skip that target until Phase 2 lands.

Done Criteria

The refactor is complete when:

  • XCEditorCore.lib exists and is linked by XCEditor.
  • XCEditor executable source is limited to host startup and concrete platform/render backend wiring.
  • Composition no longer includes concrete feature panel headers.
  • Composition and Features no longer include concrete app/Rendering/** headers; icon and viewport use flows through Core contracts.
  • Windowing no longer includes concrete composition runtime/state headers; it depends on Core/Windowing composition interfaces instead.
  • Services no longer include Features/**.
  • Win32 and D3D12 communicate through neutral host/render contracts.
  • Editor product behavior no longer depends on a compile-time repo-root macro; runtime resource roots are injected through startup/runtime context.
  • app-core and app feature/viewport tests are wired into CMake and build without running the executable.
  • editor/AGENTS.md describes the new target shape and directory rules.