Files
panopainter/docs/modernization/roadmap.md

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_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.
  • 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, so src/app_dialogs_workflow.cpp, src/app_dialogs_export.cpp, src/app_layout.cpp, src/app_layout_draw_toolbar.cpp, src/app_layout_sidebar.cpp, and src/app_dialogs_info_openers.cpp are 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_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.