diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6c6803bc..14cbeeb6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3,60 +3,24 @@ 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`. +This roadmap is the architecture guide for finishing the modernization. The +execution queue lives in `docs/modernization/tasks.md`; completed history lives +in `docs/modernization/tasks-done.md`; shortcuts and temporary adapters live in +`docs/modernization/debt.md`. ## Objective -Turn PanoPainter into a thin composition-root application over separable C++23 -components while preserving current behavior. +Turn PanoPainter into a thin C++23 composition-root application over separable, +testable components while preserving the working app. -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: +The goal is not more planners, CLI commands, or tests around the old shell. The +goal is ownership transfer out of retained app, UI, canvas, renderer, and +platform hotspots into explicit components with safe lifetime and thread +contracts. -- 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 +## 2026-06-17 Architecture Review -## 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: +The extracted components are real and currently clean at their boundaries: - `pp_foundation` - `pp_assets` @@ -69,884 +33,59 @@ What is already real: - `pp_platform_api` - `pp_app_core` -Latest slice: -- `src/platform_windows/windows_runtime_flow.*` now owns the live Win32 - composition-edge startup flow instead of leaving that dense startup/session - body inside `src/platform_windows/windows_runtime_shell.cpp`. -- The new bound runtime-flow owner now binds the live `MainWindowSession`, - performs the Win32 startup preflight (`initLog`, DPI/shcore, stylus init, - runtime data dir, retained input setup, exception handler, WMI probe, and - `App::create()`), handles the existing `convert` / `-vrmode` mode switch, - and always tears down the bound app/session through one destructor path. -- `src/platform_windows/windows_runtime_shell.cpp` is down to a thin Windows - entry wrapper over `run_main_application_flow(...)`, so the composition edge - is smaller and the repeated bind/release exit paths are gone from the shell. -- 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. +Review validation: -What is still carrying too much live ownership: +- `python scripts/dev/check_component_boundaries.py` passed with zero source or + link violations. +- `python scripts/dev/check_renderer_api_contract.py` passed with zero renderer + API violations. -- `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 +The risk is therefore not component pollution. The risk is that the working app +still bypasses those clean components through retained shells, singleton state, +raw node ownership, direct GL resource calls, and ad hoc thread access. -Current hotspot files: +Measured source pressure from `cmake/PanoPainterSources.cmake`: -- `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 +| Source group | Files | Approx. lines | Review read | +| --- | ---: | ---: | --- | +| `PP_PANOPAINTER_APP_SOURCES` | 47 | 9620 | Still a workflow/runtime shell, not composition only | +| `PP_PANOPAINTER_UI_SOURCES` | 52 | 9051 | Still owns app-specific UI plus canvas/preview rendering | +| `PP_LEGACY_PAINT_DOCUMENT_SOURCES` | 22 | 6277 | Canvas/document/render behavior still concentrated here | +| `PP_LEGACY_APP_SOURCES` | 26 | 4711 | Canvas modes, preferences, history, recording, overlays | +| `PP_LEGACY_UI_CORE_SOURCES` | 32 | 4304 | Generic controls and base node still retained | +| `PP_LEGACY_RENDERER_GL_SOURCES` | 5 | 2854 | Direct GL classes still consumed by app/UI/canvas | -Latest slice: +Measured safety pressure in `src/`: -- 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. +| Signal | Count | Main hotspots | +| --- | ---: | --- | +| `App::I` references | 383 | canvas document I/O, GL resource classes, brush/panel services, UI tree services | +| `Canvas::I` references | 364 | canvas modes, layer/stroke panels, brush UI, app shell services | +| Raw `Node*`-style references | 624 | layout/menu bindings, node tree services, panel headers, dialog code | +| `new` tokens | 198 | node loader, `canvas.cpp`, layer/panel actions, bootstrap helpers | +| `delete` tokens | 61 | retained GL/resources, asset/bootstrap/manual cleanup pockets | +| Render queue call sites | 105 | `RTT`, `Texture2D`, `Shape`, `Shader`, `CanvasLayer`, canvas I/O | +| UI queue call sites | 69 | window shell, document I/O, UI tree services, app runtime | -- `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 conclusion: -Current architecture mismatches that must be treated as real blockers: +- The pure component graph is defensible. +- The live app is not yet modern C++23 in ownership, lifetime, or thread + safety. +- Raw pointers and singletons are still architectural coupling, not just style + debt. +- Renderer API contracts exist, but retained OpenGL resource classes still leak + into app/UI/document code. +- `AppRuntime` is a useful step toward owned queues, but thread affinity and + shutdown safety are still expressed mostly by convention, mutable booleans, + and singleton call sites. +- Platform extraction improved substantially, but CMake and entrypoint cleanup + are not complete. In particular, Web platform sources still appear inside + `PP_PANOPAINTER_APP_SOURCES`, even though `pp_platform_web` also exists. -- `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. +## Target Architecture -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. +The final shape is a layered DAG with one thin composition root: ```text pp_foundation @@ -982,148 +121,114 @@ pp_app_core + pp_panopainter_ui + pp_platform_* -> panopainter_app ``` -Key ownership rules: +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. +- `panopainter_app` composes components and platform services. It does not own + document workflow, renderer execution, dialog logic, or platform state. +- `pp_app_core` owns UI-free workflow policy and service contracts. It does not + own nodes, GL resources, OS handles, or background threads. +- `pp_ui_core` owns generic scene graph, layout, controls, checked handles, + scoped connections, mutation-safe dispatch, and UI-thread affinity rules. +- `pp_panopainter_ui` owns PanoPainter-specific panels, dialogs, canvas views, + and UI-to-app bindings. +- `pp_renderer_api` owns backend-neutral resource and command contracts. +- `pp_renderer_gl` owns OpenGL implementation details while OpenGL remains the + production backend. +- `pp_paint_renderer` owns paint, export, preview, and document-frame render + contracts over `pp_renderer_api`. +- `pp_platform_api` owns platform-neutral interfaces and policy only. +- `pp_platform_*` targets own SDK handles, event loops, windows, native pickers, + render-context binding, VR/device bridges, and platform-specific state. -## Workstreams +Safety rules: -### 1. Break The Canvas And Preview Hotspots First +- Owning raw pointers are not allowed in new component code. +- Raw pointers in retained code must be treated as non-owning views and removed + from touched ownership boundaries unless the lifetime is proven by checked + handles, scoped connections, or owner objects. +- UI and render access must go through explicit runtime services, not `App::I` + or `Canvas::I` reach. +- Worker threads must be joinable, cancellable, and owned by a service with + shutdown semantics. Detached workers and static worker state are not + acceptable end-state architecture. +- Renderer-facing app and UI code must depend on `pp_renderer_api` contracts, + not `Texture2D`, `RTT`, direct GL calls, or retained render-thread helpers. +- Public modernization APIs keep exceptions out of app code and return explicit + `Status`/`Result` objects. -This is the highest-value work because it moves the largest concentration of -real app behavior out of the old shell. +## Priority Order -Required outcomes: +### P0: Move Working-App Ownership -- `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 +The next phase must reduce the working app, not expand headless planner +coverage. Priority work moves real ownership out of: -### 2. Thin The App Shell +- `src/legacy_canvas_document_io_services.cpp` +- `src/legacy_canvas_render_shell_services.cpp` +- `src/legacy_node_canvas_draw_services.cpp` +- `src/legacy_node_stroke_preview_runtime_services.cpp` +- `src/app_runtime.*` +- `src/app_dialogs*.cpp` +- `src/app_layout*.cpp` +- `src/legacy_ui_node_*` +- generic `src/node_*` controls still in `pp_legacy_ui_core` -`app.cpp`, `app_layout.cpp`, and `app_dialogs.cpp` must stop acting as mixed -workflow, UI, and composition files. +### P1: Make Safety Boundaries Enforceable -Required outcomes: +The working app should increasingly fail compile or validation when new code +uses unsafe ownership or the wrong component direction. This includes: -- `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 +- no new `App::I` or `Canvas::I` in moved code +- no new owning `Node*` +- no direct GL resource use from future-backend-facing UI/workflow code +- explicit runtime service contracts for render/UI/background queues +- explicit platform target ownership in CMake -### 3. Finish The UI Core Split +### P2: Retire Compatibility Bridges -`pp_ui_core` exists, but generic widget ownership is still incomplete. +After the biggest working-app hotspots move, thin the remaining retained +workflow bridges: -Required outcomes: +- document open/save/export/session bridges +- cloud transfer and browser bridges +- brush package import/export bridges +- retained platform/package compatibility paths -- 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 +### Deferred -### 4. Make Runtime And Thread Ownership Explicit +Do not restart these as top-priority work until the P0/P1 conditions are +materially improved: -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 +- Vulkan, Metal, WebGPU, or OpenXR expansion beyond necessary boundary design +- package-only cleanup that does not change root app architecture +- CLI-only planner work +- test-only expansion that does not guard an ownership transfer +- scorekeeping or documentation churn without code ownership implications ## Exit Criteria -The modernization is not done until these are all true: +Modernization is not complete until all are 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 +- `panopainter_app` is composition only. +- `pp_panopainter_ui` no longer depends on `pp_legacy_app`. - `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 + deleted or reduced to narrow debt-tracked adapters. +- `App::I` and `Canvas::I` are no longer cross-component access paths. +- Platform SDK handles and event loops live only in `pp_platform_*` targets. +- `pp_platform_api` is SDK-free and implementation-free. +- `canvas.cpp`, `node_canvas.cpp`, and `node_stroke_preview.cpp` no longer own + large renderer orchestration bodies. +- UI ownership uses checked handles and scoped callback connections by default. +- Render/UI/background queues are owned by explicit runtime services with + cancellation, shutdown, and thread-affinity contracts. +- Future-backend-facing app/UI/document code uses `pp_renderer_api` and + `pp_paint_renderer`, not retained OpenGL resource classes. +- The working app builds and passes focused validation for each migrated slice. -## Active Queue +## How To Execute -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. +Use `docs/modernization/tasks.md` as the active work queue. It is written as +coordinator-ready packets for smaller parallel workers. Pick disjoint write +scopes, pass only the packet context to each worker, integrate locally, run the +listed validation, update debt/tasks if the slice moves ownership, then commit +and push. diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 6bf22648..48fb29ae 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -3,1725 +3,730 @@ Status: live Last updated: 2026-06-17 -This file now tracks only active architecture work. -Completed, blocked, and superseded task history moved to -`docs/modernization/tasks-done.md`. +This file is the active execution queue. It is written for a coordinator that +can assign bounded packets to smaller parallel workers. Completed and stale +history belongs in `docs/modernization/tasks-done.md`, not here. ## Operating Rules -- Keep this file short. If a task is done, blocked for a long time, or no - longer relevant, move it to `tasks-done.md` instead of letting the active - queue sprawl. -- Keep tasks architecture-first. Build, test, tool, planner, CLI, and - automation cleanup are secondary unless they directly unblock or accompany a - real ownership transfer in the live app. -- Prefer coherent bundles over tiny adapter nibbles. Each task here should make - visible progress in a hotspot file or a legacy target, not just add another - seam around the same code. -- Use legacy-target shrinkage and hotspot reduction as the main progress signal. - If a slice does not materially reduce a large file or move code out of - `PP_PANOPAINTER_*` or `PP_LEGACY_*`, it is probably not first-priority work. -- Do not restart Vulkan, Metal, or package-only work until the app shell, - platform split, UI split, and canvas/render split are materially thinner. - -## Current Architecture Read - -- The extracted pure targets are real and useful: `pp_foundation`, - `pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`, - `pp_paint_renderer`, `pp_ui_core`, `pp_platform_api`, and `pp_app_core`. -- The remaining app still lives mostly in legacy containment or thick shell - targets: - - `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 -- The biggest remaining single-file pressure is no longer the old entrypoint - shell files; it now sits mostly in retained legacy service layers behind - `src/node_stroke_preview.cpp`, `src/app.cpp`, `src/app_dialogs.cpp`, and the - extracted canvas/platform containment files. -- The platform boundary is not finished: - - `pp_platform_api` no longer compiles Apple or Linux implementation files, - but broader concrete platform ownership is still not fully separated - - `platform_apple` no longer reaches `App::I` directly, and Linux FPS title - reporting now uses an injected callback, but retained Apple bridging and - broader platform-to-app singleton reach are still open in - `platform_legacy` - - Apple retained bridge/state ownership now lives in - `src/platform_apple/apple_platform_state.cpp` and - `src/platform_apple/apple_platform_services.*`, and the live Apple - `PlatformServices` surface now binds directly from those Apple-owned files, - and Android now also binds directly from - `src/platform_android/android_platform_services.*`, and Linux now also - binds directly from `src/platform_linux/linux_platform_services.*`, and Web - now also binds directly from `src/platform_web/web_platform_services.*`, - but the broader non-Windows fallback adapter still exists as generic - fallback policy - - the deleted `platform_legacy` shim is no longer in the root app graph, but - retained package/build compatibility edges still need cleanup -- The app runtime boundary is not finished: - - render/UI queues are static `App` state - - app-facing detached launches are no longer the main issue; preview and - recording now use owned worker threads, but those families still rely on - retained static/global ownership and ad hoc runtime control - - canvas async import/export/save/open now run through an owned in-file - worker, but their retained progress execution is still not a clean runtime - service boundary - - the Win32 input path now binds the active `WacomTablet*` through - `windows_runtime_state.*`, but that tablet binding still lives at a - composition edge instead of a broader runtime/platform-owned controller - - the touched Win32 startup/session flow now routes through - `windows_runtime_flow.*` instead of `windows_runtime_shell.cpp`, but the - broader runtime/thread host still owns composition-edge global state and - shutdown sequencing - - thread-affinity rules are enforced by convention and asserts instead of - explicit runtime contracts -- The UI ownership boundary is not finished: - - base `Node` still carries raw parent/manager pointers beside shared handles - - callback captures still frequently close over retained nodes and `App::I` - - checked overlay/lifetime helpers exist but are not yet the default scene - graph model -- Historical score/progress claims are not useful for prioritization here. - The live app still mostly runs through the same shell and hotspot files, so - the queue is now ordered by code movement instead. - -Current slice: -- `src/platform_windows/windows_runtime_flow.*` now owns the live Win32 - startup/session composition flow instead of leaving that body in - `src/platform_windows/windows_runtime_shell.cpp`. -- A bound runtime-flow owner now binds the live `MainWindowSession`, runs the - Win32 preflight startup steps, handles the existing `convert` / `-vrmode` - mode switch, and tears down the bound app/session through one destructor - path. -- `src/platform_windows/windows_runtime_shell.cpp` is down to a thin entry - wrapper over `run_main_application_flow(...)`, so the repeated app/session - teardown branches are gone from the shell. -- The orphaned `src/platform_legacy/legacy_platform_services.*` shim and - `src/platform_legacy/legacy_platform_fallback_behavior.h` helper are now - deleted. -- Root builds, the WebGL entrypoint, and the retained Android package graphs - had already moved off that fallback path, so the old generic legacy platform - adapter is no longer present 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 the retained - `src/platform_legacy/legacy_platform_services.cpp` shim. -- The retained Android package graphs now match the concrete Android - `create_platform_services(...)` path already used in - `android/src/cpp/main.cpp`. -- The retained standard Android package link gate still fails, but restoring - the old legacy source list reproduces a different unresolved-symbol link - failure too, so the package linker break is treated as preexisting noise for - this narrow source-list swap. -- `cmake/PanoPainterSources.cmake` no longer adds - `src/platform_legacy/legacy_platform_services.*` to - `PP_PANOPAINTER_APP_SOURCES`. -- The root `panopainter_app` modernization graph no longer compiles or links - the retained `platform_legacy` fallback shim. -- The retained file still exists on disk for non-root compatibility consumers, - but it is now outside the validated root 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 in `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 in - `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 in - `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. -- `src/platform_web/web_platform_services.*` now owns the concrete WebGL - `PlatformServices` surface in addition to the narrower helper interface used - by `pp_platform_api` tests. -- `webgl/src/main.cpp` now binds that owned Web `PlatformServices` instance - directly instead of constructing the retained `platform_legacy` adapter. -- The touched Web render-context acquire/present, app-close dispatch, - file/image picker routing, prepared-file handoff, and default render-target - binding now route through `src/platform_web/web_platform_services.*`. -- `src/platform_legacy/legacy_platform_services.*` no longer has a live - platform-entrypoint consumer and is now down to generic fallback scaffolding. -- 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 singleton does - not change the live entrypoint ownership path. -- `src/platform_windows/windows_runtime_state.*` now also owns the bound - Win32 `App*`, bound-runtime shutdown, and tablet binding surface alongside - the retained owned `App`, runtime, and tablet objects. -- `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 thinner and no longer open-codes that - retained runtime-state setup. -- `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. -- `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 thinner again and now - imports that binding surface from `windows_runtime_state.h` instead of - declaring shell-owned bind/release accessors itself. -- `src/platform_windows/windows_lifecycle_shell.cpp` now formats the FPS title - with a stack buffer at the callsite instead of keeping a retained - lifecycle-global scratch array. -- `src/platform_windows/windows_lifecycle_state.*` is now down to the 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. -- `src/platform_windows/windows_runtime_flow.cpp` is now only a thin handoff - into that session helper. -- `src/platform_windows/windows_runtime_shell.h` no longer exposes the removed - runtime-session entrypoint or carries bootstrap/splash declarations for it. -- `src/platform_windows/windows_runtime_shell.cpp` now calls - `run_bound_main_window_runtime(...)` directly instead of keeping a second - wrapper entrypoint around that seam. -- `src/main.cpp` now includes `windows_bootstrap_helpers.h` directly for - `run_winmain_entry(...)` instead of depending on a transitive declaration - through the shell header. -- `src/platform_windows/windows_runtime_flow.*` now owns the Win32 bound-app - startup/message-loop/shutdown orchestration. -- `src/platform_windows/windows_runtime_shell.cpp` now delegates that runtime - flow instead of carrying the whole orchestration pocket inline. -- `src/platform_windows/windows_runtime_shell.cpp` and - `src/platform_windows/windows_lifecycle_shell.cpp` now use the explicit - Windows runtime binding for thread start/stop control. -- `src/platform_windows/windows_platform_services.cpp` no longer uses - `bound_app()->runtime()` for main-thread task brokering. -- `src/platform_windows/windows_runtime_state.*` now carries the active - `AppRuntime*` binding used by that queue hook. -- `src/platform_windows/windows_runtime_state.*` now owns the retained Win32 - runtime object lifetime (`App`, `WacomTablet`). -- `src/platform_windows/windows_runtime_shell.cpp` no longer carries that - retained runtime-state storage directly. -- `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 keeps a separate - retained VR shell-state accessor. -- `src/platform_windows/windows_lifecycle_state.*` now owns the retained - Win32 lifecycle running flag and FPS-title scratch buffer. -- `src/platform_windows/windows_lifecycle_shell.cpp` no longer carries that - retained lifecycle state pocket. -- `src/platform_windows/windows_async_render_context.*` now owns the retained - Win32 async GL context lock/swap state. -- `src/platform_windows/windows_platform_services.cpp` no longer carries the - retained async render-context pocket. -- `src/platform_windows/windows_main_window_session.*` now owns the explicit - Win32 `MainWindowSession` object that the runtime shell binds for the live - session. -- `src/platform_windows/windows_runtime_shell.cpp` no longer carries the - retained `HWND` / title-buffer / sandbox pocket. -- `src/platform_legacy/legacy_platform_services.*` now takes Android storage - paths through explicit `create_platform_services(...)` config instead of the - legacy singleton state. -- `android/src/cpp/main.cpp` now seeds Android storage paths into its owned - legacy `PlatformServices` instance. -- `src/platform_legacy/legacy_platform_state.*` no longer keeps the Android - storage-path singleton API. -- `src/platform_legacy/legacy_platform_state.*` no longer keeps a separate - retained Web platform-service binding; the Web helper path now uses the - shared `platform_api` injection hook. -- `src/platform_legacy/legacy_platform_state.*` no longer keeps any retained - GLFW window state. -- `linux/src/main.cpp` and `webgl/src/main.cpp` no longer seed legacy GLFW - retained state. -- `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 uses an injected - `LegacyGlfwPlatformShell` for the remaining Linux/WebGL - acquire/present/request-close path. -- `linux/src/main.cpp` and `webgl/src/main.cpp` now bind those GLFW operations - directly from entrypoint-owned window state. -- `src/platform_legacy/legacy_platform_state.*` no longer carries the old GLFW - acquire/present/request-close helper trio. -- `src/platform_windows/windows_platform_services.cpp` now owns the Win32 - virtual-key map initialization path, so `windows_window_shell.cpp` no longer - carries that retained input setup. -- `src/platform_windows/windows_window_shell.*` no longer keeps the dead - retained raw key-state array. -- `linux/src/main.cpp` now owns the Linux FPS-title callback lifecycle - directly, and `src/app_events.cpp` no longer installs Linux-specific title - behavior from `App::set_platform_services()`. -- `src/platform_windows/windows_window_shell.*` now stores the Win32 virtual-key - map separately from the retained input-state struct. -- `src/platform_windows/windows_runtime_shell.cpp` no longer stores a second - retained `App*` alongside `App::I`. -- `src/platform_windows/windows_window_shell.*` plus - `src/platform_windows/windows_platform_services.cpp` now separate Win32 VR - state from the generic retained input-state bundle. -- `src/platform_legacy/legacy_platform_state.*` no longer keeps the dead - generic storage-path singleton for the current platform matrix. -- `src/platform_legacy/legacy_platform_services.cpp` now returns only - platform-specific storage-path branches; the dead generic fallback read is - gone. -- `src/legacy_app_startup_services.cpp` no longer writes the dead generic - storage-path fallback state on non-Android startup. -- `src/platform_legacy/legacy_platform_services.cpp` plus - `src/platform_legacy/legacy_platform_state.*` now give Android its own - retained storage-path surface instead of reading the shared legacy singleton. -- `src/legacy_app_startup_services.cpp` and `android/src/cpp/main.cpp` now - write Android-owned retained storage paths. -- `src/app_events.cpp`, `linux/src/main.cpp`, and - `src/platform_legacy/legacy_platform_state.*` now use a narrow GLFW title - helper for Linux FPS-title updates instead of direct retained-window access - in the app event layer. -- `src/platform_legacy/legacy_platform_state.h` no longer exports the raw - retained GLFW window accessor. -- `src/platform_apple/apple_platform_state.cpp` now reads Apple storage paths - from Apple-owned retained state rather than the shared legacy storage-path - singleton. -- `PanoPainter-OSX/main.cpp` and `PanoPainter/GameViewController.m` now seed - Apple-owned storage paths directly. -- `src/platform_legacy/legacy_platform_state.*` now only exposes - `create_legacy_web_platform_services()` for the retained default Web service - implementation. -- `webgl/src/main.cpp` now owns and binds the Web platform-services - implementation explicitly instead of relying only on the retained fallback - object in legacy platform state. -- `src/platform_windows/windows_runtime_shell.*` now threads `HINSTANCE` - through the startup/runtime path explicitly instead of keeping it in the - retained main-window session state. -- `webgl/src/main.cpp` now binds an owned legacy `PlatformServices` instance - instead of reading the process-global fallback directly during `StartApp()`. -- `android/src/cpp/main.cpp` now binds a function-lifetime owned legacy - `PlatformServices` instance in `android_main()`, replacing the direct bind to - the process-global fallback accessor. -- `src/platform_legacy/legacy_platform_services.*` now exposes an ownable - `create_platform_services()` entrypoint for explicit per-entrypoint - ownership. -- `linux/src/main.cpp` now binds an owned legacy `PlatformServices` instance - into `App`, making Linux the first explicit per-entrypoint owner of that - legacy service implementation. -- `src/app_events.cpp` no longer hides a fallback to - `pp::platform::legacy::platform_services()`; touched app platform dispatch - now expects an explicitly bound platform-services pointer. -- `linux/src/main.cpp`, `webgl/src/main.cpp`, and `android/src/cpp/main.cpp` - now bind owned `create_platform_services(...)` instances at app creation, - removing the hidden fallback path from live non-Windows entrypoints. -- `src/platform_windows/windows_runtime_shell.cpp` now owns the Windows tablet - object directly, removing the composition-edge `WacomTablet::I` binding from - the touched 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 that fallback behavior stays inline in the legacy platform - singleton. -- `src/platform_windows/windows_runtime_shell.cpp` no longer keeps a separate - retained `AppRuntime*`; the touched Windows lifecycle and main-thread task - dispatch paths now derive runtime ownership directly from the owned app. -- `src/platform_windows/windows_runtime_shell.cpp` now owns the Windows `App` - through a retained `std::unique_ptr`, so startup/early-return/convert - paths no longer manage raw `new`/`delete` app lifetime manually. -- `src/platform_windows/windows_lifecycle_shell.cpp` now releases the bound - app through `release_bound_app()` after runtime shutdown instead of deleting - it directly through the lifecycle shell. -- `src/platform_windows/windows_window_shell.cpp` now routes the touched - key-map synchronization and VR close state through narrow helpers instead of - carrying the broader retained window bundle live across the window-proc path. -- `src/platform_legacy/legacy_platform_state.*` no longer exports the mutable - retained GLFW hook bundle; Linux/Web fallback render-context, present, and - app-close paths now route through narrow GLFW helper functions instead. -- `scripts/automation/quiet-validate.ps1` now owns the recommended quiet - checkpoint path and can bundle Windows build/test, Android/platform sweeps, - and Apple remote compile gates into one compact JSON summary with - `-IncludePlatformBuild` and `-IncludeAppleRemote`. -- `scripts/automation/platform-build.ps1` now supports `-Quiet`, per-preset log - capture, and compact JSON-only output so standalone Android/headless sweeps - still have a targeted quiet path when they need to be isolated from the - bundled wrapper. -- `scripts/automation/apple-remote-build.ps1` now supports `-Quiet`, local SSH - session log capture, remote log path reporting, and JSON-only output so the - standalone Apple gate can still be run directly when the bundled wrapper is - too broad. - -## Active Bundles - -### Priority Order - -- `P0`: shrink the biggest live app hotspots and legacy ownership first -- `P0`: make app runtime, UI ownership, and renderer access safe enough for - future backend work -- `P1`: finish supporting boundaries that unblock or stabilize the thinner app -- `P2`: only then clean up the remaining workflow adapters - -### Bundle 1 - Break The Canvas And Preview Hotspots - -Priority: `P0` - -Why this bundle is first: -This is where the biggest block of real app behavior still lives. -If these files stay large and stateful, the rest of the modernization still -looks like a wrapper around the old renderer shell. - -#### ARC-RND-001 - Split `canvas.cpp` Into Document State And Render Execution Shells - -Status: In Progress - -Why now: -The live `Canvas` ownership boundary is still active, but `src/canvas.cpp` -itself is now down to a thin static singleton plus mode-table shell. The -remaining canvas pressure now sits in the extracted legacy canvas service -files and the preview/canvas node render paths rather than the old monolithic -translation unit. - -Current slice: -- The remaining `Canvas` member wrappers in `src/canvas.cpp` now live in the - extracted canvas service files, leaving `canvas.cpp` as the static singleton - and mode-table shell. -- Canvas state-management helpers for picking, clear/clear-all, layer - add/remove/order/lookups, animation frame control, resize, and snapshot - save/restore now live in `src/legacy_canvas_state_services.cpp` instead of - staying inline in `src/canvas.cpp`, but the file still owns the larger - document-plus-render shell. -- Canvas import/export/save/open/thumbnail ownership now lives in - `src/legacy_canvas_document_io_services.cpp` instead of staying inline in - `src/canvas.cpp`, which materially reduces document IO ownership in the live - render shell. -- Canvas point-trace, unproject, project2D, face-to-shape, and camera - push/pop/get/set helpers now also live in - `src/legacy_canvas_projection_services.*` instead of staying inline in - `src/canvas.cpp`, which trims another coherent non-UI state/query pocket - from the live canvas shell. -- `Canvas::draw_objects_direct(...)` and `Canvas::draw_objects(...)` now also - live in `src/legacy_canvas_object_draw_services.*` instead of staying inline - in `src/canvas.cpp`, which trims another coherent object-draw and - viewport-state execution pocket from the live canvas shell. -- `Canvas::stroke_draw_samples(...)`, `Canvas::stroke_commit()`, and the - larger stroke sample/commit execution family now also live in - `src/legacy_canvas_stroke_commit_services.*` instead of staying inline in - `src/canvas.cpp`, which trims another large retained stroke-render pocket - from the live canvas shell. -- Shared canvas-mode GL wrappers plus the `CanvasModeBasicCamera` and - `CanvasModeCamera` input handlers now also live in - `src/legacy_canvas_mode_helpers.*` instead of staying inline in - `src/canvas_modes.cpp`, which trims another coherent retained canvas-view - interaction pocket from the broader canvas/render hotspot family. -- The `CanvasModeTransform` interaction family now also lives in - `src/legacy_canvas_mode_transform.cpp` instead of staying inline in - `src/canvas_modes.cpp`, which materially thins another retained - transform-mode pocket from the broader canvas/render hotspot family. -- The `CanvasModeFill` and `CanvasModeFloodFill` interaction families now - also live in `src/legacy_canvas_mode_fill.cpp` instead of staying inline in - `src/canvas_modes.cpp`, which materially thins another retained fill-mode - pocket from the live canvas-mode shell. -- The live `Canvas::stroke_draw()` orchestration now also lives in - `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 from the canvas shell. -- `Canvas::layer_merge(...)`, `Canvas::flood_fill(...)`, and - `Canvas::FloodData::apply()` now also live in - `src/legacy_canvas_layer_services.cpp` instead of staying inline in - `src/canvas.cpp`, which trims another coherent retained layer/fill workflow - pocket from the live canvas shell. -- `Canvas::stroke_end(...)`, `Canvas::stroke_cancel(...)`, - `Canvas::stroke_draw_mix(...)`, `Canvas::stroke_draw_project(...)`, - `Canvas::stroke_update(...)`, and `Canvas::stroke_start(...)` now also live - in `src/legacy_canvas_stroke_runtime_services.*` instead of staying inline in - `src/canvas.cpp`, which trims another large retained live stroke/runtime - pocket from the canvas shell. -- `Canvas::draw_merge(...)`, the temporary-paint and merge-branch - orchestration helpers, final-plane composite, timelapse commit, create, - destroy, clear-context, and camera accessors now also live in - `src/legacy_canvas_render_shell_services.*` instead of staying inline in - `src/canvas.cpp`, which trims another large retained render/context shell - from the live canvas owner. -- 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. -- 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. -- The `CanvasModePen` and `CanvasModeLine` interaction families now also live - in `src/legacy_canvas_mode_pen_line.cpp` instead of staying inline in - `src/canvas_modes.cpp`, which materially thins another retained pen/line - interaction pocket from the broader canvas/render hotspot family. -- The `CanvasModeMaskFree` and `CanvasModeMaskLine` interaction families now - also live in `src/legacy_canvas_mode_mask.cpp` instead of staying inline in - `src/canvas_modes.cpp`, which materially thins another retained mask-tool - interaction pocket from the broader canvas/render hotspot family. - -Write scope: -- `src/canvas.cpp` -- `src/canvas_layer.cpp` -- `src/canvas_actions.cpp` -- `src/legacy_canvas_*services*.h` -- new `src/legacy_canvas_*.*` helpers if needed - -Read scope: -- `src/document/*` -- `src/paint_renderer/*` -- `src/renderer_api/*` - -Done when: -- `canvas.cpp` stops being the place where document state, render sequencing, - history reads, and GL-side execution all meet. -- Non-render state/query/update helpers move out first. -- The remaining file reads as a render shell with explicit helper boundaries. -- The touched slice makes a substantial reduction in inline ownership, not just - a thin wrapper extraction. - -Mini-model packet: -- Do not try to delete `Canvas` in one slice. -- Prioritize ownership separation over clever abstractions. -- Aim for a visible reduction in `canvas.cpp`, on the order of hundreds of - lines, not a token helper extraction. - -#### ARC-RND-002 - Isolate Preview And Canvas View Render Execution - -Status: In Progress - -Why now: -`src/node_stroke_preview.cpp` and `src/node_canvas.cpp` still own a large amount -of live preview/canvas render sequencing around the renderer boundary. - -Current slice: -- `NodeStrokePreview` final composite plus preview-texture copy now route - through `legacy_node_stroke_preview_execution_services.h`, but the preview - node still owns most live-pass and retained GL resource execution. -- `NodeStrokePreview` render-target setup plus immediate-pass sequence - orchestration now also route through - `legacy_node_stroke_preview_execution_services.h`, and duplicate render- - target setup was removed from `render_to_image()` and the queued worker path, - but the preview node still owns broader live-pass state and thread-facing - orchestration. -- `NodeStrokePreview` main live-pass request assembly and preview framebuffer- - copy setup now also route through - `legacy_node_stroke_preview_execution_services.h`, which trims another - coherent pass-setup block from `src/node_stroke_preview.cpp`, but broader - preview-pass orchestration is still inline. -- `NodeStrokePreview` remaining mix-pass planning and execution now also route - through `legacy_node_stroke_preview_draw_services.*`, which trims the last - dedicated mix-orchestration pocket from `src/node_stroke_preview.cpp`. -- `NodeStrokePreview::draw_stroke_immediate()` immediate preview pass - sequencing now also routes through the private - `execute_stroke_draw_immediate_pass_sequence(...)` helper, which removes - another live orchestration block from the node even though worker/readback - flow still remains in the file. -- `NodeStrokePreview` stroke preparation, dual-brush setup, and live-pass - request assembly now also route through retained preview execution helpers, - which trims another coherent setup pocket from - `src/node_stroke_preview.cpp` even though worker/readback ownership and - broader preview flow still remain inline. -- `src/node_stroke_preview.cpp` is now 192 lines after moving the remaining - preview GL dispatch and texture-slot binding pocket into - `legacy_node_stroke_preview_runtime_services.*`. -- The remaining immediate preview pass shell in - `NodeStrokePreview::draw_stroke_immediate()` now also routes through - `execute_legacy_node_stroke_preview_draw_immediate_shell(...)`, which - materially reduces the live preview-pass body even though worker/readback - ownership still remains inline. -- `NodeCanvas` merged-path per-plane merged-texture draw execution now also - routes through `execute_legacy_canvas_draw_merge_layer_texture(...)`. -- `NodeCanvas` merged-path and non-blend checkerboard background setup now also - route through `execute_legacy_canvas_draw_merge_background_setup(...)`. -- `NodeCanvas` cache-to-screen checkerboard-plane callback setup now also routes - through a retained helper in `legacy_canvas_draw_merge_services.h`. -- `NodeCanvas` display resolve plus cache-to-screen checkerboard/cache-texture - composite now route through `legacy_canvas_draw_merge_services.h`. -- `NodeCanvas` smoothing-mask overlay, smoothing-mask face pass, grid keepalive - draw, heightmap draw, and current-mode draw now also route through - `execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw - orchestration is still inline. -- `NodeCanvas` heightmap draw callback setup now also routes through - `make_legacy_canvas_draw_merge_heightmap_draw(...)`, but the node still owns - current-mode traversal and broader post-draw orchestration. -- `NodeCanvas` current-mode draw callback setup now also routes through - `make_legacy_canvas_draw_merge_current_modes_draw(...)`, and grid-mode - callback setup now also routes through - `make_legacy_canvas_draw_merge_grid_modes_draw(...)`, but broader post-draw - orchestration is still inline. -- `NodeCanvas` checkerboard background plane callback setup now also routes - through `make_legacy_canvas_draw_merge_background_checkerboard_plane(...)`, - but the node still owns broader live layer traversal and renderer-state - sequencing. -- `NodeCanvas` merged-path per-plane merged-texture draw callback setup now - also routes through `make_legacy_canvas_draw_merge_layer_texture_draw(...)`, - but the node still owns broader live layer traversal and renderer-state - sequencing. -- `NodeCanvas` non-`draw_merged` per-frame layer draw callback setup now also - routes through `make_legacy_canvas_draw_merge_layer_frame_draw(...)`, but - the node still owns broader live layer traversal and renderer-state - sequencing. -- `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution - now also route through - `execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns - the broader canvas draw flow and renderer-state sequencing around that seam. -- `NodeCanvas` non-`draw_merged` per-layer/per-plane retained draw execution - now also routes through `execute_legacy_canvas_draw_merge_layer_plane(...)`, - but the node still owns substantial live layer traversal and renderer state - orchestration. -- `NodeCanvas` post-draw callback assembly now also routes through - `execute_legacy_canvas_draw_merge_post_draw_callbacks(...)`, and the - remaining per-layer render-path orchestration now also routes through - `execute_legacy_canvas_draw_merge_layer_path(...)`, but the node still owns - broader draw-loop and renderer-state shell sequencing. -- `NodeCanvas` outer non-`draw_merged` layer/plane traversal, onion-range - failure handling, and visit payload setup now also route through - `execute_legacy_canvas_draw_layer_traversal(...)`. -- `NodeCanvas` non-`draw_merged` per-layer temporary erase/paint, layer-texture, - and blend setup now also route through - `make_legacy_canvas_draw_merge_layer_path_gl_execution(...)`, but the node - still owned the remaining draw lambdas and broader renderer-state shell. -- `NodeCanvas` now routes that remaining non-`draw_merged` per-layer blend, - composite, debug-outline, and frame callback assembly through the local - `make_node_canvas_layer_path_execution(...)` helper, which materially thins - `NodeCanvas::draw()` even though the broader draw loop still lives in - `src/node_canvas.cpp`. -- `NodeCanvas` post-draw callback assembly, smoothing-mask face execution, and - optional display resolve now also route through - `execute_node_canvas_draw_merge_tail(...)`, which trims another live tail - block from `NodeCanvas::draw()` even though the broader outer draw shell is - still inline. -- `NodeCanvas` non-`draw_merged` cache/background/layer-traversal/cache- - composite shell now also routes through - `execute_legacy_canvas_draw_unmerged_shell(...)`, which removes another - coherent orchestration block from `NodeCanvas::draw()` even though the - broader node draw loop still lives in `src/node_canvas.cpp`. -- `NodeCanvas` 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(...)`, which trims - another outer draw-shell block even though the broader node draw loop still - lives in `src/node_canvas.cpp`. -- `NodeCanvas` broader unmerged cache/viewport/background/composite pass setup - now also routes through - `execute_legacy_canvas_draw_unmerged_node_canvas_pass(...)`, which removes - another coherent outer-shell block even though the broader node draw loop - still lives in `src/node_canvas.cpp`. -- `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. -- `NodeCanvas::draw()` unmerged-pass blend-gate query, layer-orientation - assembly, and callback wiring into - `execute_legacy_canvas_draw_node_canvas_unmerged_pass(...)` now also route - through `execute_node_canvas_draw_unmerged_pass(...)`, which trims another - coherent unmerged draw-orchestration block from the live node even though - the file size remains roughly flat. -- `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-orchestration block from the live node even - though the broader draw loop still remains inline. -- `NodeCanvas::handle_event()` now routes through - `handle_legacy_node_canvas_event(...)` in - `src/legacy_canvas_tool_services.*`, which moves the live input/controller - pocket out of `src/node_canvas.cpp` and keeps the node on a thinner shell. -- `App::dialog_layer_rename()` now routes through - `open_legacy_document_layer_rename_dialog(...)` in - `src/legacy_document_layer_services.*`, which moves the remaining dialog - overlay-open/wire/close workflow out of `src/app_dialogs.cpp` and leaves - the app dialog shell thinner. -- `App::open_document(...)` now routes through - `execute_legacy_document_open(...)` in `src/legacy_document_open_services.*`, - which moves route classification and unsaved-project gating out of - `src/app.cpp` and into the retained document-open helper. -- `NodeCanvas` restore/clear context, resize handling, camera reset, buffer - creation, cursor visibility/update, tick, and destroy ownership now also - live in `src/legacy_node_canvas_state_services.*` instead of staying inline - in `src/node_canvas.cpp`, which materially thins another retained - state/control pocket without reopening the draw path. -- `NodeStrokePreview` retained lifecycle, worker-thread shell, render-to-image, - on-screen handling, and preview texture ownership now also live in - `src/legacy_node_stroke_preview_runtime_services.cpp` instead of staying - inline in `src/node_stroke_preview.cpp`, which materially thins the preview - node around its runtime-facing shell even though live pass execution still - remains. -- The remaining low-level preview GL/runtime pocket for viewport queries, - clear-color restore, texture-slot binding, mixer unbind, and - destination/pattern texture plumbing now also lives in - `src/legacy_node_stroke_preview_runtime_services.*` instead of staying - inline in `src/node_stroke_preview.cpp`, which trims another coherent - runtime-facing shell pocket from the live node. -- The remaining live render/pass orchestration in - `NodeStrokePreview::draw_stroke_immediate()` now also lives in - `src/legacy_node_stroke_preview_draw_services.*` instead of staying inline in - `src/node_stroke_preview.cpp`, which trims another coherent preview - draw-pass pocket while preserving the current runtime-facing shell. -- The immediate preview runtime/orchestration block in - `NodeStrokePreview::draw_stroke_immediate()` for stroke setup, prepared - stroke construction, pass planning, shader setup, and live render request - assembly now also lives in - `src/legacy_node_stroke_preview_runtime_services.*` instead of staying - inline in `src/node_stroke_preview.cpp`, which trims another coherent - preview runtime pocket while preserving the current live draw path. -- 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`, which trims another coherent preview - execution pocket while preserving the live draw path. -- `NodeStrokePreview::draw_stroke_immediate()` now also 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 and leaves - `src/node_stroke_preview.cpp` at 160 lines. -- `NodeStrokePreview` clone-finalize setup, stroke-frame planning, mix-pass - adapter wiring, sample-pass adapter wiring, and the remaining - immediate-draw request construction now also route through - `src/legacy_node_stroke_preview_runtime_services.*`, - `src/legacy_node_stroke_preview_draw_services.*`, and - `src/legacy_node_stroke_preview_sample_services.*`, which leaves - `src/node_stroke_preview.cpp` at 76 lines. -- `CanvasModeGrid` plus `ActionModeGrid` undo/redo now also live in - `src/legacy_canvas_mode_helpers.cpp` instead of staying inline in - `src/canvas_modes.cpp`, which leaves the live canvas-modes file as a - minimal shell. -- `NodeCanvas::init()` plus the remaining `NodeCanvas::draw()` outer shell now - also live in `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. -- The remaining generic node geometry/state pocket for `Node::SetSize(...)`, - `SetMinSize(...)`, `SetMaxSize(...)`, and `SetPosition(const glm::vec2)` - now also lives in `src/legacy_ui_node_style.*` instead of staying inline in - `src/node.cpp`, which trims another coherent base scene-graph shell pocket - without changing the public surface. -- `Node::app_redraw()` and `Node::watch(...)` now also live in - `src/legacy_ui_node_execution.cpp` instead of staying inline in `src/node.cpp`, - which trims another small generic node utility pocket from the live file. -- `NodePanelBrushPreset` global panel registration now also lives in - `src/legacy_brush_preset_list_services.*` instead of staying on the live - node type as a static registry field, which trims another retained - controller-state pocket from `src/node_panel_brush.cpp`. -- The remaining `NodePanelBrush` and `NodePanelBrushPreset` member bodies now - also 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.*`), which leaves - `src/node_panel_brush.cpp` as a 2-line translation unit. - -Write scope: -- `src/node_stroke_preview.cpp` -- `src/node_canvas.cpp` -- `src/legacy_node_stroke_preview_execution_services.h` -- `src/legacy_canvas_stroke_preview_services.h` -- `src/paint_renderer/compositor.*` - -Read scope: -- `src/renderer_api/*` -- `src/renderer_gl/*` - -Done when: -- Preview/canvas files stop carrying large inline render-pass orchestration. -- Concrete GL texture/bind/copy work is pushed behind explicit renderer-facing - service seams. -- The next renderer backend would have one place to implement preview/canvas - execution contracts instead of reading node code. -- The touched node files are materially smaller and less stateful afterward. - -Mini-model packet: -- Keep the existing `pp_paint_renderer` planner surface and extend it only when - the node files clearly need a missing renderer-owned callback contract. -- Prefer deleting inline orchestration over adding another planner layer around - the same node code. - -#### ARC-RND-003 - Replace App-Facing `Texture2D`/`RTT` Use With Renderer API Contracts - -Status: In Progress - -Why now: -Future Vulkan and Metal work needs the live app to stop treating retained -OpenGL resource classes as the renderer boundary. - -Write scope: -- `src/node_canvas.cpp` -- `src/node_stroke_preview.cpp` -- `src/canvas.cpp` -- `src/texture.*` -- `src/rtt.*` -- `src/renderer_api/*` -- `src/paint_renderer/*` - -Read scope: -- `src/renderer_gl/*` -- `src/legacy_gl_*dispatch.h` -- `src/app_shaders.cpp` - -Done when: -- App/UI-facing render work talks to `pp_renderer_api` resource and command - abstractions or narrow retained renderer services, not directly to - `Texture2D`, `RTT`, or GL dispatch helpers. -- The remaining `Texture2D`/`RTT` references are contained in retained GL - backend/adapters with explicit removal conditions. -- Canvas, preview, and export paths expose the same execution contract a Vulkan - or Metal implementation would need. -- The touched slice shrinks app/UI render ownership instead of only renaming - wrappers. - -Mini-model packet: -- Do not start a Vulkan or Metal backend in this task. -- Use existing `pp_renderer_api` and `pp_paint_renderer` contracts first. -- Treat direct GL classes in UI/app code as debt to move behind backend-owned - services. - -### Bundle 2 - Thin The App Shell - -Priority: `P0` - -Why this bundle is next: -`app_layout.cpp`, `app_dialogs.cpp`, and `app.cpp` still make the modernized -targets look like helpers under one old monolith. - -#### ARC-APP-001 - Split `app_layout.cpp` Into UI Binding Modules - -Status: In Progress - -Why now: -`src/app_layout.cpp` is still a 125-line mixed file that builds menus, -attaches callbacks, computes planner inputs, and mutates UI state directly. - -Current slice: -- Tools-menu UI binding, including the nested panels/options submenu wiring, - now lives in `src/app_layout_tools_menu.cpp`, and `App::init_menu_tools()` - is now a thin call-through, but file/about/layer/sidebar and broader layout - composition are still inline in `src/app_layout.cpp`. -- File-menu UI binding, including the export submenu wiring, now also lives in - `src/app_layout_file_menu.cpp`, and `App::init_menu_file()` is now a thin - call-through, but about/layer/sidebar and broader layout composition are - still inline in `src/app_layout.cpp`. -- 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, but edit/sidebar and - broader layout composition are still inline in `src/app_layout.cpp`. -- Sidebar panel binding plus popup wiring now also live in - `src/app_layout_sidebar.cpp`, and `App::init_sidebar()` is now a thin - call-through, but edit-menu wiring and broader layout composition are still - inline in `src/app_layout.cpp`. -- Main-toolbar binding now also lives in `src/app_layout_main_toolbar.cpp`, - and `App::init_toolbar_main()` is now a thin call-through, but edit-menu - wiring and broader layout composition are still inline in `src/app_layout.cpp`. -- Edit-menu binding now also lives in `src/app_layout_edit_menu.cpp`, and - `App::init_menu_edit()` is now a thin call-through, but draw-toolbar, - brush-refresh, and broader layout composition are still inline in - `src/app_layout.cpp`. -- UI-direction plus persisted floating/docked panel-state ownership now also - lives in `src/app_layout_ui_state.cpp`, and `src/app_layout.cpp` is down to - the remaining draw-toolbar, brush-refresh, and layout/bootstrap composition. -- Draw-toolbar binding now also lives in `src/app_layout_draw_toolbar.cpp`, and - `src/app_layout.cpp` is down to the remaining brush-refresh and - layout/bootstrap composition. -- Brush-refresh now also lives in `src/app_layout_brush.cpp`, and - `src/app_layout.cpp` is down to the remaining layout/bootstrap composition. -- Layout bootstrap plus reload/load continuation wiring now also lives in - `src/app_layout_bootstrap.cpp`, and `src/app_layout.cpp` is down to thin - call-through entrypoints plus the remaining local helper pocket. - -Write scope: -- `src/app_layout.cpp` -- `src/legacy_app_shell_services.*` -- new `src/app_layout_*.*` or `src/legacy_*_ui_services.*` files if needed - -Read scope: -- `src/app_core/*menu*.h` -- `src/app_core/brush_ui.h` -- `src/app_core/document_layer.h` -- `src/app_core/main_toolbar.h` - -Done when: -- `app_layout.cpp` becomes a composition/binding file instead of a giant mixed - controller. -- File-menu, toolbar, tools-menu, about-menu, and layer-menu wiring each live - in named helper modules or services. -- The split follows planner/service boundaries already present in `pp_app_core`. -- The touched slice materially shrinks the file instead of just moving a few - lambdas around. - -Mini-model packet: -- Start by carving out one coherent family at a time, not by reshuffling lines. -- Preserve the current planner calls; the goal is ownership, not new behavior. -- Aim for a real file-size drop, not cosmetic decomposition. - -#### ARC-APP-002 - Split `app_dialogs.cpp` Into Workflow Adapters And Widget Openers - -Status: In Progress - -Why now: -`src/app_dialogs.cpp` still mixes document workflow decisions, export routing, -dialog construction, and overlay ownership in one 106-line shell. - -Current slice: -- Informational overlay opener paths for user manual, changelog, about, - what's-new, and shortcuts now live in `src/app_dialogs_info_openers.cpp`, - and the corresponding `App::dialog_*` entrypoints are now thin call-throughs, - but document/export workflow and retained dialog execution are still inline in - `src/app_dialogs.cpp`. -- Export, video-export, and PPBR export entrypoints now also live in - `src/app_dialogs_export.cpp`, and the corresponding `App::dialog_*` - entrypoints are now thin call-throughs, but new/open/save/browse/resize and - retained dialog execution are still inline in `src/app_dialogs.cpp`. -- New/open/save/browse/resize workflow entrypoints now also live in - `src/app_dialogs_workflow.cpp`, and the layer-rename dialog open / wire / - close pocket now lives in `src/legacy_document_layer_services.*`, leaving - `src/app_dialogs.cpp` down to the remaining thin entrypoints plus other - retained dialog glue. -- `App::open_document()` now routes document classification and unsaved- - project gating through `src/legacy_document_open_services.cpp` instead of - keeping that shell pocket inline in `src/app.cpp`, which trims the live app - entry file a little further while preserving the retained open-action - behavior. -- `App::show_progress(...)`, `App::message_box(...)`, and - `App::input_box(...)` now also route through - `src/legacy_app_dialog_services.*` instead of building dialog plans and - factories inline in `src/app_dialogs.cpp`, which trims another coherent - dialog-construction pocket from the remaining app dialog shell. - -Write scope: -- `src/app_dialogs.cpp` -- `src/legacy_app_dialog_services.*` -- `src/legacy_document_session_services.*` -- `src/legacy_document_open_services.*` -- `src/legacy_document_export_services.*` - -Read scope: -- `src/app_core/app_dialog.h` -- `src/app_core/document_session.h` -- `src/app_core/document_export.h` - -Done when: -- `app_dialogs.cpp` is reduced to thin entrypoints plus named helper modules. -- Dialog creation/opening is clearly separated from document/export workflow - routing. -- The remaining direct node-specific code is isolated to retained dialog - adapters. -- The slice removes a meaningful amount of mixed live ownership from - `app_dialogs.cpp`. - -Mini-model packet: -- Preserve existing planner usage. -- Prefer new narrow helper files over leaving another giant dialog utility file. -- Do not spend time extending dialog planners or CLI surfaces unless the live - adapter gets thinner in the same slice. - -#### ARC-APP-003 - Reduce `app.cpp` To Frame, Queue, And Composition Shell - -Status: In Progress - -Why now: -`src/app.cpp` still carries startup, frame flow, queue draining, recording, -and composition logic in one 171-line file. - -Current slice: -- UI observer math now routes through `src/legacy_app_frame_services.cpp` - instead of staying inline in `src/app.cpp`. -- The repeated UI child traversal in `App::draw()` now routes through the same - retained helper. -- Canvas toolbar refresh in `App::update()` now also routes through that helper - file, materially shrinking `src/app.cpp`. -- `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 live in - `src/legacy_app_runtime_shell_services.cpp` instead of staying inline in - `src/app.cpp`, which materially thins the remaining frame/runtime shell. -- `App::create(...)`, `App::initAssets(...)`, `App::initLog(...)`, and - `App::init(...)` now also live in `src/legacy_app_startup_services.*` - instead of staying inline in `src/app.cpp`, which reduces the remaining app - file to a thinner retained composition surface around startup and runtime - delegation. -- `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`, which reduces the remaining app file to a thinner - retained shell around document routing and runtime thread entrypoints. -- `App::request_close()`, `App::renderdoc_frame_start()`, - `App::renderdoc_frame_end()`, and the render/UI thread entrypoint wrappers - now also route through `src/legacy_app_runtime_shell_services.*` instead of - staying inline in `src/app.cpp`, which reduces the live app file to a much - thinner retained composition shell. - -Write scope: -- `src/app.cpp` -- `src/legacy_app_startup_services.*` -- `src/legacy_recording_services.*` -- small new `src/app_runtime_*.*` helpers if needed - -Read scope: -- `src/app_core/app_frame.h` -- `src/app_core/app_shutdown.h` -- `src/app_core/app_startup.h` -- `src/app_core/document_recording.h` - -Done when: -- `app.cpp` reads like a shell over `pp_app_core` planners and named retained - services. -- Startup, frame/update, queue/thread, and recording glue are split into named - helpers instead of living inline. -- `App` keeps ownership of composition state only where it truly has to. -- The file becomes materially thinner in the same slice. - -Mini-model packet: -- Keep thread behavior unchanged. -- Split by responsibility boundaries already present in `pp_app_core`. -- Prefer moving live ownership out over creating new planner wrappers. - -#### ARC-APP-004 - Move Render/UI Queues Into An Owned App Runtime Service - -Status: In Progress - -Why now: -`App` still owns static render/UI queues, mutexes, condition variables, and -thread ids. That makes thread safety hard to reason about and keeps platform -entrypoints coupled to the singleton. - -Current slice: -- render/UI queues, mutexes, condition variables, and thread identity already - live in `AppRuntime` -- `AppRuntime` render/UI worker ownership now also uses `std::jthread` plus - explicit stop requests instead of raw `std::thread` -- Windows main-loop run-state and VR worker coordination flags in `main.cpp` - now use `std::atomic` ownership instead of unsynchronized globals -- `main.cpp` Win32 window handles, GL task/mutex state, splash-dialog state, - stylus timers, and VR worker state now sit behind one retained local state - object instead of separate file-scope globals -- the splash-screen dialog loop, HWND ownership, and bitmap setup now also live - in `src/platform_windows/windows_splash.cpp` instead of `src/main.cpp`, and - `main.cpp` now just owns the helper lifecycle -- Win32 async GL/context lock state now lives in - `src/platform_windows/windows_platform_services.cpp` instead of `main.cpp` - retained state, and `main.cpp` only seeds that platform-owned context handle - pair during initialization and context recreation -- `main.cpp` main-thread queued task state now lives under - `src/platform_windows/windows_platform_services.cpp` instead of staying in - the entry TU -- Win32 pointer API loading, stylus/ink timer ownership and decay, `WT_PACKET` - reset, and `WM_POINTERUPDATE` pen/touch handling now live in - `src/platform_windows/windows_stylus_input.cpp` instead of `src/main.cpp`, - but broader retained Win32 shell state is still open -- the retained Win32 VR/HMD shell, including worker start/stop and VR FPS - state, now routes through `src/platform_windows/windows_vr_shell.h` instead - of staying inline in `src/main.cpp`, but broader retained Win32 shell state - is still open -- RenderDoc startup/frame capture, SHCore DPI bootstrap, Win32 error-string - conversion, `UnadjustWindowRectEx`, and GL debug pre/post callbacks now also - live in `src/platform_windows/windows_bootstrap_helpers.cpp` instead of - `src/main.cpp` -- the WMI startup probe now also lives in - `src/platform_windows/windows_bootstrap_helpers.cpp` instead of - `src/main.cpp` -- Win32 lifecycle running-state, close/shutdown handling, FPS title update and - 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` -- Win32 startup/window/bootstrap flow now also lives in - `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 -- BugTrap/SEH recovery setup now also lives in - `src/platform_windows/windows_bootstrap_helpers.cpp` instead of - `src/main.cpp` -- the Win32 window procedure and retained message-handling shell now also live - in `src/platform_windows/windows_window_shell.*` instead of `src/main.cpp`, - which materially thins the entry file even though broader runtime/entrypoint - composition is still open -- the `WinMain` argv conversion bridge now also lives in - `src/platform_windows/windows_bootstrap_helpers.*` instead of staying inline - in `src/main.cpp`, which trims another Windows-specific entrypoint pocket -- retained input-state zeroing and reverse key-map initialization now also live - in `src/platform_windows/windows_window_shell.*` instead of `src/main.cpp`, - which trims another small but real retained-state pocket from the entry TU -- the remaining interactive Win32 runtime pocket for touch registration, - render/UI thread startup, debug GL callback hookup, Wintab init/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 reduces the entry TU to a much smaller composition root -- the remaining Windows app shell in `main(...)` now also routes through - `run_main_application(...)` in `src/platform_windows/windows_runtime_shell.*`, - which reduces `src/main.cpp` to a minimal `main`/`WinMain` wrapper -- the retained Win32 main-thread task queue now also lives in `AppRuntime` - instead of `src/platform_windows/windows_platform_services.cpp`, which - removes another runtime queue from retained platform-local static state -- prepared-file background work now runs through an `AppRuntime`-owned worker - queue instead of a retained static worker in `src/app_events.cpp` -- canvas async import/export/save/open background work now also runs through an - `AppRuntime`-owned worker queue instead of a retained static worker in - `src/canvas.cpp` -- `App::request_close()` plus the render/UI thread entrypoint wrappers now - also route through `src/legacy_app_runtime_shell_services.*`, which further - reduces direct runtime orchestration living on `src/app.cpp` even though the - owned runtime contract is still centered on `AppRuntime` -- retained `App` composition, task call sites, and platform/runtime entrypoint - coupling are still not fully reduced behind the runtime contract - -Write scope: -- `src/app.h` -- `src/app.cpp` -- `src/app_events.cpp` -- `src/main.cpp` -- new `src/app_runtime_*.*` or retained runtime service files if needed - -Read scope: -- `src/app_core/app_thread.h` -- `src/platform_api/platform_services.h` -- render/UI task call sites under `src/*.cpp` - -Done when: -- Render and UI task queues are owned by an explicit runtime object or service - with startup, drain, stop, and thread-affinity APIs. -- `App` composes that runtime instead of exposing static global queue state. -- Platform event code and retained services post work through the runtime - contract rather than by reaching `App::I` static queues. -- Shutdown behavior remains deterministic and the touched slice reduces - singleton/thread coupling. - -Mini-model packet: -- Keep public behavior and thread ordering unchanged. -- Prefer a small runtime owner over broad task-system redesign. -- Make ownership and shutdown semantics explicit before adding new features. - -#### ARC-APP-005 - Replace Detached App Workers With Joinable Or Service-Owned Work - -Status: In Progress - -Why now: -The biggest app-facing async families have been moved off detached launches, -but retained worker ownership and ad hoc runtime control are still not a safe -modernization foundation. - -Current slice: -- app-owned render/UI runtime queues and cloud worker ownership are already - moving behind owned runtime/service objects -- Windows splash-dialog and HMD renderer worker ownership in `main.cpp` now - also use `std::jthread` with explicit stop requests instead of raw - `std::thread` -- Windows VR device ownership in `main.cpp` now also uses `std::unique_ptr` - instead of a raw `Vive*` -- `LogRemote` worker ownership in `src/log.*` now also uses `std::jthread` - with explicit stop requests instead of raw `std::thread` -- brush package import/export now use service-owned `std::jthread` workers and - UI-thread completion handoff -- prepared-file save work and grid lightmap launch now also use service-owned - workers with explicit UI-thread handoff -- canvas async import/export/save/open and timelapse export now also use owned - worker queues instead of detached threads -- `src/app_events.cpp` prepared-file worker ownership and `src/canvas.cpp` - async import/export/save/open worker ownership now also sit behind named - retained local worker-state helpers instead of bare static worker accessors -- the prepared-file worker and the broader canvas async import/export/save/open - worker have now both moved into `AppRuntime`, removing the retained static - workers from `src/app_events.cpp` and `src/canvas.cpp` -- preview background rendering, recording, and the retained - `NodePanelGrid::bake_uvs()` worker now also use `std::jthread`, but their - retained loop/control flow is still open -- `App::rec_loop()` now delegates its worker-iteration orchestration into the - retained recording bridge in `src/legacy_recording_services.cpp`, while - `App::update()` no longer carries the dead update mutex residue; retained - recording loop control, readback ownership, and MP4 execution are still open -- `App::update_rec_frames()` now delegates recording label refresh through - `src/legacy_recording_services.cpp`, but retained recording label lookup, - encoder-state reads, and MP4 execution still stay on the legacy bridge - -Write scope: -- `src/canvas.cpp` -- `src/app_cloud.cpp` -- `src/app_events.cpp` -- `src/legacy_cloud_services.*` -- `src/legacy_brush_package_import_services.*` -- `src/legacy_brush_package_export_services.*` -- `src/legacy_grid_ui_services.*` -- `src/node_dialog_cloud.*` -- `src/node_stroke_preview.*` - -Read scope: -- `src/app_core/app_thread.h` -- `src/foundation/task_queue.*` -- `src/legacy_recording_services.*` - -Done when: -- Touched worker families are owned by joinable `std::jthread`, a scoped worker - object, or an injected task service with cancellation/shutdown semantics. -- Worker callbacks do not capture raw retained nodes or `this` across unknown - lifetime without a checked handle, weak ownership, or explicit owner. -- App shutdown can stop the touched worker family without racing UI/layout or - renderer destruction. -- Detached `std::thread` count drops materially in app-facing code. - -Mini-model packet: -- Start with one coherent worker family, such as cloud or brush package import. -- Do not rewrite all threading at once. -- Preserve the existing UI/progress behavior while changing ownership. - -### Bundle 3 - Finish The UI Core Split - -Priority: `P0` - -Why this bundle is still top priority: -Until generic `Node` and control code leaves `pp_legacy_ui_core`, the UI -architecture remains mostly the old one with a modern overlay/lifetime helper -attached to it. - -#### ARC-UI-001 - Move Generic Node And Control Code Out Of `pp_legacy_ui_core` - -Status: In Progress - -Why now: -`pp_ui_core` has layout, color, node lifetime, and overlay lifetime, but the -generic widget layer still sits in `pp_legacy_ui_core`. - -Current slice: -- `Node::load_internal(...)` child XML loading now routes through - `src/legacy_ui_node_loader.*` instead of staying inline in `src/node.cpp`. - That trims another coherent generic node-instantiation pocket and makes the - remaining scene-graph load path easier to isolate, even though ownership has - not yet moved into `pp_ui_core`. -- 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` instead of staying - inline in `src/node.cpp`, which materially thins the base scene-graph file - without changing its public surface. -- `Node::on_event(...)` plus mouse/key capture and release now also live in - `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. -- `Node::parse_attributes(...)` now also routes through - `src/legacy_ui_node_attributes.*` instead of staying inline in `src/node.cpp`, - which trims another coherent XML/Yoga attribute decoding pocket from the base - scene-graph shell without changing its public surface. -- `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 live in - `src/legacy_ui_node_tree_services.cpp` instead of staying inline in - `src/node.cpp`, which materially thins the remaining generic scene-graph - lifecycle/tree shell without changing the public surface. -- The generic Yoga style/visibility pocket from `Node::SetWidth(...)` through - `Node::GetRTL()` now also lives in `src/legacy_ui_node_style.*` instead of - staying inline in `src/node.cpp`, which trims another coherent generic node - shell pocket without changing the public surface. -- The remaining geometry/state pocket for `Node::SetSize(...)`, - `Node::SetMinSize(...)`, `Node::SetMaxSize(...)`, and - `Node::SetPosition(const glm::vec2)` now also lives in - `src/legacy_ui_node_style.*` instead of staying inline in `src/node.cpp`, - which materially thins the live base shell by moving the cached size/position - mutation and redraw signaling out of the node file. -- 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 lives in `src/legacy_ui_node_lifecycle.*` - instead of staying inline in `src/node.cpp`, which materially thins another - coherent generic scene-graph shell without changing the public surface. -- `Node::load_internal(...)` now also 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` - and leaves the remaining live node file thinner. -- The remaining default `Node` event/capture/resize shell now also lives in - `src/legacy_ui_node_execution.cpp`, while the remaining - width/height/padding/margin/flex/visibility/geometry wrappers now also live - in `src/legacy_ui_node_style.*`, which reduces `src/node.cpp` to the final - attribute/load entrypoints without changing the public surface. - -Write scope: -- `src/node.cpp` -- `src/layout.cpp` -- generic `src/node_*` base control files from `PP_LEGACY_UI_CORE_SOURCES` -- `src/ui_core/*` -- `CMakeLists.txt` -- `cmake/PanoPainterSources.cmake` - -Read scope: -- `src/node_panel_*` -- `src/node_dialog_*` - -Done when: -- Generic controls and base node/layout code are clearly separated from - PanoPainter-specific panels and dialogs. -- `pp_ui_core` grows as the home of generic widgets and node behavior. -- `pp_panopainter_ui` keeps only app-specific panels, dialogs, canvas, preview, - and workflow nodes. -- The touched slice removes real file ownership from `pp_legacy_ui_core`, not - just adds wrappers around it. - -Mini-model packet: -- Start with the controls that have no app-specific policy: - button, checkbox, icon, image, scroll, slider, text, text input. -- Do not mix panel/dialog rewrites into the same slice. -- Prefer target ownership moves over purely internal helper reshuffles. - -#### ARC-UI-002 - Make Checked Handles The Default UI Ownership Model +- Prioritize working-app ownership transfer over planners, CLI commands, + package-only cleanup, or test-only work. +- Every coding task must remove or narrow a real retained dependency, hotspot, + unsafe ownership path, or thread/runtime ambiguity. +- Tests are validation and guardrails. A task that only adds tests is not a P0 + modernization slice unless it directly enables a blocked ownership move. +- Do not broaden worker scopes. If a task crosses file boundaries, split it + into worker packets with disjoint write scopes and integrate centrally. +- No new `App::I`, `Canvas::I`, owning raw `Node*`, detached worker, direct GL + resource dependency, or platform SDK dependency may be introduced in moved + code. +- Raw pointers may remain only as documented non-owning implementation details + backed by a checked owner, handle, scoped connection, or explicit lifetime + contract. +- Preserve current app behavior first. UI appearance, file formats, brush + behavior, platform behavior, and rendering output are not to be redesigned in + modernization slices. +- Use CMake source ownership as the progress signal. Shrinking + `PP_PANOPAINTER_*` and `PP_LEGACY_*` ownership matters more than adding new + helpers around the same retained code. + +## Current Audit Snapshot + +Validation performed during the 2026-06-17 review: + +- `python scripts/dev/check_component_boundaries.py`: passed. +- `python scripts/dev/check_renderer_api_contract.py`: passed. + +Key facts: + +- Pure component boundaries currently pass their static checks. +- Remaining architectural risk is concentrated in the working app, retained + app/UI/canvas targets, singleton reach, raw node ownership, and direct GL + resource usage. +- `PP_PANOPAINTER_APP_SOURCES`: 47 files, about 9620 lines. +- `PP_PANOPAINTER_UI_SOURCES`: 52 files, about 9051 lines. +- `PP_LEGACY_PAINT_DOCUMENT_SOURCES`: 22 files, about 6277 lines. +- `PP_LEGACY_APP_SOURCES`: 26 files, about 4711 lines. +- `PP_LEGACY_UI_CORE_SOURCES`: 32 files, about 4304 lines. +- `App::I` still appears hundreds of times in retained app/canvas/UI/resource + code. +- `Canvas::I` still appears hundreds of times in retained canvas modes, panels, + and workflow bridges. +- Raw `Node*` and callback captures remain a dominant UI lifetime risk. +- `RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still route + render work through `App::I` queues. +- `AppRuntime` uses `std::jthread`, which is progress, but queue ownership, + running flags, thread affinity, and shutdown contracts are not yet component + contracts. +- `PP_PANOPAINTER_APP_SOURCES` still includes `${PP_PLATFORM_WEB_SOURCES}`, + which should be removed in favor of concrete `pp_platform_web` ownership. + +## Parallel Assignment Rules + +Coordinator packets for workers should include only: + +- task id and one-paragraph goal +- exact write scope +- allowed read scope +- debt ids that matter +- required validation command +- specific `rg` or `clangd_nav.py` queries +- current behavior notes needed to avoid broad rediscovery + +Safe parallel groups: + +- One worker on canvas/render execution, one worker on generic UI controls, one + worker on platform CMake cleanup, and one worker on runtime queue contracts + can run in parallel if write scopes remain disjoint. +- Do not run two workers against `src/app_runtime.*`, `src/node.*`, + `src/legacy_canvas_document_io_services.cpp`, or + `src/legacy_node_stroke_preview_runtime_services.cpp` at the same time. +- Do not assign both a CMake source-list move and code edits touching the same + source files to separate workers unless the coordinator serializes the CMake + integration. + +## P0 Queue + +### ARC-RUN-010 - Harden `AppRuntime` Into An Explicit Runtime Service Status: Ready Why now: -`pp_ui_core` has lifetime and overlay handle helpers, but retained UI code still -mixes raw `Node*`, shared ownership, direct `add_child(...)`, and callback -captures across mutation points. - -Current slice: -- `src/node.cpp` child attach/detach/reorder paths now route through named - local helpers instead of repeating raw mutation logic inline. That improves - scene-graph mutation clarity, but it does not yet change the ownership model - or move those paths behind checked handles. +Render/UI/background queues are central to memory and thread safety. The +current `AppRuntime` owns several `std::jthread` workers, but runtime state is +still mutable, partly unsynchronized, and app-specific. The working app still +uses `App::I` as the practical access path to render/UI queues. Write scope: + +- `src/app_runtime.h` +- `src/app_runtime.cpp` +- `src/app.h` +- `src/legacy_app_runtime_shell_services.cpp` +- `src/app_core/app_thread.h` +- `tests/app_core/app_thread_tests.cpp` + +Read scope: + +- `src/texture.cpp` +- `src/rtt.cpp` +- `src/shape.cpp` +- `src/shader.cpp` +- `src/platform_windows/windows_platform_services.cpp` + +Required work: + +- Make render/UI/prepared-file/canvas worker running state synchronized or + atomic, with a single shutdown path per worker. +- Add explicit runtime service methods for thread-affinity checks and + post/drain/shutdown semantics. +- Keep exceptions from escaping worker bodies. +- Stop exposing queue usage only through `App::I` wrappers for touched call + sites. +- Keep behavior identical for same-thread immediate execution and blocking + render/UI calls. + +Done when: + +- Touched queue state has no unsynchronized read/write ambiguity. +- Worker shutdown drains or rejects queued work according to documented + behavior. +- Touched app code can call an explicit runtime service instead of reaching + queues through singleton state. +- App-thread planner tests cover shutdown, stopped-worker enqueue, same-thread + execution, and queue-drain behavior that the live runtime implements. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target pp_app_core_app_thread_tests PanoPainter +ctest --preset desktop-fast --build-config Debug -R pp_app_core_app_thread --output-on-failure +python scripts/dev/check_component_boundaries.py +``` + +Mini-model packet: +Start by auditing `render_running_`, `ui_running_`, +`prepared_file_running_`, `canvas_async_running_`, thread ids, and worker stop +methods. Do not rewrite GL resources in this task; expose the runtime contract +needed for the next task. + +### ARC-RND-010 - Move GL Resource Queueing Behind Renderer Runtime Contracts + +Status: Ready + +Why now: +`RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still use +`App::I->render_task*` directly. That blocks renderer backends and hides thread +affinity behind a global app singleton. + +Write scope: + +- `src/texture.cpp` +- `src/texture.h` +- `src/rtt.cpp` +- `src/rtt.h` +- `src/shape.cpp` +- `src/shape.h` +- `src/shader.cpp` +- `src/shader.h` +- `src/font.cpp` +- `src/font.h` +- `src/canvas_layer.cpp` +- `src/canvas_layer.h` +- narrow adapter files if introduced under `src/renderer_gl/` + +Read scope: + +- `src/app_runtime.*` +- `src/renderer_api/*` +- `src/renderer_gl/*` +- `src/paint_renderer/*` + +Required work: + +- Introduce a narrow render-dispatch interface or adapter consumed by retained + GL resource classes. +- Convert one coherent GL resource family per slice; do not edit every file in + one worker pass unless the abstraction is already integrated. +- Preserve blocking versus async semantics exactly. +- Do not move app policy into `pp_renderer_gl`. +- Do not add future backend implementation work. + +Done when: + +- The touched GL resource family no longer calls `App::I` directly for queueing + or thread checks. +- Render-thread assertions use an explicit runtime/render-dispatch contract. +- CMake ownership remains consistent and no pure renderer API target depends on + app headers. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_renderer_api_tests pp_renderer_gl_capabilities_tests +ctest --preset desktop-fast --build-config Debug -R "pp_renderer|pp_paint_renderer" --output-on-failure +python scripts/dev/check_renderer_api_contract.py +``` + +Mini-model packet: +Start with either `Texture2D`/`RTT` or `Shape`/`Shader`, not both. Use `rg -n +"App::I->render_task|is_render_thread" src/texture.* src/rtt.*` for the first +slice. + +### ARC-RND-011 - Split Canvas Document I/O From Render Execution + +Status: Ready + +Why now: +`src/legacy_canvas_document_io_services.cpp` is still the largest working-app +document/export hotspot and has the highest `App::I` concentration found in the +review. It mixes license checks, worker dispatch, render readback, progress UI, +platform publish/flush, and retained `Canvas` mutation. + +Write scope: + +- `src/legacy_canvas_document_io_services.cpp` +- `src/legacy_canvas_document_io_services.h` +- `src/legacy_document_export_services.*` +- `src/app_core/document_export.h` +- `src/paint_renderer/*` +- focused tests under `tests/app_core` or `tests/paint_renderer` + +Read scope: + +- `src/canvas.*` +- `src/canvas_layer.*` +- `src/legacy_canvas_render_shell_services.*` +- `src/platform_api/platform_services.h` + +Required work: + +- Pick one export/import family first: equirectangular export, cube-face export, + layer/frame collection export, or project save/open async I/O. +- Move orchestration into an app-core or paint-renderer service request that + accepts explicit document/render/platform dependencies. +- Leave retained `Canvas` as a final adapter only for data that has not moved. +- Remove direct `App::I` calls from the touched path. +- Preserve progress and platform publish behavior. + +Done when: + +- One live document/export path is executable through an explicit service + request rather than by walking `App::I`/`Canvas::I` from the bridge. +- The retained bridge is visibly thinner and has fewer reasons to know about + UI, worker, platform, and renderer details at the same time. +- The touched path has focused validation that exercises the new request + contract and the retained adapter. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli pp_app_core_document_export_tests pp_paint_renderer_compositor_tests +ctest --preset desktop-fast --build-config Debug -R "document_export|paint_renderer" --output-on-failure +``` + +Mini-model packet: +Do not broaden into all export types. Start with the path that already has the +strongest pure planning/readiness coverage, then remove only the corresponding +direct app/canvas singleton reach. + +### ARC-RND-012 - Make Stroke Preview A Renderer-Owned Service + +Status: Ready + +Why now: +`NodeStrokePreview` has been thinned, but +`src/legacy_node_stroke_preview_runtime_services.cpp` still owns static worker +state, render-context handoff, preview texture lifetime, and direct app/canvas +access. This is a high-risk UI/render/thread boundary. + +Write scope: + +- `src/node_stroke_preview.*` +- `src/legacy_node_stroke_preview_runtime_services.*` +- `src/legacy_node_stroke_preview_draw_services.*` +- `src/legacy_node_stroke_preview_sample_services.*` +- `src/paint_renderer/*` +- `tests/paint_renderer/*` + +Read scope: + +- `src/app_runtime.*` +- `src/texture.*` +- `src/rtt.*` +- `src/canvas.*` +- `src/node_panel_stroke.*` + +Required work: + +- Move one preview execution phase behind a renderer-facing service contract. +- Replace static worker/resource state for the touched phase with owned service + state or explicit runtime dependency. +- Remove direct `App::I`/`Canvas::I` from the touched phase. +- Preserve preview output and cancellation/shutdown behavior. + +Done when: + +- The touched preview phase can be reasoned about without reading the full node + implementation. +- Preview worker lifetime is owned and cancellable for the touched phase. +- Renderer-facing tests cover the contract without linking app texture objects. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_paint_renderer_compositor_tests +ctest --preset desktop-fast --build-config Debug -R "paint_renderer|stroke_preview" --output-on-failure +``` + +Mini-model packet: +Start with result copy, live pass request assembly, or worker lifecycle. Do not +combine all preview phases in one slice. + +### ARC-UI-010 - Move Generic Controls Out Of `pp_legacy_ui_core` + +Status: Ready + +Why now: +Generic controls still live in `PP_LEGACY_UI_CORE_SOURCES`, keeping +`pp_panopainter_ui` tied to retained app/UI targets. This is working-app UI +architecture, not cosmetic cleanup. + +Write scope: + +- `src/node_button.*` +- `src/node_checkbox.*` +- `src/node_icon.*` +- `src/node_image.*` +- `src/node_scroll.*` +- `src/node_slider.*` +- `src/node_text.*` +- `src/node_text_input.*` +- `src/ui_core/*` +- `cmake/PanoPainterSources.cmake` +- `CMakeLists.txt` + +Read scope: + +- `src/node.*` +- `src/layout.*` +- app-specific `src/node_panel_*` +- app-specific `src/node_dialog_*` + +Required work: + +- Move one generic control family at a time to `pp_ui_core`. +- Split renderer-neutral state/event/layout logic from retained GL drawing + when a control still depends on GL classes. +- Keep app-specific panels and dialogs out of `pp_ui_core`. +- Update CMake ownership so the source-list change is real. + +Done when: + +- At least one generic control family is owned by `pp_ui_core` or has its + renderer-neutral core owned there with only a narrow retained draw adapter. +- `PP_LEGACY_UI_CORE_SOURCES` shrinks. +- Existing UI behavior is unchanged. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests +ctest --preset desktop-fast --build-config Debug -R "pp_ui_core" --output-on-failure +python scripts/dev/check_component_boundaries.py +``` + +Mini-model packet: +Start with the least app-specific control: checkbox, button, icon, image, +scroll, slider, text, or text input. Do not touch panels/dialogs in the same +slice. + +### ARC-UI-011 - Convert UI Ownership To Checked Handles By Default + +Status: Ready + +Why now: +`pp_ui_core` has checked lifetime helpers, but base `Node` and app panels still +mix raw parent/manager pointers, shared child vectors, raw callback parameters, +and destroy-during-callback assumptions. + +Write scope: + - `src/node.*` - `src/layout.*` - `src/legacy_ui_overlay_services.*` -- retained `src/node_dialog_*` and `src/node_panel_*` files touched by a slice +- one dialog or panel family per slice under `src/node_dialog_*` or + `src/node_panel_*` - `src/ui_core/node_lifetime.*` - `src/ui_core/overlay_lifetime.*` Read scope: -- existing popup/dialog call sites found with `add_child`, `remove_child`, and - `on_*` callback captures + +- call sites found with `rg -n "add_child|remove_child|destroy\\(|on_.*=|Node\\*" src/node_dialog_* src/node_panel_* src/legacy_ui_* src/node.*` + +Required work: + +- Convert one popup/dialog/panel family to checked handles or scoped + connections. +- Remove raw lifetime assumptions from callbacks in the touched family. +- Document any remaining raw `Node*` as non-owning views with owner proof. +- Keep visual behavior and event ordering unchanged. Done when: -- New or touched UI surfaces open, close, and dispatch callbacks through checked - handles or scoped connections by default. -- Raw `Node*` fields and callback parameters are documented or reshaped as - non-owning views, not lifetime owners. -- Destroy-during-callback and close-during-dispatch behavior is owned by - `pp_ui_core` rather than each panel/dialog. -- App-specific panels become view/controller shells over safe UI-core - lifetime primitives. + +- The touched UI family can close during callback dispatch without relying on + dangling raw pointers. +- Overlay/popup lifetime flows through `pp_ui_core` lifetime primitives by + default. +- New touched callbacks are scoped or handle-checked. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests +ctest --preset desktop-fast --build-config Debug -R "ui_core_(node_lifetime|overlay_lifetime)" --output-on-failure +``` Mini-model packet: -- Convert one popup/dialog family at a time. -- Do not redesign the UI appearance in this task. -- Prefer deleting raw lifetime assumptions over adding more guard comments. +Pick one family only, such as open/browse dialogs, picker dialog, popup menu, +layer panel, or stroke panel. Avoid broad `Node` redesign unless the family +requires a small base helper. -#### ARC-UI-003 - Split UI Rendering From Scene Graph And App State +### ARC-APP-010 - Reduce App Shells To Composition And Adapters Status: Ready Why now: -The generic node layer still mixes layout, input, rendering, direct app access, -and retained OpenGL resource usage. That blocks both a cleaner UI component and -future renderer backends. +`PP_PANOPAINTER_APP_SOURCES` is still about 9620 lines. The app shell owns +workflow, dialogs, layout binding, runtime, VR, cloud, brush package, platform +hooks, and retained document/export adapters. Write scope: -- `src/node.cpp` -- `src/node_canvas.cpp` -- `src/node_stroke_preview.cpp` -- generic control files moved toward `src/ui_core/*` -- `src/renderer_api/*` -- `src/paint_renderer/*` + +- one `src/app_*.cpp` family per slice +- matching `src/legacy_app_*` service files +- matching app-core planner/service headers only when needed +- `cmake/PanoPainterSources.cmake` if ownership moves Read scope: -- `src/font.*` -- `src/shape.*` -- `src/texture.*` -- `src/rtt.*` + +- `src/app.h` +- relevant app-core headers under `src/app_core` +- relevant UI node files for the touched workflow + +Required work: + +- Pick one shell family: layout menus, dialogs, startup/frame, cloud, brush + package, recording, VR, or document session. +- Move retained implementation into a named service with explicit dependencies. +- Make the app method a thin adapter or composition call. +- Do not add planner-only coverage unless the app method actually shrinks. Done when: -- Scene graph/layout/input code has a renderer-neutral draw contract. -- Generic controls do not need to know app singleton state or concrete GL - resource classes. -- App-specific canvas and preview rendering depends on renderer-facing services - rather than base `Node` internals. -- The touched slice makes `pp_ui_core` more reusable without hiding app policy - inside it. + +- One app shell file loses real workflow/runtime ownership. +- The new service accepts explicit `App&`, runtime, platform, UI, document, or + renderer dependencies instead of reading global state internally. +- The touched path has focused validation or an app build gate. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli +ctest --preset desktop-fast --build-config Debug -R "pp_app_core|pano_cli_plan" --output-on-failure +``` Mini-model packet: -- Keep visual behavior unchanged. -- Do not move PanoPainter-specific panel policy into `pp_ui_core`. -- Use renderer-neutral contracts first; backend implementation follows later. +Start with a single app shell family. Do not mix dialogs, layout, cloud, and VR +in one worker assignment. -### Bundle 4 - Make The Platform Boundary Real +### ARC-PLT-010 - Finish Platform Source Ownership In CMake -Priority: `P1` - -Why this bundle is not `P0`: -It matters, but moving platform code first will not change the day-to-day shape -of the app as much as reducing `canvas.cpp`, the app shell, and the generic UI -layer. - -#### ARC-PLT-001 - Split `pp_platform_api` From Concrete Platform Code - -Status: In Progress +Status: Ready Why now: -`pp_platform_api` is supposed to be the SDK-free policy and interface layer, -and while Apple implementation has now moved out, it still compiles concrete -Linux platform sources. - -Current slice: -- `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` now link that - concrete target where needed. -- `pp_platform_api` now also stops compiling - `src/platform_linux/linux_platform_services.*`. -- Linux concrete platform code now lives in the new `pp_platform_linux` - target, and `pp_legacy_app` plus `pp_platform_api_tests` now link that - concrete target where needed. -- The dependency direction is cleaner for Apple and Linux, but the broader - concrete platform family is still being separated. +Platform implementation ownership improved, but CMake still leaks Web platform +sources into `PP_PANOPAINTER_APP_SOURCES`. Platform implementation files should +belong to concrete `pp_platform_*` targets, not the app source group. Write scope: -- `CMakeLists.txt` -- `src/platform_api/*` -- `src/platform_apple/*` -Read scope: - `cmake/PanoPainterSources.cmake` -- `src/platform_windows/*` +- `CMakeLists.txt` +- `src/platform_web/*` only if build integration requires a narrow include or + factory adjustment + +Read scope: + +- `webgl/CMakeLists.txt` +- `src/platform_android/*` - `src/platform_linux/*` - -Done when: -- `pp_platform_api` contains only platform-neutral interfaces, policies, and - shared helpers. -- Apple and Linux implementation files are built by concrete platform targets - instead of the API target. -- The dependency direction is obvious from CMake without reading debt notes. - -Mini-model packet: -- Start in `CMakeLists.txt` around `pp_platform_api`. -- Keep the change structural; do not broaden into new feature work. -- Preserve current Apple service entrypoints while moving ownership. - -#### ARC-PLT-002 - Remove `App::I` Reach From Apple And Linux Services - -Status: In Progress - -Why now: -The current Apple and Linux service files still call into the app singleton, -which means the platform layer is not a platform layer yet. - -Current slice: -- Linux FPS title updates now route through an injected callback installed from - `App::set_platform_services()` -- `platform_apple` clipboard, display/share, cursor, and save-ui-state calls - now route through injected Apple bridge callbacks instead of `App::I` -- `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path - preparation through a narrow local helper instead of reading `App::I` - directly in that method body -- iOS virtual-keyboard visibility and prepared-file save handoff now also route - through explicit Apple bridge callbacks instead of direct `App::I` calls in - `LegacyPlatformServices` -- Apple render-context acquire/release/present hooks and iOS - `bind_main_render_target()` now also route through explicit Apple bridge - callbacks instead of direct `App::I` calls in `LegacyPlatformServices` -- Apple crash-test, app-close, and iOS SonarPen hooks now also route through - explicit Apple bridge callbacks instead of direct `App::I` calls in - `LegacyPlatformServices` -- retained Apple ObjC handles plus storage paths now live in one local - `platform_legacy` helper, and the iOS SonarPen bridge now starts through - that retained Apple state instead of reading `App::I` inside the bridge body -- Linux/Web GLFW render-context acquire/present hooks and Linux app-close now - also route through retained local GLFW callback hooks instead of direct - method-body `App::I` access in `LegacyPlatformServices` -- retained GLFW window hooks and the non-Linux fallback storage-path return now - also route through retained local state helpers instead of reading `App::I` - directly in those method bodies -- retained storage-path state now also lives in - `src/platform_legacy/legacy_platform_state.*` instead of staying inline in - `src/platform_legacy/legacy_platform_services.cpp` -- `active_legacy_storage_paths()` now consumes storage roots seeded explicitly - through `set_legacy_storage_paths(...)` from app startup plus the iOS, - macOS, and Android entrypoints instead of lazily snapshotting `App::I` -- retained GLFW window hooks/state and retained Apple UI/app handle state now - also live in `src/platform_legacy/legacy_platform_state.*`, and - `src/platform_legacy/legacy_platform_services.cpp` now consumes those - snapshots without direct `App::I` reads in the touched paths -- the retained Apple document bridge now also lives behind - `active_legacy_apple_document_platform_services()` in - `src/platform_legacy/legacy_platform_state.*` instead of being built inline - inside `src/platform_legacy/legacy_platform_services.cpp`, and that retained - service resets when seeded Apple handles change -- retained Apple callback injection and broader `platform_legacy` singleton - reach are still open -- the `platform_legacy`-mirrored Apple/GLFW handle cluster is now seeded - explicitly from the iOS, macOS, Linux, and WebGL entrypoints through - `src/platform_legacy/legacy_platform_state.*` instead of being copied out - of `App` -- 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`, which leaves the live entry file as a thinner - startup/runtime dispatcher. - -Write scope: - `src/platform_apple/*` -- `src/platform_linux/*` -- `src/app_events.cpp` -- `src/app.h` - -Read scope: - `src/platform_api/platform_services.h` -- `src/platform_legacy/legacy_platform_services.*` + +Required work: + +- Remove `${PP_PLATFORM_WEB_SOURCES}` from `PP_PANOPAINTER_APP_SOURCES`. +- Ensure root app/platform targets link `pp_platform_web` only when needed. +- Keep WebGL retained package entrypoint behavior unchanged. +- Do not reintroduce `platform_legacy`. Done when: -- `src/platform_apple/*` and `src/platform_linux/*` no longer call `App::I`. -- The app injects the minimum callbacks or bridge state those services need. -- Platform files stop depending on app-global state for clipboard, sharing, FPS - title updates, or native UI saves. + +- Concrete Web platform files are not compiled as app sources in the root app + source group. +- The source ownership direction is visible in CMake. +- Platform API boundary checks still pass. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_platform_api_tests +ctest --preset desktop-fast --build-config Debug -R pp_platform_api --output-on-failure +python scripts/dev/check_component_boundaries.py +``` Mini-model packet: -- Keep the interface small. Prefer injected callbacks/bridges over passing the - whole `App`. -- Do not rewrite Windows in the same slice. +Keep this structural. Do not edit platform behavior unless CMake exposes a real +link or include problem. -#### ARC-PLT-003 - Remove App-Owned Cross-Platform Handle Storage +## P1 Queue -Status: In Progress - -Why now: -`src/platform_legacy/legacy_platform_services.cpp` and `src/app.h` still keep -platform-handle state on `App`, which blocks a real `pp_platform_*` shell split. - -Current 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` and - `src/platform_apple/apple_platform_services.*`, and the macOS/iOS - entrypoints now seed that state through `pp::platform::apple`. -- `src/platform_legacy/legacy_platform_services.cpp` now consumes the - Apple-owned retained provider/factory through `pp::platform::apple::...` - call-throughs instead of constructing or caching Apple retained bridge - state in the legacy layer. -- The Win32 stylus and pointer-input path no longer reaches - `WacomTablet::I` directly in - `src/platform_windows/windows_window_shell.cpp` or - `src/platform_windows/windows_stylus_input.cpp`; - `src/platform_windows/windows_runtime_shell.*` now binds the active tablet - explicitly and the touched consumers read it through that runtime-shell - binding instead of the process-global singleton. -- 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 in - `src/platform_windows/windows_bootstrap_helpers.*`, and - `src/app_events.cpp` now dispatches platform services through the - `App` instance it is operating on instead of a global 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 - in `src/platform_legacy/legacy_platform_services.cpp`, which trims another - retained fallback/state pocket out of the legacy platform shell. -- The touched Win32 shell path no longer reaches `App::I` directly for - window-procedure dispatch, stylus state updates, lifecycle shutdown, or VR - callback/thread setup; `src/platform_windows/windows_runtime_shell.*` now - binds the active `App*` explicitly and clears that binding on shutdown. -- Windows VR session snapshot ownership no longer lives on `App`. -- `VrSessionSnapshot` now lives behind - `src/platform_windows/windows_vr_shell.h` and - `read_platform_vr_session_snapshot()` in - `src/platform_windows/windows_platform_services.*`, with app-side reads now - routed through `App::vr_session_snapshot()`. -- The `platform_legacy`-mirrored Apple/GLFW handle cluster also 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.*`. -- `App` also 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. -- Win32 main-thread task dispatch also no longer reaches `AppRuntime` through - `App::I`; `src/platform_windows/windows_runtime_shell.*` now binds the - active runtime explicitly and - `src/platform_windows/windows_platform_services.cpp` consumes that bound - runtime for enqueue/drain. -- `App` still owns broader retained legacy platform state, so this remains a - live ownership task. - -Write scope: -- `src/platform_legacy/legacy_platform_services.*` -- `src/app.h` -- `src/app_events.cpp` -- `src/platform_windows/*` - -Read scope: -- `src/main.cpp` -- Apple/Android/Web/Linux entrypoint files only as needed - -Done when: -- `App` no longer owns platform-specific handle fields that belong to shells. -- The legacy platform adapter becomes thin composition or disappears for the - touched path. -- Platform setup state lives with the relevant `pp_platform_*` implementation. - -Mini-model packet: -- Keep this slice about state ownership, not feature behavior. -- Prefer moving state to shell-local structs or service singletons owned by the - platform target. - -### Bundle 5 - Retire The Thick Workflow Bridges - -Priority: `P2` - -Why this bundle is later: -These bridges still matter, but many recent slices spent too much effort -polishing adapters without changing the bulk shape of the live app. This bundle -stays active only after the main hotspots are moving. - -#### ARC-WKF-001 - Thin Document Open/Save/Session Bridges To Pure Adapters +### ARC-SAFE-010 - Remove Manual Allocation From Touched Ownership Paths Status: Ready Why now: -The document session/open/save planners exist, but the live bridges still own a -lot of retained dialog, metadata, title, and save execution behavior. +The review found manual `new`/`delete` pockets in node loading, canvas, layer +actions, Wacom/bootstrap helpers, and retained resource cleanup. Some are +non-owning or placement-new cases, but touched working-app ownership paths +should move to RAII containers and factories. Write scope: + +- one selected ownership path at a time, such as `src/legacy_ui_node_loader.*`, + `src/node.*`, `src/canvas.cpp`, `src/platform_windows/windows_bootstrap_helpers.cpp`, + or `src/wacom.cpp` + +Read scope: + +- immediate owner/caller files for the selected path + +Required work: + +- Replace owning raw allocation with `std::unique_ptr`, `std::shared_ptr`, + `std::vector`, `std::string`, or an explicit RAII wrapper. +- Preserve non-owning views only where ownership is proven. +- Avoid mixing this with UI or renderer redesign. + +Done when: + +- The touched path has no owning raw `new`/`delete`. +- Failure paths cannot leak. +- Lifetime remains clear at call sites. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter +ctest --preset desktop-fast --build-config Debug -R "pp_ui_core|pp_app_core" --output-on-failure +``` + +Mini-model packet: +Start with a single obvious allocation family. `legacy_ui_node_loader` is a +good first target because it is UI ownership, not rendering behavior. + +### ARC-SAFE-011 - Replace Remaining Ad Hoc Workers With Runtime-Owned Services + +Status: Ready + +Why now: +Most recent worker conversions use `std::jthread`, but retained worker pockets +still sit in UI/dialog/cloud/grid/preview services and `std::async` remains in +`Asset`. The end state requires service-owned cancellation and shutdown. + +Write scope: + +- one worker family per slice: + `src/node_dialog_cloud.*`, `src/legacy_cloud_services.*`, + `src/legacy_grid_ui_services.*`, `src/asset.*`, or + `src/legacy_node_stroke_preview_runtime_services.*` + +Read scope: + +- `src/app_runtime.*` +- corresponding app-core/cloud/grid/preview planner headers +- immediate UI caller files + +Required work: + +- Move worker ownership behind a runtime/service contract. +- Add cancellation or shutdown semantics for the touched worker. +- Avoid capturing raw nodes across worker completion without checked handles. +- Preserve progress and completion callbacks. + +Done when: + +- The touched worker cannot outlive its owner. +- Shutdown behavior is explicit and validated. +- UI completion handoff is handle-safe or owner-checked. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter +ctest --preset desktop-fast --build-config Debug -R "pp_app_core|pp_ui_core" --output-on-failure +``` + +Mini-model packet: +Pick one worker family. Do not perform broad thread cleanup across unrelated +subsystems in one task. + +### ARC-WKF-010 - Thin Document Session/Open/Save Bridges + +Status: Ready + +Why now: +The pure document/session planners are extensive, but live bridges still own +retained prompts, metadata mutation, title updates, history clearing, snapshot +handoff, and legacy `Canvas` execution. + +Write scope: + - `src/legacy_document_open_services.*` - `src/legacy_document_session_services.*` -- `src/legacy_document_export_services.*` - `src/legacy_history_services.*` +- focused app-core document/session headers only when needed Read scope: + - `src/app_core/document_route.h` - `src/app_core/document_session.h` -- `src/app_core/document_export.h` +- `src/app_core/document_canvas.h` +- `src/app.h` +- `src/canvas.*` + +Required work: + +- Pick one bridge path: open-project confirmation, save-before-workflow, + save-version, new-document overwrite, history clear, or title update. +- Move decisions/mutations behind explicit service requests. +- Keep retained prompts as adapters only. Done when: -- The remaining bridge files are thin adapters from planner outputs to retained - execution. -- Save/open/session flows stop mutating app/document/UI state inline across - multiple bridge layers. -- Title updates, history clearing, overwrite prompts, and save routing are each - owned in one obvious place. + +- One document workflow path has a single obvious owner for decision, + execution, and metadata mutation. +- The retained bridge has less direct `App::I`/`Canvas::I` reach. +- Behavior is covered by existing or focused document-session validation. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli pp_app_core_document_session_tests pp_app_core_document_route_tests +ctest --preset desktop-fast --build-config Debug -R "document_(session|route)" --output-on-failure +``` Mini-model packet: -- Preserve current planner contracts. -- Favor one adapter per workflow family over catch-all helper growth. +Do not rewrite all document flows. Pick the narrowest path that removes live +bridge ownership. -#### ARC-WKF-002 - Split Cloud And Brush Package Work Out Of Retained UI Nodes +### ARC-WKF-011 - Split Cloud And Brush Package Work Out Of UI Nodes Status: Ready Why now: -Cloud browse/download/upload and brush package import/export still close over -retained nodes, worker threads, and direct UI ownership. - -Current slice: -- `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 a coherent retained brush-workflow - pocket from the live UI node even though cloud and package-worker ownership - still remain separate follow-up work. -- `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. -- `NodePanelBrushPreset` save/restore plus PPBR/ABR import/export 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 even though the broader cloud/package worker - split still remains follow-up work. -- `NodePanelBrushPreset` init/menu wiring, click handling, item construction, - and added-state update now also live in - `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 even though cloud/package worker ownership remains the - follow-up. -- `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 trims the remaining inline popup tail - from the live brush panel file. -- 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 workflow - pocket while preserving the live list-plan dispatch path. -- `NodePanelBrush` brush texture panel init, selection dispatch, - popup-close event handling, restore-failure prompt flow, and added-state - reset now also live in `src/legacy_brush_panel_ui.*` instead of staying - inline in `src/node_panel_brush.cpp`, which trims another retained brush UI - workflow pocket while preserving the live node as a thinner controller - surface. +Cloud browse/download/upload and brush package import/export still mix UI node +lifetime, worker ownership, storage, network/asset behavior, and app singleton +reach. Write scope: -- `src/legacy_cloud_services.*` -- `src/node_dialog_cloud.*` -- `src/legacy_brush_package_import_services.*` -- `src/legacy_brush_package_export_services.*` -- `src/node_panel_brush.cpp` + +- one family per slice: + `src/legacy_cloud_services.*`, `src/node_dialog_cloud.*`, + `src/legacy_brush_package_import_services.*`, + `src/legacy_brush_package_export_services.*`, + `src/legacy_brush_preset_services.*`, + `src/node_panel_brush.cpp` Read scope: + - `src/app_core/document_cloud.h` - `src/app_core/brush_package_import.h` - `src/app_core/brush_package_export.h` - `src/assets/brush_package.*` +- relevant panel/dialog headers + +Required work: + +- Separate worker/network/asset execution from node lifetime. +- Use app-core requests and asset helpers where they already exist. +- Use checked handles for UI completion callbacks. +- Preserve current cloud and brush package UX. Done when: -- Network transfer execution, thumbnail loading, and brush package worker - ownership are isolated behind named services. -- Retained nodes become view/controller shells instead of workflow owners. -- Cloud and brush package code no longer need to be understood through panel or + +- One cloud or brush package path can be understood without reading panel or dialog internals first. +- The touched UI node is a view/controller shell, not the workflow owner. +- The touched worker cannot outlive its service/UI owner. + +Validation: + +```powershell +cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli pp_app_core_document_cloud_tests pp_app_core_brush_package_import_tests pp_app_core_brush_package_export_tests pp_assets_brush_package_tests +ctest --preset desktop-fast --build-config Debug -R "document_cloud|brush_package" --output-on-failure +``` Mini-model packet: -- Split worker ownership from UI ownership first. -- Do not try to redesign cloud UX or brush preset UX in the same slice. +Cloud and brush package work are separate packets. Do not assign both to one +small worker. ## Deferred On Purpose -- Vulkan and Metal lab work -- package-only and automation-only cleanup -- scorekeeping tasks that do not move app architecture - -These remain in history only until the app shell, platform split, UI split, and -canvas/render split are materially thinner. +- Vulkan, Metal, WebGPU, and broad future-backend implementation. +- OpenXR implementation beyond boundary cleanup needed to remove OpenVR debt. +- Package-only migration that does not affect root app architecture. +- CLI/planner-only expansion. +- Broad warning cleanup without ownership movement. +- Documentation-only progress claims.