Files
panopainter/docs/modernization/tasks.md

830 lines
32 KiB
Markdown

# Modernization Task Tracker
Status: live
Last updated: 2026-06-16
This file now tracks only active architecture work.
Completed, blocked, and superseded task history moved to
`docs/modernization/tasks-done.md`.
## Operating Rules
- Keep this file short. If a task is done, blocked for a long time, or no
longer relevant, move it to `tasks-done.md` instead of letting the active
queue sprawl.
- Keep tasks architecture-first. Build, test, tool, planner, CLI, and
automation cleanup are secondary unless they directly unblock or accompany a
real ownership transfer in the live app.
- Prefer coherent bundles over tiny adapter nibbles. Each task here should make
visible progress in a hotspot file or a legacy target, not just add another
seam around the same code.
- Use legacy-target shrinkage and hotspot reduction as the main progress signal.
If a slice does not materially reduce a large file or move code out of
`PP_PANOPAINTER_*` or `PP_LEGACY_*`, it is probably not first-priority work.
- Do not restart Vulkan, Metal, or package-only work until the app shell,
platform split, UI split, and canvas/render split are materially thinner.
## Current Architecture Read
- The extracted pure targets are real and useful: `pp_foundation`,
`pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`,
`pp_paint_renderer`, `pp_ui_core`, `pp_platform_api`, and `pp_app_core`.
- The remaining app still lives mostly in legacy containment or thick shell
targets:
- `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
- The biggest single-file choke points are still `src/canvas.cpp`,
`src/app_layout.cpp`, `src/canvas_modes.cpp`, `src/node.cpp`,
`src/main.cpp`, `src/node_panel_brush.cpp`, `src/node_stroke_preview.cpp`,
`src/node_canvas.cpp`, `src/app.cpp`, and `src/app_dialogs.cpp`.
- The platform boundary is not finished:
- `pp_platform_api` still compiles Apple implementation files
- `platform_apple` no longer reaches `App::I` directly, and Linux FPS title
reporting now uses an injected callback, but retained Apple bridging and
broader platform-to-app singleton reach are still open in
`platform_legacy`
- `platform_legacy` is still part of the live app shell
- The app runtime boundary is not finished:
- render/UI queues are static `App` state
- app-facing detached launches are no longer the main issue; preview and
recording now use owned worker threads, but those families still rely on
retained static/global ownership and ad hoc runtime control
- canvas async import/export/save/open now run through an owned in-file
worker, but their retained progress execution is still not a clean runtime
service boundary
- thread-affinity rules are enforced by convention and asserts instead of
explicit runtime contracts
- The UI ownership boundary is not finished:
- base `Node` still carries raw parent/manager pointers beside shared handles
- callback captures still frequently close over retained nodes and `App::I`
- checked overlay/lifetime helpers exist but are not yet the default scene
graph model
- Historical score/progress claims are not useful for prioritization here.
The live app still mostly runs through the same shell and hotspot files, so
the queue is now ordered by code movement instead.
## Active Bundles
### Priority Order
- `P0`: shrink the biggest live app hotspots and legacy ownership first
- `P0`: make app runtime, UI ownership, and renderer access safe enough for
future backend work
- `P1`: finish supporting boundaries that unblock or stabilize the thinner app
- `P2`: only then clean up the remaining workflow adapters
### Bundle 1 - Break The Canvas And Preview Hotspots
Priority: `P0`
Why this bundle is first:
This is where the biggest block of real app behavior still lives.
If these files stay large and stateful, the rest of the modernization still
looks like a wrapper around the old renderer shell.
#### ARC-RND-001 - Split `canvas.cpp` Into Document State And Render Execution Shells
Status: In Progress
Why now:
`src/canvas.cpp` is still the biggest single architectural blocker at about
2645 lines.
Current slice:
- Canvas state-management helpers for picking, clear/clear-all, layer
add/remove/order/lookups, animation frame control, resize, and snapshot
save/restore now live in `src/legacy_canvas_state_services.cpp` instead of
staying inline in `src/canvas.cpp`, but the file still owns the larger
document-plus-render shell.
- Canvas import/export/save/open/thumbnail ownership now lives in
`src/legacy_canvas_document_io_services.cpp` instead of staying inline in
`src/canvas.cpp`, which materially reduces document IO ownership in the live
render shell.
Write scope:
- `src/canvas.cpp`
- `src/canvas_layer.cpp`
- `src/canvas_actions.cpp`
- `src/legacy_canvas_*services*.h`
- new `src/legacy_canvas_*.*` helpers if needed
Read scope:
- `src/document/*`
- `src/paint_renderer/*`
- `src/renderer_api/*`
Done when:
- `canvas.cpp` stops being the place where document state, render sequencing,
history reads, and GL-side execution all meet.
- Non-render state/query/update helpers move out first.
- The remaining file reads as a render shell with explicit helper boundaries.
- The touched slice makes a substantial reduction in inline ownership, not just
a thin wrapper extraction.
Mini-model packet:
- Do not try to delete `Canvas` in one slice.
- Prioritize ownership separation over clever abstractions.
- Aim for a visible reduction in `canvas.cpp`, on the order of hundreds of
lines, not a token helper extraction.
#### ARC-RND-002 - Isolate Preview And Canvas View Render Execution
Status: In Progress
Why now:
`src/node_stroke_preview.cpp` and `src/node_canvas.cpp` still own a large amount
of live preview/canvas render sequencing around the renderer boundary.
Current slice:
- `NodeStrokePreview` final composite plus preview-texture copy now route
through `legacy_node_stroke_preview_execution_services.h`, but the preview
node still owns most live-pass and retained GL resource execution.
- `NodeStrokePreview` render-target setup plus immediate-pass sequence
orchestration now also route through
`legacy_node_stroke_preview_execution_services.h`, and duplicate render-
target setup was removed from `render_to_image()` and the queued worker path,
but the preview node still owns broader live-pass state and thread-facing
orchestration.
- `NodeCanvas` merged-path per-plane merged-texture draw execution now also
routes through `execute_legacy_canvas_draw_merge_layer_texture(...)`.
- `NodeCanvas` merged-path and non-blend checkerboard background setup now also
route through `execute_legacy_canvas_draw_merge_background_setup(...)`.
- `NodeCanvas` cache-to-screen checkerboard-plane callback setup now also routes
through a retained helper in `legacy_canvas_draw_merge_services.h`.
- `NodeCanvas` display resolve plus cache-to-screen checkerboard/cache-texture
composite now route through `legacy_canvas_draw_merge_services.h`.
- `NodeCanvas` smoothing-mask overlay, smoothing-mask face pass, grid keepalive
draw, heightmap draw, and current-mode draw now also route through
`execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw
orchestration is still inline.
- `NodeCanvas` heightmap draw callback setup now also routes through
`make_legacy_canvas_draw_merge_heightmap_draw(...)`, but the node still owns
current-mode traversal and broader post-draw orchestration.
- `NodeCanvas` current-mode draw callback setup now also routes through
`make_legacy_canvas_draw_merge_current_modes_draw(...)`, and grid-mode
callback setup now also routes through
`make_legacy_canvas_draw_merge_grid_modes_draw(...)`, but broader post-draw
orchestration is still inline.
- `NodeCanvas` checkerboard background plane callback setup now also routes
through `make_legacy_canvas_draw_merge_background_checkerboard_plane(...)`,
but the node still owns broader live layer traversal and renderer-state
sequencing.
- `NodeCanvas` merged-path per-plane merged-texture draw callback setup now
also routes through `make_legacy_canvas_draw_merge_layer_texture_draw(...)`,
but the node still owns broader live layer traversal and renderer-state
sequencing.
- `NodeCanvas` non-`draw_merged` per-frame layer draw callback setup now also
routes through `make_legacy_canvas_draw_merge_layer_frame_draw(...)`, but
the node still owns broader live layer traversal and renderer-state
sequencing.
- `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution
now also route through
`execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns
the broader canvas draw flow and renderer-state sequencing around that seam.
- `NodeCanvas` non-`draw_merged` per-layer/per-plane retained draw execution
now also routes through `execute_legacy_canvas_draw_merge_layer_plane(...)`,
but the node still owns substantial live layer traversal and renderer state
orchestration.
- `NodeCanvas` post-draw callback assembly now also routes through
`execute_legacy_canvas_draw_merge_post_draw_callbacks(...)`, and the
remaining per-layer render-path orchestration now also routes through
`execute_legacy_canvas_draw_merge_layer_path(...)`, but the node still owns
broader draw-loop and renderer-state shell sequencing.
Write scope:
- `src/node_stroke_preview.cpp`
- `src/node_canvas.cpp`
- `src/legacy_node_stroke_preview_execution_services.h`
- `src/legacy_canvas_stroke_preview_services.h`
- `src/paint_renderer/compositor.*`
Read scope:
- `src/renderer_api/*`
- `src/renderer_gl/*`
Done when:
- Preview/canvas files stop carrying large inline render-pass orchestration.
- Concrete GL texture/bind/copy work is pushed behind explicit renderer-facing
service seams.
- The next renderer backend would have one place to implement preview/canvas
execution contracts instead of reading node code.
- The touched node files are materially smaller and less stateful afterward.
Mini-model packet:
- Keep the existing `pp_paint_renderer` planner surface and extend it only when
the node files clearly need a missing renderer-owned callback contract.
- Prefer deleting inline orchestration over adding another planner layer around
the same node code.
#### ARC-RND-003 - Replace App-Facing `Texture2D`/`RTT` Use With Renderer API Contracts
Status: Ready
Why now:
Future Vulkan and Metal work needs the live app to stop treating retained
OpenGL resource classes as the renderer boundary.
Write scope:
- `src/node_canvas.cpp`
- `src/node_stroke_preview.cpp`
- `src/canvas.cpp`
- `src/texture.*`
- `src/rtt.*`
- `src/renderer_api/*`
- `src/paint_renderer/*`
Read scope:
- `src/renderer_gl/*`
- `src/legacy_gl_*dispatch.h`
- `src/app_shaders.cpp`
Done when:
- App/UI-facing render work talks to `pp_renderer_api` resource and command
abstractions or narrow retained renderer services, not directly to
`Texture2D`, `RTT`, or GL dispatch helpers.
- The remaining `Texture2D`/`RTT` references are contained in retained GL
backend/adapters with explicit removal conditions.
- Canvas, preview, and export paths expose the same execution contract a Vulkan
or Metal implementation would need.
- The touched slice shrinks app/UI render ownership instead of only renaming
wrappers.
Mini-model packet:
- Do not start a Vulkan or Metal backend in this task.
- Use existing `pp_renderer_api` and `pp_paint_renderer` contracts first.
- Treat direct GL classes in UI/app code as debt to move behind backend-owned
services.
### Bundle 2 - Thin The App Shell
Priority: `P0`
Why this bundle is next:
`app_layout.cpp`, `app_dialogs.cpp`, and `app.cpp` still make the modernized
targets look like helpers under one old monolith.
#### ARC-APP-001 - Split `app_layout.cpp` Into UI Binding Modules
Status: In Progress
Why now:
`src/app_layout.cpp` is still a 1498-line mixed file that builds menus,
attaches callbacks, computes planner inputs, and mutates UI state directly.
Current slice:
- Tools-menu UI binding, including the nested panels/options submenu wiring,
now lives in `src/app_layout_tools_menu.cpp`, and `App::init_menu_tools()`
is now a thin call-through, but file/about/layer/sidebar and broader layout
composition are still inline in `src/app_layout.cpp`.
Write scope:
- `src/app_layout.cpp`
- `src/legacy_app_shell_services.*`
- new `src/app_layout_*.*` or `src/legacy_*_ui_services.*` files if needed
Read scope:
- `src/app_core/*menu*.h`
- `src/app_core/brush_ui.h`
- `src/app_core/document_layer.h`
- `src/app_core/main_toolbar.h`
Done when:
- `app_layout.cpp` becomes a composition/binding file instead of a giant mixed
controller.
- File-menu, toolbar, tools-menu, about-menu, and layer-menu wiring each live
in named helper modules or services.
- The split follows planner/service boundaries already present in `pp_app_core`.
- The touched slice materially shrinks the file instead of just moving a few
lambdas around.
Mini-model packet:
- Start by carving out one coherent family at a time, not by reshuffling lines.
- Preserve the current planner calls; the goal is ownership, not new behavior.
- Aim for a real file-size drop, not cosmetic decomposition.
#### ARC-APP-002 - Split `app_dialogs.cpp` Into Workflow Adapters And Widget Openers
Status: Ready
Why now:
`src/app_dialogs.cpp` still mixes document workflow decisions, export routing,
dialog construction, and overlay ownership.
Write scope:
- `src/app_dialogs.cpp`
- `src/legacy_app_dialog_services.*`
- `src/legacy_document_session_services.*`
- `src/legacy_document_open_services.*`
- `src/legacy_document_export_services.*`
Read scope:
- `src/app_core/app_dialog.h`
- `src/app_core/document_session.h`
- `src/app_core/document_export.h`
Done when:
- `app_dialogs.cpp` is reduced to thin entrypoints plus named helper modules.
- Dialog creation/opening is clearly separated from document/export workflow
routing.
- The remaining direct node-specific code is isolated to retained dialog
adapters.
- The slice removes a meaningful amount of mixed live ownership from
`app_dialogs.cpp`.
Mini-model packet:
- Preserve existing planner usage.
- Prefer new narrow helper files over leaving another giant dialog utility file.
- Do not spend time extending dialog planners or CLI surfaces unless the live
adapter gets thinner in the same slice.
#### ARC-APP-003 - Reduce `app.cpp` To Frame, Queue, And Composition Shell
Status: Ready
Why now:
`src/app.cpp` still carries startup, frame flow, queue draining, recording,
observer math, and composition logic in one 950-line file.
Write scope:
- `src/app.cpp`
- `src/legacy_app_startup_services.*`
- `src/legacy_recording_services.*`
- small new `src/app_runtime_*.*` helpers if needed
Read scope:
- `src/app_core/app_frame.h`
- `src/app_core/app_shutdown.h`
- `src/app_core/app_startup.h`
- `src/app_core/document_recording.h`
Done when:
- `app.cpp` reads like a shell over `pp_app_core` planners and named retained
services.
- Startup, frame/update, queue/thread, and recording glue are split into named
helpers instead of living inline.
- `App` keeps ownership of composition state only where it truly has to.
- The file becomes materially thinner in the same slice.
Mini-model packet:
- Keep thread behavior unchanged.
- Split by responsibility boundaries already present in `pp_app_core`.
- Prefer moving live ownership out over creating new planner wrappers.
#### ARC-APP-004 - Move Render/UI Queues Into An Owned App Runtime Service
Status: In Progress
Why now:
`App` still owns static render/UI queues, mutexes, condition variables, and
thread ids. That makes thread safety hard to reason about and keeps platform
entrypoints coupled to the singleton.
Current slice:
- render/UI queues, mutexes, condition variables, and thread identity already
live in `AppRuntime`
- `AppRuntime` render/UI worker ownership now also uses `std::jthread` plus
explicit stop requests instead of raw `std::thread`
- Windows main-loop run-state and VR worker coordination flags in `main.cpp`
now use `std::atomic` ownership instead of unsynchronized globals
- `main.cpp` Win32 window handles, GL task/mutex state, splash-dialog state,
stylus timers, and VR worker state now sit behind one retained local state
object instead of separate file-scope globals
- Win32 async GL/context lock state now lives in
`src/platform_windows/windows_platform_services.cpp` instead of `main.cpp`
retained state, and `main.cpp` only seeds that platform-owned context handle
pair during initialization and context recreation
- `main.cpp` main-thread queued task state now sits behind a narrow retained
helper instead of `RetainedState.main_tasklist` / `main_task_mutex` directly
- prepared-file background work now runs through an `AppRuntime`-owned worker
queue instead of a retained static worker in `src/app_events.cpp`
- canvas async import/export/save/open background work now also runs through an
`AppRuntime`-owned worker queue instead of a retained static worker in
`src/canvas.cpp`
- retained `App` composition, task call sites, and platform/runtime entrypoint
coupling are still not fully reduced behind the runtime contract
Write scope:
- `src/app.h`
- `src/app.cpp`
- `src/app_events.cpp`
- `src/main.cpp`
- new `src/app_runtime_*.*` or retained runtime service files if needed
Read scope:
- `src/app_core/app_thread.h`
- `src/platform_api/platform_services.h`
- render/UI task call sites under `src/*.cpp`
Done when:
- Render and UI task queues are owned by an explicit runtime object or service
with startup, drain, stop, and thread-affinity APIs.
- `App` composes that runtime instead of exposing static global queue state.
- Platform event code and retained services post work through the runtime
contract rather than by reaching `App::I` static queues.
- Shutdown behavior remains deterministic and the touched slice reduces
singleton/thread coupling.
Mini-model packet:
- Keep public behavior and thread ordering unchanged.
- Prefer a small runtime owner over broad task-system redesign.
- Make ownership and shutdown semantics explicit before adding new features.
#### ARC-APP-005 - Replace Detached App Workers With Joinable Or Service-Owned Work
Status: In Progress
Why now:
The biggest app-facing async families have been moved off detached launches,
but retained worker ownership and ad hoc runtime control are still not a safe
modernization foundation.
Current slice:
- app-owned render/UI runtime queues and cloud worker ownership are already
moving behind owned runtime/service objects
- Windows splash-dialog and HMD renderer worker ownership in `main.cpp` now
also use `std::jthread` with explicit stop requests instead of raw
`std::thread`
- Windows VR device ownership in `main.cpp` now also uses `std::unique_ptr`
instead of a raw `Vive*`
- `LogRemote` worker ownership in `src/log.*` now also uses `std::jthread`
with explicit stop requests instead of raw `std::thread`
- brush package import/export now use service-owned `std::jthread` workers and
UI-thread completion handoff
- prepared-file save work and grid lightmap launch now also use service-owned
workers with explicit UI-thread handoff
- canvas async import/export/save/open and timelapse export now also use owned
worker queues instead of detached threads
- `src/app_events.cpp` prepared-file worker ownership and `src/canvas.cpp`
async import/export/save/open worker ownership now also sit behind named
retained local worker-state helpers instead of bare static worker accessors
- the prepared-file worker and the broader canvas async import/export/save/open
worker have now both moved into `AppRuntime`, removing the retained static
workers from `src/app_events.cpp` and `src/canvas.cpp`
- preview background rendering, recording, and the retained
`NodePanelGrid::bake_uvs()` worker now also use `std::jthread`, but their
retained loop/control flow is still open
- `App::rec_loop()` now delegates its worker-iteration orchestration into the
retained recording bridge in `src/legacy_recording_services.cpp`, while
`App::update()` no longer carries the dead update mutex residue; retained
recording loop control, readback ownership, and MP4 execution are still open
- `App::update_rec_frames()` now delegates recording label refresh through
`src/legacy_recording_services.cpp`, but retained recording label lookup,
encoder-state reads, and MP4 execution still stay on the legacy bridge
Write scope:
- `src/canvas.cpp`
- `src/app_cloud.cpp`
- `src/app_events.cpp`
- `src/legacy_cloud_services.*`
- `src/legacy_brush_package_import_services.*`
- `src/legacy_brush_package_export_services.*`
- `src/legacy_grid_ui_services.*`
- `src/node_dialog_cloud.*`
- `src/node_stroke_preview.*`
Read scope:
- `src/app_core/app_thread.h`
- `src/foundation/task_queue.*`
- `src/legacy_recording_services.*`
Done when:
- Touched worker families are owned by joinable `std::jthread`, a scoped worker
object, or an injected task service with cancellation/shutdown semantics.
- Worker callbacks do not capture raw retained nodes or `this` across unknown
lifetime without a checked handle, weak ownership, or explicit owner.
- App shutdown can stop the touched worker family without racing UI/layout or
renderer destruction.
- Detached `std::thread` count drops materially in app-facing code.
Mini-model packet:
- Start with one coherent worker family, such as cloud or brush package import.
- Do not rewrite all threading at once.
- Preserve the existing UI/progress behavior while changing ownership.
### Bundle 3 - Finish The UI Core Split
Priority: `P0`
Why this bundle is still top priority:
Until generic `Node` and control code leaves `pp_legacy_ui_core`, the UI
architecture remains mostly the old one with a modern overlay/lifetime helper
attached to it.
#### ARC-UI-001 - Move Generic Node And Control Code Out Of `pp_legacy_ui_core`
Status: Ready
Why now:
`pp_ui_core` has layout, color, node lifetime, and overlay lifetime, but the
generic widget layer still sits in `pp_legacy_ui_core`.
Write scope:
- `src/node.cpp`
- `src/layout.cpp`
- generic `src/node_*` base control files from `PP_LEGACY_UI_CORE_SOURCES`
- `src/ui_core/*`
- `CMakeLists.txt`
- `cmake/PanoPainterSources.cmake`
Read scope:
- `src/node_panel_*`
- `src/node_dialog_*`
Done when:
- Generic controls and base node/layout code are clearly separated from
PanoPainter-specific panels and dialogs.
- `pp_ui_core` grows as the home of generic widgets and node behavior.
- `pp_panopainter_ui` keeps only app-specific panels, dialogs, canvas, preview,
and workflow nodes.
- The touched slice removes real file ownership from `pp_legacy_ui_core`, not
just adds wrappers around it.
Mini-model packet:
- Start with the controls that have no app-specific policy:
button, checkbox, icon, image, scroll, slider, text, text input.
- Do not mix panel/dialog rewrites into the same slice.
- Prefer target ownership moves over purely internal helper reshuffles.
#### ARC-UI-002 - Make Checked Handles The Default UI Ownership Model
Status: Ready
Why now:
`pp_ui_core` has lifetime and overlay handle helpers, but retained UI code still
mixes raw `Node*`, shared ownership, direct `add_child(...)`, and callback
captures across mutation points.
Write scope:
- `src/node.*`
- `src/layout.*`
- `src/legacy_ui_overlay_services.*`
- retained `src/node_dialog_*` and `src/node_panel_*` files touched by a slice
- `src/ui_core/node_lifetime.*`
- `src/ui_core/overlay_lifetime.*`
Read scope:
- existing popup/dialog call sites found with `add_child`, `remove_child`, and
`on_*` callback captures
Done when:
- New or touched UI surfaces open, close, and dispatch callbacks through checked
handles or scoped connections by default.
- Raw `Node*` fields and callback parameters are documented or reshaped as
non-owning views, not lifetime owners.
- Destroy-during-callback and close-during-dispatch behavior is owned by
`pp_ui_core` rather than each panel/dialog.
- App-specific panels become view/controller shells over safe UI-core
lifetime primitives.
Mini-model packet:
- Convert one popup/dialog family at a time.
- Do not redesign the UI appearance in this task.
- Prefer deleting raw lifetime assumptions over adding more guard comments.
#### ARC-UI-003 - Split UI Rendering From Scene Graph And App State
Status: Ready
Why now:
The generic node layer still mixes layout, input, rendering, direct app access,
and retained OpenGL resource usage. That blocks both a cleaner UI component and
future renderer backends.
Write scope:
- `src/node.cpp`
- `src/node_canvas.cpp`
- `src/node_stroke_preview.cpp`
- generic control files moved toward `src/ui_core/*`
- `src/renderer_api/*`
- `src/paint_renderer/*`
Read scope:
- `src/font.*`
- `src/shape.*`
- `src/texture.*`
- `src/rtt.*`
Done when:
- Scene graph/layout/input code has a renderer-neutral draw contract.
- Generic controls do not need to know app singleton state or concrete GL
resource classes.
- App-specific canvas and preview rendering depends on renderer-facing services
rather than base `Node` internals.
- The touched slice makes `pp_ui_core` more reusable without hiding app policy
inside it.
Mini-model packet:
- Keep visual behavior unchanged.
- Do not move PanoPainter-specific panel policy into `pp_ui_core`.
- Use renderer-neutral contracts first; backend implementation follows later.
### Bundle 4 - Make The Platform Boundary Real
Priority: `P1`
Why this bundle is not `P0`:
It matters, but moving platform code first will not change the day-to-day shape
of the app as much as reducing `canvas.cpp`, the app shell, and the generic UI
layer.
#### ARC-PLT-001 - Split `pp_platform_api` From Concrete Platform Code
Status: Ready
Why now:
`pp_platform_api` is supposed to be the SDK-free policy and interface layer,
but it still compiles `src/platform_apple/apple_platform_services.*`.
Write scope:
- `CMakeLists.txt`
- `src/platform_api/*`
- `src/platform_apple/*`
Read scope:
- `cmake/PanoPainterSources.cmake`
- `src/platform_windows/*`
- `src/platform_linux/*`
Done when:
- `pp_platform_api` contains only platform-neutral interfaces, policies, and
shared helpers.
- Apple implementation files are built by a concrete platform target instead of
the API target.
- The dependency direction is obvious from CMake without reading debt notes.
Mini-model packet:
- Start in `CMakeLists.txt` around `pp_platform_api`.
- Keep the change structural; do not broaden into new feature work.
- Preserve current Apple service entrypoints while moving ownership.
#### ARC-PLT-002 - Remove `App::I` Reach From Apple And Linux Services
Status: In Progress
Why now:
The current Apple and Linux service files still call into the app singleton,
which means the platform layer is not a platform layer yet.
Current slice:
- Linux FPS title updates now route through an injected callback installed from
`App::set_platform_services()`
- `platform_apple` clipboard, display/share, cursor, and save-ui-state calls
now route through injected Apple bridge callbacks instead of `App::I`
- `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path
preparation through a narrow local helper instead of reading `App::I`
directly in that method body
- iOS virtual-keyboard visibility and prepared-file save handoff now also route
through explicit Apple bridge callbacks instead of direct `App::I` calls in
`LegacyPlatformServices`
- Apple render-context acquire/release/present hooks and iOS
`bind_main_render_target()` now also route through explicit Apple bridge
callbacks instead of direct `App::I` calls in `LegacyPlatformServices`
- Apple crash-test, app-close, and iOS SonarPen hooks now also route through
explicit Apple bridge callbacks instead of direct `App::I` calls in
`LegacyPlatformServices`
- retained Apple ObjC handles plus storage paths now live in one local
`platform_legacy` helper, and the iOS SonarPen bridge now starts through
that retained Apple state instead of reading `App::I` inside the bridge body
- Linux/Web GLFW render-context acquire/present hooks and Linux app-close now
also route through retained local GLFW callback hooks instead of direct
method-body `App::I` access in `LegacyPlatformServices`
- retained GLFW window hooks and the non-Linux fallback storage-path return now
also route through retained local state helpers instead of reading `App::I`
directly in those method bodies
- retained Apple callback injection and broader `platform_legacy` singleton
reach are still open
Write scope:
- `src/platform_apple/*`
- `src/platform_linux/*`
- `src/app_events.cpp`
- `src/app.h`
Read scope:
- `src/platform_api/platform_services.h`
- `src/platform_legacy/legacy_platform_services.*`
Done when:
- `src/platform_apple/*` and `src/platform_linux/*` no longer call `App::I`.
- The app injects the minimum callbacks or bridge state those services need.
- Platform files stop depending on app-global state for clipboard, sharing, FPS
title updates, or native UI saves.
Mini-model packet:
- Keep the interface small. Prefer injected callbacks/bridges over passing the
whole `App`.
- Do not rewrite Windows in the same slice.
#### ARC-PLT-003 - Remove App-Owned Cross-Platform Handle Storage
Status: Ready
Why now:
`src/platform_legacy/legacy_platform_services.cpp` and `src/app.h` still keep
platform-handle state on `App`, which blocks a real `pp_platform_*` shell split.
Write scope:
- `src/platform_legacy/legacy_platform_services.*`
- `src/app.h`
- `src/app_events.cpp`
- `src/platform_windows/*`
Read scope:
- `src/main.cpp`
- Apple/Android/Web/Linux entrypoint files only as needed
Done when:
- `App` no longer owns platform-specific handle fields that belong to shells.
- The legacy platform adapter becomes thin composition or disappears for the
touched path.
- Platform setup state lives with the relevant `pp_platform_*` implementation.
Mini-model packet:
- Keep this slice about state ownership, not feature behavior.
- Prefer moving state to shell-local structs or service singletons owned by the
platform target.
### Bundle 5 - Retire The Thick Workflow Bridges
Priority: `P2`
Why this bundle is later:
These bridges still matter, but many recent slices spent too much effort
polishing adapters without changing the bulk shape of the live app. This bundle
stays active only after the main hotspots are moving.
#### ARC-WKF-001 - Thin Document Open/Save/Session Bridges To Pure Adapters
Status: Ready
Why now:
The document session/open/save planners exist, but the live bridges still own a
lot of retained dialog, metadata, title, and save execution behavior.
Write scope:
- `src/legacy_document_open_services.*`
- `src/legacy_document_session_services.*`
- `src/legacy_document_export_services.*`
- `src/legacy_history_services.*`
Read scope:
- `src/app_core/document_route.h`
- `src/app_core/document_session.h`
- `src/app_core/document_export.h`
Done when:
- The remaining bridge files are thin adapters from planner outputs to retained
execution.
- Save/open/session flows stop mutating app/document/UI state inline across
multiple bridge layers.
- Title updates, history clearing, overwrite prompts, and save routing are each
owned in one obvious place.
Mini-model packet:
- Preserve current planner contracts.
- Favor one adapter per workflow family over catch-all helper growth.
#### ARC-WKF-002 - Split Cloud And Brush Package Work Out Of Retained UI Nodes
Status: Ready
Why now:
Cloud browse/download/upload and brush package import/export still close over
retained nodes, worker threads, and direct UI ownership.
Write scope:
- `src/legacy_cloud_services.*`
- `src/node_dialog_cloud.*`
- `src/legacy_brush_package_import_services.*`
- `src/legacy_brush_package_export_services.*`
- `src/node_panel_brush.cpp`
Read scope:
- `src/app_core/document_cloud.h`
- `src/app_core/brush_package_import.h`
- `src/app_core/brush_package_export.h`
- `src/assets/brush_package.*`
Done when:
- Network transfer execution, thumbnail loading, and brush package worker
ownership are isolated behind named services.
- Retained nodes become view/controller shells instead of workflow owners.
- Cloud and brush package code no longer need to be understood through panel or
dialog internals first.
Mini-model packet:
- Split worker ownership from UI ownership first.
- Do not try to redesign cloud UX or brush preset UX in the same slice.
## Deferred On Purpose
- Vulkan and Metal lab work
- package-only and automation-only cleanup
- scorekeeping tasks that do not move app architecture
These remain in history only until the app shell, platform split, UI split, and
canvas/render split are materially thinner.