63 KiB
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_foundationpp_assetspp_paintpp_documentpp_renderer_apipp_renderer_glpp_paint_rendererpp_ui_corepp_platform_apipp_app_core
Latest slice:
cmake/PanoPainterSources.cmakeno longer addssrc/platform_legacy/legacy_platform_services.*toPP_PANOPAINTER_APP_SOURCES, so the rootpanopainter_apptarget graph stops compiling and linking that retained fallback shim.- The retained
platform_legacyadapter 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 WebGLPlatformServicesimplementation as well as the narrowerWebPlatformServiceshelper surface.webgl/src/main.cppnow binds that owned concrete WebPlatformServicesinstance directly instead of constructing the cross-platformplatform_legacyadapter 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_testsnow 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 WebPlatformServicesimplementation andwebgl/src/main.cppnow 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 retainedWebPlatformServices*dependency, andsrc/platform_legacy/legacy_platform_state.*is gone.pp_platform_webnow exists as the concrete Web target in root CMake and is linked intopp_platform_api_tests, whilewebgl/CMakeLists.txtalso compiles the concrete Web service directly for the retained WebGL app build.src/platform_linux/linux_platform_services.*now owns the concrete LinuxPlatformServicesimplementation andcreate_platform_services(...)instead of leaving the live Linux execution surface insideplatform_legacy.linux/src/main.cppnow binds that owned LinuxPlatformServicesinstance intoAppdirectly 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 AndroidPlatformServicesimplementation andcreate_platform_services()instead of leaving the live Android execution surface insideplatform_legacy.android/src/cpp/main.cppnow binds that owned AndroidPlatformServicesinstance intoAppdirectly 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 ApplePlatformServicesimplementation plus thecreate_apple_platform_services()factory instead of leaving the live Apple execution surface insideplatform_legacy.PanoPainter-OSX/main.cppandPanoPainter/GameViewController.mnow bind owned ApplePlatformServicesinstances intoAppdirectly 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 deadpp::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 ownedApp,AppRuntime*, andWacomTabletobjects instead of leaving that binding surface inwindows_runtime_shell.cpp.src/platform_windows/windows_runtime_state.*now also owns the Win32 owned-app creation plus app/runtime binding handoff, sowindows_runtime_shell.cppis 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, sowindows_lifecycle_shell.cppis 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, sowindows_lifecycle_shell.cppis 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, sowindows_lifecycle_shell.cppis down to a one-line adapter over the retained lifecycle state helper.src/platform_windows/windows_runtime_shell.his now a thinner runtime entrypoint header that picks up the retained binding surface fromwindows_runtime_state.hinstead of declaring a second shell-owned binding API.src/platform_windows/windows_lifecycle_shell.cppnow 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 insidewindows_runtime_flow.cpp.src/platform_windows/windows_runtime_flow.cppis now a thinner handoff layer over that Windows session helper.src/platform_windows/windows_runtime_shell.hno longer exposes the removed runtime-session entrypoint or drags bootstrap/splash declarations through the broader shell header surface.src/platform_windows/windows_runtime_shell.cppnow hands runtime execution directly torun_bound_main_window_runtime(...)instead of keeping a second wrapper around that seam.src/main.cppnow includeswindows_bootstrap_helpers.hdirectly forrun_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 inwindows_runtime_shell.cpp.src/platform_windows/windows_runtime_shell.cppis thinner again and now delegates the main runtime flow through that Windows-owned helper.src/platform_windows/windows_runtime_shell.cppandsrc/platform_windows/windows_lifecycle_shell.cppnow drive thread start/stop through the explicit Windows runtime binding instead of routing that control throughApp::runtime().src/platform_windows/windows_platform_services.cppno longer brokers the Win32 main-thread queue throughbound_app()->runtime(); it now uses an explicit Windows runtime binding.src/platform_windows/windows_runtime_state.*now carries the activeAppRuntime*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 insidewindows_runtime_shell.cpp.src/platform_windows/windows_runtime_shell.cppnow orchestrates startup and shutdown over the Windows runtime-state helper instead of directly owning the retained runtime pocket.src/platform_windows/windows_platform_services.cppnow owns the retained Win32 VR shell state directly behindplatform_vr_state().src/platform_windows/windows_window_shell.cppno longer exposes or owns a separateretained_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 insidewindows_lifecycle_shell.cpp.src/platform_windows/windows_lifecycle_shell.cppnow 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 insidewindows_platform_services.cpp.src/platform_windows/windows_platform_services.cppnow 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 Win32MainWindowSessionobject (HWND, title buffer, sandbox flag) that the runtime shell binds for the live session instead of leaving those fields behind retained accessors insidewindows_runtime_shell.cpp.src/platform_windows/windows_runtime_shell.cppnow keeps Windows runtime ownership focused onAppand tablet lifetime rather than also owning main-window session metadata.src/platform_legacy/legacy_platform_services.*now owns Android storage paths through explicitcreate_platform_services(...)configuration instead of reading them from a shared legacy singleton.android/src/cpp/main.cppnow seeds Android storage paths into its owned legacyPlatformServicesinstance.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 sharedplatform_apiinjection surface instead.src/platform_legacy/legacy_platform_state.*no longer carries any retained GLFW window state; the leftoverset_legacy_glfw_window*surface is gone.linux/src/main.cppandwebgl/src/main.cppno longer seed legacy GLFW retained state at startup.src/platform_windows/windows_window_shell.cppno 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 injectedLegacyGlfwPlatformShellfor Linux/WebGLacquire_render_context,present_render_context, and app-close callbacks.linux/src/main.cppandwebgl/src/main.cppnow 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 GLFWacquire/present/request_closehelper trio.src/platform_windows/windows_platform_services.cppnow owns the Win32 virtual-key map andinitialize_retained_input_state()path, sowindows_window_shell.cppno 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 plusAppkey state.linux/src/main.cppnow 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.cppno longer keeps a second retainedApp*copy alongsideApp::I; the runtime shell now reads the bound app directly from the singleton composition edge.src/platform_windows/windows_window_shell.*andsrc/platform_windows/windows_platform_services.cppnow 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.cppnow drops the dead generic storage-path fallback read; only the platform-specific storage-path branches remain live.src/legacy_app_startup_services.cppno longer seeds the dead generic storage-path fallback state on non-Android startup.src/platform_legacy/legacy_platform_services.cppnow reads Android storage paths from Android-owned retained state instead of the shared legacy storage-path singleton.src/legacy_app_startup_services.cppandandroid/src/cpp/main.cppnow update Android-owned retained storage paths instead of the shared legacy storage-path singleton.src/app_events.cpp,linux/src/main.cpp, andsrc/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 fromApp::set_platform_services(...).src/platform_legacy/legacy_platform_state.hno longer exports the raw retained GLFW window-state accessor; callers now go through narrow GLFW helpers only.src/platform_apple/apple_platform_state.cppno longer reads Apple storage paths frompp::platform::legacy::active_legacy_storage_paths(); Apple-owned retained state now carries that path bundle.PanoPainter-OSX/main.cppandPanoPainter/GameViewController.mnow 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.cppnow 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 keepsHINSTANCEin the retained main-window session state; the startup/shutdown path now threads the module handle explicitly.webgl/src/main.cppnow owns a TU-local legacyPlatformServicesinstance and binds that owned service intoAppduringStartApp().android/src/cpp/main.cppnow owns a function-lifetime legacyPlatformServicesinstance inandroid_main()and binds that owned service intoAppinstead of binding the process-global fallback directly.src/platform_legacy/legacy_platform_services.*now exposes an ownablecreate_platform_services()entrypoint for explicit per-entrypoint ownership.linux/src/main.cppnow owns a local legacyPlatformServicesinstance and binds it intoAppexplicitly instead of binding the process-global legacy accessor directly.src/app_events.cppno longer silently falls back topp::platform::legacy::platform_services()whenApphas no bound platform services; the live app path now expects explicit platform-service ownership.linux/src/main.cpp,webgl/src/main.cpp, andandroid/src/cpp/main.cppnow bind ownedcreate_platform_services(...)instances at app creation instead of relying on a hidden fallback inAppevent/platform dispatch.src/platform_windows/windows_runtime_shell.cppnow owns the Windows tablet object directly; the composition edge no longer binds&WacomTablet::Iinto 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.cppno longer keeps a separate retainedAppRuntime*binding; the touched Windows shell and platform helpers now derive runtime ownership directly from the ownedApp.src/platform_windows/windows_runtime_shell.cppnow explicitly owns the WindowsApplifetime through a retainedstd::unique_ptr<App>instead of rawnew/deleteplus shutdown-side manual cleanup in the lifecycle shell.src/platform_windows/windows_lifecycle_shell.cppnow releases the bound Windows app throughrelease_bound_app()after runtime shutdown instead of deleting it directly through the global shutdown path.src/platform_windows/windows_window_shell.cppnow 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.ps1is now the bundled checkpoint path for Windows build/test plus optional platform and Apple remote validation, with one compact JSON summary underout/logs/quiet-validation.scripts/automation/platform-build.ps1quiet 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.ps1quiet mode now writes the local SSH session log, reports the remoteplatform-build.shlog 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 linespanopainter_app: 29 files, about 8817 linespp_legacy_paint_document: 7 files, about 5709 linespp_legacy_app: 20 files, about 4368 linespp_legacy_ui_core: 20 files, about 3770 lines
Current hotspot files:
src/canvas.cpp: 17 linessrc/app_layout.cpp: 109 linessrc/canvas_modes.cpp: 1 linesrc/node.cpp: 12 linessrc/main.cpp: 10 linessrc/node_panel_brush.cpp: 2 linessrc/node_stroke_preview.cpp: 76 linessrc/node_canvas.cpp: 69 linessrc/app.cpp: 94 linessrc/app_dialogs.cpp: 95 lines
Latest slice:
-
The remaining Windows entry/exit singleton write no longer lives at the
run_main_application(...)andhandle_window_close_message(...)callsites;src/platform_windows/windows_runtime_shell.cppnow centralizes that legacyApp::Iside effect insidebind_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.cppfan-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.cppno longer usesCanvas::Ifor crash-recovery saves; the BugTrap pre-error handler now uses the app-ownedNodeCanvasdocument (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-ownedsrc/platform_apple/apple_platform_state.cppplussrc/platform_apple/apple_platform_services.*, and the macOS/iOS entrypoints now seed that state throughpp::platform::apple::set_legacy_apple_state(...)instead of the legacy namespace. -
src/platform_legacy/legacy_platform_services.cppnow consumes the Apple-owned retained provider throughpp::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::Idirectly insidesrc/platform_windows/windows_window_shell.cpporsrc/platform_windows/windows_stylus_input.cpp; the live Windows runtime shell now binds the active tablet explicitly throughsrc/platform_windows/windows_runtime_shell.*, leavingWacomTablet::Ionly 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 insrc/platform_windows/windows_bootstrap_helpers.*, and the app-side platform dispatch helpers insrc/app_events.cppnow also use the instance they are invoked on instead of a globalApp::Ifallback. -
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 insidesrc/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::Idirectly; the live Windows shell now binds the activeApp*explicitly throughsrc/platform_windows/windows_runtime_shell.*, leavingApp::Ionly 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 behindactive_legacy_apple_document_platform_services()insrc/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
AppRuntimethroughApp::Iinsidesrc/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. -
Appno longer ownsand_apporand_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 snapshotsApp::Ilazily insidesrc/platform_legacy/legacy_platform_state.*; storage roots are now seeded explicitly from app startup plus the iOS, macOS, and Android entrypoints throughset_legacy_storage_paths(...). -
pp_platform_apino longer compilessrc/platform_linux/linux_platform_services.*; Linux concrete platform code now lives inpp_platform_linux, whichpp_legacy_appandpp_platform_api_testslink where needed. -
Win32 main-thread queued task ownership now lives in
AppRuntimeinstead ofsrc/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 onApp; retained Apple/GLFW platform state is now seeded explicitly from the iOS, macOS, Linux, and WebGL entrypoints throughsrc/platform_legacy/legacy_platform_state.*. -
pp_platform_apino longer compilessrc/platform_apple/apple_platform_services.*; Apple concrete platform code now lives in the newpp_platform_appletarget, andpanopainter_apppluspp_platform_api_testslink 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 insrc/platform_legacy/legacy_platform_services.cpp, which trims another process-global platform-state pocket out of the legacy platform shell and removes more directApp::Ireads from touched platform paths. -
Windows VR session snapshot ownership now lives in
src/platform_windows/windows_vr_shell.handsrc/platform_windows/windows_platform_services.*instead of onApp, with app-side reads now routed throughApp::vr_session_snapshot(). -
The live Windows entry shell now routes through
run_main_application(...)insrc/platform_windows/windows_runtime_shell.*, leavingsrc/main.cppas a minimal entry wrapper aroundmain(...)andWinMain(...). -
Retained legacy storage-path state now lives in
src/platform_legacy/legacy_platform_state.*instead of staying inline insrc/platform_legacy/legacy_platform_services.cpp, which trims another process-global platform-state pocket out of the legacy platform shell. -
The remaining
NodeStrokePreviewclone-init, stroke-frame planning, mix-pass adapter wiring, sample-pass adapter wiring, and immediate-draw request construction now route throughsrc/legacy_node_stroke_preview_runtime_services.*,src/legacy_node_stroke_preview_draw_services.*, andsrc/legacy_node_stroke_preview_sample_services.*, leavingsrc/node_stroke_preview.cppas a thinner live adapter. -
NodeCanvas::handle_event()now routes throughhandle_legacy_node_canvas_event(...)insrc/legacy_canvas_tool_services.*, leavingsrc/node_canvas.cppas a much thinner controller shell. -
App::open_document()now routes throughsrc/legacy_document_open_services.cpp, which moved the document route classification and unsaved-project gating out ofsrc/app.cppand into the retained document-open helper. -
App::dialog_layer_rename()now routes throughopen_legacy_document_layer_rename_dialog(...)insrc/legacy_document_layer_services.*, which moved the remaining overlay-open/wire/close workflow out ofsrc/app_dialogs.cpp. -
The remaining low-level
NodeStrokePreviewviewport/query and texture-slot plumbing now lives insrc/legacy_node_stroke_preview_runtime_services.*instead of staying inline insrc/node_stroke_preview.cpp. -
NodePanelBrushPresetglobal panel registration now lives insrc/legacy_brush_preset_list_services.*instead of staying on the live node type as a static registry field. -
The remaining generic
Nodegeometry/state pocket forSetSize(...),SetMinSize(...),SetMaxSize(...), andSetPosition(const glm::vec2)now lives insrc/legacy_ui_node_style.*instead of staying inline insrc/node.cpp. -
Node::app_redraw()andNode::watch(...)now live insrc/legacy_ui_node_execution.cppinstead of staying inline insrc/node.cpp. -
NodeStrokePreview::draw_stroke_immediate()now routes throughexecute_legacy_node_stroke_preview_immediate_draw(...)insrc/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
NodePanelBrushandNodePanelBrushPresetmember 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.*, andsrc/legacy_brush_preset_services.*), leavingsrc/node_panel_brush.cppas a thin translation unit. -
Node::load_internal(...)now routes throughload_legacy_ui_node(...)insrc/legacy_ui_node_loader.*, which moves the init/attribute-parse/create/child-load/loaded shell out ofsrc/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.cppinstead ofsrc/main.cpp, leavingmain.cppas a thinner entry/runtime dispatcher. -
The entire
CanvasModeGridimplementation plusActionModeGridundo/redo glue now live insrc/legacy_canvas_mode_helpers.cppinstead ofsrc/canvas_modes.cpp, leavingsrc/canvas_modes.cppas a minimal shell. -
App::request_close(), the RenderDoc frame wrappers, and the render/UI thread entrypoint wrappers now route throughsrc/legacy_app_runtime_shell_services.*instead of staying inline insrc/app.cpp, leavingapp.cppas a thinner retained app shell. -
App::show_progress(),App::message_box(), andApp::input_box()now route throughsrc/legacy_app_dialog_services.*instead of building dialog plans and factories inline insrc/app_dialogs.cpp. -
The remaining generic
Nodeevent/capture/resize shell plus the width/height/padding/margin/flex/visibility/geometry wrappers now live insrc/legacy_ui_node_execution.cppandsrc/legacy_ui_node_style.*instead of staying inline insrc/node.cpp, leavingnode.cppas a near-trivial attribute/load shell.
Current architecture mismatches that must be treated as real blockers:
pp_platform_apino 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.cppno longer reachesApp::Idirectly, and Linux FPS title reporting now uses an injected callback, but retained Apple bridging inplatform_legacyand 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 localplatform_legacyhelper instead of being re-read throughApp::Iin 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 onApp, theplatform_legacy-mirrored Apple/GLFW handle cluster is now seeded explicitly from platform entrypoints instead of being copied out ofApp, and retained storage roots are now also seeded explicitly instead of being lazily copied fromApp::Iinsideactive_legacy_storage_paths(), while the retained Apple document bridge now also lives inlegacy_platform_state.*instead of being built inline inlegacy_platform_services.cpp.src/platform_legacy/legacy_platform_services.*is still part of the live app shell.pp_panopainter_uistill depends onpp_legacy_app.Canvas,NodeCanvas, andNodeStrokePreviewstill own too much live OpenGL execution around the renderer boundary, even thoughNodeCanvasdisplay 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_mergedper-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_mergedouter layer/plane traversal now also routing throughexecute_legacy_canvas_draw_layer_traversal(...), while the heavier per-layer GL setup now also routes throughmake_legacy_canvas_draw_merge_layer_path_gl_execution(...)even though the remaining draw lambdas and broader node draw loop still live insrc/node_canvas.cpp, where the post-draw/display-resolve tail now also routes throughexecute_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 throughexecute_legacy_canvas_draw_unmerged_node_canvas_shell(...), while the broader unmerged cache/viewport/background/composite pass setup now also routes throughexecute_legacy_canvas_draw_unmerged_node_canvas_pass(...), whileNodeCanvas::draw()setup, merged-pass shell, and unmerged-pass shell now also route throughprepare_legacy_node_canvas_draw_setup(...),execute_legacy_canvas_draw_node_canvas_shell(...),execute_legacy_canvas_draw_merged_pass(...), andexecute_legacy_canvas_draw_node_canvas_unmerged_pass(...), which materially shortens the liveNodeCanvas::draw()body even though the file itself is still large.app_layout.cppandapp_dialogs.cppare still mixed shell/controller files rather than thin composition/binding surfaces, even though tools-menu binding plus nested panels/options submenu wiring now live insrc/app_layout_tools_menu.cppandApp::init_menu_tools()is now a thin call-through, while file-menu binding plus the export submenu wiring now also live insrc/app_layout_file_menu.cppandApp::init_menu_file()is now a thin call-through, while about-menu and layer-menu wiring now also live insrc/app_layout_about_layer_menu.cppandApp::init_menu_about()plusApp::init_menu_layer()are now thin call-throughs, while sidebar panel binding and popup wiring now also live insrc/app_layout_sidebar.cppandApp::init_sidebar()is now a thin call-through, while main-toolbar binding now also lives insrc/app_layout_main_toolbar.cppandApp::init_toolbar_main()is now a thin call-through, while edit-menu binding now also lives insrc/app_layout_edit_menu.cppandApp::init_menu_edit()is now a thin call-through, while UI-direction and persisted floating/docked panel-state ownership now also live insrc/app_layout_ui_state.cpp, while draw-toolbar binding now also lives insrc/app_layout_draw_toolbar.cpp, while brush-refresh now also lives insrc/app_layout_brush.cpp, while layout bootstrap plus reload/load continuation wiring now also lives insrc/app_layout_bootstrap.cpp, andsrc/app_layout.cppis now mostly thin call-through entrypoints, while the informational overlay opener family now also lives insrc/app_dialogs_info_openers.cppand the correspondingApp::dialog_*entrypoints are thinner, while the export/video/PPBR dialog family now also lives insrc/app_dialogs_export.cppand thoseApp::dialog_*entrypoints are thinner too, while new/open/save/browse/resize workflow entrypoints now also live insrc/app_dialogs_workflow.cpp, while the layer-rename dialog open / wire / close pocket now lives insrc/legacy_document_layer_services.*, andsrc/app_dialogs.cppis 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 ownedstd::jthreador service-owned worker queues andAppRuntimenow owns render/UI workers with explicitstd::jthreadshutdown semantics while the Windows splash-dialog and HMD renderer workers also use ownedstd::jthreadlifecycle,LogRemotenow uses the same ownership model, the Windows VR device now has explicitstd::unique_ptrownership instead of raw global lifetime, and the Windows main-loop/VR coordination flags now usestd::atomicinstead 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 undersrc/platform_windows/windows_platform_services.cppinstead ofmain.cppretained state, the main-thread queued task state now lives undersrc/platform_windows/windows_platform_services.cppinstead of staying insrc/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 underAppRuntimeinstead of retained static app-events/canvas workers, and the splash-screen dialog loop, HWND ownership, and bitmap setup now live insrc/platform_windows/windows_splash.cppinstead ofsrc/main.cpp, while Win32 pointer API loading, stylus/ink timer decay, Wintab packet reset, andWM_POINTERUPDATEpen/touch handling now also live insrc/platform_windows/windows_stylus_input.cppinstead ofsrc/main.cpp, while the retained Win32 VR/HMD shell now also routes throughsrc/platform_windows/windows_vr_shell.hinstead of staying inline insrc/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 insrc/platform_windows/windows_bootstrap_helpers.cppinstead ofsrc/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 insrc/platform_windows/windows_lifecycle_shell.cppinstead ofsrc/main.cpp, while the Win32 startup/window/bootstrap path now also lives undersrc/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 insrc/platform_windows/windows_bootstrap_helpers.cppinstead ofsrc/main.cpp, while the Win32 window procedure and retained message-handling shell now also live insrc/platform_windows/windows_window_shell.*instead ofsrc/main.cpp, while theWinMainargv conversion bridge now also lives insrc/platform_windows/windows_bootstrap_helpers.*instead of staying inline insrc/main.cpp, while retained input-state zeroing and reverse key-map initialization now also live insrc/platform_windows/windows_window_shell.*instead ofsrc/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 insrc/platform_windows/windows_runtime_shell.*instead ofsrc/main.cpp, which materially thinssrc/main.cppeven though broader entrypoint/runtime composition still remains there, whileApp::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 insrc/legacy_app_frame_services.cppinstead of staying inline insrc/app.cpp, while the larger document/export/save/open/thumbnail document-IO cluster now lives insrc/legacy_canvas_document_io_services.cppandsrc/app.cppis materially thinner, whileApp::clear(),App::check_license(),App::async_start(),App::async_redraw(),App::async_end(), andApp::async_swap()now also live insrc/legacy_app_runtime_shell_services.cppinstead of staying inline insrc/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 insrc/legacy_canvas_state_services.cppinstead ofsrc/canvas.cpp, while the larger import/export/save/open/thumbnail document-IO cluster now lives insrc/legacy_canvas_document_io_services.cppandNodeStrokePreviewrender-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 throughsrc/legacy_node_stroke_preview_execution_services.h, even though broader preview-pass orchestration still lives insrc/node_stroke_preview.cpp, while the immediate preview pass-sequencing family insidedraw_stroke_immediate()now also routes throughNodeStrokePreview::execute_stroke_draw_immediate_pass_sequence(...), while the remaining immediate preview pass shell now also routes throughexecute_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 throughsrc/legacy_node_stroke_preview_runtime_services.*instead of staying inline insrc/node_stroke_preview.cpp, while the low-level preview GL dispatch and texture-slot binding pocket now also routes throughsrc/legacy_node_stroke_preview_runtime_services.*instead of staying inline insrc/node_stroke_preview.cpp, whileNodeStrokePreviewremaining mix-pass planning and execution now also route throughsrc/legacy_node_stroke_preview_draw_services.*, which trims the last dedicated mix-orchestration pocket fromsrc/node_stroke_preview.cpp, whileNodeCanvas::draw()unmerged-pass blend-gate, layer-orientation, and callback-assembly setup now also route throughexecute_node_canvas_draw_unmerged_pass(...), which trims another coherent unmerged draw shell from the live node even though the file itself remains large, whileNodeCanvas::draw()merged-pass callback wiring and pass setup now also route throughexecute_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, whileCanvas::draw_objects_direct(...)andCanvas::draw_objects(...)now route throughsrc/legacy_canvas_object_draw_services.*instead of staying inline insrc/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 insrc/legacy_canvas_plane_data.cppinstead of staying inline insrc/canvas.cpp, which trims another retained data-ownership pocket from the live canvas shell, whileCanvas::stroke_draw_samples(...),Canvas::stroke_commit(), and the larger stroke commit/sample execution family now also route throughsrc/legacy_canvas_stroke_commit_services.*instead of staying inline insrc/canvas.cpp, which trims another large retained stroke-render and viewport-state execution family from the live canvas shell, while the liveCanvas::stroke_draw()orchestration now also routes throughsrc/legacy_canvas_stroke_live_services.cppinstead of staying inline insrc/canvas.cpp, which materially thins another large retained live stroke-render pocket, whileCanvas::layer_merge(...),Canvas::flood_fill(...), andCanvas::FloodData::apply()now also route throughsrc/legacy_canvas_layer_services.cppinstead of staying inline insrc/canvas.cpp, which trims another coherent retained layer/fill workflow pocket, whileCanvas::stroke_end(...),Canvas::stroke_cancel(...),Canvas::stroke_draw_mix(...),Canvas::stroke_draw_project(...),Canvas::stroke_update(...), andCanvas::stroke_start(...)now also route throughsrc/legacy_canvas_stroke_runtime_services.*instead of staying inline insrc/canvas.cpp, which trims another large retained stroke/runtime pocket, while the final camera/timelapse member definitions now also live insrc/legacy_canvas_camera_services.cppinstead of staying inline insrc/canvas.cpp, which trims another retained canvas shell pocket, while theCanvasModeTransforminteraction family now also routes throughsrc/legacy_canvas_mode_transform.cppinstead of staying inline insrc/canvas_modes.cpp, which materially thins another retained canvas-view and transform-mode execution pocket, while theCanvasModePenandCanvasModeLineinteraction families now also route throughsrc/legacy_canvas_mode_pen_line.cppinstead of staying inline insrc/canvas_modes.cpp, while theCanvasModeFillandCanvasModeFloodFillinteraction families now also route throughsrc/legacy_canvas_mode_fill.cppinstead of staying inline insrc/canvas_modes.cpp, which materially thins another retained fill-mode execution pocket from the broader canvas/render hotspot family, whileNodePanelBrushsave/restore/scan/reload/find/get-path ownership now routes throughsrc/legacy_brush_panel_services.*instead of staying inline insrc/node_panel_brush.cpp, which trims another retained brush-workflow pocket from the live UI node even though the broader panel still remains large, whileNodePanelBrushPresetsave/restore and package import/export/import-ABR routing now also lives insrc/legacy_brush_preset_services.*instead of staying inline insrc/node_panel_brush.cpp, which trims another large preset-workflow pocket from the live UI node, whileNodePanelBrushPresetinit/menu wiring, click handling, item construction, and added-state update now also route throughsrc/legacy_brush_preset_panel_ui.*instead of staying inline insrc/node_panel_brush.cpp, which materially thins another retained preset panel UI pocket, while the retainedLegacyBrushPresetListServicesblock now also lives insrc/legacy_brush_preset_list_services.*instead of staying inline insrc/node_panel_brush.cpp, which trims another retained preset-list pocket, whileNodeButtonBrushclone/init/icon/read/write/draw behavior andNodeBrushPresetItemclone/init/draw behavior now also live insrc/legacy_brush_panel_item_ui.*instead of staying inline insrc/node_panel_brush.cpp, which trims the remaining brush-item UI pocket from the live brush panel file, whileNodePanelBrushPresetpopup-close event handling now also lives insrc/legacy_brush_preset_panel_ui.*instead of staying inline insrc/node_panel_brush.cpp, which removes the last inline brush-panel popup close handler from the live node. TheNodePanelBrushPresetregistration/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 fromsrc/node_panel_brush.cpp, and preset-restore notification visibility now also stays withsrc/legacy_brush_preset_services.*instead of the node wrapper. The broader preset workflow pocket still remains, whileNodeCanvas::handle_event()now also routes throughexecute_node_canvas_handle_event(...), which trims another coherent input-routing block fromsrc/node_canvas.cppeven though the file is still a live canvas/controller shell, whileNodeCanvasrestore/clear context, resize handling, camera reset, buffer creation, cursor visibility/update, tick, and destroy ownership now also route throughsrc/legacy_node_canvas_state_services.*instead of staying inline insrc/node_canvas.cpp, which materially thins another retained state/control pocket, while shared canvas-mode GL wrappers plus theCanvasModeBasicCameraandCanvasModeCamerainput handlers now also route throughsrc/legacy_canvas_mode_helpers.*instead of staying inline insrc/canvas_modes.cpp, while preview stroke preparation, dual-brush setup, and live pass-orchestration request assembly now also route through retained preview execution helpers, whileNodeStrokePreviewretained lifecycle, worker-thread shell, render-to-image path, on-screen handling, and preview texture ownership now also route throughsrc/legacy_node_stroke_preview_runtime_services.cppinstead of staying inline insrc/node_stroke_preview.cpp, whileNodeCanvas::init()plus the remainingNodeCanvas::draw()outer shell now also route throughsrc/legacy_node_canvas_draw_services.*instead of staying inline insrc/node_canvas.cpp, which materially reduces the live node to a thinner controller surface around event routing and state wrappers, whileNode::on_event(...)plus mouse/key capture and release ownership now also route throughsrc/legacy_ui_node_event.*instead of staying inline insrc/node.cpp, which materially thins the base scene-graph event shell without changing its public surface, whileNodechild attach/detach/reorder operations now route through named local helpers insrc/node.cpp, andNode::load_internal(...)child XML loading now also routes throughsrc/legacy_ui_node_loader.*, which makes the scene-graph mutation and child-instantiation paths easier to reason about without yet moving ownership intopp_ui_core, while the generic per-frame node execution/traversal family forrestore_context,clear_context,update,update_internal, andticknow also lives insrc/legacy_ui_node_execution.cpp, whileNode::parse_attributes(...)now also routes throughsrc/legacy_ui_node_attributes.*instead of staying inline insrc/node.cpp, while the remaining genericNodelifecycle/state pocket for no-op lifecycle hooks, add/remove propagation, move construction, destruction cleanup, and base clone plumbing now also routes throughsrc/legacy_ui_node_lifecycle.*instead of staying inline insrc/node.cpp, whileCanvaspoint-trace/unproject/project/camera push-pop-get-set and face-to-shape helpers now also route throughsrc/legacy_canvas_projection_services.*instead of staying inline insrc/canvas.cpp, whileApp::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(...), andApp::render_thread_tick(...)now also route throughsrc/legacy_app_runtime_shell_services.cppinstead of staying inline insrc/app.cpp, whileCanvas::draw_merge(...), the temporary paint/branch orchestration helpers, final-plane composite, timelapse commit, create/destroy, clear-context, and camera accessors now also route throughsrc/legacy_canvas_render_shell_services.*instead of staying inline insrc/canvas.cpp, whileNode::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 throughsrc/legacy_ui_node_tree_services.cppinstead of staying inline insrc/node.cpp, while theCanvasModeMaskFreeandCanvasModeMaskLineinteraction families now also route throughsrc/legacy_canvas_mode_mask.cppinstead of staying inline insrc/canvas_modes.cpp, while the remaining live render/pass orchestration inNodeStrokePreview::draw_stroke_immediate()now also routes throughsrc/legacy_node_stroke_preview_draw_services.*instead of staying inline insrc/node_stroke_preview.cpp, and while the generic Yoga style/visibility pocket fromNode::SetWidth(...)throughNode::GetRTL()now also routes throughsrc/legacy_ui_node_style.*instead of staying inline insrc/node.cpp, while the preview sample execution pocket for sample-point conversion, brush vertex upload, request assembly, and theexecute_legacy_canvas_stroke_sample(...)call now also lives insrc/legacy_node_stroke_preview_sample_services.*instead of staying inline insrc/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.
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_apiis 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_coreowns generic node/layout/overlay behavior and generic controls.pp_panopainter_uiowns app-specific panels, dialogs, canvas views, and UI-to-app bindings.pp_app_coreowns planner logic, workflow policy, and service contracts. It does not own nodes, GL objects, or platform handles.pp_paint_rendererowns renderer-facing paint/export/preview contracts.panopainter_appowns 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
Appor 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_apiresources 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.cpploses major document-plus-render ownershipnode_canvas.cppandnode_stroke_preview.cpplose 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.cppbecomes menu/toolbar binding compositionapp_dialogs.cppbecomes workflow dispatch plus retained dialog opening glueapp.cppbecomes 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
Nodeand control code moves out ofpp_legacy_ui_core pp_panopainter_uikeeps 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::IandCanvas::Istop 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::Ireach from platform service implementations - remove app-owned cross-platform handle storage
- reduce
platform_legacyto 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_legacypp_legacy_apppp_legacy_ui_corepp_legacy_paint_document- large GL-heavy node and canvas files
Exit Criteria
The modernization is not done until these are all true:
pp_platform_apicontains only platform-neutral codesrc/platform_apple/*,src/platform_linux/*, and other concrete platform implementations do not reachApp::Iplatform_legacyis gone or reduced to a trivial temporary shimAppno longer stores cross-platform handle state that belongs to platform shellspp_panopainter_uino longer depends onpp_legacy_apppanopainter_appis a composition root, not a second workflow layercanvas.cpp,node_canvas.cpp, andnode_stroke_preview.cppno 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, andpp_legacy_paint_documentare 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.