# PanoPainter Modernization Roadmap Status: live Last updated: 2026-06-17 This roadmap is now architecture-first. The active execution queue lives in `docs/modernization/tasks.md`. Completed and superseded task history moved to `docs/modernization/tasks-done.md`. The debt log remains `docs/modernization/debt.md`. ## Objective Turn PanoPainter into a thin composition-root application over separable C++23 components while preserving current behavior. The target end state is not "more planners around the same legacy shell". The next phase is measured by ownership transfer in the live app, not by planner count, CLI breadth, or test count. The target end state is: - real component ownership - real platform boundaries - real renderer boundaries - a thin `panopainter_app` - legacy containment targets either deleted or reduced to trivial, debt-tracked adapters ## What This Roadmap Covers - app architecture - component boundaries - platform boundaries - renderer/app ownership boundaries - the order of work needed to finish the split It does not try to be the full build, test, or automation reference. Those details live in the other modernization docs when needed. ## What Does Not Count As Top-Priority Progress These can still be useful, but they are not first-order modernization work while the app shell still mostly looks like the old codebase: - planner-only extraction that leaves the same live owner in place - new CLI surface without corresponding live app ownership reduction - test-only or automation-only expansion that does not unblock code movement - backend lab scaffolds - debt-log churn without a target or ownership change ## Reality Check The codebase is meaningfully farther along than the old flat app, but it is not close to the final architecture yet. Historical percentage claims such as the earlier 67% score should not be used as a proxy for architectural completion. The live app still mostly runs through the same large shell and hotspot files. What is already real: - `pp_foundation` - `pp_assets` - `pp_paint` - `pp_document` - `pp_renderer_api` - `pp_renderer_gl` - `pp_paint_renderer` - `pp_ui_core` - `pp_platform_api` - `pp_app_core` Latest slice: - The orphaned `src/platform_legacy/legacy_platform_services.*` shim and its `src/platform_legacy/legacy_platform_fallback_behavior.h` helper are now deleted from the repo. - The root app graph, WebGL entrypoint, and retained Android package graphs had already moved off that fallback path, so the old generic platform-legacy adapter no longer exists as a compiled code path. - `android/android/CMakeLists.txt`, `android/quest/CMakeLists.txt`, and `android/focus/CMakeLists.txt` now compile `src/platform_android/android_platform_services.cpp` instead of `src/platform_legacy/legacy_platform_services.cpp`. - The retained Android package graphs now match `android/src/cpp/main.cpp`, which already constructs concrete Android `PlatformServices` directly. - The retained standard Android package link gate still fails after this slice, but it also fails after restoring the old legacy source list, so the current retained-package linker break remains preexisting and outside the behavior change of this swap. - `cmake/PanoPainterSources.cmake` no longer adds `src/platform_legacy/legacy_platform_services.*` to `PP_PANOPAINTER_APP_SOURCES`, so the root `panopainter_app` target graph stops compiling and linking that retained fallback shim. - The retained `platform_legacy` adapter still exists on disk for non-root compatibility paths, but it is no longer part of the root modernization app graph validated by the quiet Windows/Android/Apple matrix. - `src/platform_web/web_platform_services.*` now owns the concrete WebGL `PlatformServices` implementation as well as the narrower `WebPlatformServices` helper surface. - `webgl/src/main.cpp` now binds that owned concrete Web `PlatformServices` instance directly instead of constructing the cross-platform `platform_legacy` adapter around Web-specific injections. - The touched Web render-context acquire/present, app-close dispatch, file/image picker routing, prepared-file handoff, default render-target binding, and default canvas / prepared-file policy path now route through `src/platform_web/web_platform_services.*` instead of the fallback adapter. - `pp_platform_api_tests` now compiles and exercises the concrete Web platform service surface on the Windows host build, so the Web platform target is no longer only indirectly covered through the narrower helper interface. - `src/platform_legacy/legacy_platform_services.*` no longer has a live platform-entrypoint consumer; it remains only as generic fallback scaffolding still linked into the retained app target graph. - `src/platform_web/web_platform_services.*` now owns the concrete Web `PlatformServices` implementation and `webgl/src/main.cpp` now binds that owned service directly at the WebGL entrypoint. - The touched Web storage-path setup, GLFW render-context acquire/present, app-close dispatch, prepared-file save handoff, persistent-storage flush, default render-target binding, and picker execution now route through `src/platform_web/web_platform_services.*` instead of the cross-platform fallback adapter. - `src/platform_legacy/legacy_platform_services.*` no longer carries the touched Web method branches or the retained `WebPlatformServices*` dependency, and `src/platform_legacy/legacy_platform_state.*` is gone. - `pp_platform_web` now exists as the concrete Web target in root CMake and is linked into `pp_platform_api_tests`, while `webgl/CMakeLists.txt` also compiles the concrete Web service directly for the retained WebGL app build. - `src/platform_linux/linux_platform_services.*` now owns the concrete Linux `PlatformServices` implementation and `create_platform_services(...)` instead of leaving the live Linux execution surface inside `platform_legacy`. - `linux/src/main.cpp` now binds that owned Linux `PlatformServices` instance into `App` directly at the Linux entrypoint. - The touched Linux storage-path setup, GLFW render-context acquire/present, app-close dispatch, default render-target binding, desktop file picking, and FPS reporting now route through `src/platform_linux/linux_platform_services.*` instead of the cross-platform fallback adapter. - `src/platform_legacy/legacy_platform_services.*` no longer carries the touched Linux method branches, so the retained fallback adapter is narrower again and now focused on the Web path plus generic fallback policy. - `src/platform_android/android_platform_services.*` now owns the concrete Android `PlatformServices` implementation and `create_platform_services()` instead of leaving the live Android execution surface inside `platform_legacy`. - `android/src/cpp/main.cpp` now binds that owned Android `PlatformServices` instance into `App` directly at the Android entrypoint. - The touched Android clipboard, keyboard visibility, JNI thread attach/detach, async render-context, and file-picker execution now route through `src/platform_android/android_platform_services.*` instead of the cross-platform fallback adapter. - `src/platform_legacy/legacy_platform_services.*` no longer carries the Android bridge/configuration surface or the touched Android method branches, so the retained fallback adapter was narrowed again before the Linux cut. - `src/platform_apple/apple_platform_services.*` now owns the concrete Apple `PlatformServices` implementation plus the `create_apple_platform_services()` factory instead of leaving the live Apple execution surface inside `platform_legacy`. - `PanoPainter-OSX/main.cpp` and `PanoPainter/GameViewController.m` now bind owned Apple `PlatformServices` instances into `App` directly at the Apple entrypoints. - The touched Apple clipboard, keyboard visibility, render-context, document-picker, display/share, save-ui-state, app-close, SonarPen, recording cleanup, exported-image publish, and prepared-file execution now route through `src/platform_apple/apple_platform_services.*` instead of the cross-platform fallback adapter. - `src/platform_legacy/legacy_platform_services.*` no longer carries the Apple document-service provider/configuration surface or the touched Apple method branches, so the retained fallback adapter is narrower and now focused on the Android/Linux/Web path. - The previous Web narrowing step had already moved WebGL publish/flush/default-canvas/save-prepared-file behavior off the old `try_*legacy_web*` fallback path before this concrete Web service cut. - `src/platform_legacy/legacy_platform_services.*` no longer exposes the dead `pp::platform::legacy::platform_services()` singleton accessor. - Linux, WebGL, and Android were already on owned `create_platform_services(...)` instances, so removing that legacy singleton surface does not change the live entrypoint ownership path. - `src/platform_windows/windows_runtime_state.*` now owns the Win32 bound-app, bound-runtime shutdown, and bound-tablet bindings beside the retained owned `App`, `AppRuntime*`, and `WacomTablet` objects instead of leaving that binding surface in `windows_runtime_shell.cpp`. - `src/platform_windows/windows_runtime_state.*` now also owns the Win32 owned-app creation plus app/runtime binding handoff, so `windows_runtime_shell.cpp` is down to a thinner startup dispatcher over the retained runtime-state helper. - `src/platform_windows/windows_runtime_state.*` now also owns the bound Win32 app/runtime shutdown order, so `windows_lifecycle_shell.cpp` is down to close-message dispatch instead of manually stopping threads and terminating the bound app itself. - `src/platform_windows/windows_lifecycle_state.*` now also owns the Win32 lifecycle close/VR control handoff, so `windows_lifecycle_shell.cpp` is down to a thinner message adapter over the retained lifecycle state helper. - `src/platform_windows/windows_lifecycle_state.*` now also owns the Win32 close-message shutdown/quit handoff, so `windows_lifecycle_shell.cpp` is down to a one-line adapter over the retained lifecycle state helper. - `src/platform_windows/windows_runtime_shell.h` is now a thinner runtime entrypoint header that picks up the retained binding surface from `windows_runtime_state.h` instead of declaring a second shell-owned binding API. - `src/platform_windows/windows_lifecycle_shell.cpp` now formats the FPS title with a stack buffer at the callsite instead of routing through a retained lifecycle-global scratch array. - `src/platform_windows/windows_lifecycle_state.*` is down to the retained Win32 running flag only; the redundant global FPS-title buffer is gone. - `src/platform_windows/windows_runtime_session.*` now owns the bound-session Win32 runtime loop/startup/shutdown body that had still been sitting inside `windows_runtime_flow.cpp`. - `src/platform_windows/windows_runtime_flow.cpp` is now a thinner handoff layer over that Windows session helper. - `src/platform_windows/windows_runtime_shell.h` no longer exposes the removed runtime-session entrypoint or drags bootstrap/splash declarations through the broader shell header surface. - `src/platform_windows/windows_runtime_shell.cpp` now hands runtime execution directly to `run_bound_main_window_runtime(...)` instead of keeping a second wrapper around that seam. - `src/main.cpp` now includes `windows_bootstrap_helpers.h` directly for `run_winmain_entry(...)` instead of relying on a transitive declaration through the Windows runtime shell header. - `src/platform_windows/windows_runtime_flow.*` now owns the Win32 bound-app startup/message-loop/shutdown orchestration that used to live in `windows_runtime_shell.cpp`. - `src/platform_windows/windows_runtime_shell.cpp` is thinner again and now delegates the main runtime flow through that Windows-owned helper. - `src/platform_windows/windows_runtime_shell.cpp` and `src/platform_windows/windows_lifecycle_shell.cpp` now drive thread start/stop through the explicit Windows runtime binding instead of routing that control through `App::runtime()`. - `src/platform_windows/windows_platform_services.cpp` no longer brokers the Win32 main-thread queue through `bound_app()->runtime()`; it now uses an explicit Windows runtime binding. - `src/platform_windows/windows_runtime_state.*` now carries the active `AppRuntime*` binding beside the retained Windows-owned runtime objects. - `src/platform_windows/windows_runtime_state.*` now owns the retained Win32 runtime object lifetime (`App`, `WacomTablet`) instead of leaving that storage inside `windows_runtime_shell.cpp`. - `src/platform_windows/windows_runtime_shell.cpp` now orchestrates startup and shutdown over the Windows runtime-state helper instead of directly owning the retained runtime pocket. - `src/platform_windows/windows_platform_services.cpp` now owns the retained Win32 VR shell state directly behind `platform_vr_state()`. - `src/platform_windows/windows_window_shell.cpp` no longer exposes or owns a separate `retained_vr_shell_state()` pocket. - `src/platform_windows/windows_lifecycle_state.*` now owns the retained Win32 lifecycle running flag and FPS-title scratch buffer instead of leaving them inside `windows_lifecycle_shell.cpp`. - `src/platform_windows/windows_lifecycle_shell.cpp` now keeps lifecycle/VR control behavior without also owning the retained lifecycle state pocket. - `src/platform_windows/windows_async_render_context.*` now owns the retained Win32 async OpenGL context lock/swap state instead of leaving it inside `windows_platform_services.cpp`. - `src/platform_windows/windows_platform_services.cpp` now keeps the VR/runtime adapter surface without also owning the async GL context pocket. - `src/platform_windows/windows_main_window_session.*` now owns the explicit Win32 `MainWindowSession` object (`HWND`, title buffer, sandbox flag) that the runtime shell binds for the live session instead of leaving those fields behind retained accessors inside `windows_runtime_shell.cpp`. - `src/platform_windows/windows_runtime_shell.cpp` now keeps Windows runtime ownership focused on `App` and tablet lifetime rather than also owning main-window session metadata. - `src/platform_legacy/legacy_platform_services.*` now owns Android storage paths through explicit `create_platform_services(...)` configuration instead of reading them from a shared legacy singleton. - `android/src/cpp/main.cpp` now seeds Android storage paths into its owned legacy `PlatformServices` instance. - `src/platform_legacy/legacy_platform_state.*` no longer carries the Android storage-path singleton API. - `src/platform_legacy/legacy_platform_state.*` no longer keeps its own retained Web platform-service binding; the legacy Web helper path now uses the shared `platform_api` injection surface instead. - `src/platform_legacy/legacy_platform_state.*` no longer carries any retained GLFW window state; the leftover `set_legacy_glfw_window*` surface is gone. - `linux/src/main.cpp` and `webgl/src/main.cpp` no longer seed legacy GLFW retained state at startup. - `src/platform_windows/windows_window_shell.cpp` no longer keeps any retained mouse-position pocket for button events; it now reads client coordinates directly from the Win32 messages that already carry them. - `src/platform_legacy/legacy_platform_services.*` now accepts an injected `LegacyGlfwPlatformShell` for Linux/WebGL `acquire_render_context`, `present_render_context`, and app-close callbacks. - `linux/src/main.cpp` and `webgl/src/main.cpp` now provide that GLFW shell directly from the entrypoint-owned window state instead of relying on legacy free helpers for those operations. - `src/platform_legacy/legacy_platform_state.*` no longer exports the old GLFW `acquire/present/request_close` helper trio. - `src/platform_windows/windows_platform_services.cpp` now owns the Win32 virtual-key map and `initialize_retained_input_state()` path, so `windows_window_shell.cpp` no longer carries that retained input setup. - `src/platform_windows/windows_window_shell.*` no longer carries the dead retained raw key-state array or its accessor; Win32 key synchronization now relies only on the virtual-key map plus `App` key state. - `linux/src/main.cpp` now binds and clears the FPS-title callback directly in the Linux entrypoint-owned shell; `App::set_platform_services()` no longer installs Linux-specific GLFW title behavior. - `src/platform_windows/windows_window_shell.*` now keeps the Win32 virtual-key map outside the retained input-state struct, shrinking that shared pocket. - `src/platform_windows/windows_runtime_shell.cpp` no longer keeps a second retained `App*` copy alongside `App::I`; the runtime shell now reads the bound app directly from the singleton composition edge. - `src/platform_windows/windows_window_shell.*` and `src/platform_windows/windows_platform_services.cpp` now keep Win32 VR state outside the generic retained input-state bundle. - `src/platform_legacy/legacy_platform_state.*` no longer carries the dead generic storage-path singleton for the current runtime matrix. - `src/platform_legacy/legacy_platform_services.cpp` now drops the dead generic storage-path fallback read; only the platform-specific storage-path branches remain live. - `src/legacy_app_startup_services.cpp` no longer seeds the dead generic storage-path fallback state on non-Android startup. - `src/platform_legacy/legacy_platform_services.cpp` now reads Android storage paths from Android-owned retained state instead of the shared legacy storage-path singleton. - `src/legacy_app_startup_services.cpp` and `android/src/cpp/main.cpp` now update Android-owned retained storage paths instead of the shared legacy storage-path singleton. - `src/app_events.cpp`, `linux/src/main.cpp`, and `src/platform_legacy/legacy_platform_state.*` now route the Linux FPS-title callback through a narrow legacy GLFW title helper instead of reaching into retained GLFW window state from `App::set_platform_services(...)`. - `src/platform_legacy/legacy_platform_state.h` no longer exports the raw retained GLFW window-state accessor; callers now go through narrow GLFW helpers only. - `src/platform_apple/apple_platform_state.cpp` no longer reads Apple storage paths from `pp::platform::legacy::active_legacy_storage_paths()`; Apple-owned retained state now carries that path bundle. - `PanoPainter-OSX/main.cpp` and `PanoPainter/GameViewController.m` now seed Apple-owned storage paths directly instead of writing them through the shared legacy storage-path singleton. - `src/platform_legacy/legacy_platform_state.*` now exposes an ownable Web platform-services entrypoint plus an explicit binding hook instead of only a retained fallback object. - `webgl/src/main.cpp` now owns the Web platform-services implementation and seeds that owned instance into legacy platform state during startup. - `src/platform_windows/windows_runtime_shell.*` no longer keeps `HINSTANCE` in the retained main-window session state; the startup/shutdown path now threads the module handle explicitly. - `webgl/src/main.cpp` now owns a TU-local legacy `PlatformServices` instance and binds that owned service into `App` during `StartApp()`. - `android/src/cpp/main.cpp` now owns a function-lifetime legacy `PlatformServices` instance in `android_main()` and binds that owned service into `App` instead of binding the process-global fallback directly. - `src/platform_legacy/legacy_platform_services.*` now exposes an ownable `create_platform_services()` entrypoint for explicit per-entrypoint ownership. - `linux/src/main.cpp` now owns a local legacy `PlatformServices` instance and binds it into `App` explicitly instead of binding the process-global legacy accessor directly. - `src/app_events.cpp` no longer silently falls back to `pp::platform::legacy::platform_services()` when `App` has no bound platform services; the live app path now expects explicit platform-service ownership. - `linux/src/main.cpp`, `webgl/src/main.cpp`, and `android/src/cpp/main.cpp` now bind owned `create_platform_services(...)` instances at app creation instead of relying on a hidden fallback in `App` event/platform dispatch. - `src/platform_windows/windows_runtime_shell.cpp` now owns the Windows tablet object directly; the composition edge no longer binds `&WacomTablet::I` into the Windows runtime path. - `src/platform_legacy/legacy_platform_state.*` now exposes narrow Web helper functions for the touched publish/flush/default-canvas/prepared-file paths, so less of the Web fallback behavior lives inline in the legacy platform singleton implementation. - `src/platform_windows/windows_runtime_shell.cpp` no longer keeps a separate retained `AppRuntime*` binding; the touched Windows shell and platform helpers now derive runtime ownership directly from the owned `App`. - `src/platform_windows/windows_runtime_shell.cpp` now explicitly owns the Windows `App` lifetime through a retained `std::unique_ptr` instead of raw `new`/`delete` plus shutdown-side manual cleanup in the lifecycle shell. - `src/platform_windows/windows_lifecycle_shell.cpp` now releases the bound Windows app through `release_bound_app()` after runtime shutdown instead of deleting it directly through the global shutdown path. - `src/platform_windows/windows_window_shell.cpp` now routes the touched key-map and VR-state reads through narrow helpers instead of keeping the broader retained-state bundle live across the main window-proc body. - `src/platform_legacy/legacy_platform_state.*` no longer exposes the mutable retained GLFW hook bundle; Linux/Web fallback render-context/present/close calls now go through narrow GLFW helper functions instead of an exported hook struct. - `scripts/automation/quiet-validate.ps1` is now the bundled checkpoint path for Windows build/test plus optional platform and Apple remote validation, with one compact JSON summary under `out/logs/quiet-validation`. - `scripts/automation/platform-build.ps1` quiet mode now writes per-preset configure/build logs and compact JSON output so Android/headless sweeps can plug into the bundled quiet wrapper without flooding the console. - `scripts/automation/apple-remote-build.ps1` quiet mode now writes the local SSH session log, reports the remote `platform-build.sh` log path, and emits compact JSON output so the bundled quiet wrapper can include the Apple gate in the same checkpoint run. What is still carrying too much live ownership: - `pp_panopainter_ui`: 34 files, about 9102 lines - `panopainter_app`: 29 files, about 8817 lines - `pp_legacy_paint_document`: 7 files, about 5709 lines - `pp_legacy_app`: 20 files, about 4368 lines - `pp_legacy_ui_core`: 20 files, about 3770 lines Current hotspot files: - `src/canvas.cpp`: 17 lines - `src/app_layout.cpp`: 109 lines - `src/canvas_modes.cpp`: 1 line - `src/node.cpp`: 12 lines - `src/main.cpp`: 10 lines - `src/node_panel_brush.cpp`: 2 lines - `src/node_stroke_preview.cpp`: 76 lines - `src/node_canvas.cpp`: 69 lines - `src/app.cpp`: 94 lines - `src/app_dialogs.cpp`: 95 lines Latest slice: - The remaining Windows entry/exit singleton write no longer lives at the `run_main_application(...)` and `handle_window_close_message(...)` callsites; `src/platform_windows/windows_runtime_shell.cpp` now centralizes that legacy `App::I` side effect inside `bind_app(...)`, leaving the touched runtime and lifecycle shell as explicit binder users instead of direct singleton writers. - The touched `src/platform_windows/windows_platform_services.cpp` fan-out no longer reaches the broader retained window bundle directly for main-window, sandbox, and VR/session reads; the touched window/VR queries now route through narrow runtime-shell accessors instead. - `src/platform_windows/windows_bootstrap_helpers.cpp` no longer uses `Canvas::I` for crash-recovery saves; the BugTrap pre-error handler now uses the app-owned `NodeCanvas` document (`app.canvas->m_canvas`) and the new runtime-shell window/sandbox accessors instead of direct singleton or retained-state reads in the touched recovery path. - The retained Apple document bridge/state pocket no longer lives in `src/platform_legacy/legacy_platform_state.*`; it now lives in the Apple-owned `src/platform_apple/apple_platform_state.cpp` plus `src/platform_apple/apple_platform_services.*`, and the macOS/iOS entrypoints now seed that state through `pp::platform::apple::set_legacy_apple_state(...)` instead of the legacy namespace. - `src/platform_legacy/legacy_platform_services.cpp` now consumes the Apple-owned retained provider through `pp::platform::apple::...` call-throughs instead of constructing or caching Apple retained bridge state inside the legacy platform layer. - The Win32 stylus and pointer-input path no longer reaches `WacomTablet::I` directly inside `src/platform_windows/windows_window_shell.cpp` or `src/platform_windows/windows_stylus_input.cpp`; the live Windows runtime shell now binds the active tablet explicitly through `src/platform_windows/windows_runtime_shell.*`, leaving `WacomTablet::I` only at the entry composition edge where the runtime shell binds the active tablet instance. - The remaining dense Windows bootstrap singleton pocket moved off `App::I`: `setup_exception_handler(...)`, `initialize_main_window_startup_state(...)`, and `_pre_call_callback(...)` now use explicit app/bound-runtime state instead of reading the app singleton directly in `src/platform_windows/windows_bootstrap_helpers.*`, and the app-side platform dispatch helpers in `src/app_events.cpp` now also use the instance they are invoked on instead of a global `App::I` fallback. - The retained Web fallback service object and the Apple storage-path preparation helper now also live in `src/platform_legacy/legacy_platform_state.*` instead of being built inline inside `src/platform_legacy/legacy_platform_services.cpp`, which trims another process-global fallback/service pocket out of the legacy platform shell. - The Win32 window procedure, stylus state updates, lifecycle shutdown path, and VR shell callback wiring no longer reach `App::I` directly; the live Windows shell now binds the active `App*` explicitly through `src/platform_windows/windows_runtime_shell.*`, leaving `App::I` only at the entry/shutdown composition edge in the touched Win32 path. - The full retained Apple document bridge construction no longer lives inline in `src/platform_legacy/legacy_platform_services.cpp`; it now lives behind `active_legacy_apple_document_platform_services()` in `src/platform_legacy/legacy_platform_state.*`, and that retained service now resets when seeded Apple handles change so first-use bridge capture stays in sync with the active entrypoint state. - Win32 main-thread task dispatch no longer reaches `AppRuntime` through `App::I` inside `src/platform_windows/windows_platform_services.cpp`; `src/platform_windows/windows_runtime_shell.*` now binds the active runtime explicitly and clears that binding on shutdown, leaving the Windows queue helper as a thinner runtime forwarder. - `App` no longer owns `and_app` or `and_engine`; the retained Android entrypoint now seeds only the explicit legacy platform storage snapshot needed by touched platform services instead of storing Android-native handles on the app singleton. - `active_legacy_storage_paths()` no longer snapshots `App::I` lazily inside `src/platform_legacy/legacy_platform_state.*`; storage roots are now seeded explicitly from app startup plus the iOS, macOS, and Android entrypoints through `set_legacy_storage_paths(...)`. - `pp_platform_api` no longer compiles `src/platform_linux/linux_platform_services.*`; Linux concrete platform code now lives in `pp_platform_linux`, which `pp_legacy_app` and `pp_platform_api_tests` link where needed. - Win32 main-thread queued task ownership now lives in `AppRuntime` instead of `src/platform_windows/windows_platform_services.cpp`, which removes another runtime queue from retained platform-local static state and leaves the Windows shell as a thin forwarder. - The `platform_legacy`-mirrored Apple and GLFW handle cluster no longer lives on `App`; retained Apple/GLFW platform state is now seeded explicitly from the iOS, macOS, Linux, and WebGL entrypoints through `src/platform_legacy/legacy_platform_state.*`. - `pp_platform_api` no longer compiles `src/platform_apple/apple_platform_services.*`; Apple concrete platform code now lives in the new `pp_platform_apple` target, and `panopainter_app` plus `pp_platform_api_tests` link that concrete target where needed. - Retained GLFW window hooks/state and retained Apple UI/app handle snapshots now live in `src/platform_legacy/legacy_platform_state.*` instead of staying inline in `src/platform_legacy/legacy_platform_services.cpp`, which trims another process-global platform-state pocket out of the legacy platform shell and removes more direct `App::I` reads from touched platform paths. - Windows VR session snapshot ownership now lives in `src/platform_windows/windows_vr_shell.h` and `src/platform_windows/windows_platform_services.*` instead of on `App`, with app-side reads now routed through `App::vr_session_snapshot()`. - The live Windows entry shell now routes through `run_main_application(...)` in `src/platform_windows/windows_runtime_shell.*`, leaving `src/main.cpp` as a minimal entry wrapper around `main(...)` and `WinMain(...)`. - Retained legacy storage-path state now lives in `src/platform_legacy/legacy_platform_state.*` instead of staying inline in `src/platform_legacy/legacy_platform_services.cpp`, which trims another process-global platform-state pocket out of the legacy platform shell. - The remaining `NodeStrokePreview` clone-init, stroke-frame planning, mix-pass adapter wiring, sample-pass adapter wiring, and immediate-draw request construction now route through `src/legacy_node_stroke_preview_runtime_services.*`, `src/legacy_node_stroke_preview_draw_services.*`, and `src/legacy_node_stroke_preview_sample_services.*`, leaving `src/node_stroke_preview.cpp` as a thinner live adapter. - `NodeCanvas::handle_event()` now routes through `handle_legacy_node_canvas_event(...)` in `src/legacy_canvas_tool_services.*`, leaving `src/node_canvas.cpp` as a much thinner controller shell. - `App::open_document()` now routes through `src/legacy_document_open_services.cpp`, which moved the document route classification and unsaved-project gating out of `src/app.cpp` and into the retained document-open helper. - `App::dialog_layer_rename()` now routes through `open_legacy_document_layer_rename_dialog(...)` in `src/legacy_document_layer_services.*`, which moved the remaining overlay-open/wire/close workflow out of `src/app_dialogs.cpp`. - The remaining low-level `NodeStrokePreview` viewport/query and texture-slot plumbing now lives in `src/legacy_node_stroke_preview_runtime_services.*` instead of staying inline in `src/node_stroke_preview.cpp`. - `NodePanelBrushPreset` global panel registration now lives in `src/legacy_brush_preset_list_services.*` instead of staying on the live node type as a static registry field. - The remaining generic `Node` geometry/state pocket for `SetSize(...)`, `SetMinSize(...)`, `SetMaxSize(...)`, and `SetPosition(const glm::vec2)` now lives in `src/legacy_ui_node_style.*` instead of staying inline in `src/node.cpp`. - `Node::app_redraw()` and `Node::watch(...)` now live in `src/legacy_ui_node_execution.cpp` instead of staying inline in `src/node.cpp`. - `NodeStrokePreview::draw_stroke_immediate()` now routes through `execute_legacy_node_stroke_preview_immediate_draw(...)` in `src/legacy_node_stroke_preview_runtime_services.*`, which moves render-target validation, viewport/clear-color save-restore, and immediate-runtime request assembly out of the live node file. - The remaining `NodePanelBrush` and `NodePanelBrushPreset` member bodies now live in the existing retained helper layers (`src/legacy_brush_panel_item_ui.*`, `src/legacy_brush_panel_ui.*`, `src/legacy_brush_panel_services.*`, `src/legacy_brush_preset_panel_ui.*`, `src/legacy_brush_preset_list_services.*`, and `src/legacy_brush_preset_services.*`), leaving `src/node_panel_brush.cpp` as a thin translation unit. - `Node::load_internal(...)` now routes through `load_legacy_ui_node(...)` in `src/legacy_ui_node_loader.*`, which moves the init/attribute-parse/create/child-load/loaded shell out of `src/node.cpp`. - The remaining Win32 shell wrappers for close, async lock/swap, stylus/FPS updates, VR start/stop, window-state save, and the window-handle accessor now live in `src/platform_windows/windows_platform_services.cpp` instead of `src/main.cpp`, leaving `main.cpp` as a thinner entry/runtime dispatcher. - The entire `CanvasModeGrid` implementation plus `ActionModeGrid` undo/redo glue now live in `src/legacy_canvas_mode_helpers.cpp` instead of `src/canvas_modes.cpp`, leaving `src/canvas_modes.cpp` as a minimal shell. - `App::request_close()`, the RenderDoc frame wrappers, and the render/UI thread entrypoint wrappers now route through `src/legacy_app_runtime_shell_services.*` instead of staying inline in `src/app.cpp`, leaving `app.cpp` as a thinner retained app shell. - `App::show_progress()`, `App::message_box()`, and `App::input_box()` now route through `src/legacy_app_dialog_services.*` instead of building dialog plans and factories inline in `src/app_dialogs.cpp`. - The remaining generic `Node` event/capture/resize shell plus the width/height/padding/margin/flex/visibility/geometry wrappers now live in `src/legacy_ui_node_execution.cpp` and `src/legacy_ui_node_style.*` instead of staying inline in `src/node.cpp`, leaving `node.cpp` as a near-trivial attribute/load shell. Current architecture mismatches that must be treated as real blockers: - `pp_platform_api` no longer compiles Apple implementation files, but it still owns too much concrete platform implementation instead of only platform-neutral policy and interface code. - `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I` directly, and Linux FPS title reporting now uses an injected callback, but retained Apple bridging in `platform_legacy` and other platform/app coupling remain, even though iOS keyboard visibility and prepared-file save handoff now also route through explicit Apple bridge callbacks and Apple render- context hooks plus iOS main-render-target binding now route through the same bridge style, as do Apple crash-test, app-close, and iOS SonarPen hooks, while Linux/Web GLFW render-context acquire/present and Linux app-close now route through retained local GLFW callback hooks, and retained Apple ObjC handles plus storage paths now sit behind one local `platform_legacy` helper instead of being re-read through `App::I` in each touched path, with the retained GLFW window hooks, Apple handle snapshots, and fallback storage-path return now also using local retained-state helpers instead of direct method-body reads, while Windows VR session snapshot state now also lives behind platform-owned helpers instead of on `App`, the `platform_legacy`-mirrored Apple/GLFW handle cluster is now seeded explicitly from platform entrypoints instead of being copied out of `App`, and retained storage roots are now also seeded explicitly instead of being lazily copied from `App::I` inside `active_legacy_storage_paths()`, while the retained Apple document bridge now also lives in `legacy_platform_state.*` instead of being built inline in `legacy_platform_services.cpp`. - `src/platform_legacy/legacy_platform_services.*` is still part of the live app shell. - `pp_panopainter_ui` still depends on `pp_legacy_app`. - `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live OpenGL execution around the renderer boundary, even though `NodeCanvas` display resolve, cache-to-screen composite, post-draw mask/grid/current-mode sequencing, per-layer/per-plane retained draw execution, and shared checkerboard background setup now route through retained draw-merge helpers, with the cache-to-screen checkerboard-plane callback setup also reduced and the merged-path checkerboard background-plane callback plus per-plane merged-texture draw callback plus non-`draw_merged` per-frame layer draw callback plus the smoothing-mask face shader/draw pass plus heightmap, current-mode, and grid-mode callback setup now routed through the same retained helper family, while post-draw callback assembly and the remaining per-layer render-path orchestration now also route through retained draw-merge helpers even though the broader node draw loop is still inline, with the non-`draw_merged` outer layer/plane traversal now also routing through `execute_legacy_canvas_draw_layer_traversal(...)`, while the heavier per-layer GL setup now also routes through `make_legacy_canvas_draw_merge_layer_path_gl_execution(...)` even though the remaining draw lambdas and broader node draw loop still live in `src/node_canvas.cpp`, where the post-draw/display-resolve tail now also routes through `execute_node_canvas_draw_merge_tail(...)`, while the unmerged-path onion-range planning, plane filtering, per-layer visit handling, and per-visit layer-path execution now also route through `execute_legacy_canvas_draw_unmerged_node_canvas_shell(...)`, while the broader unmerged cache/viewport/background/composite pass setup now also routes through `execute_legacy_canvas_draw_unmerged_node_canvas_pass(...)`, while `NodeCanvas::draw()` setup, merged-pass shell, and unmerged-pass shell now also route through `prepare_legacy_node_canvas_draw_setup(...)`, `execute_legacy_canvas_draw_node_canvas_shell(...)`, `execute_legacy_canvas_draw_merged_pass(...)`, and `execute_legacy_canvas_draw_node_canvas_unmerged_pass(...)`, which materially shortens the live `NodeCanvas::draw()` body even though the file itself is still large. - `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files rather than thin composition/binding surfaces, even though tools-menu binding plus nested panels/options submenu wiring now live in `src/app_layout_tools_menu.cpp` and `App::init_menu_tools()` is now a thin call-through, while file-menu binding plus the export submenu wiring now also live in `src/app_layout_file_menu.cpp` and `App::init_menu_file()` is now a thin call-through, while about-menu and layer-menu wiring now also live in `src/app_layout_about_layer_menu.cpp` and `App::init_menu_about()` plus `App::init_menu_layer()` are now thin call-throughs, while sidebar panel binding and popup wiring now also live in `src/app_layout_sidebar.cpp` and `App::init_sidebar()` is now a thin call-through, while main-toolbar binding now also lives in `src/app_layout_main_toolbar.cpp` and `App::init_toolbar_main()` is now a thin call-through, while edit-menu binding now also lives in `src/app_layout_edit_menu.cpp` and `App::init_menu_edit()` is now a thin call-through, while UI-direction and persisted floating/docked panel-state ownership now also live in `src/app_layout_ui_state.cpp`, while draw-toolbar binding now also lives in `src/app_layout_draw_toolbar.cpp`, while brush-refresh now also lives in `src/app_layout_brush.cpp`, while layout bootstrap plus reload/load continuation wiring now also lives in `src/app_layout_bootstrap.cpp`, and `src/app_layout.cpp` is now mostly thin call-through entrypoints, while the informational overlay opener family now also lives in `src/app_dialogs_info_openers.cpp` and the corresponding `App::dialog_*` entrypoints are thinner, while the export/video/PPBR dialog family now also lives in `src/app_dialogs_export.cpp` and those `App::dialog_*` entrypoints are thinner too, while new/open/save/browse/resize workflow entrypoints now also live in `src/app_dialogs_workflow.cpp`, while the layer-rename dialog open / wire / close pocket now lives in `src/legacy_document_layer_services.*`, and `src/app_dialogs.cpp` is now a thinner dialog dispatch surface. - `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use global singleton reach, raw observer pointers, retained static worker ownership in several app families, and ad hoc mutex/condition-variable ownership, even though most previously detached or raw app-facing worker launches now use owned `std::jthread` or service-owned worker queues and `AppRuntime` now owns render/UI workers with explicit `std::jthread` shutdown semantics while the Windows splash-dialog and HMD renderer workers also use owned `std::jthread` lifecycle, `LogRemote` now uses the same ownership model, the Windows VR device now has explicit `std::unique_ptr` ownership instead of raw global lifetime, and the Windows main-loop/VR coordination flags now use `std::atomic` instead of unsynchronized globals, while the main Win32 entrypoint now groups window/GL/task/VR state behind a retained local state object instead of separate process-wide globals, the Win32 async GL/context lock state now lives under `src/platform_windows/windows_platform_services.cpp` instead of `main.cpp` retained state, the main-thread queued task state now lives under `src/platform_windows/windows_platform_services.cpp` instead of staying in `src/main.cpp`, the canvas async worker now sits behind a named retained local worker-state helper instead of a bare static accessor, the prepared-file worker and the canvas async import/export/save/open worker now live under `AppRuntime` instead of retained static app-events/canvas workers, and the splash-screen dialog loop, HWND ownership, and bitmap setup now live in `src/platform_windows/windows_splash.cpp` instead of `src/main.cpp`, while Win32 pointer API loading, stylus/ink timer decay, Wintab packet reset, and `WM_POINTERUPDATE` pen/touch handling now also live in `src/platform_windows/windows_stylus_input.cpp` instead of `src/main.cpp`, while the retained Win32 VR/HMD shell now also routes through `src/platform_windows/windows_vr_shell.h` instead of staying inline in `src/main.cpp`, while RenderDoc startup/frame capture, SHCore DPI bootstrap, Win32 error-string conversion, the GL debug pre/post callbacks, and the WMI startup probe now also live in `src/platform_windows/windows_bootstrap_helpers.cpp` instead of `src/main.cpp`, while Win32 lifecycle running-state, close/shutdown sequencing, FPS title update/wakeup posting, stylus frame update, window preference save, and VR lifecycle wrappers now also live in `src/platform_windows/windows_lifecycle_shell.cpp` instead of `src/main.cpp`, while the Win32 startup/window/bootstrap path now also lives under `src/platform_windows/windows_bootstrap_helpers.*` for runtime-data discovery, startup-state initialization, window creation, pixel-format setup, GL loader init, runtime-info logging, and core-context upgrade sequencing, while BugTrap/SEH recovery setup now also lives in `src/platform_windows/windows_bootstrap_helpers.cpp` instead of `src/main.cpp`, while the Win32 window procedure and retained message-handling shell now also live in `src/platform_windows/windows_window_shell.*` instead of `src/main.cpp`, while the `WinMain` argv conversion bridge now also lives in `src/platform_windows/windows_bootstrap_helpers.*` instead of staying inline in `src/main.cpp`, while retained input-state zeroing and reverse key-map initialization now also live in `src/platform_windows/windows_window_shell.*` instead of `src/main.cpp`, while the remaining interactive Win32 runtime pocket for touch registration, render/UI thread startup, GL debug callback hookup, Wintab initialization/skip, icon setup, placement restore, optional VR start, splash dismissal, message loop, and shutdown cleanup now also lives in `src/platform_windows/windows_runtime_shell.*` instead of `src/main.cpp`, which materially thins `src/main.cpp` even though broader entrypoint/runtime composition still remains there, while `App::rec_loop()` now delegates worker-iteration orchestration into the retained recording bridge, `App::update_rec_frames()` now delegates recording label refresh through that same retained recording path, and the UI observer math, repeated UI child traversal, and canvas toolbar refresh now live in `src/legacy_app_frame_services.cpp` instead of staying inline in `src/app.cpp`, while the larger document/export/save/open/thumbnail document-IO cluster now lives in `src/legacy_canvas_document_io_services.cpp` and `src/app.cpp` is materially thinner, while `App::clear()`, `App::check_license()`, `App::async_start()`, `App::async_redraw()`, `App::async_end()`, and `App::async_swap()` now also live in `src/legacy_app_runtime_shell_services.cpp` instead of staying inline in `src/app.cpp`, while the canvas state-management cluster for picking, clear/clear-all, layer add/remove/order/lookups, animation frame control, resize, and snapshot save/restore now lives in `src/legacy_canvas_state_services.cpp` instead of `src/canvas.cpp`, while the larger import/export/save/open/thumbnail document-IO cluster now lives in `src/legacy_canvas_document_io_services.cpp` and `NodeStrokePreview` render-target setup plus immediate-pass sequencing now route through retained preview execution helpers, even though the bridge still owns worker-side readback flow and encoder-state label reads, while the main live-pass request assembly and framebuffer-copy setup now also route through `src/legacy_node_stroke_preview_execution_services.h`, even though broader preview-pass orchestration still lives in `src/node_stroke_preview.cpp`, while the immediate preview pass-sequencing family inside `draw_stroke_immediate()` now also routes through `NodeStrokePreview::execute_stroke_draw_immediate_pass_sequence(...)`, while the remaining immediate preview pass shell now also routes through `execute_legacy_node_stroke_preview_draw_immediate_shell(...)`, which materially reduces the live preview-pass body even though broader worker/readback flow still remains inline, while the immediate preview runtime/orchestration block for stroke setup, prepared-stroke construction, pass planning, shader setup, and live render request assembly now also routes through `src/legacy_node_stroke_preview_runtime_services.*` instead of staying inline in `src/node_stroke_preview.cpp`, while the low-level preview GL dispatch and texture-slot binding pocket now also routes through `src/legacy_node_stroke_preview_runtime_services.*` instead of staying inline in `src/node_stroke_preview.cpp`, while `NodeStrokePreview` remaining mix-pass planning and execution now also route through `src/legacy_node_stroke_preview_draw_services.*`, which trims the last dedicated mix-orchestration pocket from `src/node_stroke_preview.cpp`, while `NodeCanvas::draw()` unmerged-pass blend-gate, layer-orientation, and callback-assembly setup now also route through `execute_node_canvas_draw_unmerged_pass(...)`, which trims another coherent unmerged draw shell from the live node even though the file itself remains large, while `NodeCanvas::draw()` merged-pass callback wiring and pass setup now also route through `execute_node_canvas_draw_merged_pass(...)`, which trims another coherent merged draw shell from the live node even though the broader draw loop still remains inline, while `Canvas::draw_objects_direct(...)` and `Canvas::draw_objects(...)` now route through `src/legacy_canvas_object_draw_services.*` instead of staying inline in `src/canvas.cpp`, which trims another coherent object-draw and viewport-state execution family from the live canvas shell, while the static canvas plane geometry/orientation data now also lives in `src/legacy_canvas_plane_data.cpp` instead of staying inline in `src/canvas.cpp`, which trims another retained data-ownership pocket from the live canvas shell, while `Canvas::stroke_draw_samples(...)`, `Canvas::stroke_commit()`, and the larger stroke commit/sample execution family now also route through `src/legacy_canvas_stroke_commit_services.*` instead of staying inline in `src/canvas.cpp`, which trims another large retained stroke-render and viewport-state execution family from the live canvas shell, while the live `Canvas::stroke_draw()` orchestration now also routes through `src/legacy_canvas_stroke_live_services.cpp` instead of staying inline in `src/canvas.cpp`, which materially thins another large retained live stroke-render pocket, while `Canvas::layer_merge(...)`, `Canvas::flood_fill(...)`, and `Canvas::FloodData::apply()` now also route through `src/legacy_canvas_layer_services.cpp` instead of staying inline in `src/canvas.cpp`, which trims another coherent retained layer/fill workflow pocket, while `Canvas::stroke_end(...)`, `Canvas::stroke_cancel(...)`, `Canvas::stroke_draw_mix(...)`, `Canvas::stroke_draw_project(...)`, `Canvas::stroke_update(...)`, and `Canvas::stroke_start(...)` now also route through `src/legacy_canvas_stroke_runtime_services.*` instead of staying inline in `src/canvas.cpp`, which trims another large retained stroke/runtime pocket, while the final camera/timelapse member definitions now also live in `src/legacy_canvas_camera_services.cpp` instead of staying inline in `src/canvas.cpp`, which trims another retained canvas shell pocket, while the `CanvasModeTransform` interaction family now also routes through `src/legacy_canvas_mode_transform.cpp` instead of staying inline in `src/canvas_modes.cpp`, which materially thins another retained canvas-view and transform-mode execution pocket, while the `CanvasModePen` and `CanvasModeLine` interaction families now also route through `src/legacy_canvas_mode_pen_line.cpp` instead of staying inline in `src/canvas_modes.cpp`, while the `CanvasModeFill` and `CanvasModeFloodFill` interaction families now also route through `src/legacy_canvas_mode_fill.cpp` instead of staying inline in `src/canvas_modes.cpp`, which materially thins another retained fill-mode execution pocket from the broader canvas/render hotspot family, while `NodePanelBrush` save/restore/scan/reload/find/get-path ownership now routes through `src/legacy_brush_panel_services.*` instead of staying inline in `src/node_panel_brush.cpp`, which trims another retained brush-workflow pocket from the live UI node even though the broader panel still remains large, while `NodePanelBrushPreset` save/restore and package import/export/import-ABR routing now also lives in `src/legacy_brush_preset_services.*` instead of staying inline in `src/node_panel_brush.cpp`, which trims another large preset-workflow pocket from the live UI node, while `NodePanelBrushPreset` init/menu wiring, click handling, item construction, and added-state update now also route through `src/legacy_brush_preset_panel_ui.*` instead of staying inline in `src/node_panel_brush.cpp`, which materially thins another retained preset panel UI pocket, while the retained `LegacyBrushPresetListServices` block now also lives in `src/legacy_brush_preset_list_services.*` instead of staying inline in `src/node_panel_brush.cpp`, which trims another retained preset-list pocket, while `NodeButtonBrush` clone/init/icon/read/write/draw behavior and `NodeBrushPresetItem` clone/init/draw behavior now also live in `src/legacy_brush_panel_item_ui.*` instead of staying inline in `src/node_panel_brush.cpp`, which trims the remaining brush-item UI pocket from the live brush panel file, while `NodePanelBrushPreset` popup-close event handling now also lives in `src/legacy_brush_preset_panel_ui.*` instead of staying inline in `src/node_panel_brush.cpp`, which removes the last inline brush-panel popup close handler from the live node. The `NodePanelBrushPreset` registration/lifecycle pocket now also routes through the preset-list helper registry instead of a node-local static vector, which removes the remaining live preset-panel ownership glue from `src/node_panel_brush.cpp`, and preset-restore notification visibility now also stays with `src/legacy_brush_preset_services.*` instead of the node wrapper. The broader preset workflow pocket still remains, while `NodeCanvas::handle_event()` now also routes through `execute_node_canvas_handle_event(...)`, which trims another coherent input-routing block from `src/node_canvas.cpp` even though the file is still a live canvas/controller shell, while `NodeCanvas` restore/clear context, resize handling, camera reset, buffer creation, cursor visibility/update, tick, and destroy ownership now also route through `src/legacy_node_canvas_state_services.*` instead of staying inline in `src/node_canvas.cpp`, which materially thins another retained state/control pocket, while shared canvas-mode GL wrappers plus the `CanvasModeBasicCamera` and `CanvasModeCamera` input handlers now also route through `src/legacy_canvas_mode_helpers.*` instead of staying inline in `src/canvas_modes.cpp`, while preview stroke preparation, dual-brush setup, and live pass-orchestration request assembly now also route through retained preview execution helpers, while `NodeStrokePreview` retained lifecycle, worker-thread shell, render-to-image path, on-screen handling, and preview texture ownership now also route through `src/legacy_node_stroke_preview_runtime_services.cpp` instead of staying inline in `src/node_stroke_preview.cpp`, while `NodeCanvas::init()` plus the remaining `NodeCanvas::draw()` outer shell now also route through `src/legacy_node_canvas_draw_services.*` instead of staying inline in `src/node_canvas.cpp`, which materially reduces the live node to a thinner controller surface around event routing and state wrappers, while `Node::on_event(...)` plus mouse/key capture and release ownership now also route through `src/legacy_ui_node_event.*` instead of staying inline in `src/node.cpp`, which materially thins the base scene-graph event shell without changing its public surface, while `Node` child attach/detach/reorder operations now route through named local helpers in `src/node.cpp`, and `Node::load_internal(...)` child XML loading now also routes through `src/legacy_ui_node_loader.*`, which makes the scene-graph mutation and child-instantiation paths easier to reason about without yet moving ownership into `pp_ui_core`, while the generic per-frame node execution/traversal family for `restore_context`, `clear_context`, `update`, `update_internal`, and `tick` now also lives in `src/legacy_ui_node_execution.cpp`, while `Node::parse_attributes(...)` now also routes through `src/legacy_ui_node_attributes.*` instead of staying inline in `src/node.cpp`, while the remaining generic `Node` lifecycle/state pocket for no-op lifecycle hooks, add/remove propagation, move construction, destruction cleanup, and base clone plumbing now also routes through `src/legacy_ui_node_lifecycle.*` instead of staying inline in `src/node.cpp`, while `Canvas` point-trace/unproject/project/camera push-pop-get-set and face-to-shape helpers now also route through `src/legacy_canvas_projection_services.*` instead of staying inline in `src/canvas.cpp`, while `App::draw(...)`, `App::update(...)`, `App::terminate(...)`, `App::update_memory_usage(...)`, `App::update_rec_frames(...)`, `App::res_from_index(...)`, `App::res_to_index(...)`, `App::res_to_string(...)`, `App::rec_clear(...)`, `App::rec_start(...)`, `App::rec_stop(...)`, `App::rec_export(...)`, `App::rec_loop(...)`, and `App::render_thread_tick(...)` now also route through `src/legacy_app_runtime_shell_services.cpp` instead of staying inline in `src/app.cpp`, while `Canvas::draw_merge(...)`, the temporary paint/branch orchestration helpers, final-plane composite, timelapse commit, create/destroy, clear-context, and camera accessors now also route through `src/legacy_canvas_render_shell_services.*` instead of staying inline in `src/canvas.cpp`, while `Node::destroy()`, `root()`, `set_manager(...)`, `added_to_root()`, `handle_on_screen(...)`, template loading helpers, child add/remove/move helpers, and child query helpers now also route through `src/legacy_ui_node_tree_services.cpp` instead of staying inline in `src/node.cpp`, while the `CanvasModeMaskFree` and `CanvasModeMaskLine` interaction families now also route through `src/legacy_canvas_mode_mask.cpp` instead of staying inline in `src/canvas_modes.cpp`, while the remaining live render/pass orchestration in `NodeStrokePreview::draw_stroke_immediate()` now also routes through `src/legacy_node_stroke_preview_draw_services.*` instead of staying inline in `src/node_stroke_preview.cpp`, and while the generic Yoga style/visibility pocket from `Node::SetWidth(...)` through `Node::GetRTL()` now also routes through `src/legacy_ui_node_style.*` instead of staying inline in `src/node.cpp`, while the preview sample execution pocket for sample-point conversion, brush vertex upload, request assembly, and the `execute_legacy_canvas_stroke_sample(...)` call now also lives in `src/legacy_node_stroke_preview_sample_services.*` instead of staying inline in `src/node_stroke_preview.cpp`. - Modern C++23 usage exists in extracted components, especially `std::span`, explicit result/status objects, and a few concepts, but the live app still does not consistently express ownership, thread affinity, or renderer resources through safe component contracts. Conclusion: - the base component extraction is real - the app shell thinning is not done - the platform split is not done - the UI split is not done - the renderer/app ownership split is not done - future backend lab work is still premature until those four statements change ## Final Target Architecture The old roadmap showed a straight chain. That was too simple. The real target is a layered DAG with a thin composition root. ```text pp_foundation -> pp_assets -> pp_paint -> pp_document pp_foundation -> pp_renderer_api -> pp_renderer_gl pp_document + pp_paint + pp_renderer_api -> pp_paint_renderer pp_foundation + pp_document -> pp_app_core pp_foundation -> pp_ui_core pp_platform_api -> pp_platform_windows -> pp_platform_apple -> pp_platform_linux -> pp_platform_android -> pp_platform_web -> pp_platform_vr pp_app_core + pp_ui_core + pp_paint_renderer + pp_platform_api -> pp_panopainter_ui pp_app_core + pp_panopainter_ui + pp_platform_* -> panopainter_app ``` Key ownership rules: - `pp_platform_api` is interface and policy only. No concrete platform service implementation files belong there. - `pp_platform_*` owns platform SDK, OS handles, platform event loops, and concrete service bridges. - `pp_ui_core` owns generic node/layout/overlay behavior and generic controls. - `pp_panopainter_ui` owns app-specific panels, dialogs, canvas views, and UI-to-app bindings. - `pp_app_core` owns planner logic, workflow policy, and service contracts. It does not own nodes, GL objects, or platform handles. - `pp_paint_renderer` owns renderer-facing paint/export/preview contracts. - `panopainter_app` owns composition only. It should stop being a second home for document workflow, dialog orchestration, platform state, or renderer execution. - Threading and task dispatch are app runtime services, not incidental static queues on `App` or detached workers launched from panels, dialogs, canvas, or cloud helpers. - UI ownership is handle/registry based at component boundaries. Raw `Node*` can remain as non-owning implementation detail only when lifetime is proven by checked handles or scoped connections. - Renderer-facing app code uses `pp_renderer_api` resources and command/context contracts. `Texture2D`, `RTT`, direct GL dispatch, and render-thread helpers must not leak into future-backend-facing UI or document code. ## Workstreams ### 1. Break The Canvas And Preview Hotspots First This is the highest-value work because it moves the largest concentration of real app behavior out of the old shell. Required outcomes: - `canvas.cpp` loses major document-plus-render ownership - `node_canvas.cpp` and `node_stroke_preview.cpp` lose major render-pass orchestration - concrete GL execution moves behind renderer-facing services instead of living in app/node files ### 2. Thin The App Shell `app.cpp`, `app_layout.cpp`, and `app_dialogs.cpp` must stop acting as mixed workflow, UI, and composition files. Required outcomes: - `app_layout.cpp` becomes menu/toolbar binding composition - `app_dialogs.cpp` becomes workflow dispatch plus retained dialog opening glue - `app.cpp` becomes startup/frame/queue composition over named helpers ### 3. Finish The UI Core Split `pp_ui_core` exists, but generic widget ownership is still incomplete. Required outcomes: - generic `Node` and control code moves out of `pp_legacy_ui_core` - `pp_panopainter_ui` keeps only app-specific nodes - shared overlay/lifetime behavior stays centered in `pp_ui_core` - the scene graph has explicit ownership, non-owning references, scoped callback connections, and documented UI-thread affinity ### 4. Make Runtime And Thread Ownership Explicit This is crucial for a modern app architecture and must move with the app-shell split, not after it. Required outcomes: - render/UI/worker queues are owned by explicit runtime services - detached worker threads are replaced by joinable/cancellable ownership or a task service with shutdown semantics - render-thread and UI-thread access are expressed through small contracts that can be implemented by future platform shells - `App::I` and `Canvas::I` stop being the way cross-thread code reaches state ### 5. Finish The Platform Split This is still a real blocker, but it should follow the bulk code-moving work above instead of taking priority over the main app hotspots. Required outcomes: - remove concrete platform code from `pp_platform_api` - remove `App::I` reach from platform service implementations - remove app-owned cross-platform handle storage - reduce `platform_legacy` to thin composition or delete it ### 6. Retire Thick Workflow Bridges Open/save/session/export/cloud/brush package flows are still too distributed across retained app, panel, and dialog files. Required outcomes: - document workflow bridges become thin adapters over `pp_app_core` - cloud transfer and cloud browser ownership move out of retained UI nodes - brush package import/export ownership moves out of retained panel code and no longer depends on detached worker launch sites ### 7. Only Then Resume Future Backend Work Vulkan, Metal, and package-only cleanup are explicitly downstream of the app architecture work above. Do not treat future backend scaffolds as proof that modernization is near done while the current shell still depends on: - `platform_legacy` - `pp_legacy_app` - `pp_legacy_ui_core` - `pp_legacy_paint_document` - large GL-heavy node and canvas files ## Exit Criteria The modernization is not done until these are all true: - `pp_platform_api` contains only platform-neutral code - `src/platform_apple/*`, `src/platform_linux/*`, and other concrete platform implementations do not reach `App::I` - `platform_legacy` is gone or reduced to a trivial temporary shim - `App` no longer stores cross-platform handle state that belongs to platform shells - `pp_panopainter_ui` no longer depends on `pp_legacy_app` - `panopainter_app` is a composition root, not a second workflow layer - `canvas.cpp`, `node_canvas.cpp`, and `node_stroke_preview.cpp` no longer own large renderer-orchestration bodies - live app, UI, canvas, cloud, and platform code no longer launch detached worker threads without owned shutdown/cancellation - render/UI task queues are owned behind explicit app runtime services - raw app/UI pointers are non-owning implementation details only, backed by checked handles, scoped connections, or documented owner objects - future-backend-facing app and UI code depends on renderer API contracts, not retained OpenGL resource classes or direct GL execution - `pp_legacy_ui_core`, `pp_legacy_app`, and `pp_legacy_paint_document` are removed or reduced to narrow, explicit adapter seams with debt ids and clear removal conditions ## Active Queue Use `docs/modernization/tasks.md` for the current architecture task bundles, ordered by real code-moving priority. Use `docs/modernization/tasks-done.md` only for history.