10 KiB
PanoPainter Modernization Roadmap
Status: live Last updated: 2026-06-17
This roadmap is the architecture guide for finishing the modernization. The
execution queue lives in docs/modernization/tasks.md; completed history lives
in docs/modernization/tasks-done.md; shortcuts and temporary adapters live in
docs/modernization/debt.md.
Objective
Turn PanoPainter into a thin C++23 composition-root application over separable, testable components while preserving the working app.
The goal is not more planners, CLI commands, or tests around the old shell. The goal is ownership transfer out of retained app, UI, canvas, renderer, and platform hotspots into explicit components with safe lifetime and thread contracts.
2026-06-17 Architecture Review
The extracted components are real and currently clean at their boundaries:
pp_foundationpp_assetspp_paintpp_documentpp_renderer_apipp_renderer_glpp_paint_rendererpp_ui_corepp_platform_apipp_app_core
Review validation:
python scripts/dev/check_component_boundaries.pypassed with zero source or link violations.python scripts/dev/check_renderer_api_contract.pypassed with zero renderer API violations.
The risk is therefore not component pollution. The risk is that the working app still bypasses those clean components through retained shells, singleton state, raw node ownership, direct GL resource calls, and ad hoc thread access.
Measured source pressure from cmake/PanoPainterSources.cmake:
| Source group | Files | Approx. lines | Review read |
|---|---|---|---|
PP_PANOPAINTER_APP_SOURCES |
47 | 9620 | Still a workflow/runtime shell, not composition only |
PP_PANOPAINTER_UI_SOURCES |
52 | 9051 | Still owns app-specific UI plus canvas/preview rendering |
PP_LEGACY_PAINT_DOCUMENT_SOURCES |
22 | 6277 | Canvas/document/render behavior still concentrated here |
PP_LEGACY_APP_SOURCES |
26 | 4711 | Canvas modes, preferences, history, recording, overlays |
PP_LEGACY_UI_CORE_SOURCES |
32 | 4304 | Generic controls and base node still retained |
PP_LEGACY_RENDERER_GL_SOURCES |
5 | 2854 | Direct GL classes still consumed by app/UI/canvas |
Measured safety pressure in src/:
| Signal | Count | Main hotspots |
|---|---|---|
App::I references |
383 | canvas document I/O, GL resource classes, brush/panel services, UI tree services |
Canvas::I references |
364 | canvas modes, layer/stroke panels, brush UI, app shell services |
Raw Node*-style references |
624 | layout/menu bindings, node tree services, panel headers, dialog code |
new tokens |
198 | node loader, canvas.cpp, layer/panel actions, bootstrap helpers |
delete tokens |
61 | retained GL/resources, asset/bootstrap/manual cleanup pockets |
| Render queue call sites | 105 | RTT, Texture2D, Shape, Shader, CanvasLayer, canvas I/O |
| UI queue call sites | 69 | window shell, document I/O, UI tree services, app runtime |
Current conclusion:
- The pure component graph is defensible.
- The live app is not yet modern C++23 in ownership, lifetime, or thread safety.
- Raw pointers and singletons are still architectural coupling, not just style debt.
- Renderer API contracts exist, but retained OpenGL resource classes still leak into app/UI/document code.
AppRuntimenow owns synchronized running flags and explicit same-thread/post-reject queue behavior, but broader app/runtime singleton reach and retained shell ownership still remain.- The document-browse dialog handoff now lives in
src/legacy_document_open_services.*, and retained cloud upload/download, brush-package import, and timelapse-export async paths now useAppRuntime::canvas_async_taskinstead of file-static worker singletons. - Main-toolbar, File-menu, About-menu, and Tools > Panels binding ownership now
lives in dedicated
legacy_*_binding_services.*helpers, so the correspondingapp_layout_*files are thinner adapters even though retained execution still lives in the app shell. - App-frame update, tick, and resize execution now route through
src/legacy_app_frame_services.*, and floating/docked panel persistence now routes throughsrc/legacy_app_ui_state_services.*, so more of the retained app shell is down to adapter calls even though runtime draw/event/sidebar execution still remains. - New-document/save dialog session wiring, app-title rendering, draw-toolbar
binding, sidebar stroke/grid-popup binding, export-dialog start wiring, and
the full info-opener family now live in dedicated
legacy_*services.*seams, sosrc/app_dialogs_workflow.cpp,src/app_dialogs_export.cpp,src/app_layout.cpp,src/app_layout_draw_toolbar.cpp,src/app_layout_sidebar.cpp, andsrc/app_dialogs_info_openers.cppare thinner adapters even though broader retained dialog/sidebar execution still remains. - Platform extraction improved substantially and the root app source group no longer compiles Web platform sources directly, but broader CMake and entrypoint cleanup are not complete.
Target Architecture
The final shape is a layered DAG with one 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
Ownership rules:
panopainter_appcomposes components and platform services. It does not own document workflow, renderer execution, dialog logic, or platform state.pp_app_coreowns UI-free workflow policy and service contracts. It does not own nodes, GL resources, OS handles, or background threads.pp_ui_coreowns generic scene graph, layout, controls, checked handles, scoped connections, mutation-safe dispatch, and UI-thread affinity rules.pp_panopainter_uiowns PanoPainter-specific panels, dialogs, canvas views, and UI-to-app bindings.pp_renderer_apiowns backend-neutral resource and command contracts.pp_renderer_glowns OpenGL implementation details while OpenGL remains the production backend.pp_paint_rendererowns paint, export, preview, and document-frame render contracts overpp_renderer_api.pp_platform_apiowns platform-neutral interfaces and policy only.pp_platform_*targets own SDK handles, event loops, windows, native pickers, render-context binding, VR/device bridges, and platform-specific state.
Safety rules:
- Owning raw pointers are not allowed in new component code.
- Raw pointers in retained code must be treated as non-owning views and removed from touched ownership boundaries unless the lifetime is proven by checked handles, scoped connections, or owner objects.
- UI and render access must go through explicit runtime services, not
App::IorCanvas::Ireach. - Worker threads must be joinable, cancellable, and owned by a service with shutdown semantics. Detached workers and static worker state are not acceptable end-state architecture.
- Renderer-facing app and UI code must depend on
pp_renderer_apicontracts, notTexture2D,RTT, direct GL calls, or retained render-thread helpers. - Public modernization APIs keep exceptions out of app code and return explicit
Status/Resultobjects.
Priority Order
P0: Move Working-App Ownership
The next phase must reduce the working app, not expand headless planner coverage. Priority work moves real ownership out of:
src/legacy_canvas_document_io_services.cppsrc/legacy_canvas_render_shell_services.cppsrc/legacy_node_canvas_draw_services.cppsrc/legacy_node_stroke_preview_runtime_services.cppsrc/app_runtime.*src/app_dialogs*.cppsrc/app_layout*.cppsrc/legacy_ui_node_*- generic
src/node_*controls still inpp_legacy_ui_core
P1: Make Safety Boundaries Enforceable
The working app should increasingly fail compile or validation when new code uses unsafe ownership or the wrong component direction. This includes:
- no new
App::IorCanvas::Iin moved code - no new owning
Node* - no direct GL resource use from future-backend-facing UI/workflow code
- explicit runtime service contracts for render/UI/background queues
- explicit platform target ownership in CMake
P2: Retire Compatibility Bridges
After the biggest working-app hotspots move, thin the remaining retained workflow bridges:
- document open/save/export/session bridges
- cloud transfer and browser bridges
- brush package import/export bridges
- retained platform/package compatibility paths
Deferred
Do not restart these as top-priority work until the P0/P1 conditions are materially improved:
- Vulkan, Metal, WebGPU, or OpenXR expansion beyond necessary boundary design
- package-only cleanup that does not change root app architecture
- CLI-only planner work
- test-only expansion that does not guard an ownership transfer
- scorekeeping or documentation churn without code ownership implications
Exit Criteria
Modernization is not complete until all are true:
panopainter_appis composition only.pp_panopainter_uino longer depends onpp_legacy_app.pp_legacy_ui_core,pp_legacy_app, andpp_legacy_paint_documentare deleted or reduced to narrow debt-tracked adapters.App::IandCanvas::Iare no longer cross-component access paths.- Platform SDK handles and event loops live only in
pp_platform_*targets. pp_platform_apiis SDK-free and implementation-free.canvas.cpp,node_canvas.cpp, andnode_stroke_preview.cppno longer own large renderer orchestration bodies.- UI ownership uses checked handles and scoped callback connections by default.
- Render/UI/background queues are owned by explicit runtime services with cancellation, shutdown, and thread-affinity contracts.
- Future-backend-facing app/UI/document code uses
pp_renderer_apiandpp_paint_renderer, not retained OpenGL resource classes. - The working app builds and passes focused validation for each migrated slice.
How To Execute
Use docs/modernization/tasks.md as the active work queue. It is written as
coordinator-ready packets for smaller parallel workers. Pick disjoint write
scopes, pass only the packet context to each worker, integrate locally, run the
listed validation, update debt/tasks if the slice moves ownership, then commit
and push.