Files
panopainter/docs/modernization/roadmap.md

382 lines
16 KiB
Markdown

# PanoPainter Modernization Roadmap
Status: live
Last updated: 2026-06-16
This roadmap is now architecture-first.
The active execution queue lives in `docs/modernization/tasks.md`.
Completed and superseded task history moved to
`docs/modernization/tasks-done.md`.
The debt log remains `docs/modernization/debt.md`.
## Objective
Turn PanoPainter into a thin composition-root application over separable C++23
components while preserving current behavior.
The target end state is not "more planners around the same legacy shell".
The next phase is measured by ownership transfer in the live app, not by
planner count, CLI breadth, or test count.
The target end state is:
- real component ownership
- real platform boundaries
- real renderer boundaries
- a thin `panopainter_app`
- legacy containment targets either deleted or reduced to trivial,
debt-tracked adapters
## What This Roadmap Covers
- app architecture
- component boundaries
- platform boundaries
- renderer/app ownership boundaries
- the order of work needed to finish the split
It does not try to be the full build, test, or automation reference.
Those details live in the other modernization docs when needed.
## What Does Not Count As Top-Priority Progress
These can still be useful, but they are not first-order modernization work
while the app shell still mostly looks like the old codebase:
- planner-only extraction that leaves the same live owner in place
- new CLI surface without corresponding live app ownership reduction
- test-only or automation-only expansion that does not unblock code movement
- backend lab scaffolds
- debt-log churn without a target or ownership change
## Reality Check
The codebase is meaningfully farther along than the old flat app, but it is not
close to the final architecture yet.
Historical percentage claims such as the earlier 67% score should not be used
as a proxy for architectural completion. The live app still mostly runs through
the same large shell and hotspot files.
What is already real:
- `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`
What is still carrying too much live ownership:
- `pp_panopainter_ui`: 34 files, about 9102 lines
- `panopainter_app`: 29 files, about 8817 lines
- `pp_legacy_paint_document`: 7 files, about 5709 lines
- `pp_legacy_app`: 20 files, about 4368 lines
- `pp_legacy_ui_core`: 20 files, about 3770 lines
Current hotspot files:
- `src/canvas.cpp`: 2645 lines
- `src/app_layout.cpp`: 1249 lines
- `src/canvas_modes.cpp`: 1798 lines
- `src/node.cpp`: 1551 lines
- `src/main.cpp`: 1117 lines
- `src/node_panel_brush.cpp`: 1197 lines
- `src/node_stroke_preview.cpp`: 933 lines
- `src/node_canvas.cpp`: 953 lines
- `src/app.cpp`: 950 lines
- `src/app_dialogs.cpp`: 789 lines
Current architecture mismatches that must be treated as real blockers:
- `pp_platform_api` still compiles Apple implementation files instead of only
platform-neutral policy and interface code.
- `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I`
directly, and Linux FPS title reporting now uses an injected callback, but
retained Apple bridging in `platform_legacy` and other platform/app coupling
remain, even though iOS keyboard visibility and prepared-file save handoff
now also route through explicit Apple bridge callbacks and Apple render-
context hooks plus iOS main-render-target binding now route through the same
bridge style, as do Apple crash-test, app-close, and iOS SonarPen hooks,
while Linux/Web GLFW render-context acquire/present and Linux app-close now
route through retained local GLFW callback hooks, and retained Apple ObjC
handles plus storage paths now sit behind one local `platform_legacy`
helper instead of being re-read through `App::I` in each touched path, with
the retained GLFW window hooks and fallback storage-path return now also
using local retained-state helpers instead of direct method-body reads.
- `src/platform_legacy/legacy_platform_services.*` is still part of the live
app shell.
- `pp_panopainter_ui` still depends on `pp_legacy_app`.
- `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live
OpenGL execution around the renderer boundary, even though `NodeCanvas`
display resolve, cache-to-screen composite, post-draw mask/grid/current-mode
sequencing, per-layer/per-plane retained draw execution, and shared
checkerboard background setup now route through retained draw-merge helpers,
with the cache-to-screen checkerboard-plane callback setup also reduced and
the merged-path checkerboard background-plane callback plus per-plane
merged-texture draw callback plus non-`draw_merged` per-frame layer draw
callback plus the smoothing-mask face shader/draw pass plus heightmap,
current-mode, and grid-mode callback setup now routed through the same
retained helper family, while post-draw callback assembly and the remaining
per-layer render-path orchestration now also route through retained
draw-merge helpers even though the broader node draw loop is still inline,
with the non-`draw_merged` outer layer/plane traversal now also routing
through `execute_legacy_canvas_draw_layer_traversal(...)`, while the heavier
per-layer GL setup now also routes through
`make_legacy_canvas_draw_merge_layer_path_gl_execution(...)` even though the
remaining draw lambdas and broader node draw loop still live in
`src/node_canvas.cpp`.
- `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files
rather than thin composition/binding surfaces, even though tools-menu binding
plus nested panels/options submenu wiring now live in
`src/app_layout_tools_menu.cpp` and `App::init_menu_tools()` is now a thin
call-through, while file-menu binding plus the export submenu wiring now also
live in `src/app_layout_file_menu.cpp` and `App::init_menu_file()` is now a
thin call-through, while about-menu and layer-menu wiring now also live in
`src/app_layout_about_layer_menu.cpp` and `App::init_menu_about()` plus
`App::init_menu_layer()` are now thin call-throughs, while the informational
overlay opener family now also lives in `src/app_dialogs_info_openers.cpp`
and the corresponding `App::dialog_*` entrypoints are thinner.
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
global singleton reach, raw observer pointers, retained static worker
ownership in several app families, and ad hoc mutex/condition-variable
ownership, even though most previously detached or raw app-facing worker
launches now use owned `std::jthread` or service-owned worker queues and
`AppRuntime` now owns render/UI workers with explicit `std::jthread`
shutdown semantics while the Windows splash-dialog and HMD renderer workers
also use owned `std::jthread` lifecycle, `LogRemote` now uses the same
ownership model, the Windows VR device now has explicit `std::unique_ptr`
ownership instead of raw global lifetime, and the Windows main-loop/VR
coordination flags now use `std::atomic` instead of unsynchronized globals,
while the main Win32 entrypoint now groups window/GL/task/VR state behind a
retained local state object instead of separate process-wide globals, the
Win32 async GL/context lock state now lives under
`src/platform_windows/windows_platform_services.cpp` instead of `main.cpp`
retained state, the main-thread queued task state now sits behind a narrow
retained helper instead of `RetainedState.main_tasklist` /
`main_task_mutex`, the canvas async worker now sits behind a named retained
local worker-state helper instead of a bare static accessor, the
prepared-file worker and the canvas async import/export/save/open worker now
live under `AppRuntime` instead of retained static app-events/canvas
workers, and the splash-screen dialog loop, HWND ownership, and bitmap setup
now live in `src/platform_windows/windows_splash.cpp` instead of
`src/main.cpp`, while Win32 pointer API loading, stylus/ink timer decay,
Wintab packet reset, and `WM_POINTERUPDATE` pen/touch handling now also live
in `src/platform_windows/windows_stylus_input.cpp` instead of `src/main.cpp`,
while the retained Win32 VR/HMD shell now also routes through
`src/platform_windows/windows_vr_shell.h` instead of staying inline in
`src/main.cpp`,
while `App::rec_loop()` now delegates worker-iteration orchestration into
the retained recording bridge, `App::update_rec_frames()` now delegates
recording label refresh through that same retained recording path, and the
canvas state-management cluster for picking, clear/clear-all, layer
add/remove/order/lookups, animation frame control, resize, and snapshot
save/restore now lives in `src/legacy_canvas_state_services.cpp` instead of
`src/canvas.cpp`, while the larger import/export/save/open/thumbnail
document-IO cluster now lives in `src/legacy_canvas_document_io_services.cpp`
and `NodeStrokePreview` render-target setup plus immediate-pass sequencing
now route through retained preview execution helpers, even though the bridge
still owns worker-side readback flow and encoder-state label reads.
- Modern C++23 usage exists in extracted components, especially `std::span`,
explicit result/status objects, and a few concepts, but the live app still
does not consistently express ownership, thread affinity, or renderer
resources through safe component contracts.
Conclusion:
- the base component extraction is real
- the app shell thinning is not done
- the platform split is not done
- the UI split is not done
- the renderer/app ownership split is not done
- future backend lab work is still premature until those four statements change
## Final Target Architecture
The old roadmap showed a straight chain.
That was too simple.
The real target is a layered DAG with a 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
```
Key ownership rules:
- `pp_platform_api` is interface and policy only. No concrete platform service
implementation files belong there.
- `pp_platform_*` owns platform SDK, OS handles, platform event loops, and
concrete service bridges.
- `pp_ui_core` owns generic node/layout/overlay behavior and generic controls.
- `pp_panopainter_ui` owns app-specific panels, dialogs, canvas views, and
UI-to-app bindings.
- `pp_app_core` owns planner logic, workflow policy, and service contracts. It
does not own nodes, GL objects, or platform handles.
- `pp_paint_renderer` owns renderer-facing paint/export/preview contracts.
- `panopainter_app` owns composition only. It should stop being a second home
for document workflow, dialog orchestration, platform state, or renderer
execution.
- Threading and task dispatch are app runtime services, not incidental static
queues on `App` or detached workers launched from panels, dialogs, canvas, or
cloud helpers.
- UI ownership is handle/registry based at component boundaries. Raw `Node*`
can remain as non-owning implementation detail only when lifetime is proven
by checked handles or scoped connections.
- Renderer-facing app code uses `pp_renderer_api` resources and command/context
contracts. `Texture2D`, `RTT`, direct GL dispatch, and render-thread helpers
must not leak into future-backend-facing UI or document code.
## Workstreams
### 1. Break The Canvas And Preview Hotspots First
This is the highest-value work because it moves the largest concentration of
real app behavior out of the old shell.
Required outcomes:
- `canvas.cpp` loses major document-plus-render ownership
- `node_canvas.cpp` and `node_stroke_preview.cpp` lose major render-pass
orchestration
- concrete GL execution moves behind renderer-facing services instead of living
in app/node files
### 2. Thin The App Shell
`app.cpp`, `app_layout.cpp`, and `app_dialogs.cpp` must stop acting as mixed
workflow, UI, and composition files.
Required outcomes:
- `app_layout.cpp` becomes menu/toolbar binding composition
- `app_dialogs.cpp` becomes workflow dispatch plus retained dialog opening glue
- `app.cpp` becomes startup/frame/queue composition over named helpers
### 3. Finish The UI Core Split
`pp_ui_core` exists, but generic widget ownership is still incomplete.
Required outcomes:
- generic `Node` and control code moves out of `pp_legacy_ui_core`
- `pp_panopainter_ui` keeps only app-specific nodes
- shared overlay/lifetime behavior stays centered in `pp_ui_core`
- the scene graph has explicit ownership, non-owning references, scoped
callback connections, and documented UI-thread affinity
### 4. Make Runtime And Thread Ownership Explicit
This is crucial for a modern app architecture and must move with the app-shell
split, not after it.
Required outcomes:
- render/UI/worker queues are owned by explicit runtime services
- detached worker threads are replaced by joinable/cancellable ownership or a
task service with shutdown semantics
- render-thread and UI-thread access are expressed through small contracts that
can be implemented by future platform shells
- `App::I` and `Canvas::I` stop being the way cross-thread code reaches state
### 5. Finish The Platform Split
This is still a real blocker, but it should follow the bulk code-moving work
above instead of taking priority over the main app hotspots.
Required outcomes:
- remove concrete platform code from `pp_platform_api`
- remove `App::I` reach from platform service implementations
- remove app-owned cross-platform handle storage
- reduce `platform_legacy` to thin composition or delete it
### 6. Retire Thick Workflow Bridges
Open/save/session/export/cloud/brush package flows are still too distributed
across retained app, panel, and dialog files.
Required outcomes:
- document workflow bridges become thin adapters over `pp_app_core`
- cloud transfer and cloud browser ownership move out of retained UI nodes
- brush package import/export ownership moves out of retained panel code and
no longer depends on detached worker launch sites
### 7. Only Then Resume Future Backend Work
Vulkan, Metal, and package-only cleanup are explicitly downstream of the app
architecture work above.
Do not treat future backend scaffolds as proof that modernization is near done
while the current shell still depends on:
- `platform_legacy`
- `pp_legacy_app`
- `pp_legacy_ui_core`
- `pp_legacy_paint_document`
- large GL-heavy node and canvas files
## Exit Criteria
The modernization is not done until these are all true:
- `pp_platform_api` contains only platform-neutral code
- `src/platform_apple/*`, `src/platform_linux/*`, and other concrete platform
implementations do not reach `App::I`
- `platform_legacy` is gone or reduced to a trivial temporary shim
- `App` no longer stores cross-platform handle state that belongs to platform
shells
- `pp_panopainter_ui` no longer depends on `pp_legacy_app`
- `panopainter_app` is a composition root, not a second workflow layer
- `canvas.cpp`, `node_canvas.cpp`, and `node_stroke_preview.cpp` no longer own
large renderer-orchestration bodies
- live app, UI, canvas, cloud, and platform code no longer launch detached
worker threads without owned shutdown/cancellation
- render/UI task queues are owned behind explicit app runtime services
- raw app/UI pointers are non-owning implementation details only, backed by
checked handles, scoped connections, or documented owner objects
- future-backend-facing app and UI code depends on renderer API contracts, not
retained OpenGL resource classes or direct GL execution
- `pp_legacy_ui_core`, `pp_legacy_app`, and `pp_legacy_paint_document` are
removed or reduced to narrow, explicit adapter seams with debt ids and clear
removal conditions
## Active Queue
Use `docs/modernization/tasks.md` for the current architecture task bundles,
ordered by real code-moving priority.
Use `docs/modernization/tasks-done.md` only for history.