# PanoPainter Modernization Roadmap Status: live Last updated: 2026-06-04 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: ```text 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, and license-warning visibility. `App::init` now plans those decisions before heavy initialization, executes run-counter persistence through `src/legacy_app_startup_services.*` before asset/layout setup, 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. `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. 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 - 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. `pp_ui_core` has started with XML-layout-facing length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid input tests. `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 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, and MP4 suggested names used by the live export dialogs. `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-recording-session` exposes the app-core recording start, stop, clear, platform recorded-file cleanup, frame reset, and export progress-total 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. `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-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. `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-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-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-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-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. `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. `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. `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-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. 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`; Windows keeps the retained OpenVR bridge in `WindowsPlatformServices`, 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 now forward-declares Android asset-manager types, hides the Android asset handles behind `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 actual Android asset-reader implementation remains inside `pp_legacy_assets_io`. 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 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. 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. 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. 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. 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 retained OpenVR app path is split incrementally. VR draw blend/depth state snapshots, transitions, restore, and depth-buffer clears now use generic tested `pp_renderer_gl` capability query/apply and clear dispatch contracts, reducing direct OpenGL execution in the retained VR app path without changing state restore behavior. 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. 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. 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`. 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`. `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`. `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, `NodePanelBrushPreset::export_ppbr`, and existing success messages while 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 - 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`. `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`. 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`. 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. 2D framebuffer-to-texture copies used by canvas, transform, layer-conversion, panorama UI, and brush preview paths now route through a tested `pp_renderer_gl` copy dispatch via the retained `copy_framebuffer_to_texture_2d` utility bridge; the remaining cube-map copy is tracked 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`. Legacy `Shape` mesh buffer/VAO creation, dynamic vertex/index uploads, fill/stroke draws, and buffer/VAO deletion now execute through tested `pp_renderer_gl` dispatch contracts, leaving the retained shape utility with thin GL adapter functions. 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. 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, and indexed draw calls now execute through the same tested mesh dispatch contracts used by `Shape`, leaving the retained `Font` utility with thin GL adapter functions for mesh operations. Canvas undo/redo dirty-region texture updates and readbacks now also delegate their 2D texture target, RGBA pixel format, and unsigned-byte component type mapping to `pp_renderer_gl`. `NodeViewport` preview rendering now also delegates viewport query, clear-color query, color-buffer clear mask, and blend-state tokens to `pp_renderer_gl`. `NodeImageTexture` preview drawing now delegates its fallback 2D texture bind target and blend-state tokens to `pp_renderer_gl`. `NodeImage` drawing and remote-image texture creation now delegate mipmapped sampler filters, blend-state tokens, 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 tokens to `pp_renderer_gl`. Simple UI text, text-input, border, scroll, and animation timeline draw paths now also delegate blend-state tokens to `pp_renderer_gl`. Canvas layer cube/equirect generation, clear, restore, and snapshot paths now also delegate cube/2D texture targets, active texture units, blend/clear state, and RGBA8 read/write pixel mapping to `pp_renderer_gl`. `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. Legacy `util.cpp` OpenGL error naming and `gl_state` save/restore now delegate error codes, state queries, framebuffer targets, texture binding targets, and active texture units to `pp_renderer_gl`. `NodeStrokePreview` brush preview rendering now delegates depth/scissor/blend state, viewport/clear-color queries, 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 with only local OpenGL adapter endpoints retained. 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 now route through the same backend dispatch contracts. 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`, app-core tests, and platform API tests. `package-smoke` now emits a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, and WebGL output, with blocked prerequisites tied to DEBT-0011. 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. 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: ```powershell 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 cmake --fresh --preset windows-clangcl-asan ``` 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. 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_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. - `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_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`, `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_startup_tests`, and `pano_cli` built after startup preference/runtime execution 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`. - `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, and oversized progress-total clamping. - `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. - `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, 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_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. - `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, 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_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. - `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, and invalid execution color rejection. - `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. - `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. - `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, and attribute-location lookups now execute through tested `pp_renderer_gl` dispatch contracts. Legacy shader source compilation, shader deletion, program attach/link, attribute rebinding, active-uniform count/enumeration, and uniform-location discovery also execute through tested `pp_renderer_gl` dispatch contracts, leaving only thin GL adapter functions in the retained `Shader` utility. Legacy `Shape` 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, leaving only thin GL adapter functions in the retained shape utility. Legacy `Font` text mesh creation now covers the one-VAO/deferred-upload case, and its dynamic index/vertex uploads and indexed draw calls execute through the same tested dispatch contracts. - `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. - 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, and WebGL outputs. Actual package building remains blocked by DEBT-0011 until those targets are migrated to root CMake. - Android arm64 configured with NDK 29.0.14206865 through the platform-build wrapper and compiled headless foundation/tool/test targets. - 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 the renderer GL backend mapping; 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, active texture units, 2D framebuffer-to-texture copy dispatch, RGBA8 readback formats, and RTT-backed transform history region readbacks through the renderer GL backend mapping. - `NodeCanvas` panorama UI rendering now routes sampler defaults, saved viewport/clear/blend/depth/scissor state, color clears, 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 and generic blend/depth/scissor capability changes now execute through tested `pp_renderer_gl` dispatch adapters. - 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 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 contains raw `GL_*` constants. - 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`. - 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: OpenVR desktop, 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.