Files
panopainter/docs/modernization/roadmap.md

171 KiB

PanoPainter Modernization Roadmap

Status: live Last updated: 2026-06-05

This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this file current as phases are implemented. Do not let shortcuts, skipped platforms, or temporary adapters live only in chat history.

How To Keep This Roadmap Live

  • Update the phase status before and after each implementation pass.
  • When a shortcut is introduced, add it to the debt log section in this file until docs/modernization/debt.md exists, then move debt entries there.
  • When a major architectural decision is made, add an ADR under docs/adr/ once that directory exists.
  • Every phase must preserve old behavior unless the roadmap explicitly says otherwise.
  • Each phase must leave the repo in a buildable and testable state.
  • Do not add stubs without a debt entry, validation command, and removal condition.

Locked Decisions

  • Graphics path: keep OpenGL working first; add Vulkan and Metal after the renderer boundary exists.
  • Required platforms at phase gates: Windows desktop/AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, and WebGL.
  • Dependency policy: use vcpkg where reliable; keep SDK, patched, or vendor-only dependencies with documented reasons.
  • Test stack: Catch2, golden/approval tests, and fuzz/property tests where useful.
  • Automation: local reproducible matrix first; hosted CI can be added later.
  • Documentation: ADRs, debt log, and this living roadmap.
  • "vkpkg" in older notes means vcpkg.
  • Target C++ standard: C++23.
  • Initial Windows CMake generator target: Visual Studio 2026 when available.

Phase Status

Phase Name Status Gate
0 Inventory, Safety Rails, And Memory Complete No behavior changes; old builds still work
1 Unified CMake Skeleton In progress Root CMake builds the Windows app and owns the source list
2 Toolchain, Diagnostics, And Dependencies In progress Strict desktop library builds compile cleanly
3 Test Harness And Agent-Ready Automation In progress ctest --preset desktop-fast runs headlessly
4 Component Split Without Behavior Change Started Each extracted target builds and tests
5 Renderer Boundary And OpenGL Parity Started OpenGL output matches golden readbacks
6 Platform Alignment Started Every supported platform has named validation
7 Hardening, Coverage, And Breaking-Point Tests Not started Each component has edge/failure tests
8 Future Backend Readiness Not started Vulkan/Metal lab targets remain non-default

Target Component Architecture

The refactor should move toward one-way dependencies:

pp_foundation
  -> pp_assets
  -> pp_paint
  -> pp_document
  -> pp_renderer_api
  -> pp_renderer_gl
  -> pp_paint_renderer
  -> pp_ui_core
  -> pp_panopainter_ui
  -> pp_platform_*
  -> panopainter_app

Intended responsibilities:

  • pp_foundation: logging facade, math/util helpers, events, task queues, binary streams.
  • pp_assets: Asset, Image, Settings, serialization, ABR, PPBR, and PPI helpers.
  • pp_paint: pure Brush, Stroke, stroke sampling, and CPU reference blend math.
  • pp_document: canvas document model, layers, animation frames, and undo/redo model.
  • pp_renderer_api: renderer-neutral interfaces for textures, render targets, shaders, meshes, readback, frame capture, and tracing.
  • pp_renderer_gl: current OpenGL implementation behind renderer interfaces.
  • pp_paint_renderer: stroke rasterization, layer compositing, cube/equirect export using pp_renderer_api.
  • pp_ui_core: Node, layout, generic controls, text/image primitives.
  • pp_panopainter_ui: panels, dialogs, NodeCanvas, and app-specific workflows.
  • pp_platform_api: SDK-free service interfaces for clipboard, cursor, virtual keyboard, file pickers, sharing, and future platform automation.
  • pp_platform_*: Windows, macOS/iOS, Android, Linux, and WebGL shells.
  • panopainter_app: composition root only.

Rules:

  • Component headers must not include platform SDK or graphics API headers unless the component name includes that backend or platform.
  • Pure libraries must build and test without a window, GL context, network, tablet, VR headset, or filesystem outside test temp directories.
  • Public APIs should return explicit status/result objects. PanoPainter app code keeps exceptions disabled unless isolated SDK wrappers require them.
  • Singleton access should be replaced at component boundaries with context or service objects. Temporary facade shims require debt entries.

Phase 0: Inventory, Safety Rails, And Memory

Status: complete on 2026-05-31. Created this roadmap, docs/modernization/debt.md, docs/modernization/capability-map.md, docs/modernization/build-inventory.md, and ADR 0001.

Goal: create durable project memory and prevent silent shortcuts before large refactors begin.

Implementation tasks:

  • Add docs/modernization/roadmap.md, docs/modernization/debt.md, and docs/adr/.
  • Add a shortcut rule: every temporary adapter, fallback, skipped platform, or retained vendored dependency must have owner, reason, validation command, and removal condition.
  • Generate a current capability map covering:
    • project open/save and PPI compatibility
    • image import/export and thumbnails
    • brush presets, ABR import, PPBR export/import
    • layers, blend modes, alpha lock, selection mask
    • animation frames and MP4/timelapse recording
    • VR, tablet, touch, mouse, keyboard, gestures
    • cloud upload/download/browse
    • UI dialogs, panels, layout XML, settings
    • Windows/AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, WebGL
  • Record current build commands and known platform prerequisites.

Gate:

  • No behavior changes.
  • Existing Visual Studio, platform CMake, Gradle, Apple, Linux, and WebGL paths are not removed.

Phase 1: Unified CMake Skeleton

Goal: make CMake the canonical source list without breaking existing projects.

Status: in progress. Root CMakeLists.txt, CMakePresets.json, and project option targets exist. The Windows desktop app builds through CMake as PanoPainter; the raw Visual Studio solution/project files were removed on 2026-05-31 by user decision. The root CMake Windows app graph now includes a panopainter_app composition target and pp_platform_windows shell target so PanoPainter is only the executable/resource wrapper; Windows and vendor link dependencies now belong to the platform shell target, and Windows runtime payload deployment lives behind cmake/PanoPainterRuntime.cmake. pp_legacy_vendor now owns the retained third-party source bundle as an interim containment boundary until vcpkg, SDK imports, or documented permanent vendoring decisions replace each dependency. pp_legacy_engine now contains the retained legacy tablet, video, and low-level runtime sources as an interim containment boundary while pure replacement components take over. pp_legacy_assets_io now owns retained ABR, asset/file, binary stream, image, serializer, and settings implementations until pp_assets fully replaces those paths in the app. pp_legacy_paint_document now owns retained action, bezier, brush, canvas, canvas-layer, and event implementations until pp_paint and pp_document fully replace those paths in the app. pp_legacy_renderer_gl now owns the retained OpenGL runtime implementations for Font, RTT, Shader, Shape, Texture2D, TextureCube, Sampler, and TextureManager as an object-library boundary folded into the retained engine until the renderer API inversion is complete. pp_legacy_ui_core now owns retained base Node, layout, text, image, input, popup, slider, scroll, and settings UI controls as an object-library boundary folded into the legacy app adapter until those paths are replaced by pp_ui_core and app-specific UI targets. pp_app_core now owns tested app-level document-open routing for project files, ABR imports, and PPBR imports without UI, filesystem, platform, or renderer dependencies; App::open_document and pano_cli classify-open consume this route contract. It also owns tested session decisions for project-open, app-close, save, save-as, and save-version flows; App::open_document, App::request_close, file-menu save actions, NodeCanvas save hotkeys, and pano_cli simulate-app-session consume those contracts while legacy canvas/project loading remains in place. pp_app_core also owns tested app preference plans for UI scale/font scale, scale option selection, viewport scale, RTL layout direction, timelapse recording toggles, VR mode start/stop, VR controller enablement, and canvas cursor mode; the live tools/options menu and pano_cli plan-app-preferences consume those contracts. Options-menu preference execution now dispatches through AppPreferenceServices and src/legacy_app_preference_services.* before legacy widgets, settings persistence, recording toggles, and canvas cursor updates continue. It also owns tested startup plans for run-counter increments, preference-save intent, auto-timelapse startup, stored VR-controller state, license-warning visibility, and main startup resource sequencing for shader, asset, layout, title, and UI render-target setup. App::init now plans those decisions before heavy initialization, executes run-counter persistence through src/legacy_app_startup_services.* before resource setup, dispatches the resource sequence through the same bridge, and executes runtime startup side effects after the UI layout and main render target exist. It also owns tested app status/display plans for document title text, resolution mapping/labels, DPI text, history-memory text, and recording-frame status text, plus renderer diagnostic indicator labels for framebuffer fetch and floating-point render targets; App::title_update, App::update_memory_usage, App::update_rec_frames, resolution helpers, App::initLayout, and pano_cli plan-app-status consume those contracts while legacy UI nodes still render the strings and status lights. App-level progress, message, and input dialog metadata now also lives in pp_app_core through plan_app_progress_dialog, plan_app_message_dialog, and plan_app_input_dialog; App::show_progress, App::message_box, App::input_box, and pano_cli plan-app-dialog consume those plans before src/legacy_app_dialog_services.* creates retained NodeProgressBar, NodeMessageBox, and NodeInputBox instances. Legacy dialog node lifetime/layout ownership remains tracked under DEBT-0058. Frame-level app decisions for the initial surface size, redraw/animation update gating, layout ticking, resize render-target recreation, canvas-stroke drawing, VR UI drawing, main UI drawing, UI observer clipping/on-screen transition/scissor projection, and redraw reset now live in pp_app_core; App::create, App::tick, App::resize, App::update, App::draw, and pano_cli plan-app-frame consume those plans while retained layout traversal, render-target recreation, Node parent walking, on-screen callbacks, and OpenGL/UI drawing stay in the legacy app. App input dispatch decisions for pointer coordinate normalization, mouse designer-first routing, gesture midpoint/delta math, touch/key main-layout routing, VR spacebar camera-sync intent, UI visibility toggling, and stylus touch-lock attachment now live in pp_app_core; App::mouse_*, App::gesture_*, App::touch_tap, App::key_*, App::toggle_ui, App::set_stylus, and pano_cli plan-app-input consume those plans while retained event objects, child-node mutation, and legacy Node dispatch stay in the app shell. App thread orchestration decisions for render/UI task dispatch, unique queued task replacement, queue draining, render-context wrapping, async redraw notification, UI tick redraw scheduling, UI-loop timer/report/reload cadence, and thread start/stop intents now live in pp_app_core; App::render_task*, App::ui_task*, App::async_redraw, App::render_thread_*, App::ui_thread_*, and pano_cli plan-app-thread consume those plans while retained std::thread, condition-variable, OpenGL context, live reload, and task execution remain in the app shell. Shutdown lifecycle staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target destruction, panel-node release, and quick-mode cleanup now lives in pp_app_core; App::terminate and pano_cli plan-app-shutdown consume that plan while retained cleanup execution stays in the legacy app. Command-line panorama conversion planning for renderer-state setup, temporary canvas allocation, project open, and equirectangular export now lives in pp_app_core; App::cmd_convert and pano_cli plan-command-convert consume that sequence while retained OpenGL state dispatch and legacy Canvas open/export execution stay in the legacy app. panopainter_app is now a real static target that owns app orchestration sources, app version metadata, and version-header generation. pp_panopainter_ui now owns app-specific modal, dialog, panel, canvas, viewport, color-picker, stroke-preview, and tool UI workflow nodes outside pp_legacy_app; base Node controls and layout plumbing remain in the legacy target until the UI core/app UI boundary is tightened. Android arm64 now configures and builds headless foundation/tool targets through the root CMake/NDK path. Non-Windows platform app/package files remain during Phase 6 alignment.

Implementation tasks:

  • Add root CMakeLists.txt and shared CMake modules under cmake/.
  • Add CMakePresets.json with at least:
    • windows-vs2026-x64
    • windows-clangcl-asan
    • linux-clang
    • android-arm64
    • android-x64
    • emscripten
    • macos
    • ios-device
    • ios-simulator
  • Keep Android CMake, Linux CMake, WebGL CMake, Apple project files, and AppX packaging during the transition until each consumes shared component targets.
  • Move version generation into a CMake custom command using scripts/pre-build.py.
  • Fix scripts/pre-build.py only if required to avoid unnecessary rewrites or missing-tag failures.
  • Add CMake options:
    • PP_BUILD_APP
    • PP_BUILD_TESTS
    • PP_BUILD_TOOLS
    • PP_ENABLE_OPENGL
    • PP_ENABLE_VULKAN_EXPERIMENTAL=OFF
    • PP_ENABLE_VR
    • PP_ENABLE_CLOUD
    • PP_ENABLE_VIDEO
  • Define source-list helper targets so per-platform source duplication can be reduced incrementally.

Gate:

  • Windows desktop app builds through CMake.
  • New CMake can configure on Windows.
  • Source list differences are understood and documented.
  • Non-Windows platform migration is debt-tracked until Phase 6.

Phase 2: Toolchain, Diagnostics, And Dependencies

Goal: turn the build into an error-finding system before deep refactors.

Status: in progress. Initial warning/sanitizer option targets, vcpkg.json, a validated Windows headless vcpkg preset, pp_ui_core support for vcpkg tinyxml2 on that preset, and a headless panopainter_validate_shaders target exist. windows-clangcl-asan now configures as a headless Ninja/clang-cl ASan preset and uses the release MSVC runtime required by clang-cl ASan, but local ASan builds are blocked by DEBT-0014 until Clang and the selected MSVC STL are compatible. Dependency migration is not complete until remaining component dependencies and mobile/Apple triplets are validated. Root CMake now also exposes panopainter_platform_build_vcpkg_ui_core, a focused automation target that resolves VCPKG_ROOT through the platform-build wrapper and validates the vcpkg-backed pp_ui_core/tinyxml2 XML test boundary from the CMake target graph.

Implementation tasks:

  • Set C++23 through target features, not raw compiler flags.
  • Add warning profiles:
    • MSVC: /W4 /permissive- /Zc:__cplusplus /Zc:preprocessor, with C4100 muted temporarily under DEBT-0019.
    • Optional MSVC analysis preset: /analyze.
    • Clang/GCC: -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wnull-dereference, with -Wunused-parameter muted temporarily under DEBT-0019.
  • Keep exceptions disabled for PanoPainter targets, except isolated SDK wrapper targets when unavoidable.
  • Add sanitizer presets:
    • Clang/GCC ASan and UBSan for headless libraries.
    • MSVC ASan where supported.
    • TSan only for pure/headless targets.
  • Add tooling hooks:
    • clang-tidy
    • cppcheck
    • shader validation or compile checks
    • CTest dashboard output
  • Add vcpkg.json.
  • Move reliable dependencies to vcpkg first:
    • fmt
    • glm
    • tinyxml2
    • stb
    • curl
    • sqlite3
    • glad
    • Catch2
  • Keep vendored until proven:
    • OpenVR only as the temporary desktop compatibility fallback while OpenXR is introduced behind pp_platform_vr
    • OVR/Wave SDKs
    • Wacom WinTab
    • AppCenter
    • openh264
    • mp4v2
    • libyuv
    • patched or SDK-specific libraries

Gate:

  • Desktop library targets compile with strict diagnostics.
  • New warnings caused by refactor are fixed or locally justified.
  • Any global warning suppression must have an open debt entry, validation command, and removal condition.

Phase 3: Test Harness And Agent-Ready Automation

Goal: make each component reachable by automated tools and future agents.

Status: in progress. tests/ exists, desktop-fast, fuzz, and stress CTest presets run headlessly, and PowerShell/bash wrappers exist for configure/build/test/analyze/platform-build/package-smoke. pano_cli exists with JSON automation commands for app document-open routing, app session dirty-state and save decisions, creating a pp_document model, metadata-only PPI project loading, and inspecting image signatures, PPI headers, and layout XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI body parsing is debt-tracked as DEBT-0013. Agent code navigation now includes scripts/dev/clangd_nav.py with symbol/detail/path regex filters and a panopainter_clangd_nav_regex_self_test CTest so broad symbol-family searches can be validated before they guide refactors. Agents must use the panopainter-code-navigation skill before broad text search whenever C++ symbol identity, generated-style symbol families, declarations/definitions, override groups, or platform/backend path slices are the real question.

Implementation tasks:

  • Add tests/ with one executable per component.
  • Register CTest labels:
    • foundation
    • assets
    • paint
    • document
    • renderer
    • ui
    • platform
    • integration
    • fuzz
    • slow
    • gpu
  • Add tools/pano_cli for headless automation.
  • pano_cli should support:
    • create document
    • load project
    • save project
    • apply scripted strokes
    • import/export images
    • inspect layers
    • run layout parse
    • emit JSON results
  • Add local automation wrappers under scripts/automation/:
    • configure
    • build
    • test
    • analyze
    • package smoke
  • All wrappers must return machine-readable logs or summaries.
  • Establish tests/data/ fixtures:
    • tiny PPI files
    • corrupt/truncated PPI cases
    • PNG/JPEG fixtures
    • ABR/PPBR samples
    • layout XML
    • shader snippets
    • brush stroke scripts

Gate:

  • ctest --preset desktop-fast --build-config Debug runs without a GL context.
  • Non-render components can be tested on a headless machine.

Phase 4: Component Split Without Behavior Change

Goal: split libraries while keeping current app behavior.

Status: started. pp_foundation exists with binary stream utilities and boundary/overread/overlapping-write tests. It also owns strict decimal uint32 parsing used by pano_cli, with rejection tests for empty, signed, mixed, and overflowing input. A synchronous event dispatcher, structured logging facade, bounded FIFO task queue, and deterministic TraceRecorder now record component/name/thread/frame/stroke metadata with filtering, capacity, and invalid-end tests. pp_assets has started with PNG/JPEG signature detection, PNG IHDR metadata parsing, PPI header/project byte-layout/body-summary recognition, layer/frame indexing, dirty-face PNG payload metadata validation, asset-level RGBA PNG payload decoding, and a pure typed settings document model, with corrupt/truncated/unsupported, non-finite opacity, unsupported blend-mode, extreme-dimension, and key/value limit tests. pp_paint has started with pure brush parameter validation/stamp evaluation, CPU reference math for the five current final RGBA shader blend modes plus the shader-style stroke-alpha blend modes used by pattern/dual-brush mixing, and deterministic stroke spacing/interpolation plus duplicate-segment, non-finite, sample-limit, and 1001-sample stress coverage, plus a pure text stroke-script parser. pp_document has started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot construction, per-layer frame metadata, layer metadata operations, frame move/duration queries, renderer-free RGBA8 cube-face payload storage, renderer-free alpha8 selection-mask storage, PPI image import/export, and layer/frame/undo-redo history invariant tests. Snapshot construction validates embedded face-pixel payload bounds, byte counts, duplicate face payloads, and duplicate selection masks. pp_renderer_api has started with renderer-neutral texture/readback descriptors and validation tests. pp_paint_renderer has started with deterministic CPU layer compositing over renderer extents using the paint blend reference, and now exposes pure pp_document face and six-face frame compositors that expand per-layer dirty face payload rectangles into full renderer-sized RGBA buffers for a requested document frame. It can also upload those six composited faces through the renderer-neutral IRenderDevice texture API, with the recording backend validating upload and explicit-transition command streams. pp_ui_core has started with XML-layout-facing length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid input tests. Retained Node tree lifetime safety is now an explicit modernization track under DEBT-0063: the current UI still mixes raw parent and lookup pointers, public mutable child ownership, raw callback targets, and manual destroy flags. pp_ui_core should introduce owned tree/handle APIs, scoped callback connections, mutation-safe event dispatch, and focused destroy-during-callback tests before broad NodePanel*/NodeDialog* migration accelerates. pano_cli inspect-image exposes PNG IHDR metadata as JSON, pano_cli import-image accepts a PNG path and imports decoded RGBA8 pixels into a new pure pp_document face payload, with checked-in decodable PNG and truncated PNG automation coverage, pano_cli export-image writes a deterministic RGBA8 PNG through pp_assets and round-trips it back through file import automation, pano_cli inspect-project reports validated PPI thumbnail/body byte layout, body summary, layer/frame descriptors, dirty-face PNG payload metadata, and asset-level decode coverage, and pano_cli load-project creates a pp_document projection with per-layer frame counts, durations, and decoded face-pixel payload attachment when PPI image payloads are present. pp_assets can write generated PPI projects with explicit per-layer names, visibility, opacity, blend mode, alpha lock, per-layer frame durations, and dirty-face payloads targeted to layer/frame/face slots. pano_cli save-project exposes the generated writer for metadata-only and test dirty-face-payload round-trips through load-project and rejects non-finite automation float inputs before writing files. pp_document::export_ppi_project_document converts pure documents into PPI bytes using that writer, including PNG-encoded layer/frame face payloads. pano_cli simulate-document-export exercises that pure document export path, decodes the generated PPI bytes, reimports them, and emits JSON round-trip metadata. pano_cli simulate-document-render exercises the pure document-to-renderer frame compositor and renderer texture-upload bridge, emitting six-face render summaries, renderer upload-command summaries, and OpenGL command-planner support counts for headless automation. pano_cli save-document-project writes the same pure document export to a PPI file for inspect/load round-trip automation. pano_cli create-document can create simple animation documents with explicit frame count/duration. pano_cli simulate-document-edits exercises pure layer metadata, frame reordering, active-index preservation, tiny face-payload attachment, and selection-mask attachment. pano_cli simulate-document-history exercises the pure pp_document::DocumentHistory apply/undo/redo path and emits JSON state summaries. pano_cli simulate-image-import decodes an embedded tiny PNG through pp_assets and attaches the resulting RGBA8 payload to pp_document. pano_cli simulate-document-export exercises pure document-to-PPI export, asset-level PPI image decode, and document reimport in one automation command. pano_cli save-document-project writes a deterministic pure document export PPI and verifies it through inspect/load smoke coverage. pano_cli simulate-blend exposes deterministic final RGBA and stroke-alpha blend reference vectors through JSON automation. pano_cli simulate-stroke exercises the pure stroke sampler for scripted-stroke automation. pano_cli simulate-stroke-script loads stroke script fixtures, parses them through pp_paint, and samples every stroke. pano_cli apply-stroke-script maps sampled script points into a bounded pp_document RGBA8 face payload, writes a PPI file, and verifies that the applied stroke payload survives inspect/load round-trip automation, with a rejection smoke test for unsafe tiny canvas dimensions. pano_cli classify-open exposes the pure pp_app_core document-open route contract for project files, ABR imports, PPBR imports, and malformed path rejection. pano_cli plan-open-route exposes the pure pp_app_core document-open action plan for clean project open, dirty project prompt, and brush-import prompt flows. pano_cli simulate-app-session exposes the pure pp_app_core session decisions used by project-open, app-close, save, save-as, and save-version flows, plus the save-before-continue workflow gate used by new-document/open/browse dialogs. pano_cli plan-new-document exposes the same app-core new-document target, legacy resolution-index mapping, and overwrite decision used by the live new-document dialog, including invalid resolution rejection. pano_cli plan-document-file exposes the same app-core document-name validation, legacy .ppi path construction, and overwrite prompt decision used by save-as dialogs through one combined save-file plan. pano_cli plan-document-version exposes the save-version suffix parsing, candidate generation, collision skipping, and no-slot failure behavior used by the live save-version dialog. pano_cli plan-export-target exposes app-core export target planning for equirectangular image files, layer/frame collection stems, picked-directory stems, cube-face work-directory file sets, and MP4 suggested names used by the live export dialogs and cube-face writer bridge. pano_cli plan-export-start exposes the app-core export availability decision used by live image, layer, animation-frame, depth, and cube-face export dialogs plus MP4 animation and timelapse export dialogs before they call legacy canvas/recording export execution. pano_cli plan-export-message exposes the app-core export completion dialog metadata now consumed by the live legacy export bridge for equirectangular, layer/frame, depth/cube, animation MP4, and timelapse success reporting, including platform-style destinations and no-message/suppressed branches. pano_cli plan-export-report exposes app-core failure and license-disabled dialog metadata now consumed by live export dialogs before retained legacy export execution/logging continues. pano_cli plan-recording-session exposes the app-core recording start, stop, clear, platform recorded-file cleanup, frame reset, export progress-total, and export progress-dialog decisions used by the live recording controls. Recording lifecycle and MP4 export execution now dispatch through RecordingServices in src/legacy_recording_services.* before legacy recording threads, PBO readback, and MP4 encoder execution continue. The retained MP4 export progress bar now uses src/legacy_app_dialog_services.* for creation while progress lifetime and MP4 writing remain legacy-owned. pano_cli plan-share-file exposes the app-core saved-path decision used by the live platform share command before iOS/macOS sharing bridges or retained no-op platform branches execute. pano_cli plan-picked-path exposes the app-core selected-path decision used by live image, file, save-file, and directory picker branches before retained platform callbacks or legacy picker bridges continue. pano_cli plan-display-file exposes the app-core external file presentation decision used by live display-file requests before retained platform open-file bridges continue. pano_cli plan-keyboard-visibility exposes the app-core virtual keyboard visibility decision used by live show/hide keyboard requests before retained mobile platform keyboard bridges continue. pano_cli plan-cursor-visibility exposes the app-core cursor visibility decision used by live canvas cursor requests before retained desktop platform cursor bridges continue. pano_cli plan-clipboard-read and pano_cli plan-clipboard-write expose the app-core clipboard text decisions used by live clipboard get/set requests before retained platform clipboard bridges continue. pano_cli plan-document-resize exposes the app-core resize dialog state and selected-resolution commit plan used by the live document resize dialog, and resize execution now dispatches through DocumentResizeServices before the shared app-shell document-canvas bridge runs the legacy Canvas resize adapter and history clearing. pano_cli plan-layer-rename exposes the app-core layer rename decision used by the live layer rename dialog, and rename execution now dispatches through DocumentLayerRenameServices in the shared app-shell layer bridge src/legacy_document_layer_services.* before legacy Canvas, NodeLayer, and ActionManager undo adapters continue. pano_cli plan-layer-operation exposes app-core planning for layer add, duplicate, select, reorder, remove, opacity, visibility, alpha-lock, blend-mode, and highlight actions used by the live layer panel. Direct layer-panel operations now dispatch through DocumentLayerOperationServices before the shared app-shell layer bridge continues legacy Canvas and UI layer execution. pano_cli plan-layer-panel-view exposes the app-core layer panel view model for current opacity, alpha-lock, blend mode, and per-layer visibility state, and live NodePanelLayer::update_attributes() now consumes that tested projection before writing the retained legacy UI controls. pano_cli plan-layer-menu exposes app-core planning for Layer menu clear, rename, and merge-down labels/actions, and direct Layer menu commands now dispatch through DocumentLayerMenuServices before the legacy canvas/layer UI adapter in src/legacy_document_layer_services.* continues execution. Layer menu clear now routes through the shared DocumentCanvasClearServices executor from that bridge before the legacy canvas-clear adapter continues, and Layer menu merge now validates and dispatches through DocumentLayerMergeServices before the legacy layer-panel merge adapter continues. pano_cli plan-animation-operation exposes app-core planning for animation frame add, duplicate, remove, duration adjustment, timeline moves, timeline goto/next/previous, onion-size updates, frame selection, no-reload playback stepping, and play-mode toggles used by the live animation panel. pp_app_core also owns onion-skin frame range and alpha falloff planning now consumed by live NodeCanvas panorama drawing. pano_cli plan-animation-panel-action exposes the higher-level animation panel state/action planner for goto, next, previous, playback-step, and play-toggle automation without requiring the legacy UI or canvas. Panel-control, timeline, selected-frame click, playback tick, and play-button toggle execution now dispatch through DocumentAnimationServices before the shared src/legacy_document_animation_services.* bridge continues legacy Canvas/Layer/canvas-mode and animation-panel state execution. pano_cli plan-brush-operation exposes app-core planning for brush color changes, tip/pattern/dual texture changes, preset brush replacement, and stroke settings refreshes used by the live brush, quick, color, and floating panel callbacks. Brush UI execution now dispatches through BrushUiServices before the shared src/legacy_brush_ui_services.* bridge mutates legacy Brush and panel state or loads brush resources. pano_cli plan-brush-texture-list exposes app-core planning for brush/pattern texture add, remove, and reorder actions, and NodePanelBrush now dispatches those actions through BrushTextureListServices in the shared brush bridge before the legacy image load/save and UI-list adapter continues. pano_cli plan-brush-stroke-control exposes app-core planning for the live stroke panel's slider, checkbox, blend-mode, tip-aspect reset, and default brush reset commands. NodePanelStroke now dispatches those controls through BrushStrokeControlServices in the shared brush bridge before the legacy Canvas::I/Brush/stroke-panel adapter continues. pano_cli plan-brush-stroke-panel-view exposes the app-core stroke-panel view projection for brush float settings, toggles, blend modes, and thumbnail paths, and live NodePanelStroke::update_controls() now consumes that tested projection before applying retained slider-curve, preview, and thumbnail UI updates. pano_cli plan-brush-refresh exposes app-core planning for app-level brush refresh fan-out, and live App::brush_update() now consumes that view before applying retained stroke, quick, and floating color widget updates. pano_cli plan-canvas-tool exposes app-core planning for draw/erase/line, camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar commands. Canvas tool execution now dispatches through CanvasToolServices in src/legacy_canvas_tool_services.* before legacy toolbar selection, Canvas mode, pen picking, touch-lock, and transform state adapters continue. pano_cli plan-canvas-tool-toolbar now exposes the full draw-toolbar binding set, including button ids, select/toggle actions, button-class expectation, and the default draw-mode initialization. Live App::init_toolbar_draw consumes the same app-core toolbar plan to install handlers and apply the initial draw tool while retained NodeButton/NodeButtonCustom lookup and legacy canvas-tool execution remain under DEBT-0027. pano_cli plan-canvas-tool-state exposes the matching toolbar active-state refresh used by App::update before legacy Canvas mode state remains the source of truth. NodeCanvas stylus eraser mode switching consumes the same shared bridge through its input-only path before legacy canvas mode execution continues. Canvas mode pointer-tip visibility and Windows pressure remapping now dispatch through PlatformServices, preserving iOS tip behavior and the Windows pressure curve outside canvas_modes.cpp. NodeCanvas keyboard and touch command handling now consumes pp_app_core canvas-hotkey planning for E draw/erase, Ctrl+Z, Ctrl+Shift+Z, Ctrl+S, Ctrl+Shift+S, Tab UI toggle, brush-size brackets, Android back, Alt cursor reveal, and two-finger undo before the shared bridge delegates to legacy UI/canvas/history adapters. pano_cli plan-canvas-cursor exposes the canvas-specific cursor visibility policy for draw/erase versus non-paint modes, small-brush thresholds, active-stroke hiding, and modifier/tool forced visibility; live NodeCanvas::update_cursor() consumes that planner before retained App::show_cursor/App::hide_cursor platform dispatch. pano_cli plan-canvas-clear exposes app-core planning for the main toolbar clear-current-layer command, including clear color validation, no-canvas handling, undo recording intent, and dirty-state intent; live toolbar execution and Layer menu clear now dispatch through the shared app-shell document-canvas bridge before the legacy Canvas::clear adapter continues. pano_cli plan-canvas-document-snapshot exposes the app-core projection from live canvas metadata into a pure pp_document::CanvasDocument, including dimensions, active layer/frame, layer visibility/opacity/alpha/blend metadata, frame durations, captured RGBA8 face payloads, and remaining renderer payload-readback counts; the retained legacy_document_canvas_services bridge now builds the same metadata snapshot from live Canvas state and has an opt-in dirty-face payload snapshot path backed by retained Layer::snapshot() readback. Live Save, Save As, Save Version, and save-before-workflow paths now prepare and log a payload-completeness report from that snapshot before delegating to retained Canvas::project_save; the app-core snapshot boundary also has a tested pure PPI export helper, and pano_cli plan-canvas-document-snapshot runs that helper for payload-complete snapshots and reports generated byte/dirty-face summaries. The same automation now feeds payload-complete snapshots through the shared pp_paint_renderer::prepare_document_frame_export_readiness report, which records renderer-neutral six-face texture upload commands and encodes the active document frame's six composited faces to PNG bytes. This gives CLI automation and live export adapters the same document/canvas-to-renderer readiness boundary before broader writer replacement. Live save writer replacement, export adoption, and renderer-owned readback remain under DEBT-0010/DEBT-0013/DEBT-0036. The same CLI snapshot report now emits depthExport readiness from pp_paint_renderer::plan_document_depth_export_render: metadata-only snapshots report pending renderer payload readback, while payload-complete snapshots report the legacy 1024x1024 depth-render draw plan and still flag the final 3D view/depth image readback as renderer-owned. Live equirectangular, layer, animation-frame, and cube-face export adapters now prepare the same payload-bearing document snapshot and shared renderer export readiness report. Cube-face export writes those document/renderer-owned PNG bytes through a tested app-core write/publish executor using the app-core-planned legacy face filenames when available and falls back to retained Canvas::export_cube_faces on snapshot/write failure. PNG equirectangular export now uses the same document/composite payload to generate an equirectangular PNG through pp_paint_renderer before the retained fallback. Payload-complete layer and animation-frame PNG collections now use pure pp_paint_renderer equirectangular PNG generation plus app-core collection write/publish execution before retained fallback. Payload-complete desktop JPEG equirectangular export now uses the same projection through pp_paint_renderer, pp_assets JPEG encoding, and GPano XMP injection before retained fallback. Web handoff, video, and incomplete-readback collection cases still delegate to retained Canvas writers after readiness reporting. Depth export now also plans the retained image/depth file targets in pp_app_core and logs a pp_paint_renderer document depth render plan for the legacy 1024x1024 perspective render plus per-layer depth pass before falling back to retained Canvas::export_depth; actual depth rendering, readback, and format parity remain retained. pano_cli plan-image-import exposes app-core planning for File > Import image route decisions, including wide equirectangular images, legacy vertical cube strips, regular transform-placement images, and invalid image dimensions; live File > Import execution now dispatches through DocumentImageImportServices before legacy image loading, Canvas::import_equirectangular, or import transform-mode setup continues. pano_cli plan-file-menu exposes app-core planning for the top-level File menu commands, including new/open/import, save/save-as/save-version, share, resize, cloud upload/browse, JPEG export, and export-submenu routing. Direct File menu commands now dispatch through FileMenuServices in the shared app-shell bridge src/legacy_app_shell_services.* before legacy dialogs, pickers, platform services, cloud code, and canvas workflows continue. pano_cli plan-export-menu exposes app-core planning for File menu export choices, including image, layer, cube-face, depth, animation-frame, MP4, and timelapse dialog routing plus license/canvas gating. Export menu commands now dispatch through DocumentExportMenuServices in the shared app-shell bridge before legacy export dialogs and renderer/video execution continue. Export success-message metadata now also comes from pp_app_core through pano_cli plan-export-message and the legacy document-export bridge, reducing the bridge to export execution, platform handoff, and retained threading. Export failure/license dialog metadata now comes from pp_app_core through pano_cli plan-export-report, with legacy App::dialog_export* only showing the planned dialog and dispatching retained export calls. pano_cli plan-grid-operation exposes app-core planning for grid heightmap pick/load/reload/clear, lightmap render capability/limit checks, and heightmap commit used by the live grid panel. Grid execution now dispatches through GridUiServices in src/legacy_grid_ui_services.* before legacy image loading, OpenGL texture updates, nanort lightmap baking, and Canvas::draw_objects execution continue. The retained NodePanelGrid lightmap bake now uses the shared parallel_for helper instead of platform-specific Win32 Concurrency Runtime and Apple dispatch_apply branches, keeping the row-dispatch policy in common legacy infrastructure while the bake itself remains debt-tracked. pano_cli plan-history-operation exposes app-core planning for undo, redo, and clear-history availability used by toolbar buttons and canvas shortcuts; live toolbar and canvas-hotkey execution now dispatch through a shared app-shell legacy history bridge before the legacy ActionManager stack adapter continues. The bridge also centralizes saturated history metrics so app-core plans never receive wrapped negative counts from oversized legacy stacks. pano_cli plan-main-toolbar exposes app-core planning for the live main toolbar/status-bar shell, including open/save dialogs, undo/redo availability, clear-history availability, clear-canvas no-canvas blocking, message-box creation, and settings dialog routing. The toolbar test-message dialog metadata now lives in pp_app_core through plan_main_toolbar_message_dialog, and pano_cli plan-main-toolbar --command message-box exposes it for automation. pp_app_core now also owns a MainToolbarServices executor boundary, so App::init_toolbar_main dispatches through src/legacy_app_shell_services.* before legacy dialogs, history/canvas adapters, and settings UI execution continue; retained toolbar message-box creation now uses src/legacy_app_dialog_services.*. pano_cli plan-quick-operation exposes app-core planning for quick brush/color slot selection versus popup opening, plus quick mini-state restore/reset validation used by the live quick panel. Quick-panel execution now dispatches through QuickUiServices in src/legacy_quick_ui_services.* before the legacy Brush, color picker, stroke preview, and preset popup adapter continues. pano_cli plan-quick-slider-preview exposes app-core planning for quick size/flow slider preview cursor placement, RTL offset handling, and pen/line mode tip flags; live NodePanelQuick slider callbacks now consume that plan before the retained CanvasModePen/CanvasModeLine and brush-preview updates. pano_cli plan-tools-menu and pano_cli plan-tools-panel expose app-core planning for top-level Tools commands and floating-panel requests, including already-visible no-ops, panel chrome metadata, shortcuts, camera reset, grid-clear, and platform-only SonarPen gating. Direct Tools commands now dispatch through ToolsMenuServices in the shared app-shell bridge before the legacy UI/panel/canvas/platform adapters continue execution. The live animation panel route now also checks animation panel visibility and applies animation panel layout state instead of using the grid panel by mistake. pano_cli plan-canvas-camera-reset, pano_cli plan-canvas-view-density, and pano_cli plan-canvas-view-cursor-mode expose shared app-core canvas-view state used by live reset-camera, viewport-density, and cursor-mode paths. Tools reset-camera, document open/new-document reset, cloud download reset, and options viewport/cursor callbacks now dispatch through src/legacy_canvas_view_services.* before retained legacy canvas mutation and settings writes. The live SonarPen menu action now asks the active PlatformServices instance for availability and startup, removing the local iOS branch from the Tools menu and shared Tools executor while preserving the retained iOS bridge in the legacy platform adapter. Options-menu preference callbacks now dispatch UI scale, viewport scale, RTL, VR mode, VR-controller, auto-timelapse, and cursor-mode side effects through AppPreferenceServices in src/legacy_app_preference_services.* before retained settings writes, recording lifecycle calls, and legacy canvas/UI adapters continue. VR mode start/stop now enters App platform wrappers that dispatch through PlatformServices; the desktop runtime-selection policy in pp_platform_api prefers OpenXR and marks OpenVR as a legacy fallback. Windows still reaches the retained OpenVR bridge in WindowsPlatformServices until the OpenXR backend is wired, while the legacy fallback reports unsupported VR startup on non-Windows platforms until their shells own the service. pano_cli plan-about-menu exposes app-core planning for About menu help, about, what's-new, crash-test, and performance-test commands, including versioned what's-new labels, diagnostic gating, and no-canvas performance-test blocking. pp_app_core now also owns an AboutMenuServices executor boundary, so App::init_menu_about dispatches through src/legacy_app_shell_services.* before legacy dialogs, platform crash hooks, and Canvas performance strokes continue. pp_platform_api now owns a headless PlatformServices interface for startup storage path preparation, clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render-capture frame hooks, render platform hint hooks, render debug callback hooks, external file display, file sharing, recording file cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, SonarPen availability/startup, VR mode start/stop, image/file/save-file pickers, and directory pickers. It also owns the SDK-free layout/asset file load policy helper used by LayoutManager, so XML layout hot-reload timestamp checks no longer live in the shared UI parser. Windows installs an injected WindowsPlatformServices implementation from src/platform_windows/windows_platform_services.* in pp_platform_windows; other platforms still route through the debt-tracked legacy fallback adapter now isolated in src/platform_legacy/legacy_platform_services.*, so behavior is preserved while their platform shell implementations are extracted. The central App header now forward-declares retained platform handles instead of including Objective-C, Android, or GLFW SDK headers; the full platform headers live in the legacy platform adapter until those handles move out of App during Phase 6. The retained Asset header is now Android-SDK-free, hides the Android asset handles behind opaque pointers and Asset::set_android_asset_manager, and keeps concrete Android asset-manager headers in asset.cpp/the Android entrypoint. This reduces legacy asset I/O header coupling while the static manager bridge and actual Android asset-reader implementation remain inside retained legacy asset I/O. Default canvas allocation size now dispatches through PlatformServices, so NodeCanvas and command-line conversion creation paths preserve the desktop 1536 and WebGL 512 defaults without carrying the old CANVAS_RES platform macro in canvas.h; DEBT-0057 tracks moving the retained WebGL policy branch out of the legacy fallback when the Web shell owns injected services. OpenGL runtime build-target classification now lives in pp_renderer_gl through CMake-owned compile definitions and opengl_runtime_for_current_build(), so app_shaders.cpp no longer decides desktop GL/GLES/WebGL capability policy with local platform branches. OpenGL extension enumeration now also lives in pp_renderer_gl through a dispatch-tested query_opengl_extensions helper; shader startup still logs and applies the resulting feature flags, but the GL extension query loop is no longer app-owned. OpenGL shader startup feature negotiation now also flows through pp_renderer_gl::query_opengl_capability_detection and detect_opengl_feature_state, so extension enumeration, desktop/GLES/WebGL capability policy, and renderer-neutral feature conversion are tested together behind the backend boundary. App::initShaders remains a legacy adapter that copies the backend-owned feature snapshot into retained ShaderManager static flags until ShaderManager itself becomes an OpenGL backend service. Prepared-file save/download handoff is now also part of the service contract, so iOS/Web export completion routes through PlatformServices after the app writes the temporary/exported payload. Prepared-file writable target selection now also dispatches through PlatformServices, preserving the existing iOS temporary background-write path and Web data-path synchronous write path while removing those platform branches from App::pick_file_save. PPBR and MP4 export dialogs now ask PlatformServices whether prepared-file writes are used, so those dialog flows no longer spell local __IOS__ || __WEB__ branches for mobile/Web export handoff. Layer and animation-frame export dialogs now also ask PlatformServices whether work-directory collection exports are used, then feed that into the pure pp_app_core plan_document_export_collection_target policy. This removes the local iOS branches from those dialogs while preserving iOS work_path/doc_layers and work_path/doc_frames targets in the legacy adapter until Apple platform services are injected. App-owned curl helpers for download, upload, and license checks now ask PlatformServices whether network TLS verification is disabled, removing the local Android branches from those helpers while preserving Android's existing TLS-verification bypass in the legacy adapter until a network/platform service owns cloud transport. The remaining legacy curl sites in Asset::open_url, LogRemote::net_init, and NodeDialogCloud::load_thumbs_thread now consume the shared pp_platform_api default TLS policy helper instead of spelling local Android branches; this keeps the current Android behavior aligned with PlatformServices while a dedicated network service is still pending. The Tools menu SonarPen entry now asks PlatformServices whether SonarPen is available and dispatches startup through the same service, preserving the current iOS Objective-C bridge in the legacy adapter while removing iOS branches from App::init_menu_tools and LegacyToolsMenuServices. App VR lifecycle start/stop now asks PlatformServices, preserving the current Windows OpenVR startup/shutdown bridge as the selected legacy fallback in WindowsPlatformServices while non-Windows fallback adapters keep the existing unsupported/no-op behavior. Canvas image export publishing and explicit persistent-storage flushes now dispatch through PlatformServices too, preserving iOS photo-library export publication and WebGL filesystem sync behavior in the legacy adapter while removing those direct platform calls from Canvas and brush preset storage. Document-browser search root selection now dispatches through PlatformServices, preserving the iOS Inbox root in the legacy adapter while removing the iOS-specific branch from App::dialog_browse. Save, New Document, and Browse dialog working-directory picker availability and display-path formatting now also dispatch through PlatformServices, removing desktop-only branches and Win32/macOS path formatting from those UI nodes while preserving Windows and macOS picker behavior in platform adapters. Native UI/window state saving now dispatches through PlatformServices, preserving Windows window placement persistence in WindowsPlatformServices and macOS UI state persistence in the legacy adapter while removing platform guards from App::ui_save. App::show_cursor, App::hide_cursor, App::showKeyboard, and App::hideKeyboard now dispatch through the active service without local platform guards; unsupported platforms rely on their service no-op behavior. The unsaved-document close prompt now requests native app/window close through PlatformServices, with Windows implemented by WindowsPlatformServices and macOS/Linux still handled by the legacy adapter until those platform shells are injected. The UI loop's per-frame platform hooks now dispatch through PlatformServices: Windows stylus timeout polling and FPS-title updates live in WindowsPlatformServices, while Linux FPS-title updates remain in the legacy adapter pending Phase 6 platform shell extraction. Canvas input tip-visibility and pressure-remap policies now also dispatch through PlatformServices, removing the local iOS and Windows branches from pen, line, and flood-fill canvas modes. The UI thread's platform attach/detach hooks now also dispatch through PlatformServices, preserving Android JNI attach/detach behavior in the legacy adapter while removing direct Android lifecycle calls from the main app loop. The app's render context acquire/release/present path now dispatches through PlatformServices as well. Windows owns WGL acquisition, default framebuffer rebinding, and swap in WindowsPlatformServices; Apple, Android, Linux, and WebGL behavior is preserved behind the legacy adapter until their platform shells are injected. Render-task default-target binding and visible main-target binding now dispatch through PlatformServices, preserving the existing iOS drawable bind in the legacy adapter while removing the iOS drawable branch from App::draw. Initial render platform hints now also dispatch through PlatformServices, preserving the previous Windows/macOS program-point-size and line-smoothing enablement while removing the Windows/macOS branch from App::init. The Windows service now delegates the actual OpenGL program-point-size and line-smooth enable sequence to a tested pp_renderer_gl dispatch helper, so the platform shell no longer owns those backend state tokens. The legacy non-Windows fallback now consumes the same helper for retained macOS render platform hints, keeping the debt-tracked adapter as a thin GL call bridge rather than a source of backend state policy. Windows OpenGL debug callback setup now dispatches through PlatformServices, moving Win32 console coloring and debug-break callback behavior into WindowsPlatformServices while keeping other platform adapters as no-ops; the debug-output/debug-output-synchronous state enable sequence is now a tested pp_renderer_gl backend helper consumed by the Windows service. Initial PanoPainter OpenGL depth/blend startup state is now represented and applied by tested pp_renderer_gl startup-state contracts; App::init delegates to the backend dispatch path instead of hard-coding the policy or operation order, and its retained callback endpoints now reuse the shared UI GL bridge instead of a local raw callback cluster. OpenGL runtime version/vendor/renderer/GLSL string queries now also use a tested pp_renderer_gl dispatch contract, leaving App::init to log the result while the backend owns the query set and order. The Windows entrypoint also uses that contract for early context logging and renderer-name window title construction before replacing the temporary WGL context. Retained runtime info and extension query callback endpoints now share legacy_gl_runtime_dispatch. The default app clear color and color-buffer clear operation now dispatch through pp_renderer_gl as well, moving another direct OpenGL operation out of App::clear while preserving the current gray clear behavior; the live callback endpoints now share legacy_ui_gl_dispatch. Main app UI viewport and scissor execution now dispatch through tested pp_renderer_gl viewport/scissor contracts, leaving App::draw and UI node clipping to provide rectangles while the backend owns scissor-state tokens and the live OpenGL call sequence. The retained viewport/scissor callback endpoints now share legacy_ui_gl_dispatch. VR UI framebuffer viewport and scissor-test setup now also consumes those pp_renderer_gl contracts, keeping desktop and VR UI rendering aligned while the desktop XR path moves from the retained OpenVR app path toward OpenXR; its retained callback endpoints now reuse the shared UI GL bridge. VR draw blend/depth state snapshots, transitions, restore, and depth-buffer clears, active texture unit switches, and fallback 2D texture unbinds now use generic tested pp_renderer_gl capability query/apply, clear, active-texture, and texture-bind dispatch contracts, reducing direct OpenGL execution in the retained VR app path without changing state restore behavior. The remaining retained VR draw adapter endpoints for these calls now share legacy_ui_gl_dispatch. The retained gl_state save/restore utility now snapshots and restores through tested pp_renderer_gl saved-state dispatch contracts, covering capability state, viewport, clear color, framebuffer/program bindings, active texture, 2D texture slots, samplers, and cube-map binding without changing the legacy utility's public fields. Legacy Texture2D allocation, binding, deletion, mipmap generation, region update, and framebuffer readback now execute through tested pp_renderer_gl texture dispatch contracts. This keeps the app API stable while moving another resource lifecycle path behind the renderer backend boundary. Legacy RTT resize/copy blits and byte/float framebuffer readbacks now execute through tested pp_renderer_gl framebuffer dispatch contracts with draw/read framebuffer binding restore handled by the backend helper. Legacy RTT::create/RTT::destroy now route render-target texture allocation, default texture parameter setup, optional depth renderbuffer allocation, framebuffer color/depth attachment, framebuffer status checks, binding restore, and resource deletion through tested pp_renderer_gl dispatch helpers. Legacy RTT also exposes an RGBA8 region-readback helper that uses the same backend framebuffer readback dispatch; canvas pick/history/snapshot and transform history paths now call that helper instead of binding an RTT and calling glReadPixels directly. Legacy RTT now also exposes an RGBA8 region-update helper that routes dirty rectangle texture writes through the tested pp_renderer_gl texture-update dispatch; canvas undo, layer restore, and flood-fill apply paths now call it instead of issuing direct glTexSubImage2D calls. Retained PBO recording readbacks now route pixel-buffer allocation, framebuffer readback, map, unmap, and deletion through tested pp_renderer_gl dispatch helpers; recording thread ownership, progress UI, and MP4 execution remain tracked by DEBT-0037. Legacy RTT::bindFramebuffer and RTT::unbindFramebuffer now use tested pp_renderer_gl draw/read framebuffer binding snapshot and restore contracts, moving render-target pass entry/exit state management behind the backend. Legacy RTT::clear, RTT::clear_mask, RTT::bindTexture, and RTT::unbindTexture now dispatch through pp_renderer_gl clear, color-write-mask restore, and texture-bind contracts, keeping render-target utility operations behind the backend boundary. The retained RTT clear and masked-clear callback endpoints now share legacy_ui_gl_dispatch. Windows RenderDoc frame capture hooks now also dispatch through PlatformServices, keeping capture integration in the platform service while leaving non-Windows adapters as no-ops. Startup data/work/recording/temp path preparation now dispatches through PlatformServices, with Windows creating the Documents/PanoPainter folder tree in WindowsPlatformServices and Apple/Linux/Web behavior preserved in the legacy adapter until platform shells are injected. Recording clear now asks PlatformServices whether the platform owns recorded file deletion and dispatches the cleanup through the service, preserving the current Apple recorded-frame cleanup while removing Apple-specific file cleanup guards from App::rec_clear. The UI loop now asks PlatformServices whether live shader/layout reloading should run, preserving the previous Windows/macOS reload behavior while removing the direct (_WIN32 || __OSX__) guard from App::ui_thread_main. Layout XML reload read/skip decisions now go through pp_platform_api as well, preserving desktop mtime-based reloads and non-desktop single-load behavior while removing the direct Windows/macOS guard from LayoutManager::load. App::stacktrace and App::crash_test now dispatch through PlatformServices, with Windows retaining the debug-break crash hook and the legacy adapter preserving Apple stacktrace/crash and Android crash-test behavior. pano_cli plan-cloud-upload exposes the app-core cloud upload decision used by the live cloud upload command for missing-canvas, new-document warning, publish prompt, and dirty-document save-before-upload states before legacy UI, canvas, and network execution continue. pano_cli plan-cloud-upload-all exposes the app-core bulk upload file-count, progress UI, and progress-total clamping decision used by the live upload-all command before legacy asset listing, OpenGL context guard, progress UI, and network upload execution continue. pano_cli plan-cloud-browse exposes the app-core cloud browse and selected download decisions used by the live cloud browse command before legacy dialog, network download, canvas project-open, layer UI, and action-history execution continue. Cloud upload, bulk upload, and browse/download live execution now flows through the CloudServices app-core boundary and src/legacy_cloud_services.*, keeping App::cloud_upload, App::cloud_upload_all, and App::cloud_browse as thin planning adapters while legacy save, progress UI, network, dialog, canvas-open, layer-refresh, and action-history work remains tracked under DEBT-0038. Cloud warning/publish/success prompts, bulk progress, and download-progress prompt creation now route through src/legacy_app_dialog_services.*, while cloud prompt/progress lifetime and network/document execution remain legacy. The app-owned curl upload/download/license helpers now consume the platform TLS verification policy through PlatformServices, and the retained Asset, LogRemote, and cloud browse-dialog curl sites consume the same default platform policy helper; retained cloud/network execution remains tracked under DEBT-0038. pp_app_core now also owns tested cloud transfer request and progress planning through plan_cloud_download_transfer, plan_cloud_upload_transfer, and plan_cloud_transfer_progress. Live App::download and App::upload consume those plans before retained CURL setup, including missing endpoint rejection, progress-callback enablement, TLS-verification policy, and zero/overrun progress guards; pano_cli plan-cloud-transfer exposes the same path for automation. Actual CURL ownership, upload form construction, response/error handling, progress UI, and downloaded-project execution remain tracked under DEBT-0038. pano_cli parse-layout exercises the XML layout path. Continue expanding document behavior toward legacy Canvas parity and then port OpenGL classes behind the renderer boundary. Brush preset-list add/select/move/remove/clear decisions now consume pp_app_core through NodePanelBrushPreset and pano_cli plan-brush-preset-list, so preset UI callbacks share tested headless index/selection planning. Live preset-list execution now also dispatches through BrushPresetListServices and the shared app-core executor before the retained legacy bridge mutates child nodes. The remaining direct NodePanelBrushPreset child-node execution, legacy Brush cloning, friend adapter, and preset save/reload behavior stay tracked under DEBT-0023. App::open_document now routes through the app-core document-open executor and src/legacy_document_open_services.*, preserving ABR/PPBR import prompts, unsaved-project discard prompts, project open, layer refresh, title updates, and history clearing while those live effects remain tracked under DEBT-0039. Accepted ABR/PPBR import prompts now delegate import execution to the app-core brush package import executor and src/legacy_brush_package_import_services.*, preserving detached legacy preset panel import threads while retained brush asset execution remains tracked under DEBT-0048. App::request_close, App::save_document, and App::continue_document_workflow_after_optional_save now route through app-core document-session executors and src/legacy_document_session_services.*, preserving close prompts, save dialogs, save-version routing, existing-project save execution, and dirty-workflow save-before-continue prompts while retained legacy UI/canvas behavior remains tracked under DEBT-0040. The retained document-session prompt boxes now consume a pure prompt catalog for close-unsaved, save-before-workflow, new-document overwrite, Save As overwrite, and save-error metadata; pano_cli plan-document-session-prompt exposes the same titles, messages, button captions, and cancel visibility for automation. Close-unsaved, save-before-workflow, new-document overwrite, and Save As overwrite prompt creation now also goes through src/legacy_app_dialog_services.* before the document-session bridge attaches its legacy callbacks. App::dialog_newdoc now routes accepted new-document plans through the app-core new-document executor and src/legacy_document_session_services.*, preserving target overwrite prompts, legacy canvas resize/layer setup, history clearing, title updates, dirty/new-document flag mutation, and keyboard/dialog cleanup while retained execution remains tracked under DEBT-0041. App::dialog_save and App::dialog_save_ver now route accepted Save As and Save Version plans through app-core document file/version save executors and src/legacy_document_session_services.*, preserving overwrite prompts, legacy Canvas::project_save, app document field updates, title updates, and keyboard/dialog cleanup while retained execution remains tracked under DEBT-0042. App::dialog_export, App::dialog_export_layers, App::dialog_export_anim_frames, App::dialog_export_depth, and App::dialog_export_cube_faces now route accepted file/stem/collection and named export work through app-core document export executors and src/legacy_document_export_services.*; layer/frame collection export target destination is now planned in pp_app_core and selected by PlatformServices, preserving existing platform messages, directory creation, picker-selected stems, Web prepared-file handoff, and legacy Canvas export calls while retained execution remains tracked under DEBT-0043. App::dialog_timelapse_export and App::dialog_export_mp4 now route picker-selected MP4 export paths through the app-core document video export executor and src/legacy_document_export_services.*, preserving mobile/Web suggested-name save callbacks, desktop worker-thread timelapse export, App::rec_export, animation Canvas::export_anim_mp4 dispatch, and existing success messages while retained execution remains tracked under DEBT-0044. App::dialog_ppbr_export now routes picker-selected PPBR brush package exports through the app-core brush package export executor and src/legacy_brush_package_export_services.*, preserving dialog metadata collection, legacy Image header ownership, desktop worker-thread export, mobile/Web save completion, and NodePanelBrushPreset::export_ppbr while export success-dialog metadata now comes from pp_app_core and is exposed by pano_cli plan-brush-package-export. Retained execution remains tracked under DEBT-0047. PPBR package header validation and export target/data-directory planning now live in pp_assets::brush_package and are exercised by pp_assets_brush_package_tests plus pano_cli plan-brush-package-export. The macOS-specific PPBR preview data-directory override now dispatches through PlatformServices, so NodePanelBrushPreset::export_ppbr no longer spells a local __OSX__ branch while the actual PPBR serialization path remains legacy-owned. The live PPBR import/export path consumes those helpers, while legacy Serializer/Image payload reading, stroke preview generation, preset storage, and the historical permissive version check remain tracked under DEBT-0047 and DEBT-0049. ABR and PPBR import image target planning for brush tips and patterns also now uses pp_assets::brush_package, so the legacy preset panel no longer owns the data/brushes, data/brushes/thumbs, data/patterns, and data/patterns/thumbs path construction rules. Actual ABR/PPBR parsing, duplicate policy, preset creation, save/reload, and progress/UI refresh remain legacy-owned under DEBT-0048.

Implementation tasks:

  • Extract components in this order:
    1. pp_foundation
    2. pp_assets
    3. pp_paint
    4. pp_document
    5. pp_renderer_api
    6. pp_renderer_gl
    7. pp_paint_renderer
    8. pp_ui_core
    9. pp_panopainter_ui
    10. pp_platform_api
    11. pp_platform_*
    12. panopainter_app
  • Remove renderer/platform dependencies from pure headers first, especially:
    • Brush
    • document/layer model
    • serializer
    • UI core headers
  • Make UI lifetime safety a first-class extraction criterion:
    • define a pp_ui_core ownership model for the retained Node tree
    • replace raw callback targets with scoped connections or checked handles
    • move panel/dialog side effects toward app-core command dispatch
    • test destroy-during-callback, capture release, popup close, and layout reload mutation cases before replacing retained UI nodes wholesale
  • Keep facade shims where needed, but debt-track every shim.
  • Avoid large behavioral rewrites during extraction.
  • Each extracted component gets a focused test suite before moving to the next.

Gate:

  • Old app still launches.
  • Component tests pass after every extraction.
  • No undocumented stubs or shortcuts.

Phase 5: Renderer Boundary And OpenGL Parity

Goal: make OpenGL an implementation detail and establish parity tests before adding new backends.

Status: started. pp_renderer_api exists as a headless renderer-neutral target with renderer feature flags, explicit texture usage flags, texture descriptor, byte-size, viewport, mesh, readback bounds, command context, render device, shader program descriptor, mesh, render target, readback byte-size helpers, texture mip-level validation, resource debug-label validation, texture-upload/readback command validation, mipmap-generation command validation, texture-state transition validation, frame-capture byte-size helpers, readback/copy/frame-capture/blit descriptor validation, frame-capture command validation, render-target blit validation, texture-slot binding validation, blend-state validation, scissor-state validation, depth-state validation, trace marker/scope validation, sampler-state validation, texture/mesh/shader resource-label validation, recording-device reuse/reset validation, and the canonical PanoPainter shader catalog now consumed by the legacy OpenGL app initialization path. pp_renderer_gl now exists as the first OpenGL backend library and owns pure OpenGL capability detection for framebuffer fetch, map-buffer alignment, and float texture support. It also owns the OpenGL texture upload-type mapping used by legacy Texture2D and RTT creation, RGBA pixel-format mapping used by RTT texture allocation, plus image channel-count to texture format mapping for Texture2D image uploads and framebuffer status naming for RTT and Texture2D diagnostics. It also owns renderer API texture-format to OpenGL internal/pixel/component token mapping, including depth-stencil formats, for future backend texture objects. Texture2D 2D texture binding, upload, mipmap generation, framebuffer readback setup, and update component-type tokens now delegate to pp_renderer_gl; retained Texture2D, TextureCube, and RTT texture allocation, bind, parameter, update, mipmap, and delete dispatch now share the retained legacy_gl_texture_dispatch raw callback bridge. TextureCube cube-map binding, allocation face targets, RGBA allocation format, and unsigned-byte component type also delegate to pp_renderer_gl. RGBA8/RGBA32F readback formats, checked byte-count math, PBO pixel-buffer target/usage/access tokens, and PBO allocation/readback/map/unmap/delete dispatch sequences used by retained recording readbacks now live in pp_renderer_gl; retained PBO allocation, framebuffer readback, map, unmap, and delete dispatch now share the retained legacy_gl_pixel_buffer_dispatch raw callback bridge. Retained Sampler create, parameter, border-color, bind, and unbind dispatch now share the retained legacy_gl_sampler_dispatch raw callback bridge. The framebuffer blit color mask and linear/nearest filter tokens used by RTT::resize and RTT::copy, renderer API blit-filter to OpenGL token mapping, plus the default render-target texture parameters and parameter dispatch, texture/renderbuffer targets, depth format, framebuffer targets, binding queries, attachment points, render-target framebuffer allocation/delete, binding restore, and completion status used by RTT::create/RTT::destroy and framebuffer bind/restore paths, also live in pp_renderer_gl; retained Texture2D readback and RTT framebuffer allocation, deletion, bind/restore, blit, readback, and PBO readback dispatch now share the retained legacy_gl_framebuffer_dispatch raw callback bridge. Depth renderbuffer allocation/storage/delete and framebuffer depth attach/detach sequences used by retained RTT and canvas object-drawing helpers now execute through tested pp_renderer_gl dispatch contracts and share the retained legacy_gl_renderbuffer_dispatch raw callback bridge. 2D framebuffer-to-texture copies used by canvas, transform, layer-conversion, panorama UI, brush preview, and CanvasLayer cube-face generation paths now route through a tested pp_renderer_gl copy dispatch via the retained target-aware framebuffer-copy utility bridge. The copy bridge remains retained until renderer services own cube and 2D framebuffer copy commands under DEBT-0036. RTT render-target clear, masked color clear with color-write-mask restore, and texture bind/unbind dispatch now execute through pp_renderer_gl; renderer API render-pass color/depth/stencil clear-mask and clear-value mapping, and color-write-mask query tokens also live there. RTT no longer spells GL enum names directly. Renderer API primitive-topology to OpenGL draw-mode mapping, mesh index-type and primitive-mode decisions used by legacy Shape drawing, plus Shape buffer targets, static upload usage, and vertex attribute component/normalization tokens, also live in pp_renderer_gl. Retained Shape, TextMesh, and NodeColorWheel mesh buffer/VAO creation, dynamic vertex/index uploads, fill/stroke/text draws, and buffer/VAO deletion now execute through tested pp_renderer_gl dispatch contracts via the shared retained legacy_gl_mesh_dispatch raw callback bridge. The PanoPainter cube-face to OpenGL texture-target mapping used by TextureCube also lives in pp_renderer_gl. The legacy app delegates extension, upload-format, framebuffer diagnostic, framebuffer blit, render-target setup, clear-state, 2D/cube texture setup, mesh draw-mode, and cube-face texture-target interpretation to that backend library. Sampler wrap, min/mag filter, and desktop border-color parameter mapping for legacy Sampler also lives in pp_renderer_gl. Renderer API sampler filter/address-mode to OpenGL token mapping, including mirrored-repeat, and aggregate renderer API sampler-state to OpenGL min/mag/wrap mapping are also tested there. The PanoPainter shader attribute binding catalog, shader stage tokens, compile/link status queries, active-uniform count query, and matrix-uniform transpose token also live in pp_renderer_gl and are consumed by legacy Shader creation; retained shader source compilation/deletion, program attach/link/use/delete, attribute rebinding and location lookup, active-uniform enumeration, uniform-location discovery, and uniform writes now share the retained legacy_gl_shader_dispatch raw callback bridge. Renderer API blend factor/op to OpenGL token mapping also lives in pp_renderer_gl, with explicit support flags so GL_ZERO remains distinguishable from unsupported enum values. Aggregate renderer API blend-state to OpenGL enable/factor/equation/color-mask mapping, depth compare-op to OpenGL depth-function mapping, and aggregate renderer API depth-state to OpenGL enable/write/compare mapping also live in pp_renderer_gl. Shader uniform hashing, catalog validation, active-uniform mapping, and the legacy uniform uniqueness check now delegate to pp_renderer_gl as well. Shader no longer spells GL enum names directly. App OpenGL initialization debug severity, debug output, GL info string, renderer API viewport/scissor rect conversion, default depth/program-point/line-smooth state, blend factor/equation, and UI render-target RGBA8 format tokens are now also cataloged and tested in pp_renderer_gl; the legacy convert command now applies its depth, program-point-size, source-alpha blend, and add-equation startup policy through a tested backend dispatch contract, and the resize path consumes the same backend-owned mapping. App clear color-buffer masks, default framebuffer binding, scissor state, and sampler filter/wrap tokens now share that backend mapping too. OpenGL extension enumeration query tokens used before runtime capability detection also live in pp_renderer_gl. Legacy font atlas texture formats, text mesh buffer targets, attribute component/normalization, draw primitive/index type, upload usage, and active texture unit selection also delegate to pp_renderer_gl; text mesh buffer/VAO creation, deferred index and vertex uploads, indexed draw calls, and text draw texture-unit activation now execute through the same tested dispatch contracts used by Shape through legacy_gl_mesh_dispatch. Canvas undo/redo dirty-region texture updates and readbacks now also execute through retained RTT helpers backed by pp_renderer_gl, including 2D texture target, dirty-region offsets, RGBA pixel format, and unsigned-byte component type mapping. Canvas stroke commit, thumbnail generation, and object-draw history paths now query saved blend state through the same tested capability state dispatch before restoring it. NodeViewport preview rendering now also delegates viewport query, clear-color query, color-buffer clear mask, viewport execution, color clear, clear-color restore, and blend-state execution through the shared legacy_ui_gl_dispatch adapter and pp_renderer_gl mappings. NodeImageTexture preview drawing now delegates its fallback 2D texture bind and blend-state execution through the shared UI GL adapter. NodeImage drawing and remote-image texture creation now delegate mipmapped sampler filters, blend-state execution, and RGBA8/RGBA texture format mapping to pp_renderer_gl. NodeColorWheel triangle-buffer setup and draw-state handling now delegate array-buffer, static-upload, vertex-attribute, primitive-mode, and blend-state execution to pp_renderer_gl. Simple UI text, text-input, border, scroll, and animation timeline draw paths now also execute blend-state changes through the shared UI GL adapter. Canvas layer cube/equirect generation, clear, restore, and snapshot paths now also delegate cube/2D texture targets, active texture units, blend/clear state, viewport execution, cube texture binding, color-buffer clears, clear-color query/restore, and RGBA8 read/write pixel mapping to pp_renderer_gl. Its active-texture, cube-texture binding, viewport, blend capability, clear-color, and color-buffer clear adapter endpoints now share legacy_ui_gl_dispatch; the cube-face framebuffer-to-texture copy now uses the shared retained target-aware utility bridge and remains tracked under DEBT-0036 until a renderer-owned cube copy command replaces it. NodePanelGrid heightmap preview and lightmap baking now delegate texture readback formats, sampler filters, depth/blend state, depth clears, viewport queries, color-mask booleans, active texture units, and float render-target formats to pp_renderer_gl, and its CPU lightmap row dispatch now uses the shared legacy parallel_for helper rather than platform-specific worker APIs. Its live heightmap draw and bake paths now execute depth/blend state changes, depth clears, color-write-mask toggles, active texture selection, and bake viewport changes through tested pp_renderer_gl dispatch adapters. Its desktop texture-resize path now reuses Texture2D::get_image(), so grid texture readback also goes through the tested framebuffer-backed texture readback dispatch instead of direct glGetTexImage. Grid depth-state snapshots and sun overlay viewport queries now also use tested backend query dispatch instead of direct state reads. Legacy util.cpp OpenGL error naming, framebuffer-to-texture copy helper, and gl_state save/restore now delegate error codes, state queries, framebuffer targets, texture binding targets, active texture units, shader program use, and sampler binding to pp_renderer_gl through the shared retained bridge headers instead of owning local raw OpenGL callbacks. NodeStrokePreview brush preview rendering now delegates depth/scissor/blend state, tested viewport/clear-color query dispatch, clear-color restore, active texture unit execution, fallback 2D texture unbinds, 2D texture targets, copy targets, sampler filters/wraps, and destination-feedback copy/fetch decisions to pp_renderer_gl and pp_paint_renderer. Its live stroke-mixer and brush-preview viewport, scissor, and depth/blend state changes now also execute through tested pp_renderer_gl dispatch via the shared legacy_ui_gl_dispatch bridge. Retained Canvas stroke/thumbnail/object/export paths and NodeCanvas panorama rendering use the same tested active-texture dispatch for their texture-unit switches, and their live viewport, scissor, and generic depth/blend/scissor capability changes and NodeCanvas capability-state snapshots now route through the same backend dispatch contracts. Retained Canvas stroke draw/commit, thumbnail generation, object drawing, and LayerFrame::clear saved viewport/clear-color query plus clear-color restore paths also use tested pp_renderer_gl dispatch helpers. The retained Canvas active-texture, fallback texture unbind, viewport/scissor execution, viewport and clear-color query, clear-color restore, and capability query/apply adapter endpoints now share legacy_ui_gl_dispatch; Canvas and RTT depth renderbuffer allocation/attachment/delete now share legacy_gl_renderbuffer_dispatch while resource lifetime ownership remains retained under DEBT-0036. NodeCanvas saved viewport/clear-color query, density target color clear, and clear-color restore paths use the same helpers. NodeCanvas and NodeStrokePreview now share that retained UI GL dispatch bridge for active-texture, fallback texture unbind, viewport/scissor, clear-color, color-buffer clear, and capability query/apply adapter endpoints. Retained CanvasMode overlay, mask, transform, and canvas-tip pick paths now also use the same bridge for active-texture, capability query/apply, viewport, read-framebuffer query, and RGBA8 pixel readback adapter endpoints while their mode logic remains in the legacy UI implementation. NodePanelGrid heightmap draw and bake setup now also share that bridge for active-texture selection, depth/blend capability query/apply, viewport query/execution, depth clears, and color-write-mask adapter endpoints. Desktop HMD eye rendering now routes eye framebuffer viewport changes through the tested pp_renderer_gl viewport dispatch while platform VR SDK bridges remain isolated for later platform-shell extraction. Legacy Texture2D, TextureManager, Sampler, and RTT public headers no longer expose raw OpenGL enum defaults; default texture formats, sampler filters/wraps, and render-target formats are resolved through backend-owned overloads. The Windows entrypoint now delegates generic OpenGL error-code/info-string tokens, runtime string query ordering, and WGL core-context/pixel-format attribute catalogs to pp_renderer_gl. The headless OpenGL command planner now consumes pp_renderer_api recorded commands and maps render-pass clear masks/values, viewport/scissor state, blend/depth/sampler state, texture formats, primitive modes, draw counts, and blit filters into GL-facing planned command data with explicit unsupported-token rejection before a runtime GL context is needed. It also plans whole recorded command streams, preserving per-command planned data while counting render passes, draws, shader binds, shader uniforms, texture/sampler binds, texture uploads, mipmap generation, texture transitions, texture copies, texture readbacks, frame captures, passthrough commands, trace commands, unsupported commands, and render-pass ordering errors such as state changes outside a pass, nested passes, texture I/O or blits inside a pass, and unclosed passes. It also validates executable command dependencies, including shader-before-uniform and shader-plus-mesh before draw within each render pass, and rejects invalid texture/sampler bind slots in malformed recorded streams. The renderer-neutral API now also plans complex paint feedback strategies for future stroke/layer compositing work: framebuffer-fetch-capable backends can read destination color directly, while other backends must use ping-pong render targets backed by texture copy or render-target blit support. This is exposed through pano_cli plan-paint-feedback and tracked by DEBT-0036 until the live paint renderer consumes the plan. pp_paint_renderer now consumes that lower-level feedback planner through a stroke composite plan that decides whether a stroke/layer blend can use fixed-function blending or needs framebuffer-fetch/ping-pong destination feedback. pano_cli plan-stroke-composite exposes the same decision for automation, including layer blend, stroke blend, dual-brush, and pattern-blend inputs. Live Canvas::draw_merge now uses this planner for its existing shader-blend gate for layer and primary-brush blend modes while preserving the legacy trigger policy; actual canvas stroke execution, dual-brush feedback, and pattern feedback are still legacy OpenGL and remain tracked by DEBT-0036 until the app calls through renderer services for the whole compositing path. pp_paint_renderer::plan_canvas_blend_gate now also owns the compatibility mapping from persisted layer and brush blend indices to that planner, including fallback behavior for unknown nonzero indices. Both Canvas::draw_merge and NodeCanvas panorama rendering consume that shared gate, so the live app no longer has duplicate local blend-trigger logic or duplicate destination-copy versus framebuffer-fetch decisions in those paths. The OpenGL shader initialization path now stores a renderer-neutral RenderDeviceFeatures snapshot converted by pp_renderer_gl, and those live canvas gates consume that snapshot instead of rebuilding feature flags from individual ShaderManager extension booleans. RenderDeviceFeatures now carries the float32-linear-filtering bit as well, so the canvas stroke texture format decision, renderer diagnostics, and grid lightmap/bake target selection all consume the renderer-neutral feature snapshot instead of reading ShaderManager::ext_* flags directly. The retained extension booleans are now limited to the shader-manager compatibility adapter and legacy logging. pp_paint_renderer::plan_canvas_stroke_feedback now models the current stroke shader's required destination feedback without changing the legacy shader math. Live Canvas::stroke_draw consumes that plan for main-brush, dual-brush, and stroke-pad destination-copy versus framebuffer-fetch decisions. Thumbnail layer blending now consumes the same canvas destination-feedback decision for its legacy TextureBlend path. NodeStrokePreview uses the same destination feedback plan for its live brush-preview copy/fetch decision. The full thumbnail and brush-preview compositing execution remains legacy OpenGL until a fuller live paint-renderer boundary can take over. The existing renderer classes are not yet fully behind the renderer interfaces.

Implementation tasks:

  • Introduce renderer interfaces:
    • IRenderDevice
    • ITexture2D
    • IRenderTarget
    • IShaderProgram
    • IMesh
    • ICommandContext
    • IReadbackBuffer
    • IRenderTrace
  • Port current renderer classes behind OpenGL backend types:
    • RTT
    • Texture2D
    • Sampler
    • ShaderManager
    • Shape
  • Keep OpenGL runtime capability decisions in pp_renderer_gl with headless tests before moving GL object lifetimes behind backend types.
  • Preserve current shader behavior and asset paths.
  • Add deterministic GPU tests:
    • clear
    • blit
    • texture upload/download
    • stroke composite
    • erase
    • layer blend
    • equirect export
    • readback bounds
  • Add CPU reference tests for final RGBA and stroke-alpha blend modes.
  • Compare GPU output to golden/reference data with explicit tolerances.

Gate:

  • OpenGL readbacks match golden data on Windows and Linux.
  • Mobile/WebGL compile gates remain green.

Phase 6: Platform Alignment

Goal: every supported platform consumes the same component targets.

Status: started. Root CMake configure presets now have matching build presets for Windows VS 2026/default, Windows clang-cl ASan, Linux clang, Android standard x64/arm64, Android Quest arm64, Android Focus/Wave arm64, Emscripten/WebGL, macOS, iOS device, and iOS simulator. platform-build automation now builds the current headless component matrix, including pp_platform_api, pp_app_core, platform API tests, brush-package tests, and the current app-core startup/frame/shutdown/file/document/brush/canvas/history/grid/toolbar/ tools/about/preferences/status automation tests. The PowerShell wrapper also normalizes comma-separated -Presets and -Targets values for reliable machine-driven partial matrix checks. panopainter_platform_build_target_matrix_self_test keeps the PowerShell and shell wrapper defaults aligned with every current CMake test executable plus required component targets, and now verifies the default Android preset set covers standard arm64/x64, Quest arm64, and Focus/Wave arm64. The shell wrapper now mirrors the PowerShell wrapper's multi-preset behavior and reports one structured result array. Root CMake now also exposes non-default platform validation targets: panopainter_platform_build_headless, panopainter_platform_build_android_assets, panopainter_platform_build_vcpkg_ui_core, and panopainter_platform_build_apple_remote; the platform-build self-test guards those target names and the wrapper matrix now includes pp_app_core_app_dialog_tests with the rest of the CMake test executables. package-smoke now emits a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output, with blocked prerequisites tied to DEBT-0011. It also has a readiness-only mode for cheap package blocker inventory without building an app artifact, and panopainter_package_smoke_readiness_self_test keeps the PowerShell and shell readiness matrices aligned, including retained Linux/WebGL CMake baseline metadata. The PowerShell wrapper can also run the retained Android native package checks through -AndroidNativeChecks, reporting the standard native-lib build plus Quest/Focus configure checks next to the APK blocker matrix. Root CMake now exposes non-default package validation targets for package readiness, Windows app artifact smoke, and retained Android native checks: panopainter_package_readiness, panopainter_windows_app_package_smoke, panopainter_android_standard_native_package, panopainter_android_vr_native_package_configure, and panopainter_android_native_package_smoke, plus the retained Linux/WebGL blocker target panopainter_linux_webgl_package_readiness. App/package entrypoints still need to consume shared targets and remain covered by debt until package validation is migrated from legacy package projects to root CMake. Retained Linux and WebGL app CMake entrypoints now match the interim platform baseline used by Android package paths: CMake 3.10 plus target-level cxx_std_23, with panopainter_retained_platform_cmake_self_test guarding against regressions while those entrypoints wait behind root CMake package/app target migration. Apple compile validation now runs on the local Mac mini SSH host panopainter-mac through scripts/automation/apple-remote-build.ps1. The host uses Homebrew CMake/Ninja/Git plus full Xcode via DEVELOPER_DIR, pulls the modernization branch from Gitea, initializes the source submodules needed by the current headless matrix, and builds macos, ios-simulator, and ios-device. The iOS device compile gate assigns generated bundle identifiers and disables code signing for test/tool executables under DEBT-0059; signed Apple app bundle and package-smoke migration remains open under DEBT-0011. pp_platform_api now also owns a tested platform-family policy catalog used by both WindowsPlatformServices and the retained non-Windows fallback for exported-image publishing, persistent-storage flushing, document browse roots, working-directory picker availability, prepared-file target planning, work-directory collection export policy, PPBR data-directory override policy, SonarPen availability, native UI/window state saving, live asset reload policy, layout XML file mtime reload policy, recording cleanup policy, default canvas resolution, and canvas tip visibility. Platform SDK calls and filesystem probes remain in the platform shells or thin runtime wrappers while those decisions are headless-testable. The retained Android standard/Quest/Focus package CMake files now use CMake 3.10, request C++23 through target compile features, include the extracted modern component/service source set that the legacy package still links monolithically, and share a generated nanort compatibility overlay from android/cmake/PanoPainterAndroidLegacyCompat.cmake instead of dirtying the vendor submodule. The standard package native-lib arm64 target now compiles and links with the current NDK; Quest and Focus configure with the aligned Yoga source list and their SDK imported-library paths. Android automation now uses sdkmanager to compare the newest available SDK-managed NDK and CMake packages, installs newer or missing packages when needed, and selects those versions before configuring Android presets or retained package paths; on the current Windows host both NDK 30.0.14904198 and CMake 4.1.2 report already-latest-available.

Implementation tasks:

  • Convert these builds to shared component targets:
    • Windows desktop
    • Windows AppX
    • macOS
    • iOS
    • Android standard
    • Android Quest
    • Android Focus/Wave
    • Linux
    • WebGL/Emscripten
  • Keep platform entrypoints thin:
    • window lifecycle
    • input dispatch
    • clipboard
    • file picker/share
    • GL context creation
    • VR SDK bridge
    • packaging only
  • Add or refine CMake toolchain/preset support for:
    • Android NDK ABIs
    • iOS device
    • iOS simulator
    • macOS
    • Emscripten
  • Keep SDK-only imported libraries documented until vcpkg triplets are proven.

Gate:

  • Every platform has a named configure/build command.
  • Missing local prerequisites are documented.
  • Each platform has at least compile or package validation.

Phase 7: Hardening, Coverage, And Breaking-Point Tests

Goal: tests should try to break components, not only confirm current happy paths.

Implementation tasks:

  • Add property/fuzz tests for:
    • binary streams
    • serializers
    • PPI parsing
    • ABR parsing
    • layout XML parsing
    • image metadata parsing
    • brush parameter extremes
    • layer/frame operations
    • undo/redo invariants
  • Add stress tests for:
    • thousands of stroke samples
    • extreme resolutions guarded by memory limits
    • rapid layer/frame edits
    • corrupt assets
    • cancellation during export
    • concurrent render/UI task scheduling
  • Add coverage for headless libraries on Clang/GCC.
  • Require coverage reports for changed components first; do not set a global threshold until the baseline is meaningful.
  • Add tracing spans around:
    • project load/save
    • render passes
    • stroke commit
    • readback
    • export
    • UI layout
    • platform I/O
  • Logs must include component, thread, frame/stroke id, and timing.

Gate:

  • No shortcut remains undocumented.
  • Every component has unit tests and at least one failure or edge test.

Phase 8: Future Backend Readiness

Goal: prepare Vulkan and Metal without destabilizing the OpenGL parity path.

Implementation tasks:

  • Create non-default targets only after OpenGL backend parity:
    • pp_renderer_vulkan_lab
    • pp_renderer_metal_lab
  • Use D:\Dev\vkpaint as reference material for Vulkan painting experiments, not as direct production code.
  • Before integration, prove:
    • ping-pong compositing path
    • input-attachment/subpass path where applicable
    • feedback-loop or framebuffer-fetch-style path where supported
    • synchronization and layout correctness under validation layers
  • Keep WebGPU as an optional future portability backend, not the core renderer contract.

Gate:

  • Vulkan/Metal lab targets are opt-in.
  • OpenGL production backend remains stable.

Test Matrix

Preset/Label Purpose Requires
desktop-fast Pure component unit tests No GPU/window
desktop-gpu OpenGL backend golden/readback tests GPU/GL context
fuzz Deterministic parser/serializer edge corpus; future libFuzzer entrypoint No GPU/window today
stress Large and adversarial scenarios Longer runtime
platform-build Configure/build each supported platform Local toolchains
package-smoke AppX/APK/Apple/WebGL package smoke Platform SDKs

Acceptance for each phase:

  • Previous phase tests still pass.
  • New component has its own tests.
  • No undocumented stubs.
  • No skipped platform without a debt entry.
  • Automation command is recorded in this roadmap or linked docs.

Verified Commands

Last verified on 2026-06-02:

cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
ctest --preset desktop-fast --build-config Debug
ctest --preset fuzz --build-config Debug
ctest --preset stress --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
powershell -ExecutionPolicy Bypass -File scripts\automation\analyze.ps1 -Preset windows-msvc-default -NoApp
set VCPKG_ROOT=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg
cmake --preset windows-msvc-vcpkg-headless
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets windows-msvc-vcpkg-headless
ctest --preset desktop-fast-vcpkg --build-config Debug
cmake --preset android-arm64
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly
cmake --fresh --preset windows-clangcl-asan
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device

Results:

  • pp_foundation_binary_stream_tests passed.
  • pp_foundation_event_tests passed.
  • pp_foundation_log_tests passed.
  • pp_foundation_parse_tests passed.
  • pp_foundation_task_queue_tests passed.
  • pp_foundation_trace_tests passed.
  • pp_assets_image_format_tests passed.
  • pp_assets_image_metadata_tests passed.
  • pp_assets_image_pixels_tests passed, including RGBA8 PNG decode and corrupt payload rejection.
  • pp_assets_ppi_header_tests passed, including PPI thumbnail/body layout, body summary validation, layer/frame indexing, explicit per-layer metadata and per-layer frame duration writing, dirty-face PNG payload metadata validation, targeted layer/frame dirty-face writing, and decoded dirty-face payload coverage.
  • pp_assets_settings_document_tests passed.
  • pp_paint_brush_tests passed.
  • pp_paint_blend_tests passed.
  • pp_paint_stroke_tests passed.
  • pp_paint_stroke_script_tests passed.
  • pp_document_tests passed, including snapshot construction, alpha-lock metadata, per-layer frame metadata, frame move, duration, face-pixel payload storage/replacement/rejection, snapshot-embedded face-payload rejection, and history invariants.
  • pp_document_ppi_import_tests passed, including decoded PPI dirty-face payload attachment to pp_document layer/frame storage and out-of-range payload rejection.
  • pp_document_ppi_export_tests passed, including pure document metadata, per-layer frame duration, and PNG-encoded face-payload export to PPI bytes, plus malformed payload rejection at the export boundary.
  • pp_renderer_api_tests passed, including shader descriptor validation, PanoPainter shader catalog validation, explicit texture usage validation, texture mip-level validation, resource debug-label validation, readback byte-size and command-order validation, texture-upload byte-count validation, mipmap-generation command validation, trace marker/scope validation, frame-capture byte-size and command-order validation, render-target blit validation, texture-slot binding validation, blend-state validation, scissor-state validation, render-pass color/depth/stencil clear validation, shader-uniform write validation, draw descriptor/range validation, backend-neutral resource factory validation, texture-copy validation, recording render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ upload/texture-copy/readback/frame-capture/blit command capture, draw mesh-input capture, explicit draw-range capture, and invalid catalog rejection. The same suite now covers complex paint feedback planning for framebuffer-fetch backends, ping-pong texture-copy/blit fallbacks, simple no-feedback blends, invalid render-target usage, unsupported backends, and depth-target rejection.
  • pp_paint_renderer_compositor_tests passed, including pure pp_document face and six-face frame compositing over per-layer dirty face payloads plus renderer-neutral six-face texture upload through the recording backend. The suite now covers fixed-function stroke composite planning, framebuffer-fetch planning, ping-pong texture-copy/blit fallback planning, dual/pattern blend feedback detection, invalid blend mode rejection, unsupported backend rejection, and invalid render-target rejection.
  • pp_ui_core_color_tests passed.
  • pp_ui_core_layout_value_tests passed.
  • pp_ui_core_layout_xml_tests passed.
  • pano_cli_create_document_smoke passed.
  • pano_cli_create_animation_document_smoke passed and reports animation duration JSON.
  • pano_cli_simulate_document_edits_smoke passed and reports pure pp_document layer metadata, frame order, active indices, and face-payload state as JSON.
  • pano_cli_simulate_document_history_smoke passed and reports real pp_document::DocumentHistory apply/undo/redo state as JSON.
  • pano_cli_simulate_document_export_smoke passed and reports pure pp_document export to PPI bytes, asset-level decode, and document reimport round-trip state as JSON.
  • pano_cli_simulate_document_render_smoke passed and reports pure pp_document to pp_paint_renderer six-face frame compositing and renderer texture-upload command summaries plus OpenGL command-planner support counts as JSON.
  • pano_cli_simulate_image_import_smoke passed and reports embedded PNG decode plus pp_document face-payload attachment state as JSON.
  • pano_cli_inspect_image_rejects_unsupported passed as an expected failure test.
  • pano_cli_inspect_png_metadata_smoke passed and reports PNG metadata JSON for the tiny IHDR fixture.
  • pano_cli_import_image_rejects_truncated_png passed as an expected failure test, proving the file-driven image import command rejects a metadata-valid but undecodable PNG payload.
  • pano_cli_inspect_project_layout_smoke passed and reports PPI thumbnail/body byte layout, body summary, layer/frame descriptors, and dirty-face PNG payload metadata JSON.
  • pano_cli_load_project_metadata_smoke passed and reports a pp_document projection with per-layer frame counts, durations, and zero loaded face payloads for the minimal PPI fixture.
  • pano_cli_save_project_roundtrip_smoke passed and proves the metadata-only pp_assets PPI writer can save a generated multi-frame PPI and reload it through pano_cli load-project.
  • pano_cli_save_project_payload_roundtrip_smoke passed and proves the pp_assets PPI writer can save a compressed RGBA PNG dirty-face payload to an explicit layer/frame slot, inspect the serialized descriptor, and reload it as decoded pp_document face-pixel data.
  • pano_cli_save_document_project_roundtrip_smoke passed and proves a pure pp_document export can be written to a PPI file, inspected for layer/frame dirty-face descriptors, and loaded back through the PPI import path.
  • pano_cli_apply_stroke_script_roundtrip_smoke passed and proves a checked-in stroke script can be parsed, sampled, applied to a pure pp_document face payload, written to PPI, inspected for the expected dirty-face box, and loaded back as decoded document pixel data.
  • pano_cli_apply_stroke_script_rejects_tiny_canvas passed as an expected failure test, proving the stroke-script document command rejects dimensions outside its bounded automation range before payload allocation.
  • pano_cli_parse_layout_smoke passed.
  • pano_cli_simulate_stroke_smoke passed and reports deterministic stroke sample counts/distances.
  • pano_cli_simulate_stroke_script_smoke passed and reports deterministic aggregate stroke-script counts/distances.
  • pp_app_core_document_cloud_tests passed, covering cloud upload no-canvas, new-document warning, clean publish prompt, and dirty save-before-upload decisions, plus cloud browse no-canvas/show-browser and selected-download decisions, plus bulk upload progress visibility, zero-file, and clamped progress-total decisions.
  • pano_cli_plan_cloud_upload_clean_smoke, pano_cli_plan_cloud_upload_unsaved_smoke, pano_cli_plan_cloud_upload_new_document_smoke, and pano_cli_plan_cloud_upload_no_canvas_smoke passed and expose those app-core cloud upload decisions as JSON.
  • pano_cli_plan_cloud_upload_all_progress_smoke and pano_cli_plan_cloud_upload_all_headless_smoke passed and expose app-core bulk upload progress decisions as JSON.
  • pano_cli_plan_cloud_browse_waiting_smoke, pano_cli_plan_cloud_browse_selected_smoke, and pano_cli_plan_cloud_browse_no_canvas_smoke passed and expose app-core cloud browse/download-selection decisions as JSON.
  • pp_app_core_document_cloud_tests now also covers cloud transfer request validation, progress-callback enablement, TLS-verification policy, and zero/negative/overrun transfer-progress guards.
  • pano_cli_plan_cloud_transfer_download_smoke, pano_cli_plan_cloud_transfer_upload_smoke, pano_cli_plan_cloud_transfer_rejects_missing_destination, and pano_cli_plan_cloud_transfer_zero_total_smoke passed and expose the app-core cloud transfer path as JSON.
  • PanoPainter, pp_app_core_document_cloud_tests, and pano_cli built after live App::download and App::upload started consuming the transfer plans before retained CURL setup.
  • Android arm64 headless pp_app_core, pano_cli, and pp_app_core_document_cloud_tests built after the cloud transfer slice.
  • PanoPainter, pp_app_core_document_cloud_tests, and pano_cli built after cloud upload warning/publish/success prompts, bulk upload progress dialogs, and download-progress messages moved to tested pp_app_core metadata plans.
  • Focused cloud metadata CTest coverage passed for pp_app_core_document_cloud_tests and representative pano_cli_plan_cloud_* smoke tests, including prompt titles/captions, progress-dialog titles, and formatted download progress messages.
  • Android arm64 headless pp_app_core, pano_cli, and pp_app_core_document_cloud_tests built after the cloud metadata slice.
  • PanoPainter, pp_app_core_document_cloud_tests, and pano_cli built after live cloud upload, bulk upload, and browse/download execution moved behind the CloudServices boundary and src/legacy_cloud_services.*.
  • Focused cloud CTest coverage passed for pp_app_core_document_cloud_tests and all pano_cli_plan_cloud_* smoke tests after the live bridge split.
  • ctest --preset desktop-fast --build-config Debug passed with 243 tests after the cloud bridge split.
  • scripts/automation/package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug passed executable/data checks after the cloud bridge split; package target migration blockers remain under DEBT-0011.
  • PanoPainter, pp_app_core_document_cloud_tests, pp_app_core_app_dialog_tests, and pano_cli built after cloud download-progress prompt creation moved onto src/legacy_app_dialog_services.*.
  • Focused cloud/app-dialog CTest coverage passed for pp_app_core_document_cloud_tests, pp_app_core_app_dialog_tests, all pano_cli_plan_cloud_* smoke tests, and pano_cli_plan_app_dialog_* after the cloud prompt bridge split.
  • Android arm64 headless pp_app_core, pano_cli, pp_app_core_document_cloud_tests, and pp_app_core_app_dialog_tests built after the cloud prompt bridge split.
  • PanoPainter, pp_app_core_document_session_tests, and pano_cli built after App::open_document moved live execution behind the document-open services bridge. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused document-open CTest coverage passed for pp_app_core_document_route_tests, pp_app_core_document_session_tests, and the pano_cli_plan_open_route_* smoke tests after the live bridge split.
  • PanoPainter, pp_app_core_document_session_tests, and pano_cli built after close request, document save, and dirty-workflow continuation execution moved behind document-session services. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused document-session CTest coverage passed for pp_app_core_document_session_tests, pano_cli_simulate_app_session_*, and pano_cli_plan_document_file/version_* smoke tests after the live bridge split.
  • PanoPainter, pp_app_core_document_session_tests, and pano_cli built after accepted new-document execution moved behind the new-document services bridge. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused new-document/session CTest coverage passed for pp_app_core_document_session_tests, pano_cli_plan_new_document_*, and pano_cli_simulate_app_session_* smoke tests after the live bridge split.
  • PanoPainter, pp_app_core_document_session_tests, and pano_cli built after accepted Save As and Save Version execution moved behind document file/version save services. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused Save As/Version/session CTest coverage passed for pp_app_core_document_session_tests, pano_cli_plan_document_file_*, pano_cli_plan_document_version_*, and pano_cli_simulate_app_session_* smoke tests after the live bridge split.
  • PanoPainter, pano_cli, pp_app_core_app_dialog_tests, and pp_app_core_document_session_tests built after close/save/workflow, new-document overwrite, Save As overwrite, and save-error prompt metadata moved into the pure document-session prompt catalog. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused document-session prompt CTest coverage passed for pp_app_core_app_dialog_tests, pp_app_core_document_session_tests, pano_cli_plan_document_session_prompt_*, pano_cli_plan_document_file_*, pano_cli_plan_new_document_*, and representative pano_cli_simulate_app_session_* smoke tests.
  • Android arm64 headless pp_app_core, pano_cli, pp_app_core_app_dialog_tests, and pp_app_core_document_session_tests built after the same prompt-catalog change.
  • PanoPainter, pp_app_core_document_export_tests, and pano_cli built after equirectangular, layers, animation-frame, depth, and cube-face export execution moved behind document export services. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the build passed.
  • Focused export CTest coverage passed for pp_app_core_document_export_tests, pano_cli_plan_export_start/menu/target_*, and pano_cli_simulate_document_export_smoke after the live bridge split.
  • PanoPainter, pp_app_core_document_export_tests, and pano_cli built after timelapse and animation MP4 export execution moved behind document video export services. A clean rebuild was required once because MSVC reported the known Debug PDB LNK1103 corruption, after which the app, export tests, and pano_cli targets built cleanly.
  • Focused video export CTest coverage passed for pp_app_core_document_export_tests, pano_cli_plan_export_menu_*, pano_cli_plan_export_target_name_smoke, and pano_cli_simulate_document_export_smoke.
  • PanoPainter, pp_app_core_app_preferences_tests, and pano_cli built after options-menu preference execution moved behind app preference services.
  • Focused preference CTest coverage passed for pp_app_core_app_preferences_tests and the app-preferences CLI smoke tests after the live bridge split, including VR mode failed-start status coverage.
  • PanoPainter, pp_app_core_app_dialog_tests, and pano_cli built after progress/message/input dialog metadata moved into pp_app_core while live App factories kept retained Node* creation.
  • Focused app-dialog CTest coverage passed for pp_app_core_app_dialog_tests and the pano_cli_plan_app_dialog_* smoke tests, including negative progress-total clamping and rejected empty input-dialog OK captions.
  • Android arm64 headless pp_app_core, pano_cli, and pp_app_core_app_dialog_tests built after the app-dialog planning slice.
  • PanoPainter, pp_app_core_app_dialog_tests, and pano_cli built after retained progress/message/input Node* creation moved into src/legacy_app_dialog_services.*.
  • Focused app-dialog CTest coverage passed again for pp_app_core_app_dialog_tests and the pano_cli_plan_app_dialog_* smoke tests after the legacy bridge split.
  • Android arm64 headless pp_app_core, pano_cli, and pp_app_core_app_dialog_tests built after the app-dialog bridge split.
  • PanoPainter, pp_app_core_document_session_tests, pp_app_core_app_dialog_tests, and pano_cli built after document-session prompts moved onto src/legacy_app_dialog_services.*.
  • Focused document-session/app-dialog CTest coverage passed for pp_app_core_document_session_tests, pp_app_core_app_dialog_tests, pano_cli_plan_document_session_prompt_*, pano_cli_plan_app_dialog_*, and pano_cli_simulate_app_session_* after the document-session prompt bridge split.
  • Android arm64 headless pp_app_core, pano_cli, pp_app_core_document_session_tests, and pp_app_core_app_dialog_tests built after the document-session prompt bridge split.
  • PanoPainter, pp_app_core_app_startup_tests, and pano_cli built after startup preference/runtime execution and startup resource sequencing moved behind app startup services.
  • Focused startup CTest coverage passed for pp_app_core_app_startup_tests, pano_cli_plan_app_startup_smoke, and pano_cli_plan_app_startup_rejects_negative_counter, with startup resource sequencing also covered by pano_cli_plan_app_startup_resources_smoke and pano_cli_plan_app_startup_resources_rejects_bad_size.
  • PanoPainter, pp_app_core_app_frame_tests, and pano_cli built after app frame surface/update/tick/resize/draw-pass decisions moved into pp_app_core.
  • Focused frame CTest coverage passed for pp_app_core_app_frame_tests, pano_cli_plan_app_frame_vr_smoke, and pano_cli_plan_app_frame_idle_missing_canvas_smoke, with resize automation covered by pano_cli_plan_app_frame_resize_smoke and pano_cli_plan_app_frame_rejects_bad_resize. On 2026-06-05, UI observer clipping/on-screen/scissor projection coverage was added through pp_app_core_app_frame_tests, pano_cli_plan_app_frame_observer_smoke, pano_cli_plan_app_frame_observer_clipped_smoke, and pano_cli_plan_app_frame_rejects_bad_observer.
  • PanoPainter, pp_app_core_app_input_tests, and pano_cli built after app input routing and normalization moved into pp_app_core.
  • Focused app-input CTest coverage passed for pp_app_core_app_input_tests, pano_cli_plan_app_input_pointer_smoke, pano_cli_plan_app_input_gesture_smoke, pano_cli_plan_app_input_key_vr_smoke, pano_cli_plan_app_input_ui_toggle_smoke, pano_cli_plan_app_input_stylus_smoke, pano_cli_plan_app_input_rejects_bad_float, and pano_cli_plan_app_input_rejects_missing_ui_panel.
  • PanoPainter, pp_app_core_app_thread_tests, and pano_cli built after render/UI task dispatch, queue draining, UI-loop timer cadence, async redraw, and start/stop decisions moved into pp_app_core.
  • Focused app-thread CTest coverage passed for pp_app_core_app_thread_tests, pano_cli_plan_app_thread_dispatch_smoke, pano_cli_plan_app_thread_ui_loop_smoke, pano_cli_plan_app_thread_stop_smoke, and pano_cli_plan_app_thread_rejects_bad_timer.
  • PanoPainter, pp_app_core_app_shutdown_tests, and pano_cli built after shutdown cleanup staging moved into pp_app_core.
  • Focused shutdown CTest coverage passed for pp_app_core_app_shutdown_tests, pano_cli_plan_app_shutdown_smoke, and pano_cli_plan_app_shutdown_rejects_unknown_option.
  • PanoPainter, pp_app_core_command_convert_tests, and pano_cli built after command-line panorama conversion planning moved into pp_app_core.
  • Focused command-convert CTest coverage passed for pp_app_core_command_convert_tests, pano_cli_plan_command_convert_smoke, pano_cli_plan_command_convert_rejects_empty_project, and pano_cli_plan_command_convert_rejects_bad_resolution.
  • PanoPainter, pp_app_core_brush_package_export_tests, and pano_cli built after PPBR brush package export request validation and dispatch moved behind app-core brush package services.
  • Focused PPBR export CTest coverage passed for pp_app_core_brush_package_export_tests, pano_cli_plan_brush_package_export_smoke, pano_cli_plan_brush_package_export_rejects_empty_path, and pano_cli_plan_brush_package_export_dest_without_data_smoke.
  • PanoPainter, pp_app_core_brush_package_import_tests, and pano_cli built after ABR/PPBR brush package import execution moved behind app-core brush import services.
  • Focused brush import CTest coverage passed for pp_app_core_brush_package_import_tests, pano_cli_plan_brush_package_import_ppbr_smoke, pano_cli_plan_brush_package_import_abr_smoke, pano_cli_plan_brush_package_import_rejects_empty_path, and pano_cli_plan_brush_package_import_rejects_unknown_kind.
  • PanoPainter, pp_assets_brush_package_tests, pp_app_core_brush_package_export_tests, and pano_cli built after PPBR header validation and export path/data-directory planning moved into pp_assets.
  • Focused PPBR asset CTest coverage passed for pp_assets_brush_package_tests and the brush package export CLI tests, including path-without-directory rejection and legacy no-export-data data-directory planning.
  • PanoPainter, pp_assets_brush_package_tests, pp_app_core_brush_package_import_tests, and pano_cli built after ABR and PPBR imported brush tip/pattern target paths moved into pp_assets.
  • Focused brush import storage CTest coverage passed for pp_assets_brush_package_tests and the brush package import/export CLI smoke/failure tests.
  • PanoPainter, pp_app_core_brush_ui_tests, and pano_cli built after brush preset-list add/select/move/remove/clear planning moved into pp_app_core.
  • Focused brush preset-list CTest coverage passed for pp_app_core_brush_ui_tests and pano_cli_plan_brush_preset_list_* smoke tests.
  • pp_app_core_document_recording_tests passed, covering recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, oversized progress-total clamping, and recording-worker encode-wake eligibility.
  • pano_cli_plan_recording_session_stopped_smoke, pano_cli_plan_recording_session_running_smoke, and pano_cli_plan_recording_session_platform_cleanup_smoke passed and expose app-core recording lifecycle/export decisions as JSON. On 2026-06-05, pano_cli_plan_recording_session_missing_encoder_smoke was added for the worker no-encode path.
  • PanoPainter, pp_app_core_document_recording_tests, pp_app_core_app_dialog_tests, and pano_cli built after MP4 recording export progress-dialog metadata moved into pp_app_core and retained progress bar creation routed through src/legacy_app_dialog_services.*.
  • Focused recording/app-dialog CTest coverage passed for pp_app_core_document_recording_tests, pp_app_core_app_dialog_tests, pano_cli_plan_recording_session_*, and pano_cli_plan_app_dialog_*, including the MP4 export progress-dialog metadata smoke.
  • Android arm64 headless pp_app_core, pano_cli, pp_app_core_document_recording_tests, and pp_app_core_app_dialog_tests built after the recording progress bridge split.
  • pp_app_core_document_resize_tests passed, covering resize dialog state, unknown current-resolution labeling, selected-resolution mapping, square canvas sizing, history-clearing intent, invalid selection rejection, service dispatch order, optional history clearing, and invalid-dimension rejection.
  • pano_cli_plan_document_resize_smoke and pano_cli_plan_document_resize_rejects_invalid_selection passed and expose live document-resize planning as JSON automation.
  • pp_app_core_document_layer_tests passed, covering changed layer rename, unchanged no-op rename, empty-name rejection, overlong-name rejection, rename executor dispatch, no-op rename dialog finish, malformed rename-plan rejection, layer add/duplicate/select/reorder/remove planning, metadata planning, bad-index rejection, bad-opacity rejection, bad-blend-mode rejection, transient highlight behavior, layer operation executor dispatch, layer operation side-effect dispatch, no-op operation preservation, malformed operation rejection, Layer menu labels/actions, merge-down routing, animated merge blocking, missing selection handling, bad Layer menu state rejection, Layer menu executor dispatch, no-op menu execution preservation, merge-plan validation, unsupported animated merge rejection, merge executor dispatch, and malformed merge-plan rejection.
  • pano_cli_plan_layer_rename_smoke, pano_cli_plan_layer_rename_no_op_smoke, and pano_cli_plan_layer_rename_rejects_empty_name passed and expose live layer-rename planning as JSON automation.
  • pano_cli_plan_layer_menu_merge_smoke, pano_cli_plan_layer_menu_clear_smoke, pano_cli_plan_layer_menu_merge_animated_blocked_smoke, pano_cli_plan_layer_menu_missing_selection_smoke, and pano_cli_plan_layer_menu_rejects_bad_state passed and expose live Layer menu planning as JSON automation.
  • pano_cli_plan_layer_merge_smoke and pano_cli_plan_layer_merge_animated_rejected passed and expose live merge execution planning as JSON automation.
  • pano_cli_plan_layer_operation_add_smoke, pano_cli_plan_layer_operation_reorder_no_op_smoke, pano_cli_plan_layer_operation_highlight_smoke, and pano_cli_plan_layer_operation_rejects_bad_opacity passed and expose live layer-panel operation planning as JSON automation.
  • pp_app_core_document_animation_tests passed, covering animation frame add/duplicate/remove planning, selected-frame rejection, last-frame remove rejection, duration floor/overflow handling, timeline move edge behavior, goto/next/previous wrapping, onion-size rejection, service dispatch ordering, frame-click selection planning, no-reload playback step planning, playback toggle start/stop planning, animation panel action planning, invalid panel timeline state rejection, non-mutating duration no-ops, tested onion-skin frame range/alpha falloff planning consumed by live NodeCanvas panorama drawing, tested timeline mouse-scrub cursor-to-frame planning consumed by live NodeAnimationTimeline, tested animation panel layer/frame view-model projection consumed by live NodePanelAnimation, stale selected frame preservation, and malformed execution payload rejection.
  • pano_cli_plan_animation_operation_add_smoke, pano_cli_plan_animation_operation_duration_floor_smoke, pano_cli_plan_animation_operation_next_wrap_smoke, pano_cli_plan_animation_operation_select_smoke, pano_cli_plan_animation_operation_playback_smoke, pano_cli_plan_animation_operation_toggle_playback_start_smoke, pano_cli_plan_animation_operation_toggle_playback_stop_smoke, pano_cli_plan_animation_panel_action_next_smoke, pano_cli_plan_animation_panel_action_toggle_stop_smoke, pano_cli_plan_animation_panel_action_rejects_bad_timeline, pano_cli_plan_animation_panel_view_smoke, pano_cli_plan_animation_panel_view_allows_stale_selection, pano_cli_plan_animation_panel_view_rejects_empty_frames, pano_cli_plan_animation_timeline_scrub_smoke, pano_cli_plan_animation_timeline_scrub_clamps_left, pano_cli_plan_animation_timeline_scrub_rejects_bad_duration, pano_cli_plan_animation_operation_rejects_remove_last_frame, and pano_cli_plan_animation_operation_rejects_bad_selection passed and expose live animation-panel planning as JSON automation.
  • pp_app_core_brush_ui_tests passed, covering brush color channel validation, invalid color rejection, texture-path validation, preset-brush availability, preserve-current-color intent, stroke-settings refresh intent, texture-list add target path planning, user-texture removal intent, clamped reorder intent, stroke-control slider/toggle/blend/reset planning, service dispatch ordering, texture/preset/list/stroke-control execution payloads, execution failure preservation, and invalid execution payload rejection.
  • pano_cli_plan_brush_operation_color_smoke, pano_cli_plan_brush_operation_texture_smoke, pano_cli_plan_brush_operation_preset_smoke, pano_cli_plan_brush_operation_rejects_bad_color, and pano_cli_plan_brush_operation_rejects_empty_texture passed and expose live brush/color/preset UI planning as JSON automation.
  • pano_cli_plan_brush_texture_list_add_smoke, pano_cli_plan_brush_texture_list_remove_user_smoke, pano_cli_plan_brush_texture_list_move_edge_smoke, and pano_cli_plan_brush_texture_list_rejects_bad_source passed and expose live brush/pattern texture-list planning as JSON automation.
  • pano_cli_plan_brush_stroke_control_float_smoke, pano_cli_plan_brush_stroke_control_toggle_smoke, pano_cli_plan_brush_stroke_control_blend_smoke, pano_cli_plan_brush_stroke_control_reset_smoke, pano_cli_plan_brush_stroke_control_rejects_bad_setting, and pano_cli_plan_brush_stroke_control_rejects_bad_blend passed and expose live stroke-panel slider/toggle/blend/reset planning as JSON automation.
  • pano_cli_plan_brush_stroke_panel_view_smoke, pano_cli_plan_brush_stroke_panel_view_rejects_bad_float, and pano_cli_plan_brush_stroke_panel_view_rejects_bad_blend passed and expose live stroke-panel state projection as JSON automation.
  • pp_app_core_grid_ui_tests passed, covering heightmap pick/load/reload/clear planning, lightmap capability and limit checks, missing-heightmap no-op behavior, and commit canvas gating.
  • pano_cli_plan_grid_operation_pick_smoke, pano_cli_plan_grid_operation_load_smoke, pano_cli_plan_grid_operation_render_supported_smoke, pano_cli_plan_grid_operation_render_unsupported_smoke, pano_cli_plan_grid_operation_rejects_empty_reload, and pano_cli_plan_grid_operation_rejects_bad_samples passed and expose live grid/heightmap/lightmap planning as JSON automation.
  • pp_app_core_canvas_tool_ui_tests passed, covering toolbar mode selection, copy/cut transform action planning, pick no-op outside draw mode, and touch-lock toggling, full draw-toolbar binding projection, binding-to-action conversion, plus toolbar active-state derivation for draw, copy, and bucket modes, service dispatch ordering, pick no-op execution, and malformed execution payload rejection.
  • pp_app_core_canvas_hotkey_tests passed, covering E draw/erase toggles, Ctrl+Z/Ctrl+Shift+Z history planning, Ctrl+S/Ctrl+Shift+S document save intents, Tab UI toggles, brush-size brackets, Android back and two-finger undo, no-op Ctrl-less Z, bad-count rejection, executor dispatch, and malformed brush-size execution rejection.
  • pano_cli_plan_canvas_hotkey_ctrl_z_smoke, pano_cli_plan_canvas_hotkey_save_dirty_version_smoke, pano_cli_plan_canvas_hotkey_erase_smoke, pano_cli_plan_canvas_hotkey_two_finger_undo_smoke, and pano_cli_plan_canvas_hotkey_rejects_bad_count passed and expose live canvas keyboard/touch command planning as JSON automation.
  • pano_cli_plan_canvas_tool_draw_smoke, pano_cli_plan_canvas_tool_copy_smoke, pano_cli_plan_canvas_tool_pick_noop_smoke, pano_cli_plan_canvas_tool_touch_lock_smoke, and pano_cli_plan_canvas_tool_rejects_unknown passed and expose live draw toolbar planning as JSON automation.
  • pano_cli_plan_canvas_tool_toolbar_smoke and pano_cli_plan_canvas_tool_toolbar_rejects_unknown passed and expose the full live draw-toolbar binding set as JSON automation.
  • pano_cli_plan_canvas_tool_state_draw_smoke, pano_cli_plan_canvas_tool_state_copy_smoke, and pano_cli_plan_canvas_tool_state_rejects_unknown passed and expose draw toolbar active-state refresh as JSON automation.
  • PanoPainter, pano_cli, and pp_app_core_canvas_tool_ui_tests built on Windows after App::init_toolbar_draw() moved to the app-core toolbar binding plan; the build required the documented clean after a stale debug-info LNK1103.
  • Android arm64 headless pp_app_core, pano_cli, and pp_app_core_canvas_tool_ui_tests built after the same toolbar binding planner change.
  • pp_app_core_document_canvas_tests passed, covering clear-current-layer undo/dirty intent, no-canvas no-op behavior, and invalid clear color rejection, service dispatch color forwarding, no-op execution preservation, invalid execution color rejection, and canvas-to-pp_document snapshot projection with layer visibility, opacity, alpha-lock, blend mode, frame duration, active layer/frame, captured RGBA8 face payload attachment, default-name, no-canvas, bad blend, bad payload, and bad duration coverage.
  • pano_cli_plan_canvas_clear_smoke, pano_cli_plan_canvas_clear_no_canvas_smoke, and pano_cli_plan_canvas_clear_rejects_bad_color passed and expose toolbar canvas clear planning as JSON automation.
  • pano_cli_plan_canvas_document_snapshot_smoke and pano_cli_plan_canvas_document_snapshot_payload_smoke plus the no-canvas rejection smoke passed and expose live-canvas-to-pp_document projection, including captured-versus-pending renderer payload-readback counts, as JSON automation.
  • Live Save, Save As, Save Version, and save-before-workflow execution now prepare a payload-bearing canvas document snapshot and save-readiness report through src/legacy_document_session_services.* before delegating to the retained Canvas::project_save writer, keeping behavior stable while moving the app path onto the document/canvas boundary.
  • pano_cli plan-canvas-document-snapshot now emits the same save-readiness report (payloadComplete and canExportPpi) used by the live save bridge, and payload-complete snapshots now run the pure pp_document PPI exporter and decoded-project summary before emitting ppiExport JSON.
  • The same payload-complete snapshot automation now uploads the active document frame through pp_paint_renderer::upload_document_frame_faces and the RecordingRenderDevice, emitting rendererUpload JSON with texture, transition, command, byte, and active-frame payload counts.
  • Live equirectangular, layer, animation-frame, and cube-face export bridges now capture the payload-bearing canvas document snapshot and run the renderer-neutral upload report before retained Canvas export execution; failures are logged and retained export still continues to preserve behavior.
  • pp_app_core_document_import_tests passed, covering wide equirectangular, legacy vertical cube strip, regular transform-placement, and invalid-dimension import route decisions, equirectangular service dispatch, transform import dispatch, empty-path rejection, and invalid execution dimension rejection.
  • pano_cli_plan_image_import_wide_equirect_smoke, pano_cli_plan_image_import_transform_smoke, and pano_cli_plan_image_import_rejects_invalid_dimensions passed and expose File

    Import route planning as JSON automation.

  • pp_app_core_file_menu_tests passed, covering top-level File menu routing for creation/open/import, save intents, export/submenu/cloud actions, and unknown command rejection, plus executor dispatch for dialog, picker, save, export, share, resize, and cloud actions.
  • pano_cli_plan_file_menu_import_smoke, pano_cli_plan_file_menu_save_as_smoke, pano_cli_plan_file_menu_export_smoke, pano_cli_plan_file_menu_cloud_upload_smoke, and pano_cli_plan_file_menu_rejects_unknown passed and expose top-level File menu routing as JSON automation.
  • pp_app_core_document_export_tests passed, now also covering export menu dialog routing, demo-mode MP4/timelapse license gating, and missing-canvas handling, plus export menu executor dispatch for all dialog, blocked, and unavailable actions before legacy export dialogs continue.
  • pano_cli_plan_export_menu_png_smoke, pano_cli_plan_export_menu_mp4_demo_blocked_smoke, pano_cli_plan_export_menu_no_canvas_smoke, and pano_cli_plan_export_menu_rejects_unknown passed and expose File menu export routing as JSON automation.
  • pp_app_core_history_ui_tests passed, covering undo/redo availability, no-op history commands, clear-history stack/memory state, memory-only clear, negative metric rejection, service dispatch order, empty-history no-op execution, and invalid execution metric rejection.
  • pano_cli_plan_history_operation_undo_smoke, pano_cli_plan_history_operation_redo_empty_smoke, pano_cli_plan_history_operation_clear_smoke, and pano_cli_plan_history_operation_rejects_negative_count passed and expose toolbar/canvas history planning as JSON automation.
  • pp_app_core_quick_ui_tests passed, covering quick brush/color slot selection, active-slot popup decisions, invalid slot rejection, restore-state validation, reset-state validation, service dispatch order, explicit brush/color restore indices, and malformed execution payload rejection.
  • pano_cli_plan_quick_operation_select_brush_smoke, pano_cli_plan_quick_operation_open_color_smoke, pano_cli_plan_quick_operation_restore_smoke, pano_cli_plan_quick_operation_reset_smoke, pano_cli_plan_quick_operation_rejects_bad_slot, and pano_cli_plan_quick_operation_rejects_bad_restore passed and expose live quick-panel planning as JSON automation.
  • pp_app_core_tools_menu_tests passed, covering Tools submenu routing, root-closing commands, platform-only SonarPen gating, executor dispatch, unavailable no-op actions, floating panel chrome metadata, already-visible panel no-ops, and animation panel non-droppable state.
  • pano_cli_plan_tools_menu_shortcuts_smoke, pano_cli_plan_tools_menu_sonarpen_unavailable_smoke, pano_cli_plan_tools_panel_layers_smoke, pano_cli_plan_tools_panel_visible_noop_smoke, and pano_cli_plan_tools_panel_rejects_unknown passed and expose live Tools menu/panel planning as JSON automation.
  • pp_app_core_about_menu_tests passed, covering About/help/what's-new dialog routing, versioned what's-new labels, crash diagnostic gating, performance workload metadata, no-canvas performance-test blocking, dispatch through the AboutMenuServices executor boundary, and no-op unavailable actions.
  • pano_cli_plan_about_menu_news_smoke, pano_cli_plan_about_menu_performance_no_canvas_smoke, pano_cli_plan_about_menu_crash_disabled_smoke, and pano_cli_plan_about_menu_rejects_unknown passed and expose live About menu planning as JSON automation.
  • pp_app_core_main_toolbar_tests passed, covering live toolbar/status direct dialog routing, undo/redo availability, clear-history availability, no-canvas clear blocking, negative history metric rejection, and dispatch through the MainToolbarServices executor boundary without invoking no-op actions.
  • pano_cli_plan_main_toolbar_undo_smoke, pano_cli_plan_main_toolbar_redo_empty_smoke, pano_cli_plan_main_toolbar_clear_canvas_no_canvas_smoke, and pano_cli_plan_main_toolbar_rejects_negative_count passed and expose live toolbar/status planning as JSON automation.
  • PanoPainter, pp_app_core_main_toolbar_tests, pp_app_core_app_dialog_tests, and pano_cli built after toolbar test-message dialog metadata moved into pp_app_core and live message-box creation routed through src/legacy_app_dialog_services.*.
  • Focused main-toolbar/app-dialog CTest coverage passed for pp_app_core_main_toolbar_tests, pp_app_core_app_dialog_tests, pano_cli_plan_main_toolbar_*, and pano_cli_plan_app_dialog_*, including the toolbar message-box dialog metadata smoke.
  • Android arm64 headless pp_app_core, pano_cli, pp_app_core_main_toolbar_tests, and pp_app_core_app_dialog_tests built after the toolbar message-box bridge split.
  • pp_app_core_document_sharing_tests passed, covering saved-path gating before platform share execution.
  • pano_cli_plan_share_file_unsaved_smoke and pano_cli_plan_share_file_saved_smoke passed and expose app-core share decisions as JSON.
  • pp_app_core_document_platform_io_tests passed, covering empty selected-path filtering and non-empty picked-path callback planning before platform picker callbacks, plus empty/non-empty display-file planning before platform display callbacks, plus virtual keyboard show/hide planning before platform keyboard callbacks, plus cursor visibility planning before platform cursor callbacks, plus clipboard read/write planning before platform clipboard callbacks.
  • pano_cli_plan_picked_path_empty_smoke and pano_cli_plan_picked_path_selected_smoke passed and expose app-core picker selected-path decisions as JSON.
  • pano_cli_plan_display_file_empty_smoke and pano_cli_plan_display_file_selected_smoke passed and expose app-core display-file decisions as JSON.
  • pano_cli_plan_keyboard_visibility_hidden_smoke and pano_cli_plan_keyboard_visibility_visible_smoke passed and expose app-core virtual keyboard decisions as JSON.
  • pano_cli_plan_cursor_visibility_hidden_smoke and pano_cli_plan_cursor_visibility_visible_smoke passed and expose app-core cursor visibility decisions as JSON.
  • pano_cli_plan_clipboard_read_smoke, pano_cli_plan_clipboard_write_smoke, and pano_cli_plan_clipboard_write_empty_smoke passed and expose app-core clipboard decisions as JSON, including empty write text.
  • pp_platform_api_tests passed, covering the SDK-free PlatformServices interface for startup storage path preparation, clipboard read/write, empty clipboard writes, cursor visibility dispatch, virtual-keyboard visibility dispatch, external file display dispatch, file sharing dispatch, native app/window close dispatch, UI-thread lifecycle dispatch, render-context lifecycle dispatch, render-target binding dispatch, render platform hint dispatch, render debug callback dispatch, render-capture frame hook dispatch, recording cleanup dispatch, exported-image publish dispatch, persistent storage flush dispatch, document browse-root dispatch, working-directory picker policy and display-path formatting dispatch, canvas input tip visibility and pressure remap dispatch, native UI/window state save dispatch, prepared-file writable target dispatch, prepared-file export-dialog policy dispatch, work-directory document export collection policy dispatch, network TLS verification policy dispatch, default network TLS policy coverage, PPBR export data-directory policy dispatch, SonarPen availability/startup dispatch, VR lifecycle dispatch, layout/asset file load policy coverage, live asset/layout reload policy dispatch, diagnostic hook dispatch, per-frame platform hook dispatch, picker callback dispatch, and prepared-file save/download callback dispatch. The live Windows app now consumes this interface through an injected WindowsPlatformServices instance isolated in src/platform_windows/windows_platform_services.*; other platforms still use the legacy fallback adapter, now isolated in src/platform_legacy/legacy_platform_services.* instead of being owned by app_events.cpp.
  • panopainter_validate_shaders passed, validating 25 shader programs and 7 shader includes for stage markers and include graph integrity.
  • pp_renderer_gl_capabilities_tests passed on default MSVC, vcpkg-headless, and Android arm64 configure/build, covering framebuffer fetch, map-buffer alignment, desktop GL core float support, GLES float/half-float extensions, WebGL exclusion behavior, upload types for RGBA8/RGBA16F/RGBA32F internal formats, image channel-count format mapping including invalid counts, and RGBA8/RGBA32F readback format and byte-count mapping, PBO pixel-buffer target/usage/access mapping, framebuffer status names, framebuffer blit color mask and linear/nearest filters, plus Shape index-type and fill/stroke primitive mode mapping, PanoPainter cube-face texture-target order, and the linear clamp-to-edge render-target texture parameter set used by RTT::create. Sampler parameter validation covers wrap S/T/R plus min/mag filter ordering used by legacy Sampler::set and Sampler::set_filter, plus the desktop border-color parameter name used by Sampler::set_border. Legacy TextureCube allocation/bind/delete and Sampler create/configure/border/bind/unbind paths now execute through tested pp_renderer_gl dispatch contracts, keeping cube-map and sampler resource lifecycle reachable without a live GL context. Shader attribute binding catalog validation covers the current pos, uvs, uvs2, col, and nor bindings and rejects empty, unnamed, null-name, and duplicate-name catalogs while preserving legacy shared locations. Shader uniform catalog validation covers the 43 legacy uniform names used by Shader, preserves the legacy hash ids, and rejects empty, unnamed, null-name, mismatched-hash, and duplicate-name catalogs. Legacy Shader program use/delete, uniform writes, attribute-location lookups, shader source compilation, shader deletion, program attach/link, attribute rebinding, active-uniform count/enumeration, and uniform-location discovery now execute through tested pp_renderer_gl dispatch contracts via legacy_gl_shader_dispatch. Retained Shape, TextMesh, and NodeColorWheel mesh buffer/VAO creation, zero-byte dynamic-buffer creation, dynamic buffer uploads, indexed and non-indexed draws, and resource deletion now execute through tested pp_renderer_gl dispatch contracts via legacy_gl_mesh_dispatch.
  • pp_renderer_gl_command_plan_tests covers the headless OpenGL command planner for recorded render-pass clear masks/values, viewport/scissor state, blend/depth/sampler state, texture format mapping, mesh/draw primitive modes, draw counts, shader bind/uniform names and byte counts, texture upload/mipmap/transition/copy/readback/capture metadata, blit filters and byte totals, planned command names, unsupported enum/state rejection, whole recorded stream planning, valid trace/render/shader/draw/blit ordering, typed texture-command counts, broken render-pass order detection, and executable draw/uniform dependency failures.
  • PowerShell analyze automation returns JSON summaries and includes the shader validation target and renderer-boundary guard.
  • windows-msvc-vcpkg-headless configured through the Visual Studio bundled vcpkg root, installed the manifest dependencies, built the headless component matrix, and passed desktop-fast-vcpkg.
  • pp_ui_core built and tested against vcpkg tinyxml2 on windows-msvc-vcpkg-headless and against the vendored fallback on windows-msvc-default and android-arm64.
  • windows-clangcl-asan configures headlessly with clang-cl 18.1.8 and release MSVC runtime selection; build remains blocked and debt-tracked in DEBT-0014 because the selected VS 2026-preview STL requires Clang 20 or newer.
  • PanoPainter.exe built through CMake at out/build/windows-msvc-default/Debug/PanoPainter.exe.
  • PowerShell build/test automation wrappers return JSON summaries and passed local smoke checks.
  • Renderer-boundary automation fails if active non-backend source code reintroduces raw GL_*/WGL_* constants outside the allowed legacy OpenGL implementation files.
  • pp_renderer_api now includes a headless RecordingRenderDevice with strict renderer feature flags, renderer-owned resource factory and command-order/render-pass-clear/scissor-state/depth-state/blend-state/ texture-usage/texture-bind/sampler-bind/shader-uniform/texture-upload/ mipmap-generation/texture-transition/readback/frame-capture/blit validation plus explicit draw descriptor and texture-copy validation; it creates validated textures, render targets, shaders, meshes, and readback buffers with validated debug labels, then records commands, trace markers/scopes, render-pass color/depth/stencil clear intent, scissor state, depth state, blend state, shader uniform writes, texture/sampler binds, draw mesh inputs, explicit draw ranges, texture uploads/mipmap generations/state transitions/copies/readbacks, frame captures, and render-target blits, giving automation a backend-neutral render path that does not require a window or GL context. Clearing the recording device now resets active render-pass and trace-scope state so interrupted automation can reuse a recorder without carrying stale frame state forward.
  • pano_cli record-render exercises that headless recording renderer and emits JSON command counts, backend feature flags, resource creation counts, target dimensions, backend name, trace marker/scope and draw summary, labeled descriptor counts, render-pass/depth-clear counts, and draw descriptor vertex/index totals, scissor/depth/blend-state plus shader-uniform/texture/sampler-bind/upload/mipmap-generation/texture-transition/texture-copy/readback/ frame-capture/blit command/byte totals for agent automation. When pp_renderer_gl is available, it also emits an openGlPlan JSON object with planned command count, support status, render-pass/draw/shader-bind/uniform/ texture-upload/mipmap/transition/copy/readback/capture/passthrough/trace counts, unsupported command count, render-pass order error count, dependency error count, and unclosed-pass state. The --exercise-clear mode deliberately clears an interrupted trace/render pass, verifies stale trace-scope state is rejected, verifies the render context can be reused, and then emits that reset status in JSON. It also has an expected-failure smoke for oversized render/readback targets.
  • pano_cli simulate-document-history exercises pure document history apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation.
  • pano_cli simulate-document-edits exercises pure document layer/frame edit operations and emits JSON metadata, frame order, face-payload state, and selection-mask state for agent automation.
  • pano_cli simulate-image-import exercises embedded PNG decode through pp_assets and pp_document face-payload attachment through JSON automation.
  • pano_cli import-image accepts a PNG file path, decodes RGBA8 pixels through pp_assets, attaches them to a pure pp_document face payload, and has checked-in decodable-PNG plus truncated-PNG rejection smoke tests.
  • pano_cli export-image writes deterministic RGBA8 PNGs through pp_assets and has a save/import round-trip smoke test. Full legacy canvas export remains a future pano_cli task.
  • pano_cli save-project exposes generated multi-layer, multi-frame PPI writing with layer metadata and targeted dirty-face layer/frame payloads through JSON automation and is covered by metadata-only and dirty-face-payload save/load round-trip smoke tests. Full legacy canvas save parity remains tracked by DEBT-0013.
  • pp_assets::create_ppi_project exposes the underlying generated PPI writer for non-uniform layer metadata and frame-duration extraction work.
  • pp_document::export_ppi_project_document exposes pure document-to-PPI byte export through CTest coverage; legacy Canvas save integration remains tracked by DEBT-0010/DEBT-0013.
  • pano_cli simulate-document-export exposes the same export path through JSON automation for agents.
  • pano_cli save-document-project exposes file-writing document export automation for inspect/load round trips.
  • pano_cli apply-stroke-script exposes file-driven stroke-script application to a pure document face payload and writes a PPI artifact for inspect/load round-trip automation.
  • Live document save/session paths consume the payload-bearing canvas snapshot boundary before retained PPI serialization: existing Save, Save As, Save Version, and save-before-workflow now log payload completeness and PPI readiness from pp_app_core while legacy Canvas::project_save still owns the actual file write, progress/threading behavior, and compatibility quirks.
  • pp_app_core now exposes export_document_canvas_save_snapshot_to_ppi, which refuses snapshots that still need renderer payload readback and exports payload-complete or metadata-only snapshots through the pure pp_document PPI writer. The document-canvas tests decode the generated bytes, and pano_cli plan-canvas-document-snapshot reports ppiExport readiness, byte count, and dirty-face count for agent automation.
  • Payload-complete canvas snapshot automation also crosses the renderer boundary now: pano_cli plan-canvas-document-snapshot feeds the same snapshot through pp_paint_renderer::prepare_document_frame_export_readiness, which records the renderer-neutral active-frame texture upload stream and encodes the composited active-frame cube faces as PNG bytes, so agents validate the same document/canvas-to-renderer readiness report consumed by live export. It also emits depthExport readiness from the paint-renderer depth plan, keeping CLI automation aligned with the live depth export adapter while final renderer readback remains retained.
  • Live image/collection/cube export adapters now prepare and log the same document/canvas plus shared renderer-upload and face-PNG export readiness reports. Cube-face export now writes the pure document/renderer PNG bytes to the pp_app_core planned legacy face filenames through a tested write/publish service executor before falling back to retained Canvas execution on failure. PNG equirectangular export now writes a pp_paint_renderer equirectangular PNG from the same composited document frame before falling back to retained Canvas execution; payload-complete layer and animation-frame PNG collections now write pure pp_paint_renderer equirectangular PNG sequences through a tested app-core collection write/publish executor before retained fallback. Depth export now prepares the same document/canvas snapshot, logs the shared renderer-upload readiness report, and records a tested paint-renderer depth render plan before retained Canvas execution. JPEG equirectangular export now writes a pure pp_paint_renderer/pp_assets JPEG with GPano XMP metadata before retained fallback. Web prepared-file handoff, video, and incomplete-readback collection cases remain on their prior retained writer paths. Actual broader writer replacement remains tracked under export debt.
  • Snapshot creation now rejects invalid embedded RGBA8 face payloads before document export or history can persist malformed state.
  • Package-smoke wrappers validate the Windows CMake app executable/runtime data/ copy and report structured package readiness for AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL outputs. Actual package building remains blocked by DEBT-0011 until those targets are migrated to root CMake. Readiness-only mode now reports the same matrix without building the app first, and the package readiness self-test keeps wrapper package kinds, retained Linux/WebGL CMake metadata, and blocker metadata aligned. Root CMake target panopainter_linux_webgl_package_readiness now exposes the filtered retained Linux/WebGL readiness matrix from the CMake target graph.
  • Android standard arm64/x64, Quest arm64, and Focus/Wave arm64 configure through the platform-build wrapper by default. Focused validation compiled representative headless component/tool targets across all four presets, and the full refreshed component/test matrix remains the default gate for local platform sweeps. The retained Android standard package CMake path also now configures/builds native-lib directly for arm64 using C++23 and the shared modern component source set, while Quest and Focus package CMake paths configure with the same compatibility helper and current Yoga source list. The Android platform-build wrapper and retained package helper now query sdkmanager, install newer/missing SDK Manager NDK/CMake packages when needed, select that pair automatically, and report the selected versions plus update decisions in their structured output. package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk now runs those retained native checks from the package-smoke surface while keeping APK readiness blocked on root CMake package-target migration. Root CMake now has named package validation targets for that same retained Android native gate; cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke validates the latest SDK-managed NDK/CMake pair and reports the still-blocked APK package state.
  • The Windows app artifact package smoke is also reachable from root CMake via cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke, which builds the CMake PanoPainter app target, validates the executable/runtime data/ copy, and reports the still-blocked Windows AppX package state.
  • Retained Linux and WebGL app CMake files now use CMake 3.10 and target-level C++23 instead of global C++14 flags; python scripts/dev/check_retained_platform_cmake.py and CTest panopainter_retained_platform_cmake_self_test guard those baselines while the actual Linux/WebGL app/package targets remain outside root CMake.
  • Root CMake now exposes platform validation targets for the default headless platform-build sweep, the Android standard/Quest/Focus root CMake asset component sweep, the vcpkg-backed UI core dependency boundary, and the remote Apple compile gate. cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets validated pp_assets across Android arm64, Android x64, Quest arm64, and Focus/Wave arm64 with the latest SDK-managed NDK/CMake pair; cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core validated pp_ui_core and pp_ui_core_layout_xml_tests through the vcpkg tinyxml2 preset.
  • Desktop VR drawing now routes generic OpenGL scissor/depth/blend state, blend/depth state snapshots and restores, depth clears, active texture units, and fallback 2D texture unbinds through tested renderer GL backend dispatch; platform VR SDK bridges remain isolated for later platform-shell extraction. Eye framebuffer viewport execution in the retained HMD path also routes through tested pp_renderer_gl viewport dispatch.
  • Canvas mode overlay, mask, and transform paths now route generic OpenGL blend/depth state execution, active texture unit switches, transform/cut viewport execution, 2D framebuffer-to-texture copy dispatch, RGBA8 readback formats, and RTT-backed transform history region readbacks through the renderer GL backend mapping. Canvas-tip pick readback now routes through the tested framebuffer readback dispatch using the active read framebuffer, with only local OpenGL adapter endpoints retained in src/canvas_modes.cpp. Paint-mode blend/depth state snapshots also use tested capability-state query dispatch.
  • NodeCanvas panorama UI rendering now routes sampler defaults, saved viewport/clear/blend/depth/scissor state, tested viewport and clear-color query dispatch, color clears, clear-color restore, active texture units, fallback 2D texture unbinds, 2D framebuffer-to-texture copy dispatch, and RGBA8 render-target formats through the renderer GL backend mapping. Its live viewport, generic blend/depth/scissor capability changes, and density/offscreen color-buffer clears now execute through tested pp_renderer_gl dispatch adapters, and its saved blend/depth/scissor state queries now use tested capability-state query dispatch.
  • Canvas resource setup now routes stroke-buffer RGBA8/RGBA16F/RGBA32F formats, flood-fill texture upload format/type, brush/stencil/mix sampler filters and wraps, and cube-strip import channel formats through the renderer GL backend mapping. The clamp-to-border sampler wrap is now cataloged and tested in pp_renderer_gl.
  • Early canvas draw helpers now route pick readbacks, stroke mixer depth/scissor and blend state, saved viewport/clear-state queries, active texture units, fallback 2D texture unbinds, and stroke background copy targets through the renderer GL backend mapping. Stroke mixer viewport/scissor execution also routes through the tested backend dispatch contract.
  • Canvas stroke commit now routes saved viewport/clear/blend state, history readbacks, active texture units, fallback 2D texture unbinds, and layer compositing copy targets through the renderer GL backend mapping; the RTT-backed dirty-region readbacks now execute through the retained RTT region-readback helper rather than direct glReadPixels, and 2D framebuffer copies now execute through the retained utility bridge instead of direct glCopyTexSubImage2D.
  • Canvas layer merge rendering and explicit layer-merge compositing now route depth/blend state, active texture units, fallback 2D texture unbinds, and merge framebuffer copy targets through the renderer GL backend mapping.
  • Canvas layer cube/equirect generation and frame clears now share legacy_ui_gl_dispatch for active-texture selection, cube texture binding, viewport execution, blend capability execution, clear-color query/restore, and color-buffer clear adapter endpoints backed by tested renderer GL backend dispatch contracts. The cube-face framebuffer-to-texture copy now uses the shared retained target-aware utility bridge and remains tracked by DEBT-0036 until renderer services own copy execution.
  • NodePanelGrid live heightmap drawing and bake setup now route depth/blend state, depth clears, color-write-mask toggles, active texture selection, and bake viewport execution through tested renderer GL backend dispatch contracts. Its desktop texture-resize readback now uses the retained Texture2D::get_image() helper, so it consumes the same tested framebuffer-backed texture readback dispatch instead of glGetTexImage. Grid depth-state and viewport snapshots now also use tested backend query dispatch.
  • Retained simple UI draw paths now share legacy_ui_gl_dispatch for blend-state execution, fallback 2D texture unbinds, NodeViewport viewport query/restore, color-buffer clear, and clear-color restore. This covers NodeBorder, NodeImage, NodeImageTexture, NodeColorWheel, NodeAnimationTimeline, NodeScroll, NodeText, NodeTextInput, and NodeViewport without changing their legacy draw ordering.
  • Retained paint UI surface paths now use tested pp_renderer_gl viewport query, clear-color query, clear-color restore, and color-buffer clear helpers in NodeCanvas and NodeStrokePreview, removing direct query/clear calls from those draw bodies while keeping their legacy compositing order. Their active-texture, fallback texture unbind, viewport/scissor, clear-color, color-buffer clear, and capability query/apply adapter endpoints are now centralized in legacy_ui_gl_dispatch instead of being duplicated in each node implementation.
  • Retained Canvas stroke draw/commit, thumbnail, object-render, and LayerFrame::clear paths now use the same tested backend viewport query, clear-color query, and clear-color restore helpers, removing direct viewport/clear-state queries from src/canvas.cpp and the frame clear path. Their active-texture selection, fallback 2D texture unbind, viewport/scissor execution, clear-color restore, and capability query/apply adapter endpoints now share legacy_ui_gl_dispatch.
  • Retained Canvas and RTT depth renderbuffer allocation, framebuffer depth attachment, and renderbuffer deletion now share legacy_gl_renderbuffer_dispatch, removing duplicated raw renderbuffer callbacks from src/canvas.cpp and src/rtt.cpp while resource lifetime ownership remains open under DEBT-0036.
  • Retained Texture2D, TextureCube, and RTT texture allocation, deletion, binding, parameter setup, 2D update, and mipmap dispatch now share legacy_gl_texture_dispatch, removing duplicated raw texture callbacks from src/texture.cpp and src/rtt.cpp while texture resource ownership remains retained under DEBT-0036.
  • Retained Texture2D readback plus RTT framebuffer allocation, deletion, bind/restore, blit, readback, and PBO readback dispatch now share legacy_gl_framebuffer_dispatch, removing duplicated raw framebuffer and readback callbacks from src/texture.cpp and src/rtt.cpp while framebuffer/readback ownership remains retained under DEBT-0036.
  • Retained Sampler create, parameter, border-color, bind, and unbind dispatch now share legacy_gl_sampler_dispatch, and retained PBO allocation, framebuffer readback, map, unmap, and delete dispatch now share legacy_gl_pixel_buffer_dispatch; this removes another pair of raw resource callback clusters from src/texture.cpp and src/rtt.cpp while sampler and pixel-buffer ownership remain retained under DEBT-0036.
  • Retained Shape, TextMesh, and NodeColorWheel mesh buffer/VAO creation, dynamic vertex/index uploads, fill/stroke/text draws, and buffer/VAO deletion now share legacy_gl_mesh_dispatch, removing duplicated raw mesh callback clusters from src/shape.cpp, src/font.cpp, and src/node_colorwheel.cpp while mesh resource ownership remains retained under DEBT-0036.
  • Retained shader source compilation/deletion, program attach/link/use/delete, attribute rebinding and location lookup, active-uniform enumeration, uniform-location discovery, and vec/mat/scalar uniform writes now share legacy_gl_shader_dispatch, removing duplicated raw shader/program/uniform callback ownership from src/shader.cpp while shader-program ownership remains retained under DEBT-0036.
  • Retained gl_state save/restore and copy_framebuffer_to_texture_target now reuse legacy_ui_gl_dispatch, legacy_gl_framebuffer_dispatch, legacy_gl_shader_dispatch, and legacy_gl_sampler_dispatch, removing the local raw state/copy callback cluster from src/util.cpp while renderer state and framebuffer-copy execution remain retained under DEBT-0036.
  • Retained app startup, app clear, app UI viewport/scissor, command-convert renderer state, and desktop VR draw-state endpoints now share legacy_ui_gl_dispatch for capability, blend equation, clear, viewport, scissor, active-texture, and 2D texture-unbind callbacks, removing duplicated local raw callback clusters from src/app.cpp, src/app_commands.cpp, and src/app_vr.cpp while app/VR renderer execution remains retained under DEBT-0036.
  • Retained RTT clear and masked-clear endpoints now share legacy_ui_gl_dispatch for boolean color-mask query, color-mask apply, clear-color, and buffer-clear callbacks, removing the local raw clear callback cluster from src/rtt.cpp while RTT render-target execution remains retained under DEBT-0036.
  • Retained app startup logging, Windows early context logging/window-title detection, and shader capability detection now share legacy_gl_runtime_dispatch for runtime string and extension enumeration callbacks, removing duplicated raw runtime-query clusters from src/app.cpp, src/main.cpp, and src/app_shaders.cpp while runtime/capability probing remains retained under DEBT-0036.
  • Canvas draw-merge shader-blend selection now consumes the extracted pp_paint_renderer stroke composite planner for current layer and primary brush blend modes, while preserving legacy OpenGL compositing execution under DEBT-0036.
  • NodeCanvas panorama rendering now consumes the same tested pp_paint_renderer canvas blend-gate planner as Canvas::draw_merge, so layer and primary-brush blend-trigger compatibility is centralized.
  • Shader initialization now publishes the OpenGL backend's renderer-neutral feature snapshot through the legacy shader manager, and live canvas blend gates consume that RenderDeviceFeatures value instead of hand-built framebuffer-fetch/texture-copy flags.
  • Canvas draw-merge and NodeCanvas panorama shader-blend paths now use the shared canvas blend-gate plan to decide whether they can read destination color through framebuffer fetch or must copy the destination texture before the legacy OpenGL blend draw.
  • Canvas main-brush, dual-brush, and stroke-pad draw paths now use the tested pp_paint_renderer stroke-feedback plan to decide whether framebuffer fetch supplies destination color or the legacy OpenGL path must copy the target texture before drawing.
  • Canvas thumbnail layer blending now uses the same canvas destination-feedback plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw itself still executes through retained OpenGL canvas code under DEBT-0036.
  • Canvas equirectangular import drawing and depth export rendering now route depth/blend state and active texture units through the renderer GL backend mapping.
  • Canvas thumbnail generation and object-drawing helpers now route saved viewport/clear/blend state, active texture units, readback format/type, framebuffer copy targets, and depth renderbuffer allocation plus framebuffer depth attach/detach through tested renderer GL backend dispatch contracts; src/canvas.cpp no longer owns raw renderbuffer callbacks.
  • Retained Canvas, NodeCanvas, NodeStrokePreview, and HMD viewport/scissor/ capability execution now compiles through the renderer GL backend dispatch adapters with pp_legacy_paint_document, pp_panopainter_ui, and panopainter_app. CanvasMode overlay, mask, transform, and canvas-tip pick paths now also consume the shared retained UI GL dispatch for active-texture, capability query/apply, viewport, read-framebuffer query, and RGBA8 pixel readback adapter endpoints, removing another local raw-GL adapter cluster from src/canvas_modes.cpp. NodePanelGrid heightmap draw and bake setup now uses the same bridge for active-texture, depth/blend capability query/apply, viewport query/execution, depth clears, and color-write-mask adapter endpoints instead of owning another local dispatch cluster.
  • Windows desktop OpenGL context creation now consumes a tested windows_wgl_core_context_3_3_config() catalog from pp_renderer_gl, moving the active WGL context/pixel-format attribute literals out of the platform entrypoint.
  • Known remaining warnings: legacy project/vendor diagnostics, Visual Studio vcpkg-manifest warning, LNK4099 missing libyuv PDBs, and LNK4098 runtime library conflict from retained vendor binaries.

Current Debt Log

The canonical debt log is now docs/modernization/debt.md. Keep this section as a reminder only; do not add new debt entries here.

ID Status Owner Item Reason Validation Removal Condition
DEBT-0001 Open TBD Existing platform build files remain alongside new CMake Required for incremental migration Existing platform builds plus new CMake configure Remove after all platform builds consume shared CMake targets
DEBT-0002 Open TBD Vendored SDK and patched libraries retained initially Some dependencies are SDK-only or have platform-specific binaries Dependency inventory and platform build smoke tests Replace or document permanent vendored status after vcpkg triplet evaluation
DEBT-0003 Open TBD Existing singletons remain during initial split Avoid behavior changes while introducing boundaries App launch and component tests Replace singleton reaches with context/service injection at component boundaries

Current Capability Map Seed

Use this as the starting checklist for Phase 0 inventory.

  • Project I/O: PPI open/save, thumbnails, version metadata, autosave/save-as flows.
  • Image I/O: JPEG/PNG import/export, cube faces, equirectangular export, depth export.
  • Brush system: ABR import, PPBR import/export, presets, tip/pattern/dual brush, pressure, jitter, blend modes.
  • Painting: six cube faces, temporary stroke buffers, erase, flood fill, masks, alpha lock, layer compositing.
  • Layers and animation: layer add/remove/move/merge, blend/opacity/visibility, frame add/remove/duplicate/duration, MP4/timelapse export.
  • UI: XML layout, Yoga layout, panels, dialogs, color tools, brush tools, layers, animation timeline, settings, shortcuts, manual/changelog/about.
  • Input: mouse, keyboard, touch, gestures, Wacom tablet, stylus pressure, VR controllers.
  • Platform services: clipboard, file picker, save picker, directory picker, share/display file, keyboard show/hide, cursor visibility.
  • VR/platform variants: OpenXR desktop target with retained OpenVR fallback, Quest, Focus/Wave, Android standard, iOS/macOS, Linux, WebGL.
  • Cloud/network: upload, download, browse, license/check flows.
  • Recording/export: PBO readbacks, MP4 encoder, timelapse frames.