Files
panopainter/docs/modernization/tasks.md

76 KiB

Modernization Task Tracker

Status: live Last updated: 2026-06-17

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 remaining single-file pressure is no longer the old entrypoint shell files; it now sits mostly in retained legacy service layers behind src/node_stroke_preview.cpp, src/app.cpp, src/app_dialogs.cpp, and the extracted canvas/platform containment files.
  • The platform boundary is not finished:
    • pp_platform_api no longer compiles Apple or Linux implementation files, but broader concrete platform ownership is still not fully separated
    • 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
    • Apple retained bridge/state ownership now lives in src/platform_apple/apple_platform_state.cpp and src/platform_apple/apple_platform_services.*, but the legacy platform facade still routes a broad Apple service surface instead of disappearing
    • 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
    • the Win32 input path now binds the active WacomTablet* explicitly through windows_runtime_shell, but that tablet binding still lives at a composition edge instead of a broader runtime/platform-owned controller
    • the touched Win32 app/window/session queries now route through narrow runtime-shell accessors, but the broader runtime/thread host still owns composition-edge global state and shutdown sequencing
      • 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.

Current slice:

  • src/platform_legacy/legacy_platform_state.* no longer keeps a separate retained Web platform-service binding; the Web helper path now uses the shared platform_api injection hook.
  • src/platform_legacy/legacy_platform_state.* no longer keeps any retained GLFW window state.
  • linux/src/main.cpp and webgl/src/main.cpp no longer seed legacy GLFW retained state.
  • src/platform_windows/windows_window_shell.cpp no longer keeps any retained mouse-position pocket for button events; it now reads client coordinates directly from the Win32 messages that already carry them.
  • src/platform_legacy/legacy_platform_services.* now uses an injected LegacyGlfwPlatformShell for the remaining Linux/WebGL acquire/present/request-close path.
  • linux/src/main.cpp and webgl/src/main.cpp now bind those GLFW operations directly from entrypoint-owned window state.
  • src/platform_legacy/legacy_platform_state.* no longer carries the old GLFW acquire/present/request-close helper trio.
  • src/platform_windows/windows_platform_services.cpp now owns the Win32 virtual-key map initialization path, so windows_window_shell.cpp no longer carries that retained input setup.
  • src/platform_windows/windows_window_shell.* no longer keeps the dead retained raw key-state array.
  • linux/src/main.cpp now owns the Linux FPS-title callback lifecycle directly, and src/app_events.cpp no longer installs Linux-specific title behavior from App::set_platform_services().
  • src/platform_windows/windows_window_shell.* now stores the Win32 virtual-key map separately from the retained input-state struct.
  • src/platform_windows/windows_runtime_shell.cpp no longer stores a second retained App* alongside App::I.
  • src/platform_windows/windows_window_shell.* plus src/platform_windows/windows_platform_services.cpp now separate Win32 VR state from the generic retained input-state bundle.
  • src/platform_legacy/legacy_platform_state.* no longer keeps the dead generic storage-path singleton for the current platform matrix.
  • src/platform_legacy/legacy_platform_services.cpp now returns only platform-specific storage-path branches; the dead generic fallback read is gone.
  • src/legacy_app_startup_services.cpp no longer writes the dead generic storage-path fallback state on non-Android startup.
  • src/platform_legacy/legacy_platform_services.cpp plus src/platform_legacy/legacy_platform_state.* now give Android its own retained storage-path surface instead of reading the shared legacy singleton.
  • src/legacy_app_startup_services.cpp and android/src/cpp/main.cpp now write Android-owned retained storage paths.
  • src/app_events.cpp, linux/src/main.cpp, and src/platform_legacy/legacy_platform_state.* now use a narrow GLFW title helper for Linux FPS-title updates instead of direct retained-window access in the app event layer.
  • src/platform_legacy/legacy_platform_state.h no longer exports the raw retained GLFW window accessor.
  • src/platform_apple/apple_platform_state.cpp now reads Apple storage paths from Apple-owned retained state rather than the shared legacy storage-path singleton.
  • PanoPainter-OSX/main.cpp and PanoPainter/GameViewController.m now seed Apple-owned storage paths directly.
  • src/platform_legacy/legacy_platform_state.* now exposes create_legacy_web_platform_services() plus an explicit binding hook for the retained Web service surface.
  • webgl/src/main.cpp now owns and binds the Web platform-services implementation explicitly instead of relying only on the retained fallback object in legacy platform state.
  • src/platform_windows/windows_runtime_shell.* now threads HINSTANCE through the startup/runtime path explicitly instead of keeping it in the retained main-window session state.
  • webgl/src/main.cpp now binds an owned legacy PlatformServices instance instead of reading the process-global fallback directly during StartApp().
  • android/src/cpp/main.cpp now binds a function-lifetime owned legacy PlatformServices instance in android_main(), replacing the direct bind to the process-global fallback accessor.
  • src/platform_legacy/legacy_platform_services.* now exposes an ownable create_platform_services() entrypoint while keeping the fallback singleton for non-migrated platforms.
  • linux/src/main.cpp now binds an owned legacy PlatformServices instance into App, making Linux the first explicit per-entrypoint owner of that legacy service implementation.
  • src/app_events.cpp no longer hides a fallback to pp::platform::legacy::platform_services(); touched app platform dispatch now expects an explicitly bound platform-services pointer.
  • linux/src/main.cpp, webgl/src/main.cpp, and android/src/cpp/main.cpp now bind pp::platform::legacy::platform_services() explicitly at app creation, removing the hidden fallback path from live non-Windows entrypoints.
  • src/platform_windows/windows_runtime_shell.cpp now owns the Windows tablet object directly, removing the composition-edge WacomTablet::I binding from the touched Windows runtime path.
  • src/platform_legacy/legacy_platform_state.* now exposes narrow Web helper functions for the touched publish/flush/default-canvas/prepared-file paths, so less of that fallback behavior stays inline in the legacy platform singleton.
  • src/platform_windows/windows_runtime_shell.cpp no longer keeps a separate retained AppRuntime*; the touched Windows lifecycle and main-thread task dispatch paths now derive runtime ownership directly from the owned app.
  • src/platform_windows/windows_runtime_shell.cpp now owns the Windows App through a retained std::unique_ptr<App>, so startup/early-return/convert paths no longer manage raw new/delete app lifetime manually.
  • src/platform_windows/windows_lifecycle_shell.cpp now releases the bound app through release_bound_app() after runtime shutdown instead of deleting it directly through the lifecycle shell.
  • src/platform_windows/windows_window_shell.cpp now routes the touched key-map synchronization and VR close state through narrow helpers instead of carrying the broader retained window bundle live across the window-proc path.
  • src/platform_legacy/legacy_platform_state.* no longer exports the mutable retained GLFW hook bundle; Linux/Web fallback render-context, present, and app-close paths now route through narrow GLFW helper functions instead.
  • scripts/automation/quiet-validate.ps1 now owns the recommended quiet checkpoint path and can bundle Windows build/test, Android/platform sweeps, and Apple remote compile gates into one compact JSON summary with -IncludePlatformBuild and -IncludeAppleRemote.
  • scripts/automation/platform-build.ps1 now supports -Quiet, per-preset log capture, and compact JSON-only output so standalone Android/headless sweeps still have a targeted quiet path when they need to be isolated from the bundled wrapper.
  • scripts/automation/apple-remote-build.ps1 now supports -Quiet, local SSH session log capture, remote log path reporting, and JSON-only output so the standalone Apple gate can still be run directly when the bundled wrapper is too broad.

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: The live Canvas ownership boundary is still active, but src/canvas.cpp itself is now down to a thin static singleton plus mode-table shell. The remaining canvas pressure now sits in the extracted legacy canvas service files and the preview/canvas node render paths rather than the old monolithic translation unit.

Current slice:

  • The remaining Canvas member wrappers in src/canvas.cpp now live in the extracted canvas service files, leaving canvas.cpp as the static singleton and mode-table shell.
  • 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.
  • Canvas point-trace, unproject, project2D, face-to-shape, and camera push/pop/get/set helpers now also live in src/legacy_canvas_projection_services.* instead of staying inline in src/canvas.cpp, which trims another coherent non-UI state/query pocket from the live canvas shell.
  • Canvas::draw_objects_direct(...) and Canvas::draw_objects(...) now also live in src/legacy_canvas_object_draw_services.* instead of staying inline in src/canvas.cpp, which trims another coherent object-draw and viewport-state execution pocket from the live canvas shell.
  • Canvas::stroke_draw_samples(...), Canvas::stroke_commit(), and the larger stroke sample/commit execution family now also live in src/legacy_canvas_stroke_commit_services.* instead of staying inline in src/canvas.cpp, which trims another large retained stroke-render pocket from the live canvas shell.
  • Shared canvas-mode GL wrappers plus the CanvasModeBasicCamera and CanvasModeCamera input handlers now also live in src/legacy_canvas_mode_helpers.* instead of staying inline in src/canvas_modes.cpp, which trims another coherent retained canvas-view interaction pocket from the broader canvas/render hotspot family.
  • The CanvasModeTransform interaction family now also lives in src/legacy_canvas_mode_transform.cpp instead of staying inline in src/canvas_modes.cpp, which materially thins another retained transform-mode pocket from the broader canvas/render hotspot family.
  • The CanvasModeFill and CanvasModeFloodFill interaction families now also live in src/legacy_canvas_mode_fill.cpp instead of staying inline in src/canvas_modes.cpp, which materially thins another retained fill-mode pocket from the live canvas-mode shell.
  • The live Canvas::stroke_draw() orchestration now also lives in src/legacy_canvas_stroke_live_services.cpp instead of staying inline in src/canvas.cpp, which materially thins another large retained live stroke-render pocket from the canvas shell.
  • Canvas::layer_merge(...), Canvas::flood_fill(...), and Canvas::FloodData::apply() now also live in src/legacy_canvas_layer_services.cpp instead of staying inline in src/canvas.cpp, which trims another coherent retained layer/fill workflow pocket from the live canvas shell.
  • Canvas::stroke_end(...), Canvas::stroke_cancel(...), Canvas::stroke_draw_mix(...), Canvas::stroke_draw_project(...), Canvas::stroke_update(...), and Canvas::stroke_start(...) now also live in src/legacy_canvas_stroke_runtime_services.* instead of staying inline in src/canvas.cpp, which trims another large retained live stroke/runtime pocket from the canvas shell.
  • Canvas::draw_merge(...), the temporary-paint and merge-branch orchestration helpers, final-plane composite, timelapse commit, create, destroy, clear-context, and camera accessors now also live in src/legacy_canvas_render_shell_services.* instead of staying inline in src/canvas.cpp, which trims another large retained render/context shell from the live canvas owner.
  • The static canvas plane geometry/orientation data now also lives in src/legacy_canvas_plane_data.cpp instead of staying inline in src/canvas.cpp, which trims another retained data-ownership pocket from the live canvas shell.
  • The final camera/timelapse member definitions now also live in src/legacy_canvas_camera_services.cpp instead of staying inline in src/canvas.cpp, which trims another retained canvas shell pocket.
  • The CanvasModePen and CanvasModeLine interaction families now also live in src/legacy_canvas_mode_pen_line.cpp instead of staying inline in src/canvas_modes.cpp, which materially thins another retained pen/line interaction pocket from the broader canvas/render hotspot family.
  • The CanvasModeMaskFree and CanvasModeMaskLine interaction families now also live in src/legacy_canvas_mode_mask.cpp instead of staying inline in src/canvas_modes.cpp, which materially thins another retained mask-tool interaction pocket from the broader canvas/render hotspot family.

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.
  • NodeStrokePreview main live-pass request assembly and preview framebuffer- copy setup now also route through legacy_node_stroke_preview_execution_services.h, which trims another coherent pass-setup block from src/node_stroke_preview.cpp, but broader preview-pass orchestration is still inline.
  • NodeStrokePreview remaining mix-pass planning and execution now also route through legacy_node_stroke_preview_draw_services.*, which trims the last dedicated mix-orchestration pocket from src/node_stroke_preview.cpp.
  • NodeStrokePreview::draw_stroke_immediate() immediate preview pass sequencing now also routes through the private execute_stroke_draw_immediate_pass_sequence(...) helper, which removes another live orchestration block from the node even though worker/readback flow still remains in the file.
  • NodeStrokePreview stroke preparation, dual-brush setup, and live-pass request assembly now also route through retained preview execution helpers, which trims another coherent setup pocket from src/node_stroke_preview.cpp even though worker/readback ownership and broader preview flow still remain inline.
  • src/node_stroke_preview.cpp is now 192 lines after moving the remaining preview GL dispatch and texture-slot binding pocket into legacy_node_stroke_preview_runtime_services.*.
  • The remaining immediate preview pass shell in NodeStrokePreview::draw_stroke_immediate() now also routes through execute_legacy_node_stroke_preview_draw_immediate_shell(...), which materially reduces the live preview-pass body even though worker/readback ownership still remains inline.
  • 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.
  • NodeCanvas outer non-draw_merged layer/plane traversal, onion-range failure handling, and visit payload setup now also route through execute_legacy_canvas_draw_layer_traversal(...).
  • NodeCanvas non-draw_merged per-layer temporary erase/paint, layer-texture, and blend setup now also route through make_legacy_canvas_draw_merge_layer_path_gl_execution(...), but the node still owned the remaining draw lambdas and broader renderer-state shell.
  • NodeCanvas now routes that remaining non-draw_merged per-layer blend, composite, debug-outline, and frame callback assembly through the local make_node_canvas_layer_path_execution(...) helper, which materially thins NodeCanvas::draw() even though the broader draw loop still lives in src/node_canvas.cpp.
  • NodeCanvas post-draw callback assembly, smoothing-mask face execution, and optional display resolve now also route through execute_node_canvas_draw_merge_tail(...), which trims another live tail block from NodeCanvas::draw() even though the broader outer draw shell is still inline.
  • NodeCanvas non-draw_merged cache/background/layer-traversal/cache- composite shell now also routes through execute_legacy_canvas_draw_unmerged_shell(...), which removes another coherent orchestration block from NodeCanvas::draw() even though the broader node draw loop still lives in src/node_canvas.cpp.
  • NodeCanvas unmerged-path onion-range planning, plane filtering, per-layer visit handling, and per-visit layer-path execution now also route through execute_legacy_canvas_draw_unmerged_node_canvas_shell(...), which trims another outer draw-shell block even though the broader node draw loop still lives in src/node_canvas.cpp.
  • NodeCanvas broader unmerged cache/viewport/background/composite pass setup now also routes through execute_legacy_canvas_draw_unmerged_node_canvas_pass(...), which removes another coherent outer-shell block even though the broader node draw loop still lives in src/node_canvas.cpp.
  • NodeCanvas::draw() setup, merged-pass shell, and unmerged-pass shell now also route through prepare_legacy_node_canvas_draw_setup(...), execute_legacy_canvas_draw_node_canvas_shell(...), execute_legacy_canvas_draw_merged_pass(...), and execute_legacy_canvas_draw_node_canvas_unmerged_pass(...), which materially shortens the live NodeCanvas::draw() body even though the file itself is still large.
  • NodeCanvas::draw() unmerged-pass blend-gate query, layer-orientation assembly, and callback wiring into execute_legacy_canvas_draw_node_canvas_unmerged_pass(...) now also route through execute_node_canvas_draw_unmerged_pass(...), which trims another coherent unmerged draw-orchestration block from the live node even though the file size remains roughly flat.
  • NodeCanvas::draw() merged-pass callback wiring and pass setup now also route through execute_node_canvas_draw_merged_pass(...), which trims another coherent merged draw-orchestration block from the live node even though the broader draw loop still remains inline.
  • NodeCanvas::handle_event() now routes through handle_legacy_node_canvas_event(...) in src/legacy_canvas_tool_services.*, which moves the live input/controller pocket out of src/node_canvas.cpp and keeps the node on a thinner shell.
  • App::dialog_layer_rename() now routes through open_legacy_document_layer_rename_dialog(...) in src/legacy_document_layer_services.*, which moves the remaining dialog overlay-open/wire/close workflow out of src/app_dialogs.cpp and leaves the app dialog shell thinner.
  • App::open_document(...) now routes through execute_legacy_document_open(...) in src/legacy_document_open_services.*, which moves route classification and unsaved-project gating out of src/app.cpp and into the retained document-open helper.
  • NodeCanvas restore/clear context, resize handling, camera reset, buffer creation, cursor visibility/update, tick, and destroy ownership now also live in src/legacy_node_canvas_state_services.* instead of staying inline in src/node_canvas.cpp, which materially thins another retained state/control pocket without reopening the draw path.
  • NodeStrokePreview retained lifecycle, worker-thread shell, render-to-image, on-screen handling, and preview texture ownership now also live in src/legacy_node_stroke_preview_runtime_services.cpp instead of staying inline in src/node_stroke_preview.cpp, which materially thins the preview node around its runtime-facing shell even though live pass execution still remains.
  • The remaining low-level preview GL/runtime pocket for viewport queries, clear-color restore, texture-slot binding, mixer unbind, and destination/pattern texture plumbing now also lives in src/legacy_node_stroke_preview_runtime_services.* instead of staying inline in src/node_stroke_preview.cpp, which trims another coherent runtime-facing shell pocket from the live node.
  • The remaining live render/pass orchestration in NodeStrokePreview::draw_stroke_immediate() now also lives in src/legacy_node_stroke_preview_draw_services.* instead of staying inline in src/node_stroke_preview.cpp, which trims another coherent preview draw-pass pocket while preserving the current runtime-facing shell.
  • The immediate preview runtime/orchestration block in NodeStrokePreview::draw_stroke_immediate() for stroke setup, prepared stroke construction, pass planning, shader setup, and live render request assembly now also lives in src/legacy_node_stroke_preview_runtime_services.* instead of staying inline in src/node_stroke_preview.cpp, which trims another coherent preview runtime pocket while preserving the current live draw path.
  • The preview sample execution pocket for sample-point conversion, brush vertex upload, request assembly, and the execute_legacy_canvas_stroke_sample(...) call now also lives in src/legacy_node_stroke_preview_sample_services.* instead of staying inline in src/node_stroke_preview.cpp, which trims another coherent preview execution pocket while preserving the live draw path.
  • NodeStrokePreview::draw_stroke_immediate() now also routes through execute_legacy_node_stroke_preview_immediate_draw(...) in src/legacy_node_stroke_preview_runtime_services.*, which moves render-target validation, viewport/clear-color save-restore, and immediate runtime request assembly out of the live node file and leaves src/node_stroke_preview.cpp at 160 lines.
  • NodeStrokePreview clone-finalize setup, stroke-frame planning, mix-pass adapter wiring, sample-pass adapter wiring, and the remaining immediate-draw request construction now also route through src/legacy_node_stroke_preview_runtime_services.*, src/legacy_node_stroke_preview_draw_services.*, and src/legacy_node_stroke_preview_sample_services.*, which leaves src/node_stroke_preview.cpp at 76 lines.
  • CanvasModeGrid plus ActionModeGrid undo/redo now also live in src/legacy_canvas_mode_helpers.cpp instead of staying inline in src/canvas_modes.cpp, which leaves the live canvas-modes file as a minimal shell.
  • NodeCanvas::init() plus the remaining NodeCanvas::draw() outer shell now also live in src/legacy_node_canvas_draw_services.* instead of staying inline in src/node_canvas.cpp, which materially reduces the live node to a thinner controller surface around event routing and state wrappers.
  • The remaining generic node geometry/state pocket for Node::SetSize(...), SetMinSize(...), SetMaxSize(...), and SetPosition(const glm::vec2) now also lives in src/legacy_ui_node_style.* instead of staying inline in src/node.cpp, which trims another coherent base scene-graph shell pocket without changing the public surface.
  • Node::app_redraw() and Node::watch(...) now also live in src/legacy_ui_node_execution.cpp instead of staying inline in src/node.cpp, which trims another small generic node utility pocket from the live file.
  • NodePanelBrushPreset global panel registration now also lives in src/legacy_brush_preset_list_services.* instead of staying on the live node type as a static registry field, which trims another retained controller-state pocket from src/node_panel_brush.cpp.
  • The remaining NodePanelBrush and NodePanelBrushPreset member bodies now also live in the existing retained helper layers (src/legacy_brush_panel_item_ui.*, src/legacy_brush_panel_ui.*, src/legacy_brush_panel_services.*, src/legacy_brush_preset_panel_ui.*, src/legacy_brush_preset_list_services.*, and src/legacy_brush_preset_services.*), which leaves src/node_panel_brush.cpp as a 2-line translation unit.

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: In Progress

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 125-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.
  • File-menu UI binding, including the export submenu wiring, now also lives in src/app_layout_file_menu.cpp, and App::init_menu_file() is now a thin call-through, but about/layer/sidebar and broader layout composition are still inline in src/app_layout.cpp.
  • 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, but edit/sidebar and broader layout composition are still inline in src/app_layout.cpp.
  • Sidebar panel binding plus popup wiring now also live in src/app_layout_sidebar.cpp, and App::init_sidebar() is now a thin call-through, but edit-menu wiring and broader layout composition are still inline in src/app_layout.cpp.
  • Main-toolbar binding now also lives in src/app_layout_main_toolbar.cpp, and App::init_toolbar_main() is now a thin call-through, but edit-menu wiring and broader layout composition are still inline in src/app_layout.cpp.
  • Edit-menu binding now also lives in src/app_layout_edit_menu.cpp, and App::init_menu_edit() is now a thin call-through, but draw-toolbar, brush-refresh, and broader layout composition are still inline in src/app_layout.cpp.
  • UI-direction plus persisted floating/docked panel-state ownership now also lives in src/app_layout_ui_state.cpp, and src/app_layout.cpp is down to the remaining draw-toolbar, brush-refresh, and layout/bootstrap composition.
  • Draw-toolbar binding now also lives in src/app_layout_draw_toolbar.cpp, and src/app_layout.cpp is down to the remaining brush-refresh and layout/bootstrap composition.
  • Brush-refresh now also lives in src/app_layout_brush.cpp, and src/app_layout.cpp is down to the remaining layout/bootstrap composition.
  • Layout bootstrap plus reload/load continuation wiring now also lives in src/app_layout_bootstrap.cpp, and src/app_layout.cpp is down to thin call-through entrypoints plus the remaining local helper pocket.

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: In Progress

Why now: src/app_dialogs.cpp still mixes document workflow decisions, export routing, dialog construction, and overlay ownership in one 106-line shell.

Current slice:

  • Informational overlay opener paths for user manual, changelog, about, what's-new, and shortcuts now live in src/app_dialogs_info_openers.cpp, and the corresponding App::dialog_* entrypoints are now thin call-throughs, but document/export workflow and retained dialog execution are still inline in src/app_dialogs.cpp.
  • Export, video-export, and PPBR export entrypoints now also live in src/app_dialogs_export.cpp, and the corresponding App::dialog_* entrypoints are now thin call-throughs, but new/open/save/browse/resize and retained dialog execution are still inline in src/app_dialogs.cpp.
  • New/open/save/browse/resize workflow entrypoints now also live in src/app_dialogs_workflow.cpp, and the layer-rename dialog open / wire / close pocket now lives in src/legacy_document_layer_services.*, leaving src/app_dialogs.cpp down to the remaining thin entrypoints plus other retained dialog glue.
  • App::open_document() now routes document classification and unsaved- project gating through src/legacy_document_open_services.cpp instead of keeping that shell pocket inline in src/app.cpp, which trims the live app entry file a little further while preserving the retained open-action behavior.
  • App::show_progress(...), App::message_box(...), and App::input_box(...) now also route through src/legacy_app_dialog_services.* instead of building dialog plans and factories inline in src/app_dialogs.cpp, which trims another coherent dialog-construction pocket from the remaining app dialog shell.

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: In Progress

Why now: src/app.cpp still carries startup, frame flow, queue draining, recording, and composition logic in one 171-line file.

Current slice:

  • UI observer math now routes through src/legacy_app_frame_services.cpp instead of staying inline in src/app.cpp.
  • The repeated UI child traversal in App::draw() now routes through the same retained helper.
  • Canvas toolbar refresh in App::update() now also routes through that helper file, materially shrinking src/app.cpp.
  • App::draw(...), App::update(...), App::terminate(...), App::update_memory_usage(...), App::update_rec_frames(...), App::res_from_index(...), App::res_to_index(...), App::res_to_string(...), App::rec_clear(...), App::rec_start(...), App::rec_stop(...), App::rec_export(...), App::rec_loop(...), and App::render_thread_tick(...) now also live in src/legacy_app_runtime_shell_services.cpp instead of staying inline in src/app.cpp, which materially thins the remaining frame/runtime shell.
  • App::create(...), App::initAssets(...), App::initLog(...), and App::init(...) now also live in src/legacy_app_startup_services.* instead of staying inline in src/app.cpp, which reduces the remaining app file to a thinner retained composition surface around startup and runtime delegation.
  • App::clear(), App::check_license(), App::async_start(), App::async_redraw(), App::async_end(), and App::async_swap() now also live in src/legacy_app_runtime_shell_services.cpp instead of staying inline in src/app.cpp, which reduces the remaining app file to a thinner retained shell around document routing and runtime thread entrypoints.
  • App::request_close(), App::renderdoc_frame_start(), App::renderdoc_frame_end(), and the render/UI thread entrypoint wrappers now also route through src/legacy_app_runtime_shell_services.* instead of staying inline in src/app.cpp, which reduces the live app file to a much thinner retained composition shell.

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
  • the splash-screen dialog loop, HWND ownership, and bitmap setup now also live in src/platform_windows/windows_splash.cpp instead of src/main.cpp, and main.cpp now just owns the helper lifecycle
  • 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 lives under src/platform_windows/windows_platform_services.cpp instead of staying in the entry TU
  • Win32 pointer API loading, stylus/ink timer ownership and decay, WT_PACKET reset, and WM_POINTERUPDATE pen/touch handling now live in src/platform_windows/windows_stylus_input.cpp instead of src/main.cpp, but broader retained Win32 shell state is still open
  • the retained Win32 VR/HMD shell, including worker start/stop and VR FPS state, now routes through src/platform_windows/windows_vr_shell.h instead of staying inline in src/main.cpp, but broader retained Win32 shell state is still open
  • RenderDoc startup/frame capture, SHCore DPI bootstrap, Win32 error-string conversion, UnadjustWindowRectEx, and GL debug pre/post callbacks now also live in src/platform_windows/windows_bootstrap_helpers.cpp instead of src/main.cpp
  • the WMI startup probe now also lives in src/platform_windows/windows_bootstrap_helpers.cpp instead of src/main.cpp
  • Win32 lifecycle running-state, close/shutdown handling, FPS title update and wakeup posting, stylus frame update, window preference save, and VR lifecycle wrappers now also live in src/platform_windows/windows_lifecycle_shell.cpp instead of src/main.cpp
  • Win32 startup/window/bootstrap flow now also lives in src/platform_windows/windows_bootstrap_helpers.* for runtime-data discovery, startup-state initialization, window creation, pixel-format setup, GL loader init, runtime-info logging, and core-context upgrade sequencing
  • BugTrap/SEH recovery setup now also lives in src/platform_windows/windows_bootstrap_helpers.cpp instead of src/main.cpp
  • the Win32 window procedure and retained message-handling shell now also live in src/platform_windows/windows_window_shell.* instead of src/main.cpp, which materially thins the entry file even though broader runtime/entrypoint composition is still open
  • the WinMain argv conversion bridge now also lives in src/platform_windows/windows_bootstrap_helpers.* instead of staying inline in src/main.cpp, which trims another Windows-specific entrypoint pocket
  • retained input-state zeroing and reverse key-map initialization now also live in src/platform_windows/windows_window_shell.* instead of src/main.cpp, which trims another small but real retained-state pocket from the entry TU
  • the remaining interactive Win32 runtime pocket for touch registration, render/UI thread startup, debug GL callback hookup, Wintab init/skip, icon setup, placement restore, optional VR start, splash dismissal, message loop, and shutdown cleanup now also lives in src/platform_windows/windows_runtime_shell.* instead of src/main.cpp, which reduces the entry TU to a much smaller composition root
  • the remaining Windows app shell in main(...) now also routes through run_main_application(...) in src/platform_windows/windows_runtime_shell.*, which reduces src/main.cpp to a minimal main/WinMain wrapper
  • the retained Win32 main-thread task queue now also lives in AppRuntime instead of src/platform_windows/windows_platform_services.cpp, which removes another runtime queue from retained platform-local static state
  • 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
  • App::request_close() plus the render/UI thread entrypoint wrappers now also route through src/legacy_app_runtime_shell_services.*, which further reduces direct runtime orchestration living on src/app.cpp even though the owned runtime contract is still centered on AppRuntime
  • 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: In Progress

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.

Current slice:

  • Node::load_internal(...) child XML loading now routes through src/legacy_ui_node_loader.* instead of staying inline in src/node.cpp. That trims another coherent generic node-instantiation pocket and makes the remaining scene-graph load path easier to isolate, even though ownership has not yet moved into pp_ui_core.
  • The generic per-frame node execution/traversal family for restore_context, clear_context, update, update_internal, and tick now also lives in src/legacy_ui_node_execution.cpp instead of staying inline in src/node.cpp, which materially thins the base scene-graph file without changing its public surface.
  • Node::on_event(...) plus mouse/key capture and release now also live in src/legacy_ui_node_event.* instead of staying inline in src/node.cpp, which materially thins the base scene-graph event shell without changing its public surface.
  • Node::parse_attributes(...) now also routes through src/legacy_ui_node_attributes.* instead of staying inline in src/node.cpp, which trims another coherent XML/Yoga attribute decoding pocket from the base scene-graph shell without changing its public surface.
  • Node::destroy(), root(), set_manager(...), added_to_root(), handle_on_screen(...), template loading helpers, child add/remove/move helpers, and child query helpers now also live in src/legacy_ui_node_tree_services.cpp instead of staying inline in src/node.cpp, which materially thins the remaining generic scene-graph lifecycle/tree shell without changing the public surface.
  • The generic Yoga style/visibility pocket from Node::SetWidth(...) through Node::GetRTL() now also lives in src/legacy_ui_node_style.* instead of staying inline in src/node.cpp, which trims another coherent generic node shell pocket without changing the public surface.
  • The remaining geometry/state pocket for Node::SetSize(...), Node::SetMinSize(...), Node::SetMaxSize(...), and Node::SetPosition(const glm::vec2) now also lives in src/legacy_ui_node_style.* instead of staying inline in src/node.cpp, which materially thins the live base shell by moving the cached size/position mutation and redraw signaling out of the node file.
  • The remaining generic Node lifecycle/state pocket for no-op lifecycle hooks, add/remove propagation, move construction, destruction cleanup, and base clone plumbing now also lives in src/legacy_ui_node_lifecycle.* instead of staying inline in src/node.cpp, which materially thins another coherent generic scene-graph shell without changing the public surface.
  • Node::load_internal(...) now also routes through load_legacy_ui_node(...) in src/legacy_ui_node_loader.*, which moves the init/attribute-parse/create/child-load/loaded shell out of src/node.cpp and leaves the remaining live node file thinner.
  • The remaining default Node event/capture/resize shell now also lives in src/legacy_ui_node_execution.cpp, while the remaining width/height/padding/margin/flex/visibility/geometry wrappers now also live in src/legacy_ui_node_style.*, which reduces src/node.cpp to the final attribute/load entrypoints without changing the public surface.

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.

Current slice:

  • src/node.cpp child attach/detach/reorder paths now route through named local helpers instead of repeating raw mutation logic inline. That improves scene-graph mutation clarity, but it does not yet change the ownership model or move those paths behind checked handles.

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: In Progress

Why now: pp_platform_api is supposed to be the SDK-free policy and interface layer, and while Apple implementation has now moved out, it still compiles concrete Linux platform sources.

Current slice:

  • pp_platform_api no longer compiles src/platform_apple/apple_platform_services.*.
  • Apple concrete platform code now lives in the new pp_platform_apple target, and panopainter_app plus pp_platform_api_tests now link that concrete target where needed.
  • pp_platform_api now also stops compiling src/platform_linux/linux_platform_services.*.
  • Linux concrete platform code now lives in the new pp_platform_linux target, and pp_legacy_app plus pp_platform_api_tests now link that concrete target where needed.
  • The dependency direction is cleaner for Apple and Linux, but the broader concrete platform family is still being separated.

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 and Linux implementation files are built by concrete platform targets 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 storage-path state now also lives in src/platform_legacy/legacy_platform_state.* instead of staying inline in src/platform_legacy/legacy_platform_services.cpp
  • active_legacy_storage_paths() now consumes storage roots seeded explicitly through set_legacy_storage_paths(...) from app startup plus the iOS, macOS, and Android entrypoints instead of lazily snapshotting App::I
  • retained GLFW window hooks/state and retained Apple UI/app handle state now also live in src/platform_legacy/legacy_platform_state.*, and src/platform_legacy/legacy_platform_services.cpp now consumes those snapshots without direct App::I reads in the touched paths
  • the retained Apple document bridge now also lives behind active_legacy_apple_document_platform_services() in src/platform_legacy/legacy_platform_state.* instead of being built inline inside src/platform_legacy/legacy_platform_services.cpp, and that retained service resets when seeded Apple handles change
  • retained Apple callback injection and broader platform_legacy singleton reach are still open
  • the platform_legacy-mirrored Apple/GLFW handle cluster is now seeded explicitly from the iOS, macOS, Linux, and WebGL entrypoints through src/platform_legacy/legacy_platform_state.* instead of being copied out of App
  • The remaining Win32 shell wrappers for close, async lock/swap, stylus/FPS updates, VR start/stop, window-state save, and the window-handle accessor now live in src/platform_windows/windows_platform_services.cpp instead of src/main.cpp, which leaves the live entry file as a thinner startup/runtime dispatcher.

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: In Progress

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.

Current slice:

  • The remaining Windows entry/exit singleton write no longer lives at the run_main_application(...) and handle_window_close_message(...) callsites; src/platform_windows/windows_runtime_shell.cpp now centralizes that legacy App::I side effect inside bind_app(...), leaving the touched runtime and lifecycle shell as explicit binder users instead of direct singleton writers.
  • The touched src/platform_windows/windows_platform_services.cpp fan-out no longer reaches the broader retained window bundle directly for main-window, sandbox, and VR/session reads; the touched window/VR queries now route through narrow runtime-shell accessors instead.
  • src/platform_windows/windows_bootstrap_helpers.cpp no longer uses Canvas::I for crash-recovery saves; the BugTrap pre-error handler now uses the app-owned NodeCanvas document (app.canvas->m_canvas) and the new runtime-shell window/sandbox accessors instead of direct singleton or retained-state reads in the touched recovery path.
  • The retained Apple document bridge/state pocket no longer lives in src/platform_legacy/legacy_platform_state.*; it now lives in the Apple-owned src/platform_apple/apple_platform_state.cpp and src/platform_apple/apple_platform_services.*, and the macOS/iOS entrypoints now seed that state through pp::platform::apple.
  • src/platform_legacy/legacy_platform_services.cpp now consumes the Apple-owned retained provider/factory through pp::platform::apple::... call-throughs instead of constructing or caching Apple retained bridge state in the legacy layer.
  • The Win32 stylus and pointer-input path no longer reaches WacomTablet::I directly in src/platform_windows/windows_window_shell.cpp or src/platform_windows/windows_stylus_input.cpp; src/platform_windows/windows_runtime_shell.* now binds the active tablet explicitly and the touched consumers read it through that runtime-shell binding instead of the process-global singleton.
  • The remaining dense Windows bootstrap singleton pocket moved off App::I: setup_exception_handler(...), initialize_main_window_startup_state(...), and _pre_call_callback(...) now use explicit app/bound-runtime state in src/platform_windows/windows_bootstrap_helpers.*, and src/app_events.cpp now dispatches platform services through the App instance it is operating on instead of a global fallback.
  • The retained Web fallback service object and the Apple storage-path preparation helper now also live in src/platform_legacy/legacy_platform_state.* instead of being built inline in src/platform_legacy/legacy_platform_services.cpp, which trims another retained fallback/state pocket out of the legacy platform shell.
  • The touched Win32 shell path no longer reaches App::I directly for window-procedure dispatch, stylus state updates, lifecycle shutdown, or VR callback/thread setup; src/platform_windows/windows_runtime_shell.* now binds the active App* explicitly and clears that binding on shutdown.
  • Windows VR session snapshot ownership no longer lives on App.
  • VrSessionSnapshot now lives behind src/platform_windows/windows_vr_shell.h and read_platform_vr_session_snapshot() in src/platform_windows/windows_platform_services.*, with app-side reads now routed through App::vr_session_snapshot().
  • The platform_legacy-mirrored Apple/GLFW handle cluster also no longer lives on App; retained Apple/GLFW platform state is now seeded explicitly from the iOS, macOS, Linux, and WebGL entrypoints through src/platform_legacy/legacy_platform_state.*.
  • App also no longer owns and_app or and_engine; the retained Android entrypoint now seeds only the explicit legacy platform storage snapshot needed by touched platform services.
  • Win32 main-thread task dispatch also no longer reaches AppRuntime through App::I; src/platform_windows/windows_runtime_shell.* now binds the active runtime explicitly and src/platform_windows/windows_platform_services.cpp consumes that bound runtime for enqueue/drain.
  • App still owns broader retained legacy platform state, so this remains a live ownership task.

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.

Current slice:

  • NodePanelBrush save/restore/scan/reload/find/get-path ownership now routes through src/legacy_brush_panel_services.* instead of staying inline in src/node_panel_brush.cpp, which trims a coherent retained brush-workflow pocket from the live UI node even though cloud and package-worker ownership still remain separate follow-up work.
  • NodeButtonBrush clone/init/icon/read/write/draw behavior and NodeBrushPresetItem clone/init/draw behavior now also live in src/legacy_brush_panel_item_ui.* instead of staying inline in src/node_panel_brush.cpp, which trims the remaining brush-item UI pocket from the live brush panel file.
  • NodePanelBrushPreset save/restore plus PPBR/ABR import/export routing now also lives in src/legacy_brush_preset_services.* instead of staying inline in src/node_panel_brush.cpp, which trims another large preset-workflow pocket from the live UI node even though the broader cloud/package worker split still remains follow-up work.
  • NodePanelBrushPreset init/menu wiring, click handling, item construction, and added-state update now also live in src/legacy_brush_preset_panel_ui.* instead of staying inline in src/node_panel_brush.cpp, which materially thins another retained preset panel UI pocket even though cloud/package worker ownership remains the follow-up.
  • NodePanelBrushPreset popup-close event handling now also lives in src/legacy_brush_preset_panel_ui.* instead of staying inline in src/node_panel_brush.cpp, which trims the remaining inline popup tail from the live brush panel file.
  • The retained LegacyBrushPresetListServices block now also lives in src/legacy_brush_preset_list_services.* instead of staying inline in src/node_panel_brush.cpp, which trims another retained preset workflow pocket while preserving the live list-plan dispatch path.
  • NodePanelBrush brush texture panel init, selection dispatch, popup-close event handling, restore-failure prompt flow, and added-state reset now also live in src/legacy_brush_panel_ui.* instead of staying inline in src/node_panel_brush.cpp, which trims another retained brush UI workflow pocket while preserving the live node as a thinner controller surface.

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.