Files
panopainter/docs/modernization/roadmap.md

243 lines
9.7 KiB
Markdown

# 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.
- 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.