# PanoPainter Modernization Roadmap Status: live Last updated: 2026-06-03 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 controller enablement, and canvas cursor mode; the live tools/options menu and `pano_cli plan-app-preferences` consume those contracts while legacy widgets and settings persistence execute them. 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; `App::title_update`, `App::update_memory_usage`, `App::update_rec_frames`, resolution helpers, and `pano_cli plan-app-status` consume those contracts while legacy UI nodes still render the strings. `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. 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 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 before legacy `Canvas` resize execution and `ActionManager` history clearing continue. `pano_cli plan-layer-rename` exposes the app-core layer rename decision used by the live layer rename dialog before legacy `Canvas` layer mutation and `ActionManager` undo wiring 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 before legacy `Canvas` and UI layer execution continue. `pano_cli plan-animation-operation` exposes app-core planning for animation frame add, duplicate, remove, duration adjustment, timeline moves, timeline goto/next/previous, and onion-size updates used by the live animation panel before legacy `Canvas`/`Layer` frame execution continues. `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 before legacy `Brush` mutation and resource loading continue. `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 before legacy `Canvas` mode, pen picking, touch-lock, and transform state mutation 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. `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 before legacy image loading, OpenGL texture updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. `pano_cli plan-history-operation` exposes app-core planning for undo, redo, and clear-history availability used by toolbar buttons and canvas shortcuts before legacy `ActionManager` stack execution continues. `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 before legacy `Brush`, color picker, stroke preview, and preset popup execution 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, image/file/save-file pickers, and directory pickers. 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. 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. `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. 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`. Windows OpenGL debug callback setup now dispatches through `PlatformServices`, moving Win32 console coloring, debug-output enablement, and debug-break callback behavior into `WindowsPlatformServices` while keeping other platform adapters as no-ops. 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 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 transitions and depth-buffer clears now use generic tested `pp_renderer_gl` capability 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::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. 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`. `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. `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. 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, and PBO pixel-buffer target/usage/access tokens used by `RTT` and `PBO` 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, texture/renderbuffer targets, depth format, framebuffer targets, binding queries, attachment points, and completion status used by `RTT::create` and framebuffer bind/restore paths, also live in `pp_renderer_gl`. RTT clear color/depth masks, renderer API render-pass color/depth/stencil clear-mask and clear-value mapping, and color-write-mask query tokens also live in `pp_renderer_gl`. `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 and resize path consume 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`. 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 units, 2D texture targets, copy targets, and sampler filters/wraps to `pp_renderer_gl`. 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 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 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. - `pp_paint_renderer_compositor_tests` passed. - `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. - `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, and invalid selection 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, layer add/duplicate/select/reorder/remove planning, metadata planning, bad-index rejection, bad-opacity rejection, bad-blend-mode rejection, and transient highlight behavior. - `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_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, and onion-size rejection. - `pano_cli_plan_animation_operation_add_smoke`, `pano_cli_plan_animation_operation_duration_floor_smoke`, `pano_cli_plan_animation_operation_next_wrap_smoke`, and `pano_cli_plan_animation_operation_rejects_remove_last_frame` 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, and stroke-settings refresh intent. - `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. - `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. - `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_history_ui_tests` passed, covering undo/redo availability, no-op history commands, clear-history stack/memory state, memory-only clear, and negative 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, and reset-state validation. - `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_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, 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, 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. - Canvas mode overlay, mask, and transform paths now route generic OpenGL blend/depth state, active texture units, 2D copy targets, and RGBA8 readback formats 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, copy targets, and RGBA8 render-target formats through the renderer GL backend mapping. - 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. - 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. - 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 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 renderbuffer/depth attachment parameters through the renderer GL backend mapping; `src/canvas.cpp` no longer contains raw `GL_*` constants. - 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.