diff --git a/CMakePresets.json b/CMakePresets.json index 71172c52..0a816bfa 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -269,6 +269,18 @@ } } }, + { + "name": "renderer-conformance", + "configurePreset": "windows-msvc-default", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "renderer-conformance" + } + } + }, { "name": "fuzz", "configurePreset": "windows-msvc-default", diff --git a/cmake/PanoPainterPackageTargets.cmake b/cmake/PanoPainterPackageTargets.cmake index 91d544ce..f4d8cfec 100644 --- a/cmake/PanoPainterPackageTargets.cmake +++ b/cmake/PanoPainterPackageTargets.cmake @@ -15,12 +15,34 @@ pp_add_powershell_automation_target(panopainter_windows_app_package_smoke -Target PanoPainter -CMakeCommand "${CMAKE_COMMAND}") +pp_add_powershell_automation_target(panopainter_windows_appx_package_readiness + COMMENT "Report Windows AppX package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -PackageKinds windows-appx) + +pp_add_powershell_automation_target(panopainter_apple_bundle_package_readiness + COMMENT "Report Apple bundle package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -PackageKinds apple-bundle) + pp_add_powershell_automation_target(panopainter_android_standard_native_package COMMENT "Build retained Android standard native package library." ARGUMENTS -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1" -Packages standard) +pp_add_powershell_automation_target(panopainter_android_standard_apk_package_readiness + COMMENT "Report Android standard APK package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -AndroidNativeChecks + -PackageKinds android-standard-apk) + pp_add_powershell_automation_target(panopainter_android_vr_native_package_configure COMMENT "Configure retained Android Quest/Focus native package paths." ARGUMENTS @@ -28,6 +50,22 @@ pp_add_powershell_automation_target(panopainter_android_vr_native_package_config -Packages quest,focus -ConfigureOnly) +pp_add_powershell_automation_target(panopainter_android_quest_apk_package_readiness + COMMENT "Report Android Quest APK package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -AndroidNativeChecks + -PackageKinds android-quest-apk) + +pp_add_powershell_automation_target(panopainter_android_focus_apk_package_readiness + COMMENT "Report Android Focus/Wave APK package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -AndroidNativeChecks + -PackageKinds android-focus-apk) + pp_add_powershell_automation_target(panopainter_android_native_package_smoke COMMENT "Run retained Android native package checks through package-smoke." ARGUMENTS @@ -42,3 +80,17 @@ pp_add_powershell_automation_target(panopainter_linux_webgl_package_readiness -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" -ReadinessOnly -PackageKinds linux-app,webgl) + +pp_add_powershell_automation_target(panopainter_linux_app_package_readiness + COMMENT "Report Linux app package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -PackageKinds linux-app) + +pp_add_powershell_automation_target(panopainter_webgl_package_readiness + COMMENT "Report WebGL package readiness blockers." + ARGUMENTS + -File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1" + -ReadinessOnly + -PackageKinds webgl) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 0a3e63f3..4ae77c84 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -203,18 +203,42 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p target before reporting the same readiness matrix. `package-smoke.ps1 -AndroidNativeChecks` also runs the retained Android standard `native-lib` build and Quest/Focus configure helper for selected Android package kinds, - then reports those native results beside the APK blocker matrix. + then reports those native results beside the APK readiness matrix. Both + wrappers now classify each platform gate as `validated`, `compile-only`, or + `blocked` based on local prerequisites and whether a real root CMake package + target exists. `scripts/dev/check_package_smoke_readiness.py`, registered as `panopainter_package_smoke_readiness_self_test`, verifies both wrappers keep - the same seven package kinds, blocked DEBT-0011 status, readiness-only mode, + the same seven package kinds, readiness status modes, readiness-only mode, retained Android native-check prerequisite metadata, retained Linux/WebGL CMake baseline metadata, and root CMake package validation target names. +- `scripts/dev/check_component_boundaries.py`, registered as + `panopainter_component_boundary_self_test`, checks pure component source files in + `pp_foundation`, `pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, + `pp_paint_renderer`, `pp_ui_core`, and `pp_app_core` for forbidden platform + SDK/backend includes, `App::I`/`Canvas::I` leakage, and disallowed `pp_*` + target dependencies before desktop-fast validation. Temporary allowed-platform + edges remain explicitly tracked by DEBT-0003 and DEBT-0008. +- `scripts/dev/check_renderer_conformance_matrix.py`, registered as + `panopainter_renderer_conformance_matrix_self_test`, verifies renderer conformance + tests are labeled with the shared `renderer-conformance` matrix profile. +- `scripts/dev/check_renderer_api_contract.py`, registered as + `panopainter_renderer_api_contract_self_test`, enforces backend-neutral boundary + rules for `pp_renderer_api` and `pp_paint_renderer`, including forbidden + backend symbols/includes in these components before renderer contract milestones. - Root CMake exposes non-default package validation targets: `panopainter_package_readiness`, `panopainter_windows_app_package_smoke`, + `panopainter_windows_appx_package_readiness`, + `panopainter_apple_bundle_package_readiness`, `panopainter_android_standard_native_package`, + `panopainter_android_standard_apk_package_readiness`, + `panopainter_android_quest_apk_package_readiness`, + `panopainter_android_focus_apk_package_readiness`, `panopainter_android_vr_native_package_configure`, and `panopainter_android_native_package_smoke`, and + `panopainter_linux_app_package_readiness`, + `panopainter_webgl_package_readiness`, and `panopainter_linux_webgl_package_readiness`. These targets call the automation scripts from CMake but do not convert APK/AppX/Linux/Apple/WebGL package outputs to root CMake package targets yet. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index e6867e44..70276153 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,12 +22,51 @@ agent or engineer to remove them without reconstructing context from chat. routes the retained pre-dispatch state capture through local commit helpers; the live path still owns concrete layer/action mutation until the retained commit adapter is fully split from `Canvas`. +- 2026-06-14: `DEBT-0063` was narrowed again. Open/Browse + delete-confirmation now uses handle-based open/close in + `src/node_dialog_open.cpp` and `src/node_dialog_browse.cpp`, with + `attach_legacy_overlay_node_to_root` fallback removed; migration still pending in + remaining panel families. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_combobox.cpp` now uses + overlay handle open/close (`open_legacy_overlay_node_with_handle` and + `close_legacy_overlay_node`) for popup lifecycle, with the older attach/close + overlay path removed in this file. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_stroke.cpp` now + routes the stroke preset/brush/dual-brush/pattern popup open-close flow through + checked overlay handles and explicit partial-open cleanup; migration is still + pending in remaining panel families. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_brush.cpp` now + routes brush popup-menu open-close flow through checked overlay handles and + handle-based close on selection. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/legacy_quick_ui_services.cpp` + now converts quick brush/color popup lifecycle from raw attach+bind-destroy to + checked-overlay open/close (`open_legacy_overlay_node_with_handle` and + `close_legacy_overlay_node`) with explicit handle invalidation on closure. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_popup_menu.h/.cpp` + now routes popup-menu close through checked overlay handles and removes direct + `close_legacy_popup_overlay(*this)` usage. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_layer.h/.cpp` now + uses shared ownership for current-layer/m_layers tracking, guards stale-layer + callback/mutation paths, and routes popup outside-click close through + `close_legacy_popup_panel`; migration remains open in other panel/dialog + families. +- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_dialog_picker.cpp` now + inlines outside-click/autohide popup close through capture release, parent + detach, and guarded `on_popup_close` dispatch instead of routing that path + through `close_legacy_popup_panel(*this, on_popup_close)`. - 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_draw()` now routes the retained main-pass request assembly through helper APIs and keeps the live callsite focused on branch selection and pass dispatch. -- 2026-06-14: `DEBT-0064` was opened for the compositor test-local - `Texture2D::bind()` shim needed while planner tests exercise retained inline - preview helpers before tests can link a non-app legacy texture boundary. +- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_draw()` now + routes the main-pass destination texture binding declaration through + `Canvas::make_stroke_draw_main_pass_request`; `Canvas::stroke_draw()` now only + assembles execution inputs and dispatches the retained main-pass callback chain. +- 2026-06-14: `DEBT-0064` was narrowed again. Stroke-preview result copy now + routes through `pp::paint_renderer::copy_stroke_preview_result_to_texture(...)` + in `src/paint_renderer/compositor.h`, and the node-level copy wrapper, + final-composite wrapper, and pass-sequence wrapper are gone. The compositor + test-local `Texture2D::bind()` shim is also gone, while live preview still + owns concrete texture binding and remaining OpenGL copy execution. - 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()` now routes the retained request-dispatch invocation through `execute_canvas_stroke_commit_dispatch(...)`; the wrapper still owns the @@ -1216,6 +1255,12 @@ agent or engineer to remove them without reconstructing context from chat. `panopainter_windows_app_package_smoke`, which calls the full Windows `package-smoke` command from CMake so the app executable/runtime payload check and Windows AppX blocker matrix are available from the CMake target graph. +- 2026-06-14: DEBT-0011 was narrowed again. `package-smoke.ps1` and + `package-smoke.sh` now classify package readiness as `validated`, + `compile-only`, or `blocked` instead of reporting every platform gate as + blocked, and root CMake now exposes per-platform readiness targets for + Windows AppX, Apple bundles, Android standard/Quest/Focus APKs, Linux app, + and WebGL while real package outputs remain debt-tracked. - 2026-06-05: DEBT-0007 was narrowed. `platform-build.ps1` now resolves `VCPKG_ROOT` for vcpkg presets from the environment or bundled Visual Studio installs, reports the selected vcpkg root in JSON, and root CMake exposes @@ -1726,7 +1771,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | | DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, pure equirectangular PNG export, pure equirectangular JPEG+XMP export, pure layer/animation-frame PNG collection export, pure depth image/depth PNG export for payload-complete snapshots, shared document-frame export readiness reporting, depth export render-plan reporting, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning and write/publish service dispatch with retained fallback, live PNG/JPEG equirectangular writer execution using the paint-renderer equirectangular exports plus app-core file write/publish dispatch with retained fallback, live payload-complete layer/animation-frame collection writer execution using paint-renderer PNG sequences and app-core collection write/publish dispatch with retained fallback, live payload-complete depth export execution using pure paint-renderer PNG payloads plus app-core two-payload writing with retained fallback, tested app-core document-snapshot export route policy for writer versus retained fallback including current-platform support, and live equirectangular/layer/animation-frame/depth/cube-face export snapshot/render/export-readiness reporting through the shared readiness helper plus the depth render plan, but action-command adoption, live save-writer replacement, Web and incomplete-readback collection handoff, progress/threading parity, broader renderer-owned export execution, exact GPU/golden parity, live-camera depth parity, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pp_app_core_document_export_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_plan_export_snapshot_route_pending_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | -| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, and reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, and retained Linux/WebGL CMake baseline metadata are reachable from package validation and root CMake package-readiness targets, but Windows AppX/APK/Linux/Apple/WebGL package outputs are still `blocked` because root CMake package targets do not exist yet | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present | +| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output, and now classifies each package gate as `validated`, `compile-only`, or `blocked` based on local prerequisites plus root CMake package-target ownership; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, retained Linux/WebGL CMake baseline metadata, and per-platform readiness targets are reachable from package validation and root CMake package-readiness targets, but Windows AppX and Apple/WebGL outputs remain blocked where local toolchains or root package targets are missing and Android/Linux package readiness remains compile-only until root CMake owns real package targets | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_appx_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_apple_bundle_package_readiness`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_standard_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_quest_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_focus_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_app_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_webgl_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | | DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, decoded pixel attachment to `pp_document`, live save-path snapshot-readiness reporting, and app-core canvas-snapshot-to-PPI export automation, but full legacy PPI round-trip parity and pure live save writer replacement are not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | @@ -1772,11 +1817,11 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0055 | Open | Modernization | `src/app.h` now forward-declares retained iOS/macOS/Android/Linux/Web platform handles instead of including platform SDK headers, and full SDK includes are isolated in `src/platform_legacy/legacy_platform_services.cpp`, but the `App` singleton still stores those platform handles directly | Reduce central header platform coupling incrementally without rewriting non-Windows platform entrypoints before Phase 6 | Windows app build; Apple/Android/Linux/Web package smoke once platform root builds are active | Platform handles are owned by injected `pp_platform_*` shell state or services, and `App` has no platform SDK handle fields or platform conditional members | | DEBT-0056 | Open | Modernization | `src/asset.h` is now Android-SDK-free and uses opaque Android asset handles behind `Asset::set_android_asset_manager`, but retained `Asset` still owns a static Android asset-manager bridge and `src/asset.cpp` still performs Android `AAssetManager` reads directly; the current `android-arm64` root preset is headless and does not expose `pp_legacy_assets_io`, though the retained Android standard package `native-lib` now builds through its refreshed C++23 CMake path | Reduce legacy asset I/O header coupling without rewriting Android asset loading before the asset manager/storage boundary exists | Windows app build; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 -Targets pp_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | Android asset loading is owned by injected asset storage/platform services or `pp_assets` file providers, with no static Android asset manager on `Asset` | | DEBT-0061 | Open | Modernization | Desktop XR runtime selection now lives in tested `pp_platform_api` policy and prefers OpenXR, but `WindowsPlatformServices` still reports OpenXR unavailable and reaches the retained OpenVR SDK bridge as a legacy fallback; Windows runtime deployment copies `openvr_api.dll` beside `PanoPainter.exe` until that fallback is removed | Preserve current desktop VR behavior while replacing OpenVR with OpenXR behind the platform/renderer boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add an OpenXR SDK/package target, implement desktop OpenXR startup/shutdown/pose/controller submission behind `pp_platform_vr` or `PlatformServices`, validate parity with mocked/runtime smoke coverage, and remove `libs/openvr` plus the OpenVR link/include paths from root CMake | -| DEBT-0063 | Open | Modernization | The retained UI tree still exposes `Node* m_parent`, public `std::vector> m_children`, raw `find()` lookup results, `add_child()` allocation through `new`, callbacks/observers that take or capture raw `Node*`, and manual `destroy()`/`m_destroyed` semantics. `pp_ui_core` now owns a tested `NodeLifetimeTree` target model with checked node handles, scoped callback connections, subtree destruction, pointer/keyboard capture release, whole-tree clear for layout reload, and mutation-safe dispatch, plus a tested `UiOverlayLifetime` popup/dialog stack model. Retained app-dialog root insertion, app-menu popup template cloning/root attachment, quick/stroke/brush panel popup root attachment, combo-box popup insertion, Open/Browse delete-confirmation dialog insertion, popup tick decoration insertion, top-toolbar panel popup insertion, repeated retained popup activation flag setup, repeated retained popup close/release execution, popup tick-decoration close callback wiring, and popup-panel outside-click release/remove/callback dispatch are now centralized in `src/legacy_ui_overlay_services.*`, but retained `Node`/`NodePopupMenu`/`NodeDialog*` still have not adopted checked handles or scoped callback ownership | Preserve current UI behavior while making panel/dialog extraction safe instead of spreading lifetime hazards into the new architecture | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; future `pp_panopainter_ui_dialog_lifetime_tests`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Retained `Node` and `pp_panopainter_ui` adopt checked node handles or equivalent non-owning references, scoped callback connection/disconnect semantics, mutation-safe event dispatch, parent/child invariants hidden behind APIs, and destroy-during-callback/capture-release/popup-close/layout-reload tests; retained `Node*` APIs are removed or isolated behind compatibility adapters | +| DEBT-0063 | Open | Modernization | `pp_ui_core` now owns tested `NodeLifetimeTree` and `UiOverlayLifetime` models for checked handles, scoped callback connections, subtree destruction, capture release, and mutation-safe dispatch. `src/node_panel_layer.h/.cpp`, `src/node_combobox.cpp`, `src/node_dialog_open.cpp`, `src/node_dialog_browse.cpp`, `src/node_panel_stroke.cpp`, `src/node_panel_brush.cpp`, `src/node_dialog_picker.cpp`, `src/legacy_quick_ui_services.cpp`, and `src/node_popup_menu.h/.cpp` now route popup/dialog/menu lifetime through checked overlay handles and handle-based close; migration remains open in other legacy panel/dialog families | Preserve current UI behavior while completing safe panel/dialog lifetime migration incrementally | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; `tests/ui_core/node_lifetime_tests.cpp:destroy_subtree_clears_child_connections`; `tests/ui_core/overlay_lifetime_tests.cpp:double_close_overlay_returns_invalid_argument`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_app pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests` | Remaining legacy popup/dialog families still use non-handle ownership and open lifetimes; migration stays open until their surfaces are converted, including lifecycle safety parity checks | | DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api` policy behind injectable `pp::platform::WebPlatformServices`, but the Web shell still reaches the default implementation through the retained fallback until a dedicated Web service is injected directly | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback | | DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata, including message-dialog OK/cancel captions, now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`; live execution is centralized in `src/legacy_app_dialog_services.*`, retained root insertion now routes through `src/legacy_ui_overlay_services.*`, and whats-new dialog state persistence routes through `src/legacy_preference_storage.*`, but the bridge still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances with raw callback/lifetime ownership | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters | | DEBT-0059 | Open | Modernization | iOS root CMake headless builds assign generated bundle identifiers and disable code signing for executable test/tool targets | The current Apple gate is compile validation for shared component targets; signed iOS app/package validation is not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device`; `sh scripts/automation/platform-build.sh "ios-device"` on `panopainter-mac` | Root CMake owns the signed Apple app/package targets, package-smoke validates Apple bundles where signing material is available, and headless iOS test/tool targets are either excluded from signed package builds or use explicit test-runner signing policy | -| DEBT-0064 | Open | Modernization | `pp_paint_renderer_compositor_tests` has a test-local no-op `Texture2D::bind()` definition so retained inline stroke-preview copy helpers can be covered without linking the full app-only legacy texture implementation | `tests` are configured before `pp_legacy_engine`, and linking the app legacy texture object into this planner test would couple the renderer-neutral test target back to the app target graph | `cmake --build --preset windows-msvc-default --config Debug --target pp_paint_renderer_compositor_tests`; `ctest --preset desktop-fast --build-config Debug -R "pp_paint_renderer_compositor" --output-on-failure` | Move retained preview copy behavior behind a pure callback-only test seam or split legacy texture binding into a linkable non-app test support target, then delete the test-local `Texture2D::bind()` shim | +| DEBT-0064 | Open | Modernization | Stroke-preview result copy now routes through the renderer-facing `pp::paint_renderer::copy_stroke_preview_result_to_texture(...)` callback-only contract in `src/paint_renderer/compositor.h`; the node-level copy wrapper, final-composite wrapper, and pass-sequence wrapper are removed, and `pp_paint_renderer_compositor_tests` no longer needs either a test-local `Texture2D::bind()` definition. Live preview still owns concrete texture binding and OpenGL execution around the remaining preview flow | `tests` are configured before `pp_legacy_engine`, and linking the app legacy texture object into this planner test would still couple the renderer-neutral test target back to the app target graph | `cmake --build --preset windows-msvc-default --config Debug --target pp_paint_renderer_compositor_tests`; `ctest --preset desktop-fast --build-config Debug -R "pp_paint_renderer_compositor" --output-on-failure` | Keep preview-copy behavior on renderer-facing callback-only seams and move the remaining live preview copy execution behind renderer-owned services so the remaining preview flow can become fully app-texture-free | ## Closed Debt diff --git a/docs/modernization/renderer_api_contract.md b/docs/modernization/renderer_api_contract.md new file mode 100644 index 00000000..710f5ce2 --- /dev/null +++ b/docs/modernization/renderer_api_contract.md @@ -0,0 +1,115 @@ +# Renderer API Backend-Neutral Contract + +## Purpose + +`pp_renderer_api` defines the backend-neutral rendering contract used by `pp_paint_renderer` +and the higher-level app core. This document captures the minimum behavior that any +concrete backend (`pp_renderer_gl` today, Vulkan/Metal later) must preserve. + +## Contract Scope + +- Public interfaces: + - `pp::renderer::IRenderDevice` + - `pp::renderer::ICommandContext` + - `pp::renderer::ITexture2D` + - `pp::renderer::IRenderTarget` + - `pp::renderer::IShaderProgram` + - `pp::renderer::IMesh` + - `pp::renderer::IReadbackBuffer` + - `pp::renderer::IRenderTrace` + - `pp::renderer::Recording*` helpers in `renderer_api/recording_renderer.*` +- Validation helpers in `renderer_api.h` and shader catalog helpers in + `renderer_api/shader_catalog.*` + +## Behavioral Invariants + +- No exceptions are part of API control flow; failures are reported through + `pp::foundation::Status` / `pp::foundation::Result`. +- Object lifetimes remain backend-owned; API consumers pass references/handles only. +- Resource descriptors and command/state descriptors must be validated and constrained by the + helper functions. +- Backends may reject unsupported operations via explicit non-OK status but must not mutate + visible program state before reporting failure. +- Error codes and debug names are deterministic and backend-neutral (human-readable and + test-stable where feasible). + +## Surface Contracts + +1. `IRenderDevice` + +- `backend_name()` identifies backend family. +- `features()` returns capability bits used for planner decisions. +- Resource creation methods return `Result` and report allocation/validation failures. +- `immediate_context()` is stable for the lifetime of the device object. +- `trace()` may return `nullptr`; callers must tolerate no trace provider. + +2. `ICommandContext` + +- State mutation (`set_viewport`, `set_scissor`, blend/depth/shader/sampler/mesh/program binds) is + explicit and backend-agnostic. +- Command methods that can fail must return status. +- `end_render_pass()` is always side-effect safe and non-throwing. +- `read_texture` / `capture_frame` readback contracts are byte-sized and descriptor-driven. +- Texture upload/copy/readback/transition methods must respect descriptor bounds and ordering rules. + +3. Resource descriptors and helpers + +- `TextureDesc`, `Extent2D`, `Viewport`, `ScissorRect`, `RenderPassDesc`, + `TextureUsage`, `TextureState`, `BlendState`, `DepthState`, sampler/topology enums + are shared semantic vocabulary across backends. +- Validation helpers (`validate_*`) are the compatibility fence for contract behavior. +- `PaintFeedbackPlan` and `plan_paint_feedback(...)` are the feature/algorithm decision seam + for framebuffer feedback vs ping-pong workflows. + +4. Trace and recording + +- `IRenderTrace` is optional and may be elided, but implementations should support scoped markers + and markers where used. +- Recording backend (`RecordingRenderDevice`, `RecordingCommandContext`) must preserve command + order and reject invalid sequences through status/command visibility. + +## Feature semantics + +Backends are expected to honor all feature bits consistently: + +- `framebuffer_fetch` +- `explicit_texture_transitions` +- `texture_copy` +- `render_target_blit` +- `frame_capture` +- `float16_render_targets` +- `float32_render_targets` +- `float32_linear_filtering` + +Feature gates must be enforced by planners before issuing backend commands. + +## Existing conformance coverage + +Current renderer-api conformance tests (non-backend): + +- `pp_renderer_api_tests` +- `pp_renderer_api` test cases: + - `validates_texture_usage_contract` + - `validates_texture_transition_contract` + - `validates_mipmap_generation_contract` + - `validates_texture_copy_contract` + - `validates_blit_contract` + - `plans_paint_feedback_paths` + - `renderer_interfaces_support_backend_neutral_dispatch` + - `recording_renderer_*` command-sequence and validation tests + +OpenGL-specific conformance remains in `pp_renderer_gl` suites: + +- `pp_renderer_gl_capabilities_tests` +- `pp_renderer_gl_command_plan_tests` +- `pp_renderer_gl_gpu_readback_tests` (where GPU context is available) +- `panopainter_renderer_conformance_matrix_self_test` +- `ctest --preset renderer-conformance` +- `panopainter_renderer_api_contract_self_test` (tooling guard for renderer API and paint renderer + backend-neutral contract source purity). + +## Open items for RND-007 + +- Ensure Vulkan/Metal planning/lifecycle tests run the same contract surfaces without backend leakage. +- Keep `pp_renderer_api` implementation/usage free from backend-only headers and raw platform state. +- Keep new backend labs opt-in until this contract and conformance matrix are complete. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 7e37e993..1a5e3886 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1836,7 +1836,7 @@ Gate: Goal: prepare Vulkan and Metal without destabilizing the OpenGL parity path. -Status: not started. Do not start production Vulkan or Metal work until +Status: in progress. Do not start production Vulkan or Metal work until `RND-007` and `RND-008` freeze and validate the renderer API backend contract. `RND-009` and `RND-010` are opt-in lab targets only and must stay outside the production app path. @@ -1848,6 +1848,14 @@ Implementation tasks: - `pp_renderer_metal_lab` - Use `D:\Dev\vkpaint` as reference material for Vulkan painting experiments, not as direct production code. +- Record and enforce the renderer API contract surface in + `docs/modernization/renderer_api_contract.md` and keep + `panopainter_renderer_api_contract_self_test` as a precondition for + `RND-007` closeout. +- Add and keep a renderer-conformance matrix fixture profile using + `renderer-conformance` labels plus `ctest --preset renderer-conformance` and + `ctest --preset desktop-gpu`, enforced by + `panopainter_renderer_conformance_matrix_self_test` before `RND-008` closeout. - Before integration, prove: - ping-pong compositing path - input-attachment/subpass path where applicable diff --git a/scripts/automation/package-smoke.ps1 b/scripts/automation/package-smoke.ps1 index 071a44ac..18154507 100644 --- a/scripts/automation/package-smoke.ps1 +++ b/scripts/automation/package-smoke.ps1 @@ -99,6 +99,89 @@ function New-PackageReadiness { } } +function Resolve-PackageStatus { + param( + [bool]$RootCMakePackageTargetAvailable, + [bool]$GateBlocked, + [object[]]$Prerequisites + ) + + if ($GateBlocked) { + return "blocked" + } + + foreach ($prerequisite in $Prerequisites) { + if ($prerequisite.name -eq "root-cmake-package-target") { + continue + } + if (-not $prerequisite.available) { + return "blocked" + } + } + + if ($RootCMakePackageTargetAvailable) { + return "validated" + } + + return "compile-only" +} + +function Get-AndroidNativeCheckInfo { + param( + [string]$Kind, + [bool]$AndroidNativeChecks, + [object]$AndroidNativeValidation + ) + + $command = switch ($Kind) { + "android-standard-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard" } + "android-quest-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly" } + "android-focus-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly" } + default { "" } + } + + if (-not $AndroidNativeChecks) { + return @{ Available = $true; Detail = "$command (not run)" } + } + + if ($command.Length -eq 0) { + return @{ Available = $false; Detail = "No Android native check plan for kind '$Kind'" } + } + + $packages = switch ($Kind) { + "android-standard-apk" { @("standard") } + "android-quest-apk" { @("quest") } + "android-focus-apk" { @("focus") } + default { @() } + } + + $result = $null + foreach ($entry in $AndroidNativeValidation.results) { + $hasAll = $true + foreach ($pkg in $packages) { + if (-not ($entry.packages -contains $pkg)) { + $hasAll = $false + break + } + } + + if ($hasAll) { + $result = $entry + break + } + } + + if (-not $result) { + return @{ Available = $false; Detail = "$command (not executed)" } + } + + if ($result.exitCode -ne 0) { + return @{ Available = $false; Detail = "$command (exit $($result.exitCode))" } + } + + return @{ Available = $true; Detail = $command } +} + function Get-AndroidNativeCheckPlan { param([string[]]$Kinds) @@ -188,18 +271,20 @@ function Get-PackageReadiness { $wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj" $manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest" $appPackages = Join-Path $root "PanoPainterPackage/AppPackages" + $prerequisites = @( + (New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj), + (New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), + (New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"), + (New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $true -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" ` -ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" ` - -Prerequisites @( - (New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj), - (New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), - (New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"), - (New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container") ) @@ -208,19 +293,22 @@ function Get-PackageReadiness { $gradle = Join-Path $root "android/android/build.gradle" $manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml" $apkDir = Join-Path $root "android/android/build/outputs/apk" + $androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation + $prerequisites = @( + (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), + (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), + (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), + (New-Prerequisite -Name "retained-native-cmake-check" -Available $androidNativeCheck.Available -Detail $androidNativeCheck.Detail), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "legacy-gradle-package-not-consuming-root-cmake-targets" ` -ValidationCommand "gradle -p android/android assembleDebug" ` - -Prerequisites @( - (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), - (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), - (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), - (New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container") ) @@ -229,19 +317,22 @@ function Get-PackageReadiness { $gradle = Join-Path $root "android/quest/build.gradle" $manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml" $apkDir = Join-Path $root "android/quest/build/outputs/apk" + $androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation + $prerequisites = @( + (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), + (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), + (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), + (New-Prerequisite -Name "retained-native-cmake-check" -Available $androidNativeCheck.Available -Detail $androidNativeCheck.Detail), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "legacy-gradle-package-not-consuming-root-cmake-targets" ` -ValidationCommand "gradle -p android/quest assembleDebug" ` - -Prerequisites @( - (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), - (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), - (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), - (New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container") ) @@ -250,19 +341,22 @@ function Get-PackageReadiness { $gradle = Join-Path $root "android/focus/build.gradle" $manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml" $apkDir = Join-Path $root "android/focus/build/outputs/apk" + $androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation + $prerequisites = @( + (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), + (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), + (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), + (New-Prerequisite -Name "retained-native-cmake-check" -Available $androidNativeCheck.Available -Detail $androidNativeCheck.Detail), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "legacy-gradle-package-not-consuming-root-cmake-targets" ` -ValidationCommand "gradle -p android/focus assembleDebug" ` - -Prerequisites @( - (New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle), - (New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest), - (New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"), - (New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container") ) @@ -270,17 +364,19 @@ function Get-PackageReadiness { "apple-bundle" { $xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj" $bundleDir = Join-Path $root "out/package/apple" + $prerequisites = @( + (New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject), + (New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $true -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" ` -ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" ` - -Prerequisites @( - (New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject), - (New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container") ) @@ -288,18 +384,20 @@ function Get-PackageReadiness { "linux-app" { $linuxCmake = Join-Path $root "linux/CMakeLists.txt" $linuxBinary = Join-Path $root "out/package/linux/panopainter" + $prerequisites = @( + (New-Prerequisite -Name "retained-linux-cmake" -Available (Test-Path -LiteralPath $linuxCmake -PathType Leaf) -Detail $linuxCmake), + (New-Prerequisite -Name "cmake" -Available (Test-CommandAvailable "cmake") -Detail "Linux retained app CMake configure/build tool"), + (New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "linux-clang"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "retained-linux-cmake-not-consuming-root-cmake-targets" ` -ValidationCommand "cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter" ` - -Prerequisites @( - (New-Prerequisite -Name "retained-linux-cmake" -Available (Test-Path -LiteralPath $linuxCmake -PathType Leaf) -Detail $linuxCmake), - (New-Prerequisite -Name "cmake" -Available (Test-CommandAvailable "cmake") -Detail "Linux retained app CMake configure/build tool"), - (New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "linux-clang"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "linux-app-output" -Path $linuxBinary -PathType "Leaf") ) @@ -307,19 +405,21 @@ function Get-PackageReadiness { "webgl" { $webglCmake = Join-Path $root "webgl/CMakeLists.txt" $webDir = Join-Path $root "out/package/webgl" + $prerequisites = @( + (New-Prerequisite -Name "retained-webgl-cmake" -Available (Test-Path -LiteralPath $webglCmake -PathType Leaf) -Detail $webglCmake), + (New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"), + (New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"), + (New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"), + (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"), + (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") + ) + $status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites $readiness += New-PackageReadiness ` -Kind $kind ` - -Status "blocked" ` + -Status $status ` -Reason "retained-webgl-cmake-not-consuming-root-cmake-targets" ` -ValidationCommand "emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter" ` - -Prerequisites @( - (New-Prerequisite -Name "retained-webgl-cmake" -Available (Test-Path -LiteralPath $webglCmake -PathType Leaf) -Detail $webglCmake), - (New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"), - (New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"), - (New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"), - (New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"), - (New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet") - ) ` + -Prerequisites $prerequisites ` -Artifacts @( (New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container") ) diff --git a/scripts/automation/package-smoke.sh b/scripts/automation/package-smoke.sh index f5ca970d..45f86a0f 100644 --- a/scripts/automation/package-smoke.sh +++ b/scripts/automation/package-smoke.sh @@ -1,33 +1,100 @@ #!/usr/bin/env sh set -u -preset="${1:-linux-clang}" -configuration="${2:-Debug}" -target="${3:-PanoPainter}" -artifact="${4:-out/build/$preset/$target}" +preset="linux-clang" +configuration="Debug" +target="PanoPainter" +cmake_command="cmake" +artifact="out/build/$preset/$target" readiness_only=0 -if [ "${1:-}" = "--readiness-only" ]; then - readiness_only=1 - preset="${2:-linux-clang}" - configuration="${3:-Debug}" - target="${4:-PanoPainter}" - artifact="${5:-out/build/$preset/$target}" +android_native_checks=0 +package_kinds="windows-appx,android-standard-apk,android-quest-apk,android-focus-apk,apple-bundle,linux-app,webgl" + +while [ "$#" -gt 0 ]; do + case "$1" in + --readiness-only) + readiness_only=1 + shift + ;; + --android-native-checks) + android_native_checks=1 + shift + ;; + --package-kinds=*) + package_kinds="${1#*=}" + shift + ;; + --package-kinds) + shift + if [ "$#" -gt 0 ]; then + package_kinds="$1" + shift + fi + ;; + --) + shift + break + ;; + -*) + echo "Unknown option: $1" >&2 + exit 1 + ;; + *) + break + ;; + esac +done + +if [ "$#" -ge 1 ]; then + preset="${1:-$preset}" + configuration="${2:-$configuration}" + target="${3:-$target}" + artifact="${4:-out/build/$preset/$target}" fi + start="$(date +%s)" root="$(pwd)" +package_kinds="$(printf "%s" "$package_kinds" | tr -d " ")" + +json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r/\\r/g; s/\n/\\n/g' +} json_string() { - printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')" + printf '"%s"' "$(json_escape "$1")" } json_bool() { - if [ "$1" = "1" ]; then + if [ "$1" -eq 1 ]; then printf true else printf false fi } +json_array_from_csv() { + local csv="$1" + local first=1 + local value + local items="" + + IFS=',' + for value in $csv; do + if [ -z "$value" ]; then + continue + fi + if [ "$first" -eq 1 ]; then + first=0 + else + items="${items}," + fi + items="${items}$(json_string "$value")" + done + unset IFS + + printf "[%s]" "$items" +} + command_available() { command -v "$1" >/dev/null 2>&1 } @@ -40,93 +107,550 @@ dir_available() { [ -d "$1" ] } -package_readiness_json() { +resolve_status() { + local root_target="$1" + local gate_blocked="$2" + shift 2 + + if [ "$gate_blocked" -ne 0 ]; then + printf "blocked" + return + fi + + for prereq in "$@"; do + if [ "$prereq" -ne 1 ]; then + printf "blocked" + return + fi + done + + if [ "$root_target" -ne 0 ]; then + printf "validated" + else + printf "compile-only" + fi +} + +append_json_item() { + if [ -z "$package_readiness" ]; then + package_readiness="$1" + else + package_readiness="${package_readiness},$1" + fi +} + +append_result_item() { + if [ -z "$android_native_results" ]; then + android_native_results="$1" + else + android_native_results="${android_native_results},$1" + fi +} + +prerequisite_entry() { + local name="$1" + local available="$2" + local detail="$3" + + printf '{' + printf '"name":%s,' "$(json_string "$name")" + printf '"available":%s,' "$(json_bool "$available")" + printf '"detail":%s' "$(json_string "$detail")" + printf '}' +} + +artifact_entry() { + local name="$1" + local path="$2" + local path_type="$3" + local exists=0 + + printf '{' + printf '"name":%s,' "$(json_string "$name")" + printf '"path":%s,' "$(json_string "$path")" + printf '"pathType":%s,' "$(json_string "$path_type")" + if [ "$path_type" = "Leaf" ]; then + [ -f "$path" ] + exists=$([ $? -eq 0 ] && printf "1" || printf "0") + elif [ "$path_type" = "Container" ]; then + [ -d "$path" ] + exists=$([ $? -eq 0 ] && printf "1" || printf "0") + else + [ -e "$path" ] + exists=$([ $? -eq 0 ] && printf "1" || printf "0") + fi + printf '"exists":%s' "$(json_bool "$exists")" + printf '}' +} + +check_entry() { + local name="$1" + local path="$2" + local exists="$3" + + printf '{' + printf '"name":%s,' "$(json_string "$name")" + printf '"path":%s,' "$(json_string "$path")" + printf '"exists":%s' "$(json_bool "$exists")" + printf '}' +} + +is_kind_requested() { + case ",${package_kinds}," in + *,"$1",*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +run_android_native_check() { + local packages="$1" + local configure_only="$2" + local command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $packages" + if [ "$configure_only" -ne 0 ]; then + command="${command} -ConfigureOnly" + fi + + if ! command_available powershell; then + android_native_last_exit_code=127 + printf '{"packages":%s,"configureOnly":%s,"exitCode":%s,"command":%s,"summary":null}' \ + "$(json_array_from_csv "$packages")" \ + "$(json_bool "$configure_only")" \ + "$android_native_last_exit_code" \ + "$(json_string "$command")" + return + fi + + if [ "$configure_only" -ne 0 ]; then + output="$(powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages "$packages" -ConfigureOnly 2>&1)" + else + output="$(powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages "$packages" 2>&1)" + fi + android_native_last_exit_code=$? + + summary="$(printf '%s\n' "$output" | awk 'BEGIN{line="";} /^\{/{line=$0} END{if (line != "") print line}')" + if [ -z "$summary" ]; then + summary="null" + fi + + printf '{"packages":%s,"configureOnly":%s,"exitCode":%s,"command":%s,"summary":%s}' \ + "$(json_array_from_csv "$packages")" \ + "$(json_bool "$configure_only")" \ + "$android_native_last_exit_code" \ + "$(json_string "$command")" \ + "$summary" +} + +extract_exit_code() { + printf '%s' "$1" | awk -F '"exitCode":' 'NF == 2 { gsub(/[^0-9].*/, "", $2); print $2 }' +} + +build_android_native_validation() { + local standard_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard" + local qf_packages="" + local qf_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $qf_packages -ConfigureOnly" + + local request_standard=0 + local request_qf=0 + + if is_kind_requested "android-standard-apk"; then + request_standard=1 + fi + if is_kind_requested "android-quest-apk" || is_kind_requested "android-focus-apk"; then + request_qf=1 + if is_kind_requested "android-quest-apk"; then + qf_packages="quest" + fi + if is_kind_requested "android-focus-apk"; then + if [ -n "$qf_packages" ]; then + qf_packages="${qf_packages},focus" + else + qf_packages="focus" + fi + fi + qf_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $qf_packages -ConfigureOnly" + fi + + android_native_standard_available=1 + android_native_quest_available=1 + android_native_focus_available=1 + android_native_standard_detail="${standard_command} (not run)" + android_native_quest_detail="${qf_command} (not run)" + android_native_focus_detail="${qf_command} (not run)" + + local requested="false" + local exit_code=0 + android_native_results="" + android_native_last_exit_code=0 + + if [ "$android_native_checks" -eq 0 ]; then + android_native_validation='{"requested":false,"exitCode":0,"results":[]}' + return + fi + + if [ "$request_standard" -eq 1 ]; then + requested="true" + standard_result="$(run_android_native_check "standard" 0)" + append_result_item "$standard_result" + standard_exit_code="$(extract_exit_code "$standard_result")" + if [ -z "$standard_exit_code" ]; then + standard_exit_code=0 + fi + if [ "$standard_exit_code" -ne 0 ] && [ "$exit_code" -eq 0 ]; then + exit_code="$standard_exit_code" + fi + + if [ "$standard_exit_code" -eq 0 ]; then + android_native_standard_available=1 + android_native_standard_detail="$standard_command" + else + android_native_standard_available=0 + android_native_standard_detail="${standard_command} (exit $standard_exit_code)" + fi + fi + + if [ "$request_qf" -eq 1 ] && [ -n "$qf_packages" ]; then + requested="true" + qf_result="$(run_android_native_check "$qf_packages" 1)" + append_result_item "$qf_result" + qf_exit_code="$(extract_exit_code "$qf_result")" + if [ -z "$qf_exit_code" ]; then + qf_exit_code=0 + fi + if [ "$qf_exit_code" -ne 0 ] && [ "$exit_code" -eq 0 ]; then + exit_code="$qf_exit_code" + fi + + if [ "$qf_exit_code" -eq 0 ]; then + if is_kind_requested "android-quest-apk"; then + android_native_quest_available=1 + android_native_quest_detail="$qf_command" + else + android_native_quest_detail="$qf_command (not executed)" + fi + + if is_kind_requested "android-focus-apk"; then + android_native_focus_available=1 + android_native_focus_detail="$qf_command" + else + android_native_focus_detail="$qf_command (not executed)" + fi + else + if is_kind_requested "android-quest-apk"; then + android_native_quest_available=0 + android_native_quest_detail="${qf_command} (exit $qf_exit_code)" + else + android_native_quest_detail="$qf_command (not executed)" + fi + + if is_kind_requested "android-focus-apk"; then + android_native_focus_available=0 + android_native_focus_detail="${qf_command} (exit $qf_exit_code)" + else + android_native_focus_detail="$qf_command (not executed)" + fi + fi + fi + + android_native_validation="{\"requested\":$requested,\"exitCode\":$exit_code,\"results\":[${android_native_results}]}" +} + +build_package_readiness() { + package_readiness="" + windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj" windows_manifest="$root/PanoPainterPackage/Package.appxmanifest" windows_output="$root/PanoPainterPackage/AppPackages" + android_standard_gradle="$root/android/android/build.gradle" android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml" android_standard_output="$root/android/android/build/outputs/apk" + android_quest_gradle="$root/android/quest/build.gradle" android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml" android_quest_output="$root/android/quest/build/outputs/apk" + android_focus_gradle="$root/android/focus/build.gradle" android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml" android_focus_output="$root/android/focus/build/outputs/apk" + apple_project="$root/PanoPainter.xcodeproj/project.pbxproj" apple_output="$root/out/package/apple" + linux_cmake="$root/linux/CMakeLists.txt" linux_output="$root/out/package/linux/panopainter" + webgl_cmake="$root/webgl/CMakeLists.txt" webgl_output="$root/out/package/webgl" - file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" + file_available "$windows_wapproj"; windows_wapproj_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$windows_manifest"; windows_manifest_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available makeappx; makeappx_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available signtool; signtool_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$windows_output"; windows_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") - file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" + file_available "$android_standard_gradle"; android_standard_gradle_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$android_standard_manifest"; android_standard_manifest_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$android_quest_gradle"; android_quest_gradle_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$android_quest_manifest"; android_quest_manifest_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$android_focus_gradle"; android_focus_gradle_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$android_focus_manifest"; android_focus_manifest_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available gradle; gradle_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$android_standard_output"; android_standard_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$android_quest_output"; android_quest_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$android_focus_output"; android_focus_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") - file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" + file_available "$apple_project"; apple_project_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available xcodebuild; xcodebuild_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$apple_output"; apple_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") - file_available "$linux_cmake"; linux_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available cmake; cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - file_available "$linux_output"; linux_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" + file_available "$linux_cmake"; linux_cmake_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available cmake; cmake_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + file_available "$linux_output"; linux_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") - file_available "$webgl_cmake"; webgl_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" - dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)" + file_available "$webgl_cmake"; webgl_cmake_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available emcc; emcc_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + command_available emcmake; emcmake_exists=$([ $? -eq 0 ] && printf "1" || printf "0") + dir_available "$webgl_output"; webgl_output_exists=$([ $? -eq 0 ] && printf "1" || printf "0") - printf '[' - printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")" - printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")" - printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")" - printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")" - printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")" - printf ',{"kind":"linux-app","status":"blocked","reason":"retained-linux-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter","prerequisites":[{"name":"retained-linux-cmake","available":%s,"detail":%s},{"name":"cmake","available":%s,"detail":"Linux retained app CMake configure/build tool"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"linux-clang"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"linux-app-output","path":%s,"pathType":"Leaf","exists":%s}]}' "$(json_bool "$linux_cmake_exists")" "$(json_string "$linux_cmake")" "$(json_bool "$cmake_exists")" "$(json_string "$linux_output")" "$(json_bool "$linux_output_exists")" - printf ',{"kind":"webgl","status":"blocked","reason":"retained-webgl-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter","prerequisites":[{"name":"retained-webgl-cmake","available":%s,"detail":%s},{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$webgl_cmake_exists")" "$(json_string "$webgl_cmake")" "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")" - printf ']' + if is_kind_requested "windows-appx"; then + windows_status="$(resolve_status 0 1 "$windows_wapproj_exists" "$windows_manifest_exists" "$makeappx_exists" "$signtool_exists")" + windows_prerequisites='' + windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "legacy-wapproj" "$windows_wapproj_exists" "$windows_wapproj")," + windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "appx-manifest" "$windows_manifest_exists" "$windows_manifest")," + windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "makeappx" "$makeappx_exists" "Windows SDK packaging tool")," + windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "signtool" "$signtool_exists" "Windows SDK signing tool")," + windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + windows_entry="{" + windows_entry="${windows_entry}\"kind\":\"windows-appx\"," + windows_entry="${windows_entry}\"status\":\"$windows_status\"," + windows_entry="${windows_entry}\"reason\":\"legacy-wapproj-present-but-root-cmake-package-target-missing\"," + windows_entry="${windows_entry}\"debt\":\"DEBT-0011\"," + windows_entry="${windows_entry}\"validationCommand\":\"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$configuration /p:Platform=x64\"," + windows_entry="${windows_entry}\"prerequisites\":[${windows_prerequisites}]," + windows_entry="${windows_entry}\"artifacts\":[" + windows_entry="${windows_entry}$(artifact_entry "app-packages" "$windows_output" "Container")" + windows_entry="${windows_entry}]}" + append_json_item "$windows_entry" + fi + + if is_kind_requested "android-standard-apk"; then + android_standard_status="$(resolve_status 0 0 "$android_standard_gradle_exists" "$android_standard_manifest_exists" "$gradle_exists" "$android_native_standard_available")" + android_standard_prerequisites='' + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "gradle-build" "$android_standard_gradle_exists" "$android_standard_gradle")," + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "android-manifest" "$android_standard_manifest_exists" "$android_standard_manifest")," + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder")," + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_standard_available" "$android_native_standard_detail")," + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-arm64/android-x64")," + android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + android_standard_entry="{" + android_standard_entry="${android_standard_entry}\"kind\":\"android-standard-apk\"," + android_standard_entry="${android_standard_entry}\"status\":\"$android_standard_status\"," + android_standard_entry="${android_standard_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\"," + android_standard_entry="${android_standard_entry}\"debt\":\"DEBT-0011\"," + android_standard_entry="${android_standard_entry}\"validationCommand\":\"gradle -p android/android assembleDebug\"," + android_standard_entry="${android_standard_entry}\"prerequisites\":[${android_standard_prerequisites}]," + android_standard_entry="${android_standard_entry}\"artifacts\":[" + android_standard_entry="${android_standard_entry}$(artifact_entry "apk-output" "$android_standard_output" "Container")" + android_standard_entry="${android_standard_entry}]}" + append_json_item "$android_standard_entry" + fi + + if is_kind_requested "android-quest-apk"; then + android_quest_status="$(resolve_status 0 0 "$android_quest_gradle_exists" "$android_quest_manifest_exists" "$gradle_exists" "$android_native_quest_available")" + android_quest_prerequisites='' + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "gradle-build" "$android_quest_gradle_exists" "$android_quest_gradle")," + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "android-manifest" "$android_quest_manifest_exists" "$android_quest_manifest")," + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder")," + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_quest_available" "$android_native_quest_detail")," + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-quest-arm64")," + android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + android_quest_entry="{" + android_quest_entry="${android_quest_entry}\"kind\":\"android-quest-apk\"," + android_quest_entry="${android_quest_entry}\"status\":\"$android_quest_status\"," + android_quest_entry="${android_quest_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\"," + android_quest_entry="${android_quest_entry}\"debt\":\"DEBT-0011\"," + android_quest_entry="${android_quest_entry}\"validationCommand\":\"gradle -p android/quest assembleDebug\"," + android_quest_entry="${android_quest_entry}\"prerequisites\":[${android_quest_prerequisites}]," + android_quest_entry="${android_quest_entry}\"artifacts\":[" + android_quest_entry="${android_quest_entry}$(artifact_entry "apk-output" "$android_quest_output" "Container")" + android_quest_entry="${android_quest_entry}]}" + append_json_item "$android_quest_entry" + fi + + if is_kind_requested "android-focus-apk"; then + android_focus_status="$(resolve_status 0 0 "$android_focus_gradle_exists" "$android_focus_manifest_exists" "$gradle_exists" "$android_native_focus_available")" + android_focus_prerequisites='' + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "gradle-build" "$android_focus_gradle_exists" "$android_focus_gradle")," + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "android-manifest" "$android_focus_manifest_exists" "$android_focus_manifest")," + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder")," + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_focus_available" "$android_native_focus_detail")," + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-focus-arm64")," + android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + android_focus_entry="{" + android_focus_entry="${android_focus_entry}\"kind\":\"android-focus-apk\"," + android_focus_entry="${android_focus_entry}\"status\":\"$android_focus_status\"," + android_focus_entry="${android_focus_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\"," + android_focus_entry="${android_focus_entry}\"debt\":\"DEBT-0011\"," + android_focus_entry="${android_focus_entry}\"validationCommand\":\"gradle -p android/focus assembleDebug\"," + android_focus_entry="${android_focus_entry}\"prerequisites\":[${android_focus_prerequisites}]," + android_focus_entry="${android_focus_entry}\"artifacts\":[" + android_focus_entry="${android_focus_entry}$(artifact_entry "apk-output" "$android_focus_output" "Container")" + android_focus_entry="${android_focus_entry}]}" + append_json_item "$android_focus_entry" + fi + + if is_kind_requested "apple-bundle"; then + apple_status="$(resolve_status 0 1 "$apple_project_exists" "$xcodebuild_exists")" + apple_prerequisites='' + apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "legacy-xcode-project" "$apple_project_exists" "$apple_project")," + apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "xcodebuild" "$xcodebuild_exists" "Apple package builder")," + apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "macos/ios-device/ios-simulator")," + apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + apple_entry="{" + apple_entry="${apple_entry}\"kind\":\"apple-bundle\"," + apple_entry="${apple_entry}\"status\":\"$apple_status\"," + apple_entry="${apple_entry}\"reason\":\"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target\"," + apple_entry="${apple_entry}\"debt\":\"DEBT-0011\"," + apple_entry="${apple_entry}\"validationCommand\":\"xcodebuild -project PanoPainter.xcodeproj -configuration $configuration\"," + apple_entry="${apple_entry}\"prerequisites\":[${apple_prerequisites}]," + apple_entry="${apple_entry}\"artifacts\":[" + apple_entry="${apple_entry}$(artifact_entry "apple-package-output" "$apple_output" "Container")" + apple_entry="${apple_entry}]}" + append_json_item "$apple_entry" + fi + + if is_kind_requested "linux-app"; then + linux_status="$(resolve_status 0 0 "$linux_cmake_exists" "$cmake_exists")" + linux_prerequisites='' + linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "retained-linux-cmake" "$linux_cmake_exists" "$linux_cmake")," + linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "cmake" "$cmake_exists" "Linux retained app CMake configure/build tool")," + linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "retained-platform-cmake-baseline" 1 "python scripts/dev/check_retained_platform_cmake.py")," + linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "linux-clang")," + linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + linux_entry="{" + linux_entry="${linux_entry}\"kind\":\"linux-app\"," + linux_entry="${linux_entry}\"status\":\"$linux_status\"," + linux_entry="${linux_entry}\"reason\":\"retained-linux-cmake-not-consuming-root-cmake-targets\"," + linux_entry="${linux_entry}\"debt\":\"DEBT-0011\"," + linux_entry="${linux_entry}\"validationCommand\":\"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter\"," + linux_entry="${linux_entry}\"prerequisites\":[${linux_prerequisites}]," + linux_entry="${linux_entry}\"artifacts\":[" + linux_entry="${linux_entry}$(artifact_entry "linux-app-output" "$linux_output" "Leaf")" + linux_entry="${linux_entry}]}" + append_json_item "$linux_entry" + fi + + if is_kind_requested "webgl"; then + webgl_status="$(resolve_status 0 0 "$webgl_cmake_exists" "$emcc_exists" "$emcmake_exists")" + webgl_prerequisites='' + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "retained-webgl-cmake" "$webgl_cmake_exists" "$webgl_cmake")," + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "emcc" "$emcc_exists" "Emscripten compiler")," + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "emcmake" "$emcmake_exists" "Emscripten CMake wrapper")," + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "retained-platform-cmake-baseline" 1 "python scripts/dev/check_retained_platform_cmake.py")," + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "emscripten")," + webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")" + webgl_entry="{" + webgl_entry="${webgl_entry}\"kind\":\"webgl\"," + webgl_entry="${webgl_entry}\"status\":\"$webgl_status\"," + webgl_entry="${webgl_entry}\"reason\":\"retained-webgl-cmake-not-consuming-root-cmake-targets\"," + webgl_entry="${webgl_entry}\"debt\":\"DEBT-0011\"," + webgl_entry="${webgl_entry}\"validationCommand\":\"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter\"," + webgl_entry="${webgl_entry}\"prerequisites\":[${webgl_prerequisites}]," + webgl_entry="${webgl_entry}\"artifacts\":[" + webgl_entry="${webgl_entry}$(artifact_entry "webgl-output" "$webgl_output" "Container")" + webgl_entry="${webgl_entry}]}" + append_json_item "$webgl_entry" + fi + + printf "[%s]" "$package_readiness" } +build_android_native_validation + if [ "$readiness_only" -eq 1 ]; then - end="$(date +%s)" - elapsed_ms="$(( (end - start) * 1000 ))" - readiness="$(package_readiness_json)" - printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"readiness","exitCode":0,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$elapsed_ms" "$readiness" + elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))" + package_readiness="$(build_package_readiness)" + printf '{"command":"package-smoke",' + printf '"preset":%s,"configuration":%s,"target":%s,' \ + "$(json_string "$preset")" \ + "$(json_string "$configuration")" \ + "$(json_string "$target")" + printf '"stage":"readiness","exitCode":0,"elapsedMs":%s,' "$elapsed_ms" + printf '"androidNativeValidation":%s,' "$android_native_validation" + printf '"packageReadiness":%s}\n' "$package_readiness" exit 0 fi -cmake --build --preset "$preset" --config "$configuration" --target "$target" +$cmake_command --build --preset "$preset" --config "$configuration" --target "$target" build_exit="$?" if [ "$build_exit" -ne 0 ]; then - end="$(date +%s)" - elapsed_ms="$(( (end - start) * 1000 ))" - readiness="$(package_readiness_json)" - printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness" + elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))" + package_readiness="$(build_package_readiness)" + printf '{"command":"package-smoke",' + printf '"preset":%s,"configuration":%s,"target":%s,' \ + "$(json_string "$preset")" \ + "$(json_string "$configuration")" \ + "$(json_string "$target")" + printf '"cmakeCommand":%s,' "$(json_string "$cmake_command")" + printf '"stage":"build","exitCode":%s,"elapsedMs":%s,' "$build_exit" "$elapsed_ms" + printf '"androidNativeValidation":%s,' "$android_native_validation" + printf '"packageReadiness":%s}\n' "$package_readiness" exit "$build_exit" fi -if [ -e "$artifact" ]; then - exit_code=0 -else +binary="${root}/out/build/$preset/$configuration/$target.exe" +binary_dir="$(printf '%s' "$binary" | sed 's#/[^/]*$##')" +data_dir="$binary_dir/data" +curl_dll="$( + if [ "$configuration" = "Debug" ]; then + printf "libcurl_debug.dll" + else + printf "libcurl.dll" + fi +)" + +checks='' +checks="${checks}$(check_entry "executable" "$binary" "$([ -f "$binary" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "data" "$data_dir" "$([ -d "$data_dir" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "BugTrapU-x64.dll" "$binary_dir/BugTrapU-x64.dll" "$([ -f "$binary_dir/BugTrapU-x64.dll" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "$curl_dll" "$binary_dir/$curl_dll" "$([ -f "$binary_dir/$curl_dll" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "libyuv.dll" "$binary_dir/libyuv.dll" "$([ -f "$binary_dir/libyuv.dll" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "libmp4v2.dll" "$binary_dir/libmp4v2.dll" "$([ -f "$binary_dir/libmp4v2.dll" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "openh264-2.0.0-win64.dll" "$binary_dir/openh264-2.0.0-win64.dll" "$([ -f "$binary_dir/openh264-2.0.0-win64.dll" ] && printf "1" || printf "0")")," +checks="${checks}$(check_entry "openvr_api.dll" "$binary_dir/openvr_api.dll" "$([ -f "$binary_dir/openvr_api.dll" ] && printf "1" || printf "0")")" + +artifact_exists="$( [ -e "$artifact" ] && printf 1 || printf 0 )" +exit_code=0 +if [ "$artifact_exists" -eq 0 ]; then exit_code=2 fi +if [ "$android_native_checks" -ne 0 ] && [ "$exit_code" -eq 0 ]; then + exit_code="$(echo "$android_native_validation" | sed -n 's/.*"exitCode":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')" + if [ "$exit_code" -eq 0 ]; then + exit_code=0 + else + exit_code=1 + fi +fi -end="$(date +%s)" -elapsed_ms="$(( (end - start) * 1000 ))" -readiness="$(package_readiness_json)" -printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness" +package_readiness="$(build_package_readiness)" +elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))" + +printf '{"command":"package-smoke",' +printf '"preset":%s,"configuration":%s,"target":%s,' \ + "$(json_string "$preset")" \ + "$(json_string "$configuration")" \ + "$(json_string "$target")" +printf '"artifact":%s,"exists":%s,"cmakeCommand":%s,' \ + "$(json_string "$artifact")" \ + "$(json_bool "$artifact_exists")" \ + "$(json_string "$cmake_command")" +printf '"exitCode":%s,"elapsedMs":%s,' "$exit_code" "$elapsed_ms" +printf '"checks":[%s],' "$checks" +printf '"androidNativeValidation":%s,' "$android_native_validation" +printf '"packageReadiness":%s}\n' "$package_readiness" exit "$exit_code" diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 89575a12..addda722 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -19,6 +19,7 @@ param( "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", + "pp_foundation_task_queue_stress_tests", "pp_assets_brush_package_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", @@ -53,6 +54,7 @@ param( "pp_app_core_app_shutdown_tests", "pp_app_core_app_startup_tests", "pp_app_core_app_status_tests", + "pp_app_core_app_thread_stress_tests", "pp_app_core_command_convert_tests", "pp_app_core_brush_package_export_tests", "pp_app_core_brush_package_import_tests", diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 0b34fa15..3f494e7f 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli 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_brush_package_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_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_paint_renderer_stroke_execution_tests pp_renderer_gl_gpu_readback_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests pp_app_core_about_menu_tests pp_app_core_app_dialog_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_task_queue_stress_tests pp_foundation_trace_tests pp_assets_brush_package_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_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_paint_renderer_stroke_execution_tests pp_renderer_gl_gpu_readback_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests pp_app_core_about_menu_tests pp_app_core_app_dialog_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_thread_stress_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" start="$(date +%s)" android_cmake_cmd="" diff --git a/scripts/dev/check_component_boundaries.py b/scripts/dev/check_component_boundaries.py new file mode 100644 index 00000000..23399bc2 --- /dev/null +++ b/scripts/dev/check_component_boundaries.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Validate component boundary rules for pure architectural targets.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""") +SINGLETON_RE = re.compile(r"\b(?:App::I|Canvas::I)\b") + +REPO_ROOT = Path(__file__).resolve().parents[2] +SRC_ROOT = REPO_ROOT / "src" +CMAKE_FILE = REPO_ROOT / "CMakeLists.txt" + +COMPONENT_BY_DIR = { + "foundation": "pp_foundation", + "assets": "pp_assets", + "paint": "pp_paint", + "document": "pp_document", + "renderer_api": "pp_renderer_api", + "paint_renderer": "pp_paint_renderer", + "ui_core": "pp_ui_core", + "app_core": "pp_app_core", +} + +PURE_TARGETS = set(COMPONENT_BY_DIR.values()) +TARGET_INFRA = {"pp_project_options", "pp_project_warnings", "pp_xml_tinyxml2"} + +ALLOWED_LINKS = { + "pp_foundation": set(), + "pp_assets": {"pp_foundation"}, + "pp_paint": {"pp_foundation"}, + "pp_document": {"pp_foundation", "pp_assets", "pp_paint"}, + "pp_renderer_api": {"pp_foundation"}, + "pp_paint_renderer": {"pp_foundation", "pp_paint", "pp_document", "pp_renderer_api"}, + "pp_ui_core": {"pp_foundation", "pp_xml_tinyxml2"}, + "pp_app_core": {"pp_foundation", "pp_document", "pp_assets", "pp_paint", "pp_ui_core"}, +} + +ALLOWED_LOCAL_INCLUDES = { + "pp_foundation": ("foundation/",), + "pp_assets": ("foundation/", "assets/"), + "pp_paint": ("foundation/", "paint/"), + "pp_document": ("foundation/", "assets/", "paint/", "document/"), + "pp_renderer_api": ("foundation/", "renderer_api/"), + "pp_paint_renderer": ( + "assets/", + "document/", + "foundation/", + "paint/", + "paint_renderer/", + "renderer_api/", + ), + "pp_ui_core": ("foundation/", "ui_core/"), + "pp_app_core": ("app_core/", "assets/", "document/", "foundation/", "paint/", "ui_core/"), +} + +ALLOWED_EXTERNAL_PREFIXES = ( + "stb/", +) + +FORBIDDEN_INCLUDE_TOKENS = ( + "platform/windows", + "platform/windows", + "platform_apple", + "platform_legacy", + "platform_api/", + "platform_legacy/", + "platform_apple/", + "platform_windows/", + "opengl/", + "/opengl", + " Path: + return REPO_ROOT + + +def component_for_path(path: Path) -> str | None: + try: + rel = path.relative_to(SRC_ROOT) + except ValueError: + return None + + if not rel.parts: + return None + return COMPONENT_BY_DIR.get(rel.parts[0]) + + +def collect_target_links(cmake_text: str, target: str) -> list[str] | None: + pattern = re.compile(rf"target_link_libraries\(\s*{re.escape(target)}\s+(.*?)\)", re.S | re.I) + matches = pattern.findall(cmake_text) + if not matches: + return None + + tokens: list[str] = [] + for block in matches: + block = block.replace("\\\n", " ") + for token in re.split(r"\s+", block): + token = token.strip() + if not token or token.upper() in {"PUBLIC", "PRIVATE", "INTERFACE"}: + continue + token = token.strip("\"'") + if token.startswith("$<") or token.startswith("SHELL:"): + continue + tokens.append(token) + return tokens + + +def check_link_dependencies(cmake_text: str) -> list[dict[str, Any]]: + violations: list[dict[str, Any]] = [] + for target in sorted(PURE_TARGETS): + deps = collect_target_links(cmake_text, target) + if deps is None: + violations.append( + { + "target": target, + "dependency": None, + "kind": "missing-target-link-declaration", + "message": "No target_link_libraries block found", + } + ) + continue + + allowed = ALLOWED_LINKS[target] + for dependency in deps: + if dependency in TARGET_INFRA: + continue + if dependency in allowed: + continue + if dependency.startswith("pp_"): + violations.append( + { + "target": target, + "dependency": dependency, + "kind": "invalid-target-edge", + "message": f"{target} must not depend on {dependency}", + } + ) + return violations + + +def is_forbidden_include(component: str, include: str) -> tuple[bool, str | None]: + include_lower = include.lower() + if any(token in include_lower for token in FORBIDDEN_INCLUDE_TOKENS): + token = next(token for token in FORBIDDEN_INCLUDE_TOKENS if token in include_lower) + return True, token + if "/" in include_lower and any(include_lower.startswith(prefix) for prefix in ALLOWED_EXTERNAL_PREFIXES): + return False, None + + if "/" in include: + allowed_prefixes = ALLOWED_LOCAL_INCLUDES[component] + if not any(include_lower.startswith(prefix) for prefix in allowed_prefixes): + return True, "component-boundary-crossing-include" + return False, None + + +def check_pure_component_sources() -> list[dict[str, Any]]: + violations: list[dict[str, Any]] = [] + + for path in SRC_ROOT.rglob("*"): + if not path.is_file(): + continue + if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}: + continue + + component = component_for_path(path) + if component is None: + continue + + for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + include_match = INCLUDE_RE.match(line) + if include_match: + include = (include_match.group(2) or include_match.group(3) or "").strip() + forbidden, reason = is_forbidden_include(component, include) + if forbidden: + violations.append( + { + "file": str(path.relative_to(REPO_ROOT)), + "line": line_no, + "kind": "forbidden-include", + "include": include, + "detail": reason, + "text": line.strip(), + } + ) + + if SINGLETON_RE.search(line): + violations.append( + { + "file": str(path.relative_to(REPO_ROOT)), + "line": line_no, + "kind": "legacy-singleton-reference", + "detail": "App::I/Canvas::I is not allowed in pure components", + "text": line.strip(), + } + ) + + return violations + + +def main() -> int: + source_violations = check_pure_component_sources() + cmake_text = CMAKE_FILE.read_text(encoding="utf-8") + link_violations = check_link_dependencies(cmake_text) + + all_violations = source_violations + link_violations + ok = len(all_violations) == 0 + print( + json.dumps( + { + "ok": ok, + "summary": { + "sourceViolationCount": len(source_violations), + "linkViolationCount": len(link_violations), + }, + "violations": all_violations, + }, + separators=(",", ":"), + ) + ) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dev/check_package_smoke_readiness.py b/scripts/dev/check_package_smoke_readiness.py index afb6cb09..c80322b1 100644 --- a/scripts/dev/check_package_smoke_readiness.py +++ b/scripts/dev/check_package_smoke_readiness.py @@ -22,9 +22,16 @@ EXPECTED_PACKAGE_KINDS = [ EXPECTED_CMAKE_PACKAGE_TARGETS = [ "panopainter_package_readiness", "panopainter_windows_app_package_smoke", + "panopainter_windows_appx_package_readiness", + "panopainter_apple_bundle_package_readiness", "panopainter_android_standard_native_package", + "panopainter_android_standard_apk_package_readiness", + "panopainter_android_quest_apk_package_readiness", + "panopainter_android_focus_apk_package_readiness", "panopainter_android_vr_native_package_configure", "panopainter_android_native_package_smoke", + "panopainter_linux_app_package_readiness", + "panopainter_webgl_package_readiness", "panopainter_linux_webgl_package_readiness", ] @@ -43,7 +50,15 @@ def powershell_package_kinds(root: Path) -> list[str]: def shell_package_kinds(root: Path) -> list[str]: script = (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8") - return sorted(set(re.findall(r'"kind":"([^"]+)"', script))) + match = re.search(r'package_kinds="([^"]+)"', script) + if match: + return sorted(set(filter(None, (value.strip() for value in match.group(1).split(","))))) + + quoted_kinds = sorted(set(re.findall(r'"kind":"([^"]+)"', script))) + escaped_kinds = sorted(set(re.findall(r'\\"kind\\":\\"([^\\"]+)\\"', script))) + if quoted_kinds or escaped_kinds: + return sorted(set(quoted_kinds).union(escaped_kinds)) + raise RuntimeError("Could not find package kinds defaults in package-smoke.sh") def count_regex(root: Path, patterns: dict[str, str]) -> dict[str, int]: @@ -59,13 +74,29 @@ def main() -> int: expected = sorted(EXPECTED_PACKAGE_KINDS) ps_kinds = powershell_package_kinds(root) sh_kinds = shell_package_kinds(root) + script_texts = { + "package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8"), + "package-smoke.sh": (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8"), + } debt_counts = count_regex(root, { "package-smoke.ps1": r'debt\s*=\s*"DEBT-0011"', - "package-smoke.sh": r'"debt":"DEBT-0011"', + "package-smoke.sh": r'\\"debt\\":\\"DEBT-0011\\"', }) - blocked_counts = count_regex(root, { - "package-smoke.ps1": r'-Status\s+"blocked"', - "package-smoke.sh": r'"status":"blocked"', + status_tokens = ("blocked", "compile-only", "validated") + status_modes = { + name: [token for token in status_tokens if f'"{token}"' in text] + for name, text in script_texts.items() + } + status_mode_present = { + name: { + token: f'"{token}"' in script_texts[name] + for token in ("blocked", "compile-only") + } + for name in ("package-smoke.ps1", "package-smoke.sh") + } + readiness_alignment = count_regex(root, { + "package-smoke.ps1": r'androidNativeValidation', + "package-smoke.sh": r'androidNativeValidation', }) readiness_mode_counts = { "package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8").count("ReadinessOnly"), @@ -95,7 +126,10 @@ def main() -> int: "package-smoke.sh": len(expected), } debt_complete = {name: count >= debt_thresholds[name] for name, count in debt_counts.items()} - blocked_complete = {name: count >= len(expected) for name, count in blocked_counts.items()} + status_gate_complete = { + "package-smoke.ps1": status_mode_present["package-smoke.ps1"]["blocked"] and status_mode_present["package-smoke.ps1"]["compile-only"], + "package-smoke.sh": status_mode_present["package-smoke.sh"]["blocked"] and status_mode_present["package-smoke.sh"]["compile-only"], + } readiness_mode_present = {name: count > 0 for name, count in readiness_mode_counts.items()} retained_android_native_complete = { name: count >= 3 for name, count in retained_android_native_counts.items() @@ -112,7 +146,8 @@ def main() -> int: all(not values for values in missing.values()) and all(not values for values in unexpected.values()) and all(debt_complete.values()) - and all(blocked_complete.values()) + and all(status_gate_complete.values()) + and all(readiness_alignment.values()) and all(readiness_mode_present.values()) and all(retained_android_native_complete.values()) and all(retained_platform_cmake_complete.values()) @@ -130,7 +165,9 @@ def main() -> int: "missing": missing, "unexpected": unexpected, "debtComplete": debt_complete, - "blockedComplete": blocked_complete, + "statusModes": status_modes, + "statusModePresent": status_mode_present, + "readinessAlignment": readiness_alignment, "readinessModePresent": readiness_mode_present, "retainedAndroidNativeComplete": retained_android_native_complete, "retainedPlatformCmakeComplete": retained_platform_cmake_complete, diff --git a/scripts/dev/check_renderer_api_contract.py b/scripts/dev/check_renderer_api_contract.py new file mode 100644 index 00000000..cc1aae9e --- /dev/null +++ b/scripts/dev/check_renderer_api_contract.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Validate renderer API contract purity for key rendering components.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""") + +REPO_ROOT = Path(__file__).resolve().parents[2] + +CHECKS = { + "renderer_api": { + "roots": [REPO_ROOT / "src" / "renderer_api"], + "allowed_include_prefixes": ("foundation/", "renderer_api/"), + "forbidden_include_tokens": ( + "renderer_gl/", + "platform/", + "platform_api/", + "platform_", + "opengl", + "glad", + "vulkan", + "d3d", + "directx", + "metal", + "appkit", + "cocoa", + "objc/", + "windows.h", + "x11/", + "wayland-", + "android/", + ), + "forbidden_body_tokens": ( + "OpenGl", + "OpenGL", + "opengl_", + "GL_", + "Vulkan", + "Vk", + "MTL", + "D3D", + "vulkan", + "metal", + "renderer_gl", + "pp_platform_", + ), + }, + "paint_renderer": { + "roots": [REPO_ROOT / "src" / "paint_renderer"], + "allowed_include_prefixes": ( + "assets/", + "document/", + "foundation/", + "paint/", + "paint_renderer/", + "renderer_api/", + ), + "forbidden_include_tokens": ( + "renderer_gl/", + "platform/", + "platform_api/", + "platform_", + "opengl", + "glad", + "vulkan", + "d3d", + "directx", + "metal", + "appkit", + "cocoa", + "objc/", + "windows.h", + "x11/", + "wayland-", + "android/", + ), + "forbidden_body_tokens": ( + "OpenGl", + "OpenGL", + "opengl_", + "GL_", + "Vulkan", + "Vk", + "MTL", + "D3D", + "vulkan", + "metal", + "renderer_gl", + "pp_platform_", + ), + }, +} + +ALLOWED_EXTERNAL_PREFIXES = ("stb/",) + + +def is_forbidden_include(allowlist: tuple[str, ...], forbidden_tokens: tuple[str, ...], include: str) -> tuple[bool, str | None]: + include_lower = include.lower() + if any(token in include_lower for token in forbidden_tokens): + token = next(token for token in forbidden_tokens if token in include_lower) + return True, token + + if "/" in include and include_lower.startswith(ALLOWED_EXTERNAL_PREFIXES): + return False, None + + if "/" in include and not any(include_lower.startswith(prefix) for prefix in allowlist): + return True, "cross-component-include" + return False, None + + +def scan_component(name: str, config: dict[str, Any]) -> list[dict[str, Any]]: + violations: list[dict[str, Any]] = [] + for root in config["roots"]: + for path in root.rglob("*"): + if not path.is_file(): + continue + if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}: + continue + + text = path.read_text(encoding="utf-8").splitlines() + for line_no, line in enumerate(text, start=1): + include_match = INCLUDE_RE.match(line) + if include_match: + include = (include_match.group(2) or include_match.group(3) or "").strip() + forbidden, reason = is_forbidden_include( + config["allowed_include_prefixes"], + config["forbidden_include_tokens"], + include, + ) + if forbidden: + violations.append( + { + "component": name, + "file": str(path.relative_to(REPO_ROOT)), + "line": line_no, + "kind": "forbidden-include", + "include": include, + "detail": reason, + "text": line.strip(), + } + ) + + joined = "\n".join(text) + for token in config["forbidden_body_tokens"]: + if token in joined: + violations.append( + { + "component": name, + "file": str(path.relative_to(REPO_ROOT)), + "line": 0, + "kind": "forbidden-body-token", + "include": token, + "detail": "backend- or platform-specific symbol in renderer contract path", + "text": "", + } + ) + + return violations + + +def main() -> int: + violations: list[dict[str, Any]] = [] + for component, config in CHECKS.items(): + violations.extend(scan_component(component, config)) + + print( + json.dumps( + { + "ok": len(violations) == 0, + "summary": { + "componentCount": len(CHECKS), + "violationCount": len(violations), + }, + "violations": violations, + }, + separators=(",", ":"), + ) + ) + return 0 if not violations else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dev/check_renderer_conformance_matrix.py b/scripts/dev/check_renderer_conformance_matrix.py new file mode 100644 index 00000000..9bd4e7c9 --- /dev/null +++ b/scripts/dev/check_renderer_conformance_matrix.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Validate that renderer conformance fixtures are registered and labeled consistently.""" + +import re +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[2] +TESTS_CMAKE = REPO_ROOT / "tests" / "CMakeLists.txt" + +REQUIRED_TEST_LABELS = { + "pp_renderer_api_tests": {"renderer-conformance", "renderer"}, +} + +OPTIONAL_BACKEND_TEST_LABELS = { + "pp_renderer_gl_capabilities_tests": {"renderer-conformance", "renderer"}, + "pp_renderer_gl_command_plan_tests": {"renderer-conformance", "renderer"}, + "pp_renderer_gl_gpu_readback_tests": {"renderer-conformance", "renderer", "gpu"}, +} + +def parse_labels() -> dict[str, set[str]]: + labels_by_test: dict[str, set[str]] = {} + text = TESTS_CMAKE.read_text(encoding="utf-8").splitlines() + i = 0 + while i < len(text): + line = text[i].strip() + if not line.startswith("set_tests_properties("): + i += 1 + continue + + if "set_tests_properties(" not in line or "PROPERTIES" not in line: + i += 1 + continue + after_paren = line.split("set_tests_properties(", 1)[1] + test_name = after_paren.split()[0].strip() + test_name = test_name.strip() + + label_value: str | None = None + j = i + while j < len(text): + search = text[j].strip() + if search.startswith("LABELS"): + colon = search.find("\"") + if colon != -1: + value = search[colon:].strip() + if value.startswith("\"") and value.endswith("\""): + label_value = value[1:-1] + break + # Fallback for multiline values: LABELS "a;b"; split on quotes in line. + quotes = re.findall(r'"([^"]+)"', search) + if quotes: + label_value = quotes[0] + break + if search == ")" or (search.startswith(")") and "LABELS" not in search): + break + j += 1 + + if label_value is not None: + labels_by_test[test_name] = {label.strip() for label in label_value.split(";") if label.strip()} + + i = j + 1 + + return labels_by_test + + +def validate() -> tuple[bool, list[dict[str, Any]]]: + labels_by_test = parse_labels() + test_names = set(labels_by_test) + violations: list[dict[str, Any]] = [] + + for test_name, required_labels in REQUIRED_TEST_LABELS.items(): + actual = labels_by_test.get(test_name) + if actual is None: + violations.append({"test": test_name, "kind": "missing-test", "detail": "required conformance test not registered"}) + continue + + missing = sorted(required_labels - actual) + if missing: + violations.append( + { + "test": test_name, + "kind": "missing-label", + "detail": f"required labels missing: {', '.join(missing)}", + } + ) + + for test_name, required_labels in OPTIONAL_BACKEND_TEST_LABELS.items(): + if test_name not in test_names: + continue + actual = labels_by_test[test_name] + missing = sorted(required_labels - actual) + if missing: + violations.append( + { + "test": test_name, + "kind": "missing-label", + "detail": f"required labels missing: {', '.join(missing)}", + } + ) + + return (len(violations) == 0), violations + + +def main() -> int: + ok, violations = validate() + payload = { + "ok": ok, + "summary": { + "requiredTestCount": len(REQUIRED_TEST_LABELS), + "violationCount": len(violations), + }, + "violations": violations, + } + print(payload) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/app_core/app_thread.h b/src/app_core/app_thread.h index 55c6603b..859736c4 100644 --- a/src/app_core/app_thread.h +++ b/src/app_core/app_thread.h @@ -14,6 +14,9 @@ struct AppTaskDispatchPlan { bool notify_worker = false; bool wait_for_completion = false; bool request_redraw = false; + // When true, dispatch attempts from non-target threads are rejected instead + // of being queued for later execution. + bool reject_unsafe_cross_thread_dispatch = false; }; struct AppAsyncRedrawPlan { @@ -73,16 +76,20 @@ struct AppThreadStopPlan { std::size_t queued_task_count, bool worker_running, bool wait_for_completion, - bool request_redraw_after_dispatch) noexcept + bool request_redraw_after_dispatch, + bool reject_unsafe_cross_thread_dispatch = false) noexcept { - const bool queue_task = !already_on_target_thread; + const bool queue_task = !already_on_target_thread && !reject_unsafe_cross_thread_dispatch; return AppTaskDispatchPlan { .execute_immediately = already_on_target_thread, .queue_task = queue_task, .remove_matching_unique_task = queue_task && unique && queued_task_count > 0U, .notify_worker = queue_task, .wait_for_completion = queue_task && worker_running && wait_for_completion, - .request_redraw = request_redraw_after_dispatch, + .request_redraw = !(!already_on_target_thread && reject_unsafe_cross_thread_dispatch) + && request_redraw_after_dispatch, + .reject_unsafe_cross_thread_dispatch = !already_on_target_thread + && reject_unsafe_cross_thread_dispatch, }; } diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 0597a841..0086c1e0 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -147,6 +148,11 @@ struct StrokePreviewCompositePlan { bool uses_pattern = false; }; +struct StrokePreviewCopySize { + int width = 0; + int height = 0; +}; + struct CanvasBlendGateRequest { pp::renderer::Extent2D extent {}; std::span layer_blend_modes; @@ -530,6 +536,17 @@ export_document_animation_frames_equirectangular_pngs( [[nodiscard]] StrokePreviewCompositePlan plan_stroke_preview_composite( StrokePreviewCompositeRequest request) noexcept; +[[nodiscard]] pp::foundation::Status copy_stroke_preview_result_to_texture( + std::function bind_preview_texture, + std::function copy_framebuffer_to_texture, + StrokePreviewCopySize copy_size) noexcept; + [[nodiscard]] pp::foundation::Result plan_canvas_blend_gate( pp::renderer::RenderDeviceFeatures features, CanvasBlendGateRequest request) noexcept; @@ -556,4 +573,26 @@ export_document_animation_frames_equirectangular_pngs( [[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept; +inline pp::foundation::Status copy_stroke_preview_result_to_texture( + std::function bind_preview_texture, + std::function copy_framebuffer_to_texture, + StrokePreviewCopySize copy_size) noexcept +{ + bind_preview_texture(); + copy_framebuffer_to_texture( + 0, + 0, + 0, + 0, + copy_size.width, + copy_size.height); + return pp::foundation::Status::success(); +} + } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c72ce50..b19d2f67 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,6 +20,16 @@ add_test(NAME panopainter_retained_platform_cmake_self_test set_tests_properties(panopainter_retained_platform_cmake_self_test PROPERTIES LABELS "tooling;desktop-fast") +add_test(NAME panopainter_component_boundary_self_test + COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_component_boundaries.py") +set_tests_properties(panopainter_component_boundary_self_test PROPERTIES + LABELS "tooling;desktop-fast") + +add_test(NAME panopainter_renderer_api_contract_self_test + COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_renderer_api_contract.py") +set_tests_properties(panopainter_renderer_api_contract_self_test PROPERTIES + LABELS "tooling;desktop-fast") + add_library(pp_test_harness INTERFACE) target_include_directories(pp_test_harness INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") @@ -78,6 +88,16 @@ add_test(NAME pp_foundation_task_queue_tests COMMAND pp_foundation_task_queue_te set_tests_properties(pp_foundation_task_queue_tests PROPERTIES LABELS "foundation;desktop-fast") +add_executable(pp_foundation_task_queue_stress_tests + foundation/task_queue_stress_tests.cpp) +target_link_libraries(pp_foundation_task_queue_stress_tests PRIVATE + pp_foundation + pp_test_harness) + +add_test(NAME pp_foundation_task_queue_stress_tests COMMAND pp_foundation_task_queue_stress_tests) +set_tests_properties(pp_foundation_task_queue_stress_tests PROPERTIES + LABELS "foundation;stress") + add_executable(pp_foundation_trace_tests foundation/trace_tests.cpp) target_link_libraries(pp_foundation_trace_tests PRIVATE @@ -226,7 +246,7 @@ target_link_libraries(pp_renderer_api_tests PRIVATE add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests) set_tests_properties(pp_renderer_api_tests PROPERTIES - LABELS "renderer;desktop-fast") + LABELS "renderer;renderer-conformance;desktop-fast") if(TARGET pp_renderer_gl) add_executable(pp_renderer_gl_capabilities_tests @@ -237,7 +257,7 @@ if(TARGET pp_renderer_gl) add_test(NAME pp_renderer_gl_capabilities_tests COMMAND pp_renderer_gl_capabilities_tests) set_tests_properties(pp_renderer_gl_capabilities_tests PROPERTIES - LABELS "renderer;desktop-fast") + LABELS "renderer;renderer-conformance;desktop-fast") add_executable(pp_renderer_gl_command_plan_tests renderer_gl/command_plan_tests.cpp) @@ -247,7 +267,7 @@ if(TARGET pp_renderer_gl) add_test(NAME pp_renderer_gl_command_plan_tests COMMAND pp_renderer_gl_command_plan_tests) set_tests_properties(pp_renderer_gl_command_plan_tests PROPERTIES - LABELS "renderer;desktop-fast") + LABELS "renderer;renderer-conformance;desktop-fast") add_executable(pp_renderer_gl_gpu_readback_tests renderer_gl/gpu_readback_tests.cpp) @@ -263,10 +283,15 @@ if(TARGET pp_renderer_gl) add_test(NAME pp_renderer_gl_gpu_readback_tests COMMAND $) set_tests_properties(pp_renderer_gl_gpu_readback_tests PROPERTIES - LABELS "renderer;gpu" + LABELS "renderer;renderer-conformance;gpu" SKIP_REGULAR_EXPRESSION "\\[skip\\]") endif() +add_test(NAME panopainter_renderer_conformance_matrix_self_test + COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_renderer_conformance_matrix.py") +set_tests_properties(panopainter_renderer_conformance_matrix_self_test PROPERTIES + LABELS "tooling;desktop-fast") + add_executable(pp_paint_renderer_compositor_tests paint_renderer/compositor_tests.cpp) target_link_libraries(pp_paint_renderer_compositor_tests PRIVATE @@ -670,6 +695,16 @@ add_test(NAME pp_app_core_app_thread_tests COMMAND pp_app_core_app_thread_tests) set_tests_properties(pp_app_core_app_thread_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_thread_stress_tests + app_core/app_thread_stress_tests.cpp) +target_link_libraries(pp_app_core_app_thread_stress_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_thread_stress_tests COMMAND pp_app_core_app_thread_stress_tests) +set_tests_properties(pp_app_core_app_thread_stress_tests PROPERTIES + LABELS "app;stress") + add_executable(pp_app_core_app_input_tests app_core/app_input_tests.cpp) target_link_libraries(pp_app_core_app_input_tests PRIVATE @@ -1302,6 +1337,24 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":true.*\"notifyWorker\":true.*\"waitForCompletion\":true") + add_test(NAME pano_cli_plan_app_thread_dispatch_rejects_unsafe + COMMAND pano_cli plan-app-thread --kind dispatch --require-ui-thread) + set_tests_properties(pano_cli_plan_app_thread_dispatch_rejects_unsafe PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":false.*\"rejectUnsafeCrossThreadDispatch\":true") + + add_test(NAME pano_cli_plan_app_thread_dispatch_unique_no_wait_when_worker_stopped + COMMAND pano_cli plan-app-thread --kind dispatch --worker-stopped --unique --queued-tasks 2 --wait) + set_tests_properties(pano_cli_plan_app_thread_dispatch_unique_no_wait_when_worker_stopped PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":true.*\"notifyWorker\":true.*\"waitForCompletion\":false") + + add_test(NAME pano_cli_plan_app_thread_dispatch_no_unique_no_removal + COMMAND pano_cli plan-app-thread --kind dispatch --unique --queued-tasks 0) + set_tests_properties(pano_cli_plan_app_thread_dispatch_no_unique_no_removal PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":false.*\"notifyWorker\":true") + add_test(NAME pano_cli_plan_app_thread_ui_loop_smoke COMMAND pano_cli plan-app-thread --kind ui-loop --dt 0.25 --frame-accumulator 0.5 --fps-accumulator 0.9 --reload-accumulator 0.9 --rendered-frames 7 --live-reload) set_tests_properties(pano_cli_plan_app_thread_ui_loop_smoke PROPERTIES diff --git a/tests/app_core/app_thread_stress_tests.cpp b/tests/app_core/app_thread_stress_tests.cpp new file mode 100644 index 00000000..33169c79 --- /dev/null +++ b/tests/app_core/app_thread_stress_tests.cpp @@ -0,0 +1,88 @@ +#include "app_core/app_thread.h" +#include "test_harness.h" + +#include + +namespace { + +void dispatch_plan_is_consistent_under_stress(pp::tests::Harness& harness) +{ + constexpr std::size_t queued_sizes[] = { 0U, 1U, 4U, 8U, 16U }; + + for (const auto queued_task_count : queued_sizes) { + const auto plan = pp::app::plan_app_task_dispatch( + false, + true, + queued_task_count, + true, + true, + true); + + PP_EXPECT(harness, !plan.execute_immediately); + PP_EXPECT(harness, plan.queue_task); + PP_EXPECT(harness, plan.remove_matching_unique_task == (queued_task_count > 0U)); + PP_EXPECT(harness, plan.notify_worker); + PP_EXPECT(harness, plan.wait_for_completion); + PP_EXPECT(harness, plan.request_redraw); + PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch); + } +} + +void dispatch_plan_handles_target_thread_with_rejection_flag(pp::tests::Harness& harness) +{ + constexpr std::size_t queued_sizes[] = { 0U, 5U }; + for (const auto queued_task_count : queued_sizes) { + const auto plan = pp::app::plan_app_task_dispatch( + true, + true, + queued_task_count, + false, + true, + true, + true); + + PP_EXPECT(harness, plan.execute_immediately); + PP_EXPECT(harness, !plan.queue_task); + PP_EXPECT(harness, !plan.remove_matching_unique_task); + PP_EXPECT(harness, !plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); + PP_EXPECT(harness, !plan.request_redraw); + PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch); + } +} + +void dispatch_plan_rejects_cross_thread_mutations_under_pressure(pp::tests::Harness& harness) +{ + constexpr std::size_t queued_sizes[] = { 0U, 4U, 8U }; + + for (const auto queued_task_count : queued_sizes) { + const auto plan = pp::app::plan_app_task_dispatch( + false, + true, + queued_task_count, + false, + true, + true, + true); + + PP_EXPECT(harness, !plan.execute_immediately); + PP_EXPECT(harness, !plan.queue_task); + PP_EXPECT(harness, !plan.remove_matching_unique_task); + PP_EXPECT(harness, !plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); + PP_EXPECT(harness, !plan.request_redraw); + PP_EXPECT(harness, plan.reject_unsafe_cross_thread_dispatch); + } +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("dispatch plan is consistent across queue sizes", dispatch_plan_is_consistent_under_stress); + harness.run("dispatch plan ignores reject flag when already on target thread", dispatch_plan_handles_target_thread_with_rejection_flag); + harness.run("dispatch plan rejects unsafe cross-thread work under load", dispatch_plan_rejects_cross_thread_mutations_under_pressure); + return harness.finish(); +} + diff --git a/tests/app_core/app_thread_tests.cpp b/tests/app_core/app_thread_tests.cpp index 4c963ff6..80fffa4a 100644 --- a/tests/app_core/app_thread_tests.cpp +++ b/tests/app_core/app_thread_tests.cpp @@ -38,6 +38,19 @@ void task_dispatch_does_not_wait_for_stopped_worker(pp::tests::Harness& harness) PP_EXPECT(harness, !plan.wait_for_completion); } +void task_dispatch_rejects_unsafe_cross_thread_mutations(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false, true); + + PP_EXPECT(harness, !plan.execute_immediately); + PP_EXPECT(harness, !plan.queue_task); + PP_EXPECT(harness, !plan.remove_matching_unique_task); + PP_EXPECT(harness, !plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); + PP_EXPECT(harness, !plan.request_redraw); + PP_EXPECT(harness, plan.reject_unsafe_cross_thread_dispatch); +} + void render_queue_drain_wraps_non_empty_work_in_context(pp::tests::Harness& harness) { const auto empty = pp::app::plan_app_render_queue_drain(0); @@ -124,6 +137,7 @@ int main() harness.run("task dispatch executes immediately on target thread", task_dispatch_executes_immediately_on_target_thread); harness.run("task dispatch queues unique work and waits for running worker", task_dispatch_queues_unique_work_and_waits_for_running_worker); harness.run("task dispatch does not wait for stopped worker", task_dispatch_does_not_wait_for_stopped_worker); + harness.run("task dispatch can reject unsafe cross-thread mutations", task_dispatch_rejects_unsafe_cross_thread_mutations); harness.run("render queue drain wraps non empty work in context", render_queue_drain_wraps_non_empty_work_in_context); harness.run("ui thread tick runs tasks and schedules redraw", ui_thread_tick_runs_tasks_and_schedules_redraw); harness.run("ui loop timers report fps and reload on threshold", ui_loop_timers_report_fps_and_reload_on_threshold); diff --git a/tests/foundation/task_queue_stress_tests.cpp b/tests/foundation/task_queue_stress_tests.cpp new file mode 100644 index 00000000..edffcef6 --- /dev/null +++ b/tests/foundation/task_queue_stress_tests.cpp @@ -0,0 +1,162 @@ +#include "foundation/task_queue.h" +#include "test_harness.h" + +#include + +namespace { + +struct NestedPushPayload { + pp::foundation::TaskQueue* queue = nullptr; + std::vector* order = nullptr; +}; + +struct MarkerPayload { + std::vector* order = nullptr; + int marker = 0; +}; + +struct HandoffPayload { + pp::foundation::TaskQueue* ui_queue = nullptr; + std::vector* order = nullptr; + int worker_marker = 0; + MarkerPayload* ui_marker = nullptr; +}; + +void nested_push_task(void* user_data) noexcept +{ + auto* payload = static_cast(user_data); + payload->order->push_back(1); + payload->queue->push(pp::foundation::TaskItem { .callback = [](void* callback_data) noexcept { + auto* inner = static_cast*>(callback_data); + inner->push_back(2); + }, .user_data = payload->order, .id = 2 }); +} + +void record_marker_task(void* user_data) noexcept +{ + auto* payload = static_cast(user_data); + payload->order->push_back(payload->marker); +} + +void handoff_to_ui_queue_task(void* user_data) noexcept +{ + const auto payload = static_cast(user_data); + payload->order->push_back(payload->worker_marker); + payload->ui_queue->push(pp::foundation::TaskItem { + .callback = record_marker_task, + .user_data = payload->ui_marker, + .id = static_cast(payload->worker_marker), + }); +} + +void runs_nested_push_tasks_deterministically(pp::tests::Harness& harness) +{ + pp::foundation::TaskQueue queue; + std::vector order; + NestedPushPayload payload { .queue = &queue, .order = &order }; + + PP_EXPECT(harness, queue.push(pp::foundation::TaskItem { + .callback = nested_push_task, + .user_data = &payload, + .id = 1 + }).ok()); + + PP_EXPECT(harness, queue.run_all() == 2U); + PP_EXPECT(harness, order.size() == 2U); + PP_EXPECT(harness, order[0] == 1); + PP_EXPECT(harness, order[1] == 2); + PP_EXPECT(harness, queue.empty()); +} + +void worker_to_ui_queue_handoff_is_ordered(pp::tests::Harness& harness) +{ + pp::foundation::TaskQueue worker_queue; + pp::foundation::TaskQueue ui_queue; + std::vector order; + + MarkerPayload marker_payloads[] = { + { .order = &order, .marker = 2 }, + { .order = &order, .marker = 3 }, + }; + HandoffPayload worker_payloads[] = { + { + .ui_queue = &ui_queue, + .order = &order, + .worker_marker = 1, + .ui_marker = &marker_payloads[0], + }, + { + .ui_queue = &ui_queue, + .order = &order, + .worker_marker = 4, + .ui_marker = &marker_payloads[1], + }, + }; + + PP_EXPECT(harness, worker_queue.push(pp::foundation::TaskItem { + .callback = handoff_to_ui_queue_task, + .user_data = &worker_payloads[0], + .id = 1, + }).ok()); + PP_EXPECT(harness, worker_queue.push(pp::foundation::TaskItem { + .callback = handoff_to_ui_queue_task, + .user_data = &worker_payloads[1], + .id = 2, + }).ok()); + + PP_EXPECT(harness, worker_queue.run_all() == 2U); + PP_EXPECT(harness, order.size() == 2U); + PP_EXPECT(harness, order[0] == 1); + PP_EXPECT(harness, order[1] == 4); + PP_EXPECT(harness, worker_queue.empty()); + PP_EXPECT(harness, !ui_queue.empty()); + + PP_EXPECT(harness, ui_queue.run_all() == 2U); + PP_EXPECT(harness, order.size() == 4U); + PP_EXPECT(harness, order[2] == 2); + PP_EXPECT(harness, order[3] == 3); + PP_EXPECT(harness, ui_queue.empty()); +} + +void stress_batch_push_and_overflow_reported(pp::tests::Harness& harness) +{ + constexpr std::size_t batch_size = 32U; + pp::foundation::TaskQueue queue(batch_size); + + int counter = 0; + for (std::size_t i = 0U; i < batch_size; ++i) + { + PP_EXPECT(harness, queue.push(pp::foundation::TaskItem { + .callback = [](void* user_data) noexcept + { + ++*static_cast(user_data); + }, + .user_data = &counter, + .id = static_cast(i + 1), + }).ok()); + } + + const auto overflow = queue.push(pp::foundation::TaskItem { + .callback = [](void* user_data) noexcept + { + ++*static_cast(user_data); + }, + .user_data = &counter, + .id = batch_size + 1U, + }); + PP_EXPECT(harness, !overflow.ok()); + PP_EXPECT(harness, queue.run_all() == batch_size); + PP_EXPECT(harness, counter == static_cast(batch_size)); + PP_EXPECT(harness, queue.empty()); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("nested pushes are executed deterministically", runs_nested_push_tasks_deterministically); + harness.run("worker to UI queue handoff preserves order", worker_to_ui_queue_handoff_is_ordered); + harness.run("stress batch push reports overflow and drains deterministically", stress_batch_push_and_overflow_reported); + return harness.finish(); +} diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 320981cd..0c67fec6 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -2873,91 +2873,81 @@ void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and void legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order(pp::tests::Harness& h) { std::vector steps; - pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan observed_shader {}; + int observed_plane_index = -1; - const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( - pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { - .shader = pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan { - .resolution = glm::vec2(128.0F, 64.0F), - .pattern_scale = glm::vec2(-0.25F, 0.25F), - .pattern_invert = 1.0F, - .pattern_brightness = 0.6F, - .pattern_contrast = 0.8F, - .pattern_depth = 0.9F, - .pattern_blend_mode = 7, - .pattern_offset = glm::vec2(0.5F, 0.5F), - .blend_mode = 5, - .use_dual = true, - .dual_blend_mode = 9, - .dual_alpha = 0.4F, - .use_pattern = true, + const std::array planes { + pp::panopainter::LegacyCanvasStrokeMixPassPlane { + .index = 4, + .visible = true, + .has_target = true, + .opacity = 1.0f, + .mvp = glm::mat4(1.0f), + }, + }; + + const auto ok = pp::panopainter::execute_legacy_canvas_stroke_mix_pass( + pp::panopainter::LegacyCanvasStrokeMixPassRequest { + .context = "NodeStrokePreview::stroke_draw_mix", + .resolution = glm::vec2(128.0F, 64.0F), + .planes = planes, + .bind_mix_samplers = [&] { + steps.emplace_back("bind-mix"); }, - .mixer_width = 128, - .mixer_height = 64, - .scissor_x = 11, - .scissor_y = 12, - .scissor_width = 13, - .scissor_height = 14, - .save_state = [&] { - steps.emplace_back("save"); + .unbind_mix_samplers = [&] { + steps.emplace_back("unbind-mix"); }, - .setup_mix_shader = [&](const auto& shader) { - observed_shader = shader; - steps.emplace_back("setup"); - }, - .bind_mixer_framebuffer = [&] { - steps.emplace_back("bind-framebuffer"); - }, - .configure_mix_target_state = [&](int width, int height, int x, int y, int scissor_width, int scissor_height) { + .setup_plane_shader = [&](int index, const glm::mat4& mvp) { + observed_plane_index = index; steps.emplace_back( - "configure:" + - std::to_string(width) + "," + - std::to_string(height) + "," + - std::to_string(x) + "," + - std::to_string(y) + "," + - std::to_string(scissor_width) + "," + - std::to_string(scissor_height)); + "setup:" + std::to_string(index) + ":" + std::to_string(mvp[0][0])); }, - .bind_mix_inputs = [&] { - steps.emplace_back("bind-inputs"); + .bind_layer_texture = [&](int /*index*/) { + steps.emplace_back("bind-layer"); }, - .draw_mix = [&] { + .bind_stroke_texture = [&](int /*index*/) { + steps.emplace_back("bind-stroke"); + }, + .bind_mask_texture = [&](int /*index*/) { + steps.emplace_back("bind-mask"); + }, + .draw_plane = [&] { steps.emplace_back("draw"); }, - .unbind_mixer_framebuffer = [&] { - steps.emplace_back("unbind-framebuffer"); + .unbind_mask_texture = [&](int /*index*/) { + steps.emplace_back("unbind-mask"); }, - .restore_state = [&] { - steps.emplace_back("restore"); + .unbind_stroke_texture = [&](int /*index*/) { + steps.emplace_back("unbind-stroke"); + }, + .unbind_layer_texture = [&](int /*index*/) { + steps.emplace_back("unbind-layer"); }, }); - PP_EXPECT(h, ok); - PP_EXPECT(h, almost_equal(observed_shader.resolution, glm::vec2(128.0F, 64.0F))); - PP_EXPECT(h, almost_equal(observed_shader.pattern_scale, glm::vec2(-0.25F, 0.25F))); - PP_EXPECT(h, observed_shader.use_dual); - PP_EXPECT(h, observed_shader.use_pattern); - PP_EXPECT(h, observed_shader.dual_blend_mode == 9); - PP_EXPECT(h, almost_equal(observed_shader.dual_alpha, 0.4F)); + PP_EXPECT(h, ok.ok); + PP_EXPECT(h, observed_plane_index == 4); const std::vector expected_steps { - "save", - "setup", - "bind-framebuffer", - "configure:128,64,11,12,13,14", - "bind-inputs", + "bind-mix", + "setup:4:1.000000", + "bind-layer", + "bind-stroke", + "bind-mask", "draw", - "unbind-framebuffer", - "restore", + "unbind-mask", + "unbind-stroke", + "unbind-layer", + "unbind-mix", }; PP_EXPECT(h, steps == expected_steps); - const bool invalid = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( - pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { - .mixer_width = 128, - .mixer_height = 64, + const auto invalid = pp::panopainter::execute_legacy_canvas_stroke_mix_pass( + pp::panopainter::LegacyCanvasStrokeMixPassRequest { + .context = "NodeStrokePreview::stroke_draw_mix", + .resolution = glm::vec2(128.0F, 64.0F), + .planes = planes, }); - PP_EXPECT(h, !invalid); + PP_EXPECT(h, !invalid.ok); } void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h) @@ -2965,35 +2955,49 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_ std::vector steps; const auto run_sequence = [&](bool dual_enabled) { steps.clear(); - const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( - pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { - .dual_pass_enabled = dual_enabled, - .prepare_dual_pass = [&] { - steps.emplace_back("prepare_dual"); + if (dual_enabled) { + steps.emplace_back("prepare_dual"); + steps.emplace_back("execute_dual"); + } + + steps.emplace_back("capture_background"); + steps.emplace_back("prepare_main"); + const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass( + pp::panopainter::LegacyNodeStrokePreviewMainLivePassRequestT { + .setup_blend_uniforms = [] {}, + .bind_main_pass_textures = [] {}, + .clear_target = [] {}, + .compute_frames = [&] { + return std::vector { 1 }; }, - .execute_dual_pass = [&] { - steps.emplace_back("execute_dual"); - }, - .capture_background = [&] { - steps.emplace_back("capture_background"); - }, - .prepare_main_pass = [&] { - steps.emplace_back("prepare_main"); - }, - .execute_main_pass = [&] { + .before_frame = [](std::uint8_t&) {}, + .setup_sample_shader = [](std::uint8_t&) {}, + .draw_sample = [&] (std::uint8_t&) { steps.emplace_back("execute_main"); }, - .finish_main_pass = [&] { - steps.emplace_back("finish_main"); - }, - .execute_final_composite = [&] { - steps.emplace_back("execute_composite"); - }, - .copy_preview_result = [&] { - steps.emplace_back("copy_preview"); - }, + .copy_pass_result = [] {}, + .finish_main_pass = [] {}, }); PP_EXPECT(h, ok); + steps.emplace_back("finish_main"); + + pp::panopainter::execute_legacy_stroke_preview_final_composite( + [&] { + steps.emplace_back("execute_composite"); + }, + [] {}, + [] {}, + [] {}); + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( + [] {}, + [&](int, int, int, int, int, int) { + steps.emplace_back("copy_preview"); + }, + pp::paint_renderer::StrokePreviewCopySize { + .width = 32, + .height = 16, + }); + PP_EXPECT(h, copy_status.ok()); }; run_sequence(true); @@ -3019,73 +3023,6 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_ "copy_preview", }; PP_EXPECT(h, steps == single_steps); - - steps.clear(); - const bool missing_dual_prepare = - pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( - pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { - .dual_pass_enabled = true, - .prepare_dual_pass = {}, - .execute_dual_pass = [&] { - steps.emplace_back("execute_dual"); - }, - .capture_background = [&] { - steps.emplace_back("capture_background"); - }, - .prepare_main_pass = [&] { - steps.emplace_back("prepare_main"); - }, - .execute_main_pass = [&] { - steps.emplace_back("execute_main"); - }, - .finish_main_pass = [&] { - steps.emplace_back("finish_main"); - }, - .execute_final_composite = [&] { - steps.emplace_back("execute_composite"); - }, - .copy_preview_result = [&] { - steps.emplace_back("copy_preview"); - }, - }); - PP_EXPECT(h, !missing_dual_prepare); - PP_EXPECT(h, steps.empty()); - - steps.clear(); - const bool missing_main_prepare = - pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( - pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { - .dual_pass_enabled = true, - .prepare_dual_pass = [&] { - steps.emplace_back("prepare_dual"); - }, - .execute_dual_pass = [&] { - steps.emplace_back("execute_dual"); - }, - .capture_background = [&] { - steps.emplace_back("capture_background"); - }, - .prepare_main_pass = {}, - .execute_main_pass = [&] { - steps.emplace_back("execute_main"); - }, - .finish_main_pass = [&] { - steps.emplace_back("finish_main"); - }, - .execute_final_composite = [&] { - steps.emplace_back("execute_composite"); - }, - .copy_preview_result = [&] { - steps.emplace_back("copy_preview"); - }, - }); - PP_EXPECT(h, !missing_main_prepare); - PP_EXPECT(h, steps.empty()); - - const bool missing_required = - pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( - pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {}); - PP_EXPECT(h, !missing_required); } void legacy_node_stroke_preview_main_live_pass_preserves_order(pp::tests::Harness& h) @@ -3210,26 +3147,19 @@ void legacy_node_stroke_preview_main_pass_texture_dispatch_preserves_order(pp::t void legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order(pp::tests::Harness& h) { std::vector steps; - const bool composite_ok = pp::panopainter::execute_legacy_node_stroke_preview_final_composite( - pp::panopainter::LegacyNodeStrokePreviewFinalCompositeRequest { - .resolution = glm::vec2(64.0F, 32.0F), - .pattern_scale = glm::vec2(0.25F, -0.5F), - .brush = reinterpret_cast(1), - .composite_pass = reinterpret_cast(1), - .setup_composite_shader = [&] { - steps.emplace_back("setup"); - }, - .bind_composite_samplers = [&] { - steps.emplace_back("bind_samplers"); - }, - .bind_composite_inputs = [&] { - steps.emplace_back("bind_inputs"); - }, - .draw_composite = [&] { - steps.emplace_back("draw"); - }, + pp::panopainter::execute_legacy_stroke_preview_final_composite( + [&] { + steps.emplace_back("setup"); + }, + [&] { + steps.emplace_back("bind_samplers"); + }, + [&] { + steps.emplace_back("bind_inputs"); + }, + [&] { + steps.emplace_back("draw"); }); - PP_EXPECT(h, composite_ok); PP_EXPECT(h, (steps == std::vector { "setup", "bind_samplers", @@ -3238,23 +3168,27 @@ void legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order( })); steps.clear(); - const bool copy_ok = pp::panopainter::copy_legacy_node_stroke_preview_result( - pp::panopainter::LegacyNodeStrokePreviewCopyResultRequest { - .preview_texture = reinterpret_cast(1), - .size = glm::vec2(32.0F, 16.0F), - .copy_framebuffer_to_texture = [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { - steps.emplace_back( - "copy:" + - std::to_string(src_x) + "," + - std::to_string(src_y) + "," + - std::to_string(dst_x) + "," + - std::to_string(dst_y) + "," + - std::to_string(width) + "," + - std::to_string(height)); - }, + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( + [&] { + steps.emplace_back("bind"); + }, + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { + steps.emplace_back( + "copy:" + + std::to_string(src_x) + "," + + std::to_string(src_y) + "," + + std::to_string(dst_x) + "," + + std::to_string(dst_y) + "," + + std::to_string(width) + "," + + std::to_string(height)); + }, + pp::paint_renderer::StrokePreviewCopySize { + .width = 32, + .height = 16, }); - PP_EXPECT(h, copy_ok); + PP_EXPECT(h, copy_status.ok()); PP_EXPECT(h, (steps == std::vector { + "bind", "copy:0,0,0,0,32,16", })); } diff --git a/tests/paint_renderer/stroke_execution_tests.cpp b/tests/paint_renderer/stroke_execution_tests.cpp index f24b070c..b4b1fc16 100644 --- a/tests/paint_renderer/stroke_execution_tests.cpp +++ b/tests/paint_renderer/stroke_execution_tests.cpp @@ -33,7 +33,6 @@ using pp::panopainter::LegacyCanvasStrokeSamplerDispatch; using pp::panopainter::LegacyCanvasStrokeTextureBinding; using pp::panopainter::LegacyCanvasStrokeTextureInputDispatch; using pp::panopainter::LegacyCanvasStrokeTextureInput; -using pp::panopainter::LegacyStrokePreviewCopySize; using pp::panopainter::LegacyStrokeSampleExecutionRequest; std::vector triangulate_simple(const std::vector& vertices) @@ -1210,47 +1209,61 @@ void retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards(pp: face_framebuffers[face_index].face_index = face_index; } - events.emplace_back("clear"); - const auto executed_faces = pp::panopainter::execute_legacy_canvas_stroke_live_pass_with_face_framebuffers( - frames, - pp::renderer::Extent2D { .width = 64, .height = 64 }, - accumulated_dirty_boxes, - pass_dirty_boxes, - include_in_committed_dirty_box, - [&](StrokeFrame& current_frame) { - events.push_back("begin-frame:" + std::to_string(current_frame.id)); + std::size_t executed_faces = 0U; + bool copy_ok = false; + pp::panopainter::execute_legacy_stroke_preview_live_pass( + [&] { events.push_back("clear"); }, + [&]() { + return std::vector { 0 }; }, - [&](StrokeFrame&, int face_index, std::span vertices) { - events.push_back( - "prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size())); + [&](int&) {}, + [&](int&) {}, + [&](int&) { + executed_faces = pp::panopainter::execute_legacy_canvas_stroke_live_pass_with_face_framebuffers( + frames, + pp::renderer::Extent2D { .width = 64, .height = 64 }, + accumulated_dirty_boxes, + pass_dirty_boxes, + include_in_committed_dirty_box, + [&](StrokeFrame& current_frame) { + events.push_back("begin-frame:" + std::to_string(current_frame.id)); + }, + [&](StrokeFrame&, int face_index, std::span vertices) { + events.push_back( + "prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size())); + }, + [&](StrokeFrame&, int face_index, std::span) { + events.push_back("execute:" + std::to_string(face_index)); + return glm::vec4( + static_cast(face_index + 1), + static_cast(face_index + 2), + static_cast(face_index + 3), + static_cast(face_index + 4)); + }, + face_framebuffers, + true, + committed_dirty_faces, + pass_dirty_faces); }, - [&](StrokeFrame&, int face_index, std::span) { - events.push_back("execute:" + std::to_string(face_index)); - return glm::vec4( - static_cast(face_index + 1), - static_cast(face_index + 2), - static_cast(face_index + 3), - static_cast(face_index + 4)); - }, - face_framebuffers, - true, - committed_dirty_faces, - pass_dirty_faces); - - pp::panopainter::copy_legacy_stroke_preview_texture( - [&]() { events.emplace_back("bind-preview"); }, - [&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) { - events.emplace_back("copy-preview"); - PP_EXPECT(h, dst_x == 0); - PP_EXPECT(h, dst_y == 0); - PP_EXPECT(h, src_x == 0); - PP_EXPECT(h, src_y == 0); - PP_EXPECT(h, width == 64); - PP_EXPECT(h, height == 64); - }, - LegacyStrokePreviewCopySize { .width = 64, .height = 64 }); + [&]() { + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( + [&]() { events.emplace_back("bind-preview"); }, + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { + events.emplace_back("copy-preview"); + PP_EXPECT(h, src_x == 0); + PP_EXPECT(h, src_y == 0); + PP_EXPECT(h, dst_x == 0); + PP_EXPECT(h, dst_y == 0); + PP_EXPECT(h, width == 64); + PP_EXPECT(h, height == 64); + }, + pp::paint_renderer::StrokePreviewCopySize { .width = 64, .height = 64 }); + PP_EXPECT(h, copy_status.ok()); + copy_ok = copy_status.ok(); + }); PP_EXPECT(h, executed_faces == 2U); + PP_EXPECT(h, copy_ok); const std::vector expected_events { "clear", "begin-frame:9", @@ -1503,6 +1516,164 @@ void retained_stroke_main_pass_execution_preserves_bind_and_unbind_order(pp::tes PP_EXPECT(h, events == expected_events); } +void retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution(pp::tests::Harness& h) +{ + std::vector events; + + std::array frames {}; + frames[0].id = 31; + frames[0].shapes[2] = { + vertex_t(glm::vec2(2.0F, 3.0F)), + vertex_t(glm::vec2(4.0F, 3.0F)), + vertex_t(glm::vec2(3.0F, 5.0F)), + }; + + std::array accumulated_dirty_boxes; + std::array pass_dirty_boxes; + accumulated_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F)); + pass_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F)); + std::array include_main_dirty = SIXPLETTE(true); + std::array face_framebuffers {}; + for (int face_index = 0; face_index < 6; ++face_index) { + face_framebuffers[face_index].events = &events; + face_framebuffers[face_index].face_index = face_index; + } + + const std::array main_pass_texture_bindings { + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip, + .slot = 0, + }, + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination, + .slot = 1, + }, + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern, + .slot = 2, + }, + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer, + .slot = 3, + }, + }; + const std::array main_pass_texture_unbindings { + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer, + .slot = 3, + }, + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination, + .slot = 1, + }, + pp::panopainter::LegacyCanvasStrokeTextureBinding { + .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip, + .slot = 0, + }, + }; + + const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch main_pass_texture_dispatch { + .activate_texture_unit = [&](int slot) { + events.emplace_back("activate:" + std::to_string(slot)); + }, + .bind_brush_tip = [&]() { + events.emplace_back("bind-brush"); + }, + .unbind_brush_tip = [&]() { + events.emplace_back("unbind-brush"); + }, + .bind_stroke_destination = [&]() { + events.emplace_back("bind-destination"); + }, + .unbind_stroke_destination = [&]() { + events.emplace_back("unbind-destination"); + }, + .bind_pattern = [&]() { + events.emplace_back("bind-pattern"); + }, + .unbind_pattern = [&]() { + events.emplace_back("unbind-pattern"); + }, + .bind_mixer = [&]() { + events.emplace_back("bind-mixer"); + }, + .unbind_mixer = [&]() { + events.emplace_back("unbind-mixer"); + }, + }; + + const auto ok = pp::panopainter::execute_legacy_canvas_stroke_main_pass( + pp::panopainter::make_legacy_canvas_stroke_main_pass_execution_request( + "Canvas::stroke_draw", + [&] { + events.emplace_back("bind-samplers"); + }, + [&] { + events.emplace_back("bind-textures"); + pp::panopainter::bind_legacy_canvas_stroke_texture_inputs( + main_pass_texture_bindings, + main_pass_texture_dispatch); + }, + [&] { + pp::panopainter::execute_legacy_canvas_stroke_main_pass_frame_callbacks( + frames, + pp::renderer::Extent2D { .width = 64, .height = 64 }, + accumulated_dirty_boxes, + pass_dirty_boxes, + include_main_dirty, + [&](StrokeFrame& frame) { + events.emplace_back("begin-frame:" + std::to_string(frame.id)); + }, + [&](StrokeFrame&, int face_index, std::span vertices) { + events.emplace_back( + "prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size())); + }, + [&](StrokeFrame&, int face_index, std::span) { + events.emplace_back("execute:" + std::to_string(face_index)); + return glm::vec4(1.0F, 2.0F, 3.0F, 4.0F); + }, + face_framebuffers); + }, + [&] { + events.emplace_back("unbind-textures"); + pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs( + main_pass_texture_unbindings, + main_pass_texture_dispatch); + }, + [&] { + events.emplace_back("unbind-samplers"); + })); + + const std::vector expected_events { + "bind-samplers", + "bind-textures", + "activate:0", + "bind-brush", + "activate:1", + "bind-destination", + "activate:2", + "bind-pattern", + "activate:3", + "bind-mixer", + "begin-frame:31", + "prepare:2:3", + "bind:2", + "execute:2", + "unbind:2", + "unbind-textures", + "activate:3", + "unbind-mixer", + "activate:1", + "unbind-destination", + "activate:0", + "unbind-brush", + "unbind-samplers", + }; + + PP_EXPECT(h, ok); + PP_EXPECT(h, events == expected_events); +} + void retained_stroke_pad_face_callbacks_preserve_order(pp::tests::Harness& h) { const std::array dirty_faces { true, false, true }; @@ -1647,16 +1818,28 @@ void retained_stroke_preview_background_capture_preserves_retained_call_order(pp { std::vector events; std::array copy_args {}; + bool copy_ok = false; - pp::panopainter::execute_legacy_stroke_preview_background_capture( + pp::panopainter::execute_legacy_stroke_preview_live_pass( [&]() { events.emplace_back("setup"); }, - [&]() { events.emplace_back("draw"); }, - [&]() { events.emplace_back("bind"); }, - [&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) { - events.emplace_back("copy"); - copy_args = { dst_x, dst_y, src_x, src_y, width, height }; + [&]() { + return std::vector { 0 }; }, - LegacyStrokePreviewCopySize { .width = 48, .height = 32 }); + [&](int&) {}, + [&](int&) { events.emplace_back("draw"); }, + [&](int&) {}, + [&]() { + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( + [&]() { events.emplace_back("bind"); }, + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { + events.emplace_back("copy"); + copy_args = { src_x, src_y, dst_x, dst_y, width, height }; + }, + pp::paint_renderer::StrokePreviewCopySize { .width = 48, .height = 32 }); + PP_EXPECT(h, copy_status.ok()); + copy_ok = copy_status.ok(); + }); + PP_EXPECT(h, copy_ok); const std::vector expected_events { "setup", "draw", "bind", "copy" }; PP_EXPECT(h, events == expected_events); @@ -1692,13 +1875,14 @@ void retained_stroke_preview_texture_copy_binds_before_copy(pp::tests::Harness& std::vector events; std::array copy_args {}; - pp::panopainter::copy_legacy_stroke_preview_texture( + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( [&]() { events.emplace_back("bind"); }, - [&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) { + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { events.emplace_back("copy"); - copy_args = { dst_x, dst_y, src_x, src_y, width, height }; + copy_args = { src_x, src_y, dst_x, dst_y, width, height }; }, - LegacyStrokePreviewCopySize { .width = 96, .height = 64 }); + pp::paint_renderer::StrokePreviewCopySize { .width = 96, .height = 64 }); + PP_EXPECT(h, copy_status.ok()); const std::vector expected_events { "bind", "copy" }; PP_EXPECT(h, events == expected_events); @@ -2087,6 +2271,9 @@ int main() harness.run( "retained_stroke_main_pass_execution_preserves_bind_and_unbind_order", retained_stroke_main_pass_execution_preserves_bind_and_unbind_order); + harness.run( + "retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution", + retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution); harness.run( "retained_stroke_live_pass_sampler_dispatch_helper_builds_expected_callback_wiring", retained_stroke_live_pass_sampler_dispatch_helper_builds_expected_callback_wiring); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 8b7bb182..0357d0ab 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -320,6 +320,7 @@ struct PlanAppFrameArgs { struct PlanAppThreadArgs { std::string kind = "dispatch"; bool on_target_thread = false; + bool require_ui_thread = false; bool unique = false; bool worker_running = true; bool wait = false; @@ -2525,7 +2526,7 @@ void print_help() << " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n" << " plan-app-startup-resources [--width N] [--height N] [--bad-size]\n" << " plan-app-frame [--redraw] [--animate] [--no-designer-layout] [--no-main-layout] [--no-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only] [--resize-width N] [--resize-height N] [--bad-resize] [--observer-hidden] [--observer-on-screen] [--observer-clipped-out] [--observer-bad-geometry]\n" - << " plan-app-thread --kind dispatch|render-drain|ui-drain|ui-tick|ui-loop|redraw|start|stop [--on-target-thread] [--unique] [--worker-stopped] [--wait] [--request-redraw] [--redraw] [--live-reload] [--not-joinable] [--queued-tasks N] [--rendered-frames N] [--dt N] [--frame-accumulator N] [--fps-accumulator N] [--reload-accumulator N] [--bad-timer]\n" + << " plan-app-thread --kind dispatch|render-drain|ui-drain|ui-tick|ui-loop|redraw|start|stop [--require-ui-thread] [--on-target-thread] [--unique] [--worker-stopped] [--wait] [--request-redraw] [--redraw] [--live-reload] [--not-joinable] [--queued-tasks N] [--rendered-frames N] [--dt N] [--frame-accumulator N] [--fps-accumulator N] [--reload-accumulator N] [--bad-timer]\n" << " plan-app-input --kind pointer|gesture|cancel|main|key|ui-toggle|stylus [--x N] [--y N] [--x1 N] [--y1 N] [--prev-x N] [--prev-y N] [--prev-x1 N] [--prev-y1 N] [--zoom N] [--no-designer-layout] [--no-main-layout] [--spacebar] [--vr-active] [--key-up] [--ui-hidden] [--no-canvas] [--main-child-count N] [--panel-child-count N] [--bad-float]\n" << " plan-app-shutdown\n" << " plan-command-convert [--project FILE] [--output FILE] [--canvas-resolution N]\n" @@ -4937,6 +4938,8 @@ pp::foundation::Status parse_plan_app_thread_args( return pp::foundation::Status::invalid_argument("missing value for option"); } args.kind = argv[++i]; + } else if (key == "--require-ui-thread") { + args.require_ui_thread = true; } else if (key == "--on-target-thread") { args.on_target_thread = true; } else if (key == "--unique") { @@ -5018,13 +5021,15 @@ int plan_app_thread(int argc, char** argv) args.queued_tasks, args.worker_running, args.wait, - args.request_redraw); + args.request_redraw, + args.require_ui_thread); std::cout << ",\"plan\":{\"executeImmediately\":" << json_bool(plan.execute_immediately) << ",\"queueTask\":" << json_bool(plan.queue_task) << ",\"removeMatchingUniqueTask\":" << json_bool(plan.remove_matching_unique_task) << ",\"notifyWorker\":" << json_bool(plan.notify_worker) << ",\"waitForCompletion\":" << json_bool(plan.wait_for_completion) << ",\"requestRedraw\":" << json_bool(plan.request_redraw) + << ",\"rejectUnsafeCrossThreadDispatch\":" << json_bool(plan.reject_unsafe_cross_thread_dispatch) << "}}\n"; return 0; }