Files
panopainter/docs/modernization/tasks.md

26 KiB

Modernization Task Tracker

Status: live Last updated: 2026-06-17

This file is the active execution queue. It is written for a coordinator that can assign bounded packets to smaller parallel workers. Completed and stale history belongs in docs/modernization/tasks-done.md, not here.

Operating Rules

  • Prioritize working-app ownership transfer over planners, CLI commands, package-only cleanup, or test-only work.
  • Every coding task must remove or narrow a real retained dependency, hotspot, unsafe ownership path, or thread/runtime ambiguity.
  • Tests are validation and guardrails. A task that only adds tests is not a P0 modernization slice unless it directly enables a blocked ownership move.
  • Do not broaden worker scopes. If a task crosses file boundaries, split it into worker packets with disjoint write scopes and integrate centrally.
  • No new App::I, Canvas::I, owning raw Node*, detached worker, direct GL resource dependency, or platform SDK dependency may be introduced in moved code.
  • Raw pointers may remain only as documented non-owning implementation details backed by a checked owner, handle, scoped connection, or explicit lifetime contract.
  • Preserve current app behavior first. UI appearance, file formats, brush behavior, platform behavior, and rendering output are not to be redesigned in modernization slices.
  • Use CMake source ownership as the progress signal. Shrinking PP_PANOPAINTER_* and PP_LEGACY_* ownership matters more than adding new helpers around the same retained code.

Current Audit Snapshot

Validation performed during the 2026-06-17 review:

  • python scripts/dev/check_component_boundaries.py: passed.
  • python scripts/dev/check_renderer_api_contract.py: passed.

Key facts:

  • Pure component boundaries currently pass their static checks.
  • Remaining architectural risk is concentrated in the working app, retained app/UI/canvas targets, singleton reach, raw node ownership, and direct GL resource usage.
  • PP_PANOPAINTER_APP_SOURCES: 47 files, about 9620 lines.
  • PP_PANOPAINTER_UI_SOURCES: 52 files, about 9051 lines.
  • PP_LEGACY_PAINT_DOCUMENT_SOURCES: 22 files, about 6277 lines.
  • PP_LEGACY_APP_SOURCES: 26 files, about 4711 lines.
  • PP_LEGACY_UI_CORE_SOURCES: 32 files, about 4304 lines.
  • App::I still appears hundreds of times in retained app/canvas/UI/resource code.
  • Canvas::I still appears hundreds of times in retained canvas modes, panels, and workflow bridges.
  • Raw Node* and callback captures remain a dominant UI lifetime risk.
  • Retained stroke-preview/runtime draw paths still depend on legacy render/runtime helpers, but RTT, Texture2D, Shape, Shader, TextMesh, and CanvasLayer no longer call App::I directly for queueing.
  • AppRuntime now owns synchronized running flags plus explicit post/reject, same-thread execution, and queue-drain behavior, but broader singleton reach and app-shell ownership remain.
  • Retained cloud upload/download, brush-package import, and timelapse-export async paths now route through AppRuntime::canvas_async_task, but dialog and execution ownership still remains in retained app/document/cloud bridges.
  • App::dialog_browse() no longer owns browse-dialog button wiring inline; the retained document-open bridge now owns that handoff in src/legacy_document_open_services.*.
  • App::init_toolbar_main() now delegates retained main-toolbar button wiring through src/legacy_main_toolbar_binding_services.*, so src/app_layout_main_toolbar.cpp is down to a thin root lookup and adapter call while retained toolbar execution still lives in src/legacy_app_shell_services.*.
  • App::init_menu_file() now delegates retained File-menu popup and export submenu wiring through src/legacy_file_menu_binding_services.*, so src/app_layout_file_menu.cpp is down to a thin trigger lookup and adapter call while retained file/export execution still lives in src/legacy_app_shell_services.*.
  • App::init_menu_about() now delegates retained About-menu popup wiring through src/legacy_about_menu_binding_services.*, so src/app_layout_about_layer_menu.cpp no longer owns the About callback body inline while retained About execution still lives in src/legacy_app_shell_services.*.
  • App::init_menu_tools() now delegates the retained Tools > Panels submenu wiring through src/legacy_tools_menu_binding_services.*, so src/app_layout_tools_menu.cpp no longer owns that floating-panel submenu body inline while retained Tools execution and options wiring remain.

Parallel Assignment Rules

Coordinator packets for workers should include only:

  • task id and one-paragraph goal
  • exact write scope
  • allowed read scope
  • debt ids that matter
  • required validation command
  • specific rg or clangd_nav.py queries
  • current behavior notes needed to avoid broad rediscovery

Safe parallel groups:

  • One worker on canvas/render execution, one worker on generic UI controls, one worker on platform CMake cleanup, and one worker on runtime queue contracts can run in parallel if write scopes remain disjoint.
  • Do not run two workers against src/app_runtime.*, src/node.*, src/legacy_canvas_document_io_services.cpp, or src/legacy_node_stroke_preview_runtime_services.cpp at the same time.
  • Do not assign both a CMake source-list move and code edits touching the same source files to separate workers unless the coordinator serializes the CMake integration.

P0 Queue

ARC-RUN-010 - Harden AppRuntime Into An Explicit Runtime Service

Status: Ready

Why now: Render/UI/background queues are central to memory and thread safety. The current AppRuntime owns several std::jthread workers, but runtime state is still mutable, partly unsynchronized, and app-specific. The working app still uses App::I as the practical access path to render/UI queues.

Write scope:

  • src/app_runtime.h
  • src/app_runtime.cpp
  • src/app.h
  • src/legacy_app_runtime_shell_services.cpp
  • src/app_core/app_thread.h
  • tests/app_core/app_thread_tests.cpp

Read scope:

  • src/texture.cpp
  • src/rtt.cpp
  • src/shape.cpp
  • src/shader.cpp
  • src/platform_windows/windows_platform_services.cpp

Required work:

  • Make render/UI/prepared-file/canvas worker running state synchronized or atomic, with a single shutdown path per worker.
  • Add explicit runtime service methods for thread-affinity checks and post/drain/shutdown semantics.
  • Keep exceptions from escaping worker bodies.
  • Stop exposing queue usage only through App::I wrappers for touched call sites.
  • Keep behavior identical for same-thread immediate execution and blocking render/UI calls.

Done when:

  • Touched queue state has no unsynchronized read/write ambiguity.
  • Worker shutdown drains or rejects queued work according to documented behavior.
  • Touched app code can call an explicit runtime service instead of reaching queues through singleton state.
  • App-thread planner tests cover shutdown, stopped-worker enqueue, same-thread execution, and queue-drain behavior that the live runtime implements.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_app_core_app_thread_tests -TestRegex "pp_app_core_app_thread"
python scripts/dev/check_component_boundaries.py

Mini-model packet: Start by auditing render_running_, ui_running_, prepared_file_running_, canvas_async_running_, thread ids, and worker stop methods. Do not rewrite GL resources in this task; expose the runtime contract needed for the next task.

ARC-RND-010 - Move GL Resource Queueing Behind Renderer Runtime Contracts

Status: Ready

Why now: RTT, Texture2D, Shape, Shader, Font, and CanvasLayer still use App::I->render_task* directly. That blocks renderer backends and hides thread affinity behind a global app singleton.

Write scope:

  • src/texture.cpp
  • src/texture.h
  • src/rtt.cpp
  • src/rtt.h
  • src/shape.cpp
  • src/shape.h
  • src/shader.cpp
  • src/shader.h
  • src/font.cpp
  • src/font.h
  • src/canvas_layer.cpp
  • src/canvas_layer.h
  • narrow adapter files if introduced under src/renderer_gl/

Read scope:

  • src/app_runtime.*
  • src/renderer_api/*
  • src/renderer_gl/*
  • src/paint_renderer/*

Required work:

  • Introduce a narrow render-dispatch interface or adapter consumed by retained GL resource classes.
  • Convert one coherent GL resource family per slice; do not edit every file in one worker pass unless the abstraction is already integrated.
  • Preserve blocking versus async semantics exactly.
  • Do not move app policy into pp_renderer_gl.
  • Do not add future backend implementation work.

Done when:

  • The touched GL resource family no longer calls App::I directly for queueing or thread checks.
  • Render-thread assertions use an explicit runtime/render-dispatch contract.
  • CMake ownership remains consistent and no pure renderer API target depends on app headers.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_renderer_api_tests,pp_renderer_gl_capabilities_tests -TestRegex "pp_renderer|pp_paint_renderer"
python scripts/dev/check_renderer_api_contract.py

Mini-model packet: Start with either Texture2D/RTT or Shape/Shader, not both. Use rg -n "App::I->render_task|is_render_thread" src/texture.* src/rtt.* for the first slice.

ARC-RND-011 - Split Canvas Document I/O From Render Execution

Status: Ready

Why now: src/legacy_canvas_document_io_services.cpp is still the largest working-app document/export hotspot and has the highest App::I concentration found in the review. It mixes license checks, worker dispatch, render readback, progress UI, platform publish/flush, and retained Canvas mutation.

Write scope:

  • src/legacy_canvas_document_io_services.cpp
  • src/legacy_canvas_document_io_services.h
  • src/legacy_document_export_services.*
  • src/app_core/document_export.h
  • src/paint_renderer/*
  • focused tests under tests/app_core or tests/paint_renderer

Read scope:

  • src/canvas.*
  • src/canvas_layer.*
  • src/legacy_canvas_render_shell_services.*
  • src/platform_api/platform_services.h

Required work:

  • Pick one export/import family first: equirectangular export, cube-face export, layer/frame collection export, or project save/open async I/O.
  • Move orchestration into an app-core or paint-renderer service request that accepts explicit document/render/platform dependencies.
  • Leave retained Canvas as a final adapter only for data that has not moved.
  • Remove direct App::I calls from the touched path.
  • Preserve progress and platform publish behavior.

Done when:

  • One live document/export path is executable through an explicit service request rather than by walking App::I/Canvas::I from the bridge.
  • The retained bridge is visibly thinner and has fewer reasons to know about UI, worker, platform, and renderer details at the same time.
  • The touched path has focused validation that exercises the new request contract and the retained adapter.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_export_tests,pp_paint_renderer_compositor_tests -TestRegex "document_export|paint_renderer"

Mini-model packet: Do not broaden into all export types. Start with the path that already has the strongest pure planning/readiness coverage, then remove only the corresponding direct app/canvas singleton reach.

ARC-RND-012 - Make Stroke Preview A Renderer-Owned Service

Status: Ready

Why now: NodeStrokePreview has been thinned, but src/legacy_node_stroke_preview_runtime_services.cpp still owns static worker state, render-context handoff, preview texture lifetime, and direct app/canvas access. This is a high-risk UI/render/thread boundary.

Write scope:

  • src/node_stroke_preview.*
  • src/legacy_node_stroke_preview_runtime_services.*
  • src/legacy_node_stroke_preview_draw_services.*
  • src/legacy_node_stroke_preview_sample_services.*
  • src/paint_renderer/*
  • tests/paint_renderer/*

Read scope:

  • src/app_runtime.*
  • src/texture.*
  • src/rtt.*
  • src/canvas.*
  • src/node_panel_stroke.*

Required work:

  • Move one preview execution phase behind a renderer-facing service contract.
  • Replace static worker/resource state for the touched phase with owned service state or explicit runtime dependency.
  • Remove direct App::I/Canvas::I from the touched phase.
  • Preserve preview output and cancellation/shutdown behavior.

Done when:

  • The touched preview phase can be reasoned about without reading the full node implementation.
  • Preview worker lifetime is owned and cancellable for the touched phase.
  • Renderer-facing tests cover the contract without linking app texture objects.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_paint_renderer_compositor_tests -TestRegex "paint_renderer|stroke_preview"

Mini-model packet: Start with result copy, live pass request assembly, or worker lifecycle. Do not combine all preview phases in one slice.

ARC-UI-010 - Move Generic Controls Out Of pp_legacy_ui_core

Status: Ready

Why now: Generic controls still live in PP_LEGACY_UI_CORE_SOURCES, keeping pp_panopainter_ui tied to retained app/UI targets. This is working-app UI architecture, not cosmetic cleanup.

Write scope:

  • src/node_button.*
  • src/node_checkbox.*
  • src/node_icon.*
  • src/node_image.*
  • src/node_scroll.*
  • src/node_slider.*
  • src/node_text.*
  • src/node_text_input.*
  • src/ui_core/*
  • cmake/PanoPainterSources.cmake
  • CMakeLists.txt

Read scope:

  • src/node.*
  • src/layout.*
  • app-specific src/node_panel_*
  • app-specific src/node_dialog_*

Required work:

  • Move one generic control family at a time to pp_ui_core.
  • Split renderer-neutral state/event/layout logic from retained GL drawing when a control still depends on GL classes.
  • Keep app-specific panels and dialogs out of pp_ui_core.
  • Update CMake ownership so the source-list change is real.

Done when:

  • At least one generic control family is owned by pp_ui_core or has its renderer-neutral core owned there with only a narrow retained draw adapter.
  • PP_LEGACY_UI_CORE_SOURCES shrinks.
  • Existing UI behavior is unchanged.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_layout_xml_tests,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "pp_ui_core"
python scripts/dev/check_component_boundaries.py

Mini-model packet: Start with the least app-specific control: checkbox, button, icon, image, scroll, slider, text, or text input. Do not touch panels/dialogs in the same slice.

ARC-UI-011 - Convert UI Ownership To Checked Handles By Default

Status: Ready

Why now: pp_ui_core has checked lifetime helpers, but base Node and app panels still mix raw parent/manager pointers, shared child vectors, raw callback parameters, and destroy-during-callback assumptions.

Write scope:

  • src/node.*
  • src/layout.*
  • src/legacy_ui_overlay_services.*
  • one dialog or panel family per slice under src/node_dialog_* or src/node_panel_*
  • src/ui_core/node_lifetime.*
  • src/ui_core/overlay_lifetime.*

Read scope:

  • call sites found with rg -n "add_child|remove_child|destroy\\(|on_.*=|Node\\*" src/node_dialog_* src/node_panel_* src/legacy_ui_* src/node.*

Required work:

  • Convert one popup/dialog/panel family to checked handles or scoped connections.
  • Remove raw lifetime assumptions from callbacks in the touched family.
  • Document any remaining raw Node* as non-owning views with owner proof.
  • Keep visual behavior and event ordering unchanged.

Done when:

  • The touched UI family can close during callback dispatch without relying on dangling raw pointers.
  • Overlay/popup lifetime flows through pp_ui_core lifetime primitives by default.
  • New touched callbacks are scoped or handle-checked.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "ui_core_(node_lifetime|overlay_lifetime)"

Mini-model packet: Pick one family only, such as open/browse dialogs, picker dialog, popup menu, layer panel, or stroke panel. Avoid broad Node redesign unless the family requires a small base helper.

ARC-APP-010 - Reduce App Shells To Composition And Adapters

Status: Ready

Why now: PP_PANOPAINTER_APP_SOURCES is still about 9620 lines. The app shell owns workflow, dialogs, layout binding, runtime, VR, cloud, brush package, platform hooks, and retained document/export adapters.

Write scope:

  • one src/app_*.cpp family per slice
  • matching src/legacy_app_* service files
  • matching app-core planner/service headers only when needed
  • cmake/PanoPainterSources.cmake if ownership moves

Read scope:

  • src/app.h
  • relevant app-core headers under src/app_core
  • relevant UI node files for the touched workflow

Required work:

  • Pick one shell family: layout menus, dialogs, startup/frame, cloud, brush package, recording, VR, or document session.
  • Move retained implementation into a named service with explicit dependencies.
  • Make the app method a thin adapter or composition call.
  • Do not add planner-only coverage unless the app method actually shrinks.

Done when:

  • One app shell file loses real workflow/runtime ownership.
  • The new service accepts explicit App&, runtime, platform, UI, document, or renderer dependencies instead of reading global state internally.
  • The touched path has focused validation or an app build gate.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"

Mini-model packet: Start with a single app shell family. Do not mix dialogs, layout, cloud, and VR in one worker assignment.

ARC-PLT-010 - Finish Platform Source Ownership In CMake

Status: Ready

Why now: Platform implementation ownership improved, but CMake still leaks Web platform sources into PP_PANOPAINTER_APP_SOURCES. Platform implementation files should belong to concrete pp_platform_* targets, not the app source group.

Write scope:

  • cmake/PanoPainterSources.cmake
  • CMakeLists.txt
  • src/platform_web/* only if build integration requires a narrow include or factory adjustment

Read scope:

  • webgl/CMakeLists.txt
  • src/platform_android/*
  • src/platform_linux/*
  • src/platform_apple/*
  • src/platform_api/platform_services.h

Required work:

  • Remove ${PP_PLATFORM_WEB_SOURCES} from PP_PANOPAINTER_APP_SOURCES.
  • Ensure root app/platform targets link pp_platform_web only when needed.
  • Keep WebGL retained package entrypoint behavior unchanged.
  • Do not reintroduce platform_legacy.

Done when:

  • Concrete Web platform files are not compiled as app sources in the root app source group.
  • The source ownership direction is visible in CMake.
  • Platform API boundary checks still pass.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_platform_api_tests -TestRegex "pp_platform_api"
python scripts/dev/check_component_boundaries.py

Mini-model packet: Keep this structural. Do not edit platform behavior unless CMake exposes a real link or include problem.

P1 Queue

ARC-SAFE-010 - Remove Manual Allocation From Touched Ownership Paths

Status: Ready

Why now: The review found manual new/delete pockets in node loading, canvas, layer actions, Wacom/bootstrap helpers, and retained resource cleanup. Some are non-owning or placement-new cases, but touched working-app ownership paths should move to RAII containers and factories.

Write scope:

  • one selected ownership path at a time, such as src/legacy_ui_node_loader.*, src/node.*, src/canvas.cpp, src/platform_windows/windows_bootstrap_helpers.cpp, or src/wacom.cpp

Read scope:

  • immediate owner/caller files for the selected path

Required work:

  • Replace owning raw allocation with std::unique_ptr, std::shared_ptr, std::vector, std::string, or an explicit RAII wrapper.
  • Preserve non-owning views only where ownership is proven.
  • Avoid mixing this with UI or renderer redesign.

Done when:

  • The touched path has no owning raw new/delete.
  • Failure paths cannot leak.
  • Lifetime remains clear at call sites.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_ui_core|pp_app_core"

Mini-model packet: Start with a single obvious allocation family. legacy_ui_node_loader is a good first target because it is UI ownership, not rendering behavior.

ARC-SAFE-011 - Replace Remaining Ad Hoc Workers With Runtime-Owned Services

Status: Ready

Why now: Most recent worker conversions use std::jthread, but retained worker pockets still sit in UI/dialog/cloud/grid/preview services and std::async remains in Asset. The end state requires service-owned cancellation and shutdown.

Write scope:

  • one worker family per slice: src/node_dialog_cloud.*, src/legacy_cloud_services.*, src/legacy_grid_ui_services.*, src/asset.*, or src/legacy_node_stroke_preview_runtime_services.*

Read scope:

  • src/app_runtime.*
  • corresponding app-core/cloud/grid/preview planner headers
  • immediate UI caller files

Required work:

  • Move worker ownership behind a runtime/service contract.
  • Add cancellation or shutdown semantics for the touched worker.
  • Avoid capturing raw nodes across worker completion without checked handles.
  • Preserve progress and completion callbacks.

Done when:

  • The touched worker cannot outlive its owner.
  • Shutdown behavior is explicit and validated.
  • UI completion handoff is handle-safe or owner-checked.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_app_core|pp_ui_core"

Mini-model packet: Pick one worker family. Do not perform broad thread cleanup across unrelated subsystems in one task.

ARC-WKF-010 - Thin Document Session/Open/Save Bridges

Status: Ready

Why now: The pure document/session planners are extensive, but live bridges still own retained prompts, metadata mutation, title updates, history clearing, snapshot handoff, and legacy Canvas execution.

Write scope:

  • src/legacy_document_open_services.*
  • src/legacy_document_session_services.*
  • src/legacy_history_services.*
  • focused app-core document/session headers only when needed

Read scope:

  • src/app_core/document_route.h
  • src/app_core/document_session.h
  • src/app_core/document_canvas.h
  • src/app.h
  • src/canvas.*

Required work:

  • Pick one bridge path: open-project confirmation, save-before-workflow, save-version, new-document overwrite, history clear, or title update.
  • Move decisions/mutations behind explicit service requests.
  • Keep retained prompts as adapters only.

Done when:

  • One document workflow path has a single obvious owner for decision, execution, and metadata mutation.
  • The retained bridge has less direct App::I/Canvas::I reach.
  • Behavior is covered by existing or focused document-session validation.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_session_tests,pp_app_core_document_route_tests -TestRegex "document_(session|route)"

Mini-model packet: Do not rewrite all document flows. Pick the narrowest path that removes live bridge ownership.

ARC-WKF-011 - Split Cloud And Brush Package Work Out Of UI Nodes

Status: Ready

Why now: Cloud browse/download/upload and brush package import/export still mix UI node lifetime, worker ownership, storage, network/asset behavior, and app singleton reach.

Write scope:

  • one family per slice: src/legacy_cloud_services.*, src/node_dialog_cloud.*, src/legacy_brush_package_import_services.*, src/legacy_brush_package_export_services.*, src/legacy_brush_preset_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.*
  • relevant panel/dialog headers

Required work:

  • Separate worker/network/asset execution from node lifetime.
  • Use app-core requests and asset helpers where they already exist.
  • Use checked handles for UI completion callbacks.
  • Preserve current cloud and brush package UX.

Done when:

  • One cloud or brush package path can be understood without reading panel or dialog internals first.
  • The touched UI node is a view/controller shell, not the workflow owner.
  • The touched worker cannot outlive its service/UI owner.

Validation:

powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_cloud_tests,pp_app_core_brush_package_import_tests,pp_app_core_brush_package_export_tests,pp_assets_brush_package_tests -TestRegex "document_cloud|brush_package"

Mini-model packet: Cloud and brush package work are separate packets. Do not assign both to one small worker.

Deferred On Purpose

  • Vulkan, Metal, WebGPU, and broad future-backend implementation.
  • OpenXR implementation beyond boundary cleanup needed to remove OpenVR debt.
  • Package-only migration that does not affect root app architecture.
  • CLI/planner-only expansion.
  • Broad warning cleanup without ownership movement.
  • Documentation-only progress claims.