Files
panopainter/docs/modernization/roadmap.md

1113 lines
63 KiB
Markdown

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