# 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-popup binding, and usermanual/changelog/about opener
  wiring now live in dedicated `legacy_*services.*` seams, so
  `src/app_dialogs_workflow.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:

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