# 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_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` Review validation: - `python scripts/dev/check_component_boundaries.py` passed with zero source or link violations. - `python scripts/dev/check_renderer_api_contract.py` passed with zero renderer API violations. 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. - `AppRuntime` now 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 use `AppRuntime::canvas_async_task` instead 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 corresponding `app_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 through `src/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. - 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: ```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 ``` Ownership rules: - `panopainter_app` composes components and platform services. It does not own document workflow, renderer execution, dialog logic, or platform state. - `pp_app_core` owns UI-free workflow policy and service contracts. It does not own nodes, GL resources, OS handles, or background threads. - `pp_ui_core` owns generic scene graph, layout, controls, checked handles, scoped connections, mutation-safe dispatch, and UI-thread affinity rules. - `pp_panopainter_ui` owns PanoPainter-specific panels, dialogs, canvas views, and UI-to-app bindings. - `pp_renderer_api` owns backend-neutral resource and command contracts. - `pp_renderer_gl` owns OpenGL implementation details while OpenGL remains the production backend. - `pp_paint_renderer` owns paint, export, preview, and document-frame render contracts over `pp_renderer_api`. - `pp_platform_api` owns platform-neutral interfaces and policy only. - `pp_platform_*` targets own SDK handles, event loops, windows, native pickers, render-context binding, VR/device bridges, and platform-specific state. 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::I` or `Canvas::I` reach. - Worker threads must be joinable, cancellable, and owned by a service with shutdown semantics. Detached workers and static worker state are not acceptable end-state architecture. - Renderer-facing app and UI code must depend on `pp_renderer_api` contracts, not `Texture2D`, `RTT`, direct GL calls, or retained render-thread helpers. - Public modernization APIs keep exceptions out of app code and return explicit `Status`/`Result` objects. ## 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.cpp` - `src/legacy_canvas_render_shell_services.cpp` - `src/legacy_node_canvas_draw_services.cpp` - `src/legacy_node_stroke_preview_runtime_services.cpp` - `src/app_runtime.*` - `src/app_dialogs*.cpp` - `src/app_layout*.cpp` - `src/legacy_ui_node_*` - generic `src/node_*` controls still in `pp_legacy_ui_core` ### 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::I` or `Canvas::I` in moved code - no new owning `Node*` - no direct GL resource use from future-backend-facing UI/workflow code - explicit runtime service contracts for render/UI/background queues - explicit platform target ownership in CMake ### 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_app` is composition only. - `pp_panopainter_ui` no longer depends on `pp_legacy_app`. - `pp_legacy_ui_core`, `pp_legacy_app`, and `pp_legacy_paint_document` are deleted or reduced to narrow debt-tracked adapters. - `App::I` and `Canvas::I` are no longer cross-component access paths. - Platform SDK handles and event loops live only in `pp_platform_*` targets. - `pp_platform_api` is SDK-free and implementation-free. - `canvas.cpp`, `node_canvas.cpp`, and `node_stroke_preview.cpp` no longer own large renderer orchestration bodies. - UI ownership uses checked handles and scoped callback connections by default. - Render/UI/background queues are owned by explicit runtime services with cancellation, shutdown, and thread-affinity contracts. - Future-backend-facing app/UI/document code uses `pp_renderer_api` and `pp_paint_renderer`, not retained OpenGL resource classes. - The working app builds and passes focused validation for each migrated slice. ## 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.