14 KiB
PanoPainter Modernization Roadmap
Status: live Last updated: 2026-06-16
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
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: 4128 linessrc/app_layout.cpp: 2026 linessrc/canvas_modes.cpp: 1798 linessrc/node.cpp: 1551 linessrc/main.cpp: 1374 linessrc/node_panel_brush.cpp: 1197 linessrc/node_stroke_preview.cpp: 1129 linessrc/node_canvas.cpp: 962 linessrc/app.cpp: 950 linessrc/app_dialogs.cpp: 908 lines
Current architecture mismatches that must be treated as real blockers:
pp_platform_apistill compiles Apple implementation files 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 and fallback storage-path return now also using local retained-state helpers instead of direct method-body reads.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 the smoothing-mask face shader/draw pass plus heightmap, current-mode, and grid-mode callback setup now routed through the same retained helper family.app_layout.cppandapp_dialogs.cppare still mixed shell/controller files rather than thin composition/binding surfaces.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 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, andApp::rec_loop()now delegates worker-iteration orchestration into the retained recording bridge even though that retained recording path still owns the worker-side readback flow.- 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.