From e59796bfc41bbed948369b2877c3254aa3552af7 Mon Sep 17 00:00:00 2001 From: "kingstom.chen" Date: Wed, 25 Mar 2026 12:07:28 +0800 Subject: [PATCH 1/2] chore: remove openspec docs (moved to goggles-artifacts repo) --- .../.openspec.yaml | 2 - .../design.md | 116 -- .../proposal.md | 97 - .../specs/app-window/spec.md | 34 - .../specs/compositor-capture/spec.md | 43 - .../tasks.md | 30 - .../proposal.md | 83 - .../specs/render-pipeline/spec.md | 256 --- .../tasks.md | 47 - .../design.md | 50 - .../proposal.md | 32 - .../specs/render-pipeline/spec.md | 48 - .../tasks.md | 8 - .../2025-12-22-add-cli-args/proposal.md | 22 - .../specs/app-window/spec.md | 24 - .../archive/2025-12-22-add-cli-args/tasks.md | 9 - .../proposal.md | 33 - .../specs/build-system/spec.md | 70 - .../tasks.md | 17 - .../design.md | 215 -- .../proposal.md | 16 - .../specs/vk-layer-capture/spec.md | 134 -- .../tasks.md | 39 - .../2025-12-23-add-tracy-profiling/design.md | 139 -- .../proposal.md | 28 - .../specs/profiling/spec.md | 104 - .../2025-12-23-add-tracy-profiling/tasks.md | 113 -- .../design.md | 77 - .../proposal.md | 23 - .../specs/vk-layer-capture/spec.md | 159 -- .../tasks.md | 51 - .../proposal.md | 29 - .../specs/vk-layer-capture/spec.md | 11 - .../tasks.md | 5 - .../proposal.md | 38 - .../specs/dependency-management/spec.md | 86 - .../2025-12-30-improve-pixi-workflow/tasks.md | 25 - .../design.md | 220 --- .../proposal.md | 42 - .../specs/object-lifecycle/spec.md | 61 - .../tasks.md | 89 - .../design.md | 309 --- .../proposal.md | 152 -- .../specs/input-forwarding/spec.md | 209 -- .../tasks.md | 7 - .../design.md | 47 - .../proposal.md | 47 - .../specs/app-window/spec.md | 24 - .../specs/object-lifecycle/spec.md | 34 - .../tasks.md | 37 - .../design.md | 79 - .../proposal.md | 51 - .../specs/packaging/spec.md | 72 - .../tasks.md | 45 - .../2026-01-10-add-debug-overlay/proposal.md | 28 - .../2026-01-10-add-debug-overlay/tasks.md | 13 - .../proposal.md | 41 - .../specs/shader-testing/spec.md | 27 - .../2026-01-10-add-shader-batch-test/tasks.md | 7 - .../design.md | 357 ---- .../proposal.md | 60 - .../specs/input-forwarding/spec.md | 157 -- .../tasks.md | 97 - .../proposal.md | 65 - .../specs/ci/spec.md | 35 - .../specs/dependency-management/spec.md | 72 - .../tasks.md | 34 - .../proposal.md | 50 - .../specs/build-system/spec.md | 17 - .../tasks.md | 23 - .../proposal.md | 33 - .../specs/app-window/spec.md | 19 - .../tasks.md | 22 - .../proposal.md | 60 - .../tasks.md | 24 - .../proposal.md | 16 - .../specs/render-pipeline/spec.md | 13 - .../specs/vk-layer-capture/spec.md | 28 - .../tasks.md | 10 - .../proposal.md | 37 - .../specs/render-pipeline/spec.md | 42 - .../tasks.md | 10 - .../proposal.md | 25 - .../specs/app-window/spec.md | 17 - .../specs/render-pipeline/spec.md | 31 - .../tasks.md | 15 - .../design.md | 46 - .../proposal.md | 16 - .../specs/input-forwarding/spec.md | 38 - .../tasks.md | 12 - .../design.md | 58 - .../proposal.md | 32 - .../specs/input-forwarding/spec.md | 111 -- .../tasks.md | 11 - .../design.md | 61 - .../proposal.md | 53 - .../specs/input-forwarding/spec.md | 107 - .../2026-02-07-add-cursor-forwarding/tasks.md | 50 - .../design.md | 75 - .../proposal.md | 31 - .../specs/profiling/spec.md | 58 - .../tasks.md | 24 - .../proposal.md | 23 - .../specs/render-pipeline/spec.md | 28 - .../specs/vk-layer-capture/spec.md | 78 - .../tasks.md | 21 - .../design.md | 88 - .../proposal.md | 47 - .../specs/render-pipeline/spec.md | 104 - .../tasks.md | 61 - .../design.md | 46 - .../proposal.md | 14 - .../specs/app-window/spec.md | 53 - .../specs/render-pipeline/spec.md | 53 - .../tasks.md | 18 - .../design.md | 40 - .../proposal.md | 43 - .../specs/compositor-capture/spec.md | 31 - .../tasks.md | 18 - .../proposal.md | 56 - .../specs/render-pipeline/spec.md | 131 -- .../tasks.md | 45 - .../proposal.md | 43 - .../specs/render-pipeline/spec.md | 62 - .../2026-02-07-add-postchain-stage/tasks.md | 25 - .../proposal.md | 38 - .../specs/render-pipeline/spec.md | 93 - .../tasks.md | 41 - .../design.md | 28 - .../proposal.md | 21 - .../specs/render-pipeline/spec.md | 48 - .../tasks.md | 15 - .../2026-02-07-add-semaphore-export/design.md | 91 - .../proposal.md | 28 - .../specs/vk-layer-capture/spec.md | 103 - .../2026-02-07-add-semaphore-export/tasks.md | 54 - .../proposal.md | 21 - .../specs/render-pipeline/spec.md | 69 - .../tasks.md | 40 - .../design.md | 34 - .../proposal.md | 16 - .../specs/app-window/spec.md | 52 - .../specs/render-pipeline/spec.md | 39 - .../tasks.md | 24 - .../proposal.md | 51 - .../specs/input-forwarding/spec.md | 136 -- .../2026-02-07-add-surface-selector/tasks.md | 73 - .../design.md | 33 - .../proposal.md | 42 - .../specs/app-window/spec.md | 23 - .../specs/vk-layer-capture/spec.md | 154 -- .../tasks.md | 22 - .../proposal.md | 13 - .../specs/input-forwarding/spec.md | 22 - .../tasks.md | 14 - .../proposal.md | 16 - .../specs/app-window/spec.md | 37 - .../tasks.md | 24 - .../design.md | 84 - .../proposal.md | 46 - .../specs/app-window/spec.md | 30 - .../tasks.md | 19 - .../design.md | 32 - .../proposal.md | 15 - .../specs/input-forwarding/spec.md | 10 - .../tasks.md | 7 - .../design.md | 35 - .../proposal.md | 18 - .../specs/render-pipeline/spec.md | 16 - .../tasks.md | 11 - .../proposal.md | 53 - .../specs/input-forwarding/spec.md | 98 - .../tasks.md | 38 - .../design.md | 93 - .../proposal.md | 46 - .../specs/render-pipeline/spec.md | 42 - .../tasks.md | 38 - .../design.md | 80 - .../proposal.md | 45 - .../specs/config-loading/spec.md | 73 - .../specs/packaging/spec.md | 14 - .../tasks.md | 24 - .../design.md | 54 - .../proposal.md | 46 - .../specs/vk-layer-capture/spec.md | 46 - .../tasks.md | 24 - .../proposal.md | 30 - .../specs/input-forwarding/spec.md | 62 - .../tasks.md | 7 - .../proposal.md | 16 - .../specs/input-forwarding/spec.md | 46 - .../tasks.md | 6 - .../design.md | 58 - .../proposal.md | 46 - .../specs/app-window/spec.md | 48 - .../tasks.md | 46 - .../proposal.md | 15 - .../specs/surface-frame-presentation/spec.md | 29 - .../tasks.md | 6 - .../proposal.md | 27 - .../specs/vk-layer-capture/spec.md | 49 - .../tasks.md | 24 - .../.openspec.yaml | 2 - .../design.md | 130 -- .../proposal.md | 42 - .../specs/compositor-capture/spec.md | 52 - .../specs/input-forwarding/spec.md | 61 - .../specs/layer-shell-overlay/spec.md | 170 -- .../tasks.md | 46 - .../.openspec.yaml | 2 - .../2026-02-27-add-headless-mode/design.md | 81 - .../2026-02-27-add-headless-mode/proposal.md | 32 - .../specs/app-window/spec.md | 36 - .../specs/headless-mode/spec.md | 100 - .../specs/render-pipeline/spec.md | 55 - .../2026-02-27-add-headless-mode/tasks.md | 46 - .../proposal.md | 52 - .../tasks.md | 60 - .../.openspec.yaml | 2 - .../design.md | 73 - .../proposal.md | 31 - .../specs/build-system/spec.md | 30 - .../specs/test-client-apps/spec.md | 83 - .../specs/visual-regression/spec.md | 80 - .../2026-02-27-test-framework-phase1/tasks.md | 41 - .../proposal.md | 185 -- .../specs/visual-regression/spec.md | 103 - .../2026-02-27-test-framework-phase2/tasks.md | 53 - .../.openspec.yaml | 2 - .../design.md | 105 - .../proposal.md | 23 - .../specs/app-window/spec.md | 56 - .../specs/render-pipeline/spec.md | 85 - .../tasks.md | 26 - .../proposal.md | 77 - .../tasks.md | 82 - .../.openspec.yaml | 2 - .../design.md | 131 -- .../proposal.md | 80 - .../specs/goggles-filter-chain/spec.md | 192 -- .../specs/render-pipeline/spec.md | 82 - .../tasks.md | 45 - .../.openspec.yaml | 2 - .../design.md | 115 -- .../proposal.md | 58 - .../specs/filter-chain-c-api/spec.md | 137 -- .../2026-03-05-filter-chain-c-api-v1/tasks.md | 44 - .../.openspec.yaml | 2 - .../design.md | 229 --- .../implementation-handoff.md | 80 - .../proposal.md | 110 -- .../specs/compositor-module-layout/spec.md | 131 -- .../tasks.md | 109 -- .../.openspec.yaml | 2 - .../design.md | 163 -- .../proposal.md | 80 - .../specs/filter-chain-cpp-wrapper/spec.md | 104 - .../tasks.md | 63 - .../.openspec.yaml | 2 - .../design.md | 94 - .../proposal.md | 78 - .../specs/build-system/spec.md | 49 - .../specs/ci/spec.md | 34 - .../tasks.md | 29 - .../.openspec.yaml | 2 - .../design.md | 115 -- .../proposal.md | 93 - .../specs/app-window/spec.md | 24 - .../specs/compositor-capture/spec.md | 26 - .../tasks.md | 19 - .../.openspec.yaml | 2 - .../design.md | 283 --- .../proposal.md | 161 -- .../vulkan-backend-module-layout/spec.md | 199 -- .../tasks.md | 90 - .../.openspec.yaml | 2 - .../design.md | 55 - .../proposal.md | 99 - .../specs/render-pipeline/spec.md | 87 - .../tasks.md | 23 - .../.openspec.yaml | 2 - .../design.md | 145 -- .../implementation-context.json | 255 --- .../manual-host-validation-fallback.md | 9 - .../proposal.md | 124 -- .../specs/app-window/spec.md | 45 - .../specs/compositor-capture/spec.md | 27 - .../specs/render-pipeline/spec.md | 38 - .../tasks.md | 38 - .../.openspec.yaml | 2 - .../design.md | 100 - .../implementation-context.json | 192 -- .../proposal.md | 102 - .../specs/ci/spec.md | 69 - .../tasks.md | 34 - .../.openspec.yaml | 2 - .../proposal.md | 81 - .../specs/input-forwarding/spec.md | 39 - .../specs/packaging/spec.md | 22 - .../tasks.md | 26 - .../design.md | 593 ------ .../interview.md | 139 -- .../proposal.md | 124 -- .../specs/diagnostics/spec.md | 382 ---- .../specs/goggles-filter-chain/spec.md | 115 -- .../specs/profiling/spec.md | 68 - .../specs/render-pipeline/spec.md | 117 -- .../specs/shader-testing/spec.md | 82 - .../specs/visual-regression/spec.md | 138 -- .../state.yaml | 30 - .../tasks.md | 340 ---- .../verify-report.md | 229 --- .../design.md | 153 -- .../proposal.md | 84 - .../state.yaml | 13 - .../tasks.md | 52 - .../verify-report.md | 90 - .../design.md | 289 --- .../proposal.md | 126 -- .../specs/build-system/spec.md | 58 - .../specs/filter-chain-assets-package/spec.md | 44 - .../specs/filter-chain-c-api/spec.md | 41 - .../specs/filter-chain-cpp-wrapper/spec.md | 40 - .../specs/goggles-filter-chain/spec.md | 68 - .../specs/render-pipeline/spec.md | 23 - .../tasks.md | 111 -- .../verify-report.md | 153 -- .../.openspec.yaml | 2 - .../design.md | 166 -- .../proposal.md | 100 - .../specs/goggles-filter-chain/spec.md | 42 - .../specs/render-pipeline/spec.md | 81 - .../tasks.md | 44 - .../verify-report.md | 119 -- .../design.md | 608 ------ .../exploration.md | 133 -- .../proposal.md | 279 --- .../specs/build-system/spec.md | 126 -- .../specs/diagnostics/spec.md | 111 -- .../specs/filter-chain-assets-package/spec.md | 71 - .../specs/goggles-filter-chain/spec.md | 92 - .../tasks.md | 494 ----- .../verify-report.md | 183 -- .../design.md | 114 -- .../proposal.md | 133 -- .../spec.md | 143 -- .../tasks.md | 36 - .../changes/extract-filter-chain/design.md | 267 --- .../extract-filter-chain/exploration.md | 173 -- .../changes/extract-filter-chain/proposal.md | 165 -- .../specs/build-system/spec.md | 179 -- .../extract-filter-chain/specs/ci/spec.md | 79 - .../specs/filter-chain-assets-package/spec.md | 37 - .../specs/goggles-filter-chain/spec.md | 218 --- .../specs/repository-infrastructure/spec.md | 113 -- .../changes/extract-filter-chain/tasks.md | 171 -- .../extract-filter-chain/verify-report.md | 58 - .../filter-chain-gate-refactor/.openspec.yaml | 2 - .../filter-chain-gate-refactor/design.md | 178 -- .../implementation-context.json | 247 --- .../filter-chain-gate-refactor/proposal.md | 119 -- .../specs/filter-chain-cpp-wrapper/spec.md | 45 - .../filter-chain-runtime-boundary/spec.md | 79 - .../filter-chain-gate-refactor/tasks.md | 49 - .../standalone-filter-chain-api/design.md | 549 ------ .../standalone-filter-chain-api/proposal.md | 154 -- .../specs/filter-chain-assets-package/spec.md | 74 - .../specs/filter-chain-c-api/spec.md | 289 --- .../specs/filter-chain-cpp-wrapper/spec.md | 123 -- .../specs/goggles-filter-chain/spec.md | 117 -- .../standalone-filter-chain-api/tasks.md | 68 - openspec/config.yaml | 45 - openspec/specs/app-window/spec.md | 266 --- openspec/specs/build-system/spec.md | 326 ---- openspec/specs/ci/spec.md | 167 -- openspec/specs/compositor-capture/spec.md | 104 - .../specs/compositor-module-layout/spec.md | 136 -- openspec/specs/config-loading/spec.md | 75 - openspec/specs/dependency-management/spec.md | 125 -- openspec/specs/diagnostics/spec.md | 467 ----- openspec/specs/documentation/spec.md | 67 - .../specs/filter-chain-assets-package/spec.md | 104 - openspec/specs/filter-chain-c-api/spec.md | 144 -- .../specs/filter-chain-cpp-wrapper/spec.md | 93 - openspec/specs/goggles-filter-chain/spec.md | 451 ----- openspec/specs/headless-mode/spec.md | 104 - openspec/specs/input-forwarding/spec.md | 576 ------ openspec/specs/layer-shell-overlay/spec.md | 176 -- openspec/specs/object-lifecycle/spec.md | 74 - openspec/specs/packaging/spec.md | 63 - openspec/specs/profiling/spec.md | 205 -- openspec/specs/render-pipeline/spec.md | 1723 ----------------- openspec/specs/shader-testing/spec.md | 94 - .../specs/surface-frame-presentation/spec.md | 34 - openspec/specs/test-client-apps/spec.md | 87 - openspec/specs/visual-regression/spec.md | 192 -- .../vulkan-backend-module-layout/spec.md | 204 -- 397 files changed, 33818 deletions(-) delete mode 100644 openspec/changes/app-window-restore-performance-plots/.openspec.yaml delete mode 100644 openspec/changes/app-window-restore-performance-plots/design.md delete mode 100644 openspec/changes/app-window-restore-performance-plots/proposal.md delete mode 100644 openspec/changes/app-window-restore-performance-plots/specs/app-window/spec.md delete mode 100644 openspec/changes/app-window-restore-performance-plots/specs/compositor-capture/spec.md delete mode 100644 openspec/changes/app-window-restore-performance-plots/tasks.md delete mode 100644 openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/proposal.md delete mode 100644 openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/tasks.md delete mode 100644 openspec/changes/archive/2025-12-21-add-crt-royale-support/design.md delete mode 100644 openspec/changes/archive/2025-12-21-add-crt-royale-support/proposal.md delete mode 100644 openspec/changes/archive/2025-12-21-add-crt-royale-support/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2025-12-21-add-crt-royale-support/tasks.md delete mode 100644 openspec/changes/archive/2025-12-22-add-cli-args/proposal.md delete mode 100644 openspec/changes/archive/2025-12-22-add-cli-args/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2025-12-22-add-cli-args/tasks.md delete mode 100644 openspec/changes/archive/2025-12-22-add-version-management/proposal.md delete mode 100644 openspec/changes/archive/2025-12-22-add-version-management/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2025-12-22-add-version-management/tasks.md delete mode 100644 openspec/changes/archive/2025-12-23-add-async-capture-worker/design.md delete mode 100644 openspec/changes/archive/2025-12-23-add-async-capture-worker/proposal.md delete mode 100644 openspec/changes/archive/2025-12-23-add-async-capture-worker/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2025-12-23-add-async-capture-worker/tasks.md delete mode 100644 openspec/changes/archive/2025-12-23-add-tracy-profiling/design.md delete mode 100644 openspec/changes/archive/2025-12-23-add-tracy-profiling/proposal.md delete mode 100644 openspec/changes/archive/2025-12-23-add-tracy-profiling/specs/profiling/spec.md delete mode 100644 openspec/changes/archive/2025-12-23-add-tracy-profiling/tasks.md delete mode 100644 openspec/changes/archive/2025-12-25-add-wsi-virtualization/design.md delete mode 100644 openspec/changes/archive/2025-12-25-add-wsi-virtualization/proposal.md delete mode 100644 openspec/changes/archive/2025-12-25-add-wsi-virtualization/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2025-12-25-add-wsi-virtualization/tasks.md delete mode 100644 openspec/changes/archive/2025-12-25-optimize-capture-present-check/proposal.md delete mode 100644 openspec/changes/archive/2025-12-25-optimize-capture-present-check/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2025-12-25-optimize-capture-present-check/tasks.md delete mode 100644 openspec/changes/archive/2025-12-30-improve-pixi-workflow/proposal.md delete mode 100644 openspec/changes/archive/2025-12-30-improve-pixi-workflow/specs/dependency-management/spec.md delete mode 100644 openspec/changes/archive/2025-12-30-improve-pixi-workflow/tasks.md delete mode 100644 openspec/changes/archive/2026-01-02-refactor-factory-pattern/design.md delete mode 100644 openspec/changes/archive/2026-01-02-refactor-factory-pattern/proposal.md delete mode 100644 openspec/changes/archive/2026-01-02-refactor-factory-pattern/specs/object-lifecycle/spec.md delete mode 100644 openspec/changes/archive/2026-01-02-refactor-factory-pattern/tasks.md delete mode 100644 openspec/changes/archive/2026-01-04-add-input-forwarding-x11/design.md delete mode 100644 openspec/changes/archive/2026-01-04-add-input-forwarding-x11/proposal.md delete mode 100644 openspec/changes/archive/2026-01-04-add-input-forwarding-x11/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-01-04-add-input-forwarding-x11/tasks.md delete mode 100644 openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/design.md delete mode 100644 openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/proposal.md delete mode 100644 openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/object-lifecycle/spec.md delete mode 100644 openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-add-appimage-packaging/design.md delete mode 100644 openspec/changes/archive/2026-01-10-add-appimage-packaging/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-add-appimage-packaging/specs/packaging/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-add-appimage-packaging/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-add-debug-overlay/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-add-debug-overlay/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-add-shader-batch-test/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-add-shader-batch-test/specs/shader-testing/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-add-shader-batch-test/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/design.md delete mode 100644 openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/ci/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/dependency-management/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-child-process-cleanup/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-child-process-cleanup/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-child-process-cleanup/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-implement-shader-cache/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-implement-shader-cache/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-implement-shader-cache/tasks.md delete mode 100644 openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md delete mode 100644 openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-popup-support/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-popup-support/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-popup-support/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-popup-support/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-software-cursor/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-software-cursor/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-software-cursor/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-compositor-software-cursor/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-cursor-forwarding/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-cursor-forwarding/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-cursor-forwarding/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-cursor-forwarding/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/specs/profiling/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-extended-shader-support/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-extended-shader-support/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-extended-shader-support/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-extended-shader-support/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/specs/compositor-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-pass-parameter-interface/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-pass-parameter-interface/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-pass-parameter-interface/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-postchain-stage/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-postchain-stage/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-postchain-stage/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-prechain-downsample/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-prechain-downsample/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-prechain-downsample/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-semaphore-export/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-semaphore-export/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-semaphore-export/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-semaphore-export/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-shader-stage-controls/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-shader-stage-controls/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-shader-stage-controls/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-selector/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-selector/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-surface-selector/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/design.md delete mode 100644 openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-fix-gpu-device-selection/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-fix-gpu-device-selection/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-fix-gpu-device-selection/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-compositor-module/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-compositor-module/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-compositor-module/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-compositor-module/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-external-image-frame/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-external-image-frame/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-external-image-frame/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-external-image-frame/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-pass-init-interface/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-pass-init-interface/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-pass-init-interface/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-pass-init-interface/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/config-loading/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/packaging/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/design.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-remove-manual-surface-selection/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-remove-manual-surface-selection/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-remove-manual-surface-selection/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-update-auto-surface-selection/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-update-auto-surface-selection/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-update-auto-surface-selection/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-update-file-logging-path-policy/design.md delete mode 100644 openspec/changes/archive/2026-02-07-update-file-logging-path-policy/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-update-file-logging-path-policy/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-update-file-logging-path-policy/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-update-surface-frame-retention/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-update-surface-frame-retention/specs/surface-frame-presentation/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-update-surface-frame-retention/tasks.md delete mode 100644 openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/proposal.md delete mode 100644 openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/specs/vk-layer-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/tasks.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/design.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/proposal.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/compositor-capture/spec.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/layer-shell-overlay/spec.md delete mode 100644 openspec/changes/archive/2026-02-26-add-layer-shell-support/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/design.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/specs/headless-mode/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/design.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/specs/test-client-apps/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/specs/visual-regression/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase1/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase2/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase2/specs/visual-regression/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-test-framework-phase2/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/design.md delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-02-27-update-filter-chain-state-management/tasks.md delete mode 100644 openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/proposal.md delete mode 100644 openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/tasks.md delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/design.md delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/proposal.md delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/tasks.md delete mode 100644 openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/design.md delete mode 100644 openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/proposal.md delete mode 100644 openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/specs/filter-chain-c-api/spec.md delete mode 100644 openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/tasks.md delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/specs/compositor-module-layout/spec.md delete mode 100644 openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md delete mode 100644 openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/design.md delete mode 100644 openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/proposal.md delete mode 100644 openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/tasks.md delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/design.md delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/proposal.md delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/ci/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-static-check-add-semgrep/tasks.md delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/design.md delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/proposal.md delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/compositor-capture/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/tasks.md delete mode 100644 openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/design.md delete mode 100644 openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/proposal.md delete mode 100644 openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md delete mode 100644 openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/tasks.md delete mode 100644 openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/design.md delete mode 100644 openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/proposal.md delete mode 100644 openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/tasks.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/design.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/implementation-context.json delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/manual-host-validation-fallback.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/proposal.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/app-window/spec.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/compositor-capture/spec.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/tasks.md delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/design.md delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/implementation-context.json delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/proposal.md delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/specs/ci/spec.md delete mode 100644 openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/tasks.md delete mode 100644 openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/proposal.md delete mode 100644 openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/input-forwarding/spec.md delete mode 100644 openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/packaging/spec.md delete mode 100644 openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/tasks.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/design.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/interview.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/proposal.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/diagnostics/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/profiling/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/shader-testing/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/visual-regression/spec.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/state.yaml delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/tasks.md delete mode 100644 openspec/changes/archive/2026-03-11-filter-chain-diagnostics/verify-report.md delete mode 100644 openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md delete mode 100644 openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md delete mode 100644 openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml delete mode 100644 openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md delete mode 100644 openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/design.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/proposal.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-assets-package/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-c-api/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-cpp-wrapper/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/tasks.md delete mode 100644 openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/verify-report.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/design.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/proposal.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/render-pipeline/spec.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/tasks.md delete mode 100644 openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/verify-report.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/design.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/exploration.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/proposal.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/specs/build-system/spec.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/specs/diagnostics/spec.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/specs/filter-chain-assets-package/spec.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/tasks.md delete mode 100644 openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/verify-report.md delete mode 100644 openspec/changes/archive/2026-03-19-rename-compositor-namespace/design.md delete mode 100644 openspec/changes/archive/2026-03-19-rename-compositor-namespace/proposal.md delete mode 100644 openspec/changes/archive/2026-03-19-rename-compositor-namespace/spec.md delete mode 100644 openspec/changes/archive/2026-03-19-rename-compositor-namespace/tasks.md delete mode 100644 openspec/changes/extract-filter-chain/design.md delete mode 100644 openspec/changes/extract-filter-chain/exploration.md delete mode 100644 openspec/changes/extract-filter-chain/proposal.md delete mode 100644 openspec/changes/extract-filter-chain/specs/build-system/spec.md delete mode 100644 openspec/changes/extract-filter-chain/specs/ci/spec.md delete mode 100644 openspec/changes/extract-filter-chain/specs/filter-chain-assets-package/spec.md delete mode 100644 openspec/changes/extract-filter-chain/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/extract-filter-chain/specs/repository-infrastructure/spec.md delete mode 100644 openspec/changes/extract-filter-chain/tasks.md delete mode 100644 openspec/changes/extract-filter-chain/verify-report.md delete mode 100644 openspec/changes/filter-chain-gate-refactor/.openspec.yaml delete mode 100644 openspec/changes/filter-chain-gate-refactor/design.md delete mode 100644 openspec/changes/filter-chain-gate-refactor/implementation-context.json delete mode 100644 openspec/changes/filter-chain-gate-refactor/proposal.md delete mode 100644 openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md delete mode 100644 openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md delete mode 100644 openspec/changes/filter-chain-gate-refactor/tasks.md delete mode 100644 openspec/changes/standalone-filter-chain-api/design.md delete mode 100644 openspec/changes/standalone-filter-chain-api/proposal.md delete mode 100644 openspec/changes/standalone-filter-chain-api/specs/filter-chain-assets-package/spec.md delete mode 100644 openspec/changes/standalone-filter-chain-api/specs/filter-chain-c-api/spec.md delete mode 100644 openspec/changes/standalone-filter-chain-api/specs/filter-chain-cpp-wrapper/spec.md delete mode 100644 openspec/changes/standalone-filter-chain-api/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/changes/standalone-filter-chain-api/tasks.md delete mode 100644 openspec/config.yaml delete mode 100644 openspec/specs/app-window/spec.md delete mode 100644 openspec/specs/build-system/spec.md delete mode 100644 openspec/specs/ci/spec.md delete mode 100644 openspec/specs/compositor-capture/spec.md delete mode 100644 openspec/specs/compositor-module-layout/spec.md delete mode 100644 openspec/specs/config-loading/spec.md delete mode 100644 openspec/specs/dependency-management/spec.md delete mode 100644 openspec/specs/diagnostics/spec.md delete mode 100644 openspec/specs/documentation/spec.md delete mode 100644 openspec/specs/filter-chain-assets-package/spec.md delete mode 100644 openspec/specs/filter-chain-c-api/spec.md delete mode 100644 openspec/specs/filter-chain-cpp-wrapper/spec.md delete mode 100644 openspec/specs/goggles-filter-chain/spec.md delete mode 100644 openspec/specs/headless-mode/spec.md delete mode 100644 openspec/specs/input-forwarding/spec.md delete mode 100644 openspec/specs/layer-shell-overlay/spec.md delete mode 100644 openspec/specs/object-lifecycle/spec.md delete mode 100644 openspec/specs/packaging/spec.md delete mode 100644 openspec/specs/profiling/spec.md delete mode 100644 openspec/specs/render-pipeline/spec.md delete mode 100644 openspec/specs/shader-testing/spec.md delete mode 100644 openspec/specs/surface-frame-presentation/spec.md delete mode 100644 openspec/specs/test-client-apps/spec.md delete mode 100644 openspec/specs/visual-regression/spec.md delete mode 100644 openspec/specs/vulkan-backend-module-layout/spec.md diff --git a/openspec/changes/app-window-restore-performance-plots/.openspec.yaml b/openspec/changes/app-window-restore-performance-plots/.openspec.yaml deleted file mode 100644 index 4b423f3a..00000000 --- a/openspec/changes/app-window-restore-performance-plots/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-08 diff --git a/openspec/changes/app-window-restore-performance-plots/design.md b/openspec/changes/app-window-restore-performance-plots/design.md deleted file mode 100644 index 095f25be..00000000 --- a/openspec/changes/app-window-restore-performance-plots/design.md +++ /dev/null @@ -1,116 +0,0 @@ -## Context - -The Application performance panel now reports compositor-sourced `Game FPS` and -`Compositor Latency`, but the recent replacement removed the historical plot lines that previously -helped operators spot jitter and transient spikes. The compositor still owns bounded timing history, -so the missing behavior is no longer raw measurement but the runtime contract needed to present that -history safely in the UI. - -This change crosses `src/compositor`, `src/util`, `src/app`, and `src/ui`, so the design MUST keep -the compositor as the timing source of truth while restoring the plots without reviving any -legacy ImGui-side metric path. - -## Goals / Non-Goals - -**Goals:** -- Restore one live `Game FPS` plot beneath the existing `Game FPS` text readout. -- Restore one live `Compositor Latency` plot beneath the existing latency text readout. -- Keep the compositor capture path as the only metric source of truth. -- Extend the compositor-to-application runtime metrics contract so the UI consumes plot-ready, - bounded history instead of recomputing timing state. -- Reset history cleanly when the active capture target changes so plots do not mix unrelated - surfaces. - -**Non-Goals:** -- Reintroducing legacy `Render` / `Source` metric code, labels, or plots. -- Adding new metrics, persistent telemetry, or external dependencies. -- Turning the shared runtime metrics boundary into an ImGui-specific data model. -- Changing unrelated Application window layout or controls. - -## Decisions - -### Decision: Keep compositor-owned history as the authoritative metrics source - -The compositor capture path SHALL continue to own runtime metric collection, aggregation, reset, and -history retention for both `Game FPS` and `Compositor Latency`. - -Rationale: -- The compositor alone sees the active target's commit and capture boundaries accurately. -- Recomputing history in `src/ui` or `src/app` would drift back toward the retired viewer-loop model. - -Alternatives considered: -- Rebuild history in `ImGuiLayer`: rejected because it reintroduces presentation-layer timing logic. -- Reconstruct history in `Application` from sampled snapshots: rejected because it can miss commit - cadence and creates a second timing interpretation. - -### Decision: Publish plot-ready bounded series through the shared runtime metrics contract - -The shared runtime metrics contract outside `src/ui` SHALL carry both current scalar values and the -bounded history needed to draw the two plots. The exported history SHALL be plot-ready for the UI: -`Game FPS` history SHALL be represented in FPS units and `Compositor Latency` history SHALL be -represented in milliseconds, with bounded counts that identify the valid sample window. - -Rationale: -- The UI should render data, not reinterpret compositor timing internals. -- A boundary-owned snapshot keeps module ownership explicit and avoids leaking compositor-specific - ring-buffer semantics into `src/ui`. - -Alternatives considered: -- Expose raw interval buffers and let the UI convert them to FPS: rejected because it duplicates - timing semantics outside the compositor boundary. -- Expose compositor internals directly to `ImGuiLayer`: rejected because it breaks module isolation. - -### Decision: Preserve bounded storage and explicit target resets - -The implementation SHALL reuse a fixed-size bounded history window and SHALL clear the published -history whenever the active capture target changes. - -Rationale: -- Fixed-size history keeps per-frame overhead predictable and avoids heap churn in the UI path. -- Explicit reset semantics prevent mixed-surface plots that would mislead operators. - -Alternatives considered: -- Grow history dynamically: rejected because it adds unnecessary allocation and ownership churn. -- Keep old samples across target changes: rejected because the resulting plots would mix unrelated - targets. - -### Decision: Restore plots as an additive UI behavior under the existing text rows - -The Application performance panel SHALL keep the current `Game FPS` and `Compositor Latency` text -readouts and SHALL render one live plot directly beneath each text row. - -Rationale: -- This matches the intended user-facing done-state while minimizing layout churn. -- Keeping the text rows preserves the current numeric at-a-glance behavior. - -Alternatives considered: -- Replace the text rows with plots only: rejected because it removes the direct numeric readout. -- Collapse both histories into one combined chart: rejected because the units differ and the user - asked for both plot lines back under the current metrics. - -## Risks / Trade-offs - -- [Runtime contract growth] -> Keep the history bounded and transport only the fields required for - the two current metrics. -- [Semantic drift back to legacy behavior] -> Require the UI to consume compositor-published history - directly and forbid reintroducing legacy timing buffers. -- [Mixed-target histories] -> Reset published series whenever the active capture target changes. -- [UI clutter] -> Limit the layout change to one plot per current text row and avoid unrelated panel - redesign. - -## Migration Plan - -1. Extend the shared runtime metrics contract to include bounded plot-ready history for the two - existing metrics. -2. Populate and reset those histories in the compositor capture path for the active target only. -3. Thread the expanded metrics snapshot through the application boundary. -4. Restore one live plot beneath each existing Performance panel text readout. -5. Verify the plots are compositor-sourced and that no legacy metric path returns. - -Rollback strategy: -- Revert the change as one unit if the shared metrics contract or plot behavior proves incorrect; do - not preserve a dual metrics path. - -## Open Questions - -- None. diff --git a/openspec/changes/app-window-restore-performance-plots/proposal.md b/openspec/changes/app-window-restore-performance-plots/proposal.md deleted file mode 100644 index db7cd9fd..00000000 --- a/openspec/changes/app-window-restore-performance-plots/proposal.md +++ /dev/null @@ -1,97 +0,0 @@ -## Why - -The recent compositor-metrics update replaced the legacy `Render` / `Source` FPS labels with -`Game FPS` and `Compositor Latency`, but it also removed the historical plot lines from the -Application performance panel. That leaves the panel less useful during live debugging because the -numeric readouts no longer show short-term metric trends. - -This change restores the missing plots now so the new gamer-facing metrics keep their current -compositor-based semantics without regressing the panel's visual history. - -## Problem - -- The Application performance panel currently shows only scalar `Game FPS` and `Compositor Latency` - values. -- The compositor metrics path already retains bounded timing history internally, but the UI contract - no longer exposes enough history to render live plots. -- Reintroducing the removed legacy metric path would conflict with the new compositor-owned metric - model. - -## Scope - -- Restore a live `Game FPS` plot beneath the existing `Game FPS` text readout. -- Restore a live `Compositor Latency` plot beneath the existing latency text readout. -- Keep the compositor-sourced metric path as the only source of truth for both values and plots. -- Extend the runtime metric contract as needed so the Application window can render bounded history - without depending on compositor-internal event logic. - -## What Changes - -- Update the Application performance panel requirements so it reports the current metrics and renders - one live history plot for each metric. -- Update the compositor capture requirements so the runtime metrics contract includes the bounded - history needed to drive those plots for the active capture target. -- Keep the current metric names and gamer-facing meanings unchanged. -- Preserve the removal of the old legacy `Render` / `Source` metrics path and plots. - -## Capabilities - -### New Capabilities -- None. - -### Modified Capabilities -- `app-window`: the Application performance panel requirements change from text-only - `Game FPS` / `Compositor Latency` reporting to text plus live historical plots for both metrics. -- `compositor-capture`: the compositor capture requirements change to publish the bounded history - needed for those plots without reintroducing any legacy timing path. - -## Non-goals - -- Reintroduce the legacy `Render` / `Source` FPS labels, plots, or timing pipeline. -- Add new performance metrics beyond `Game FPS` and `Compositor Latency`. -- Redesign unrelated Application window layout or performance-panel controls. -- Expand `Compositor Latency` into end-to-end display or input latency. - -## Impact - -- Affected modules: `src/ui`, `src/app`, `src/compositor`, and `src/util`. -- Likely affected files: `src/ui/imgui_layer.cpp`, `src/ui/imgui_layer.hpp`, - `src/app/application.cpp`, `src/compositor/compositor_state.hpp`, - `src/compositor/compositor_present.cpp`, and `src/util/runtime_metrics.hpp`. -- Impacted OpenSpec specs: `openspec/specs/app-window/spec.md` and - `openspec/specs/compositor-capture/spec.md`. -- No packaging, dependency, or external API changes are expected. - -## Risks - -- The runtime contract can become UI-shaped if history is exposed without a boundary-owned metrics - structure. -- Plot restoration can accidentally drift back toward legacy semantics if the panel recomputes its - own history instead of consuming compositor-owned history. -- The panel can show misleading history if the active-target reset behavior is not explicit when the - capture target changes. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `pixi run test -p asan` -- Environment-sensitive checks: - - `pixi run test -p test` when the local runtime supports the compositor/UI path -- Manual fallback: - - allowed only for validating visible Performance panel plot behavior when automated UI coverage is - unavailable - - record prerequisites, target-switch observations, and proof location -- Mandatory checks with no fallback: - - build and static-analysis gates above -- Pass criteria: - - the Application performance UI shows `Game FPS` and `Compositor Latency` text plus one live plot - for each metric - - the implementation continues to use the compositor-sourced metrics path only - - `Game FPS` and `Compositor Latency` histories reset before accumulating samples for a new capture - target - - no legacy `Render` / `Source` metric labels or legacy timing fallback path return diff --git a/openspec/changes/app-window-restore-performance-plots/specs/app-window/spec.md b/openspec/changes/app-window-restore-performance-plots/specs/app-window/spec.md deleted file mode 100644 index fc3e4d4c..00000000 --- a/openspec/changes/app-window-restore-performance-plots/specs/app-window/spec.md +++ /dev/null @@ -1,34 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Application Performance Panel Reports Gamer-Facing Metrics - -The Application performance panel SHALL report `Game FPS` and `Compositor Latency` instead of the -legacy `Render` and `Source` FPS metrics. - -The panel SHALL display compositor-provided current `Game FPS` and `Compositor Latency` values. - -The panel SHALL also render one live historical plot for each metric directly beneath the -corresponding text readout using compositor-provided bounded history for the current capture target. - -#### Scenario: Performance panel shows replacement metrics -- **WHEN** the Application performance panel is rendered -- **THEN** it SHALL display `Game FPS` and `Compositor Latency` -- **AND** it SHALL NOT display `Render` FPS or `Source` FPS - -#### Scenario: Performance panel renders both metric plots -- **GIVEN** compositor-provided history is available for the current capture target -- **WHEN** the Application performance panel is rendered -- **THEN** it SHALL render one live `Game FPS` plot directly beneath the `Game FPS` text readout -- **AND** it SHALL render one live `Compositor Latency` plot directly beneath the latency text - readout - -#### Scenario: Performance panel does not recreate legacy timing state -- **WHEN** the Application performance panel renders metric plots -- **THEN** it SHALL consume compositor-provided metric history -- **AND** it SHALL NOT reintroduce legacy `Render` / `Source` timing buffers or plots - -#### Scenario: Game FPS follows active captured game surface only -- **GIVEN** a game surface is the current capture target -- **WHEN** the performance panel reports or plots `Game FPS` -- **THEN** the displayed value and plotted history SHALL come from the compositor-provided metric - snapshot for that capture target only diff --git a/openspec/changes/app-window-restore-performance-plots/specs/compositor-capture/spec.md b/openspec/changes/app-window-restore-performance-plots/specs/compositor-capture/spec.md deleted file mode 100644 index 3c21f2d0..00000000 --- a/openspec/changes/app-window-restore-performance-plots/specs/compositor-capture/spec.md +++ /dev/null @@ -1,43 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Compositor Capture Publishes Gameplay Metrics - -The compositor capture path SHALL publish the timing data required for the Application performance -panel to report and plot `Game FPS` and `Compositor Latency`. - -The runtime metrics contract SHALL include current scalar values plus bounded history for both -metrics for the current capture target. - -`Game FPS` current value and published history SHALL be derived from presents or commits for the -currently captured game surface only. - -`Compositor Latency` current value and published history SHALL be derived from the interval between -an eligible active-surface commit and the corresponding compositor capture publication. - -When the current capture target changes, the published metric history SHALL reset before accumulating -samples for the new target. - -#### Scenario: Active surface commit updates Game FPS source and history -- **GIVEN** a game surface is the current capture target -- **WHEN** that surface produces an eligible commit for capture -- **THEN** the compositor capture path SHALL update the `Game FPS` metric source from that event -- **AND** it SHALL update the published `Game FPS` history for that capture target - -#### Scenario: Non-target surface does not change Game FPS source or history -- **GIVEN** a different surface is not the current capture target -- **WHEN** that non-target surface commits -- **THEN** the compositor capture path SHALL NOT count that event toward `Game FPS` -- **AND** it SHALL NOT update the published `Game FPS` history for the current target - -#### Scenario: Commit-to-capture latency is published with bounded history -- **GIVEN** an eligible active-surface commit produces a captured frame -- **WHEN** the compositor publishes the captured frame for viewer consumption -- **THEN** the compositor capture path SHALL publish `Compositor Latency` for that commit as the - elapsed commit-to-capture interval -- **AND** it SHALL update the published bounded latency history for the current target - -#### Scenario: Capture target change clears published histories -- **GIVEN** runtime metric history exists for one capture target -- **WHEN** the compositor switches to a different capture target -- **THEN** the published `Game FPS` and `Compositor Latency` histories SHALL reset before samples for - the new target are exposed diff --git a/openspec/changes/app-window-restore-performance-plots/tasks.md b/openspec/changes/app-window-restore-performance-plots/tasks.md deleted file mode 100644 index b815d58a..00000000 --- a/openspec/changes/app-window-restore-performance-plots/tasks.md +++ /dev/null @@ -1,30 +0,0 @@ -## 1. Runtime metrics contract - -- [x] 1.1 Extend the shared compositor-to-application runtime metrics contract so it carries current - `Game FPS` and `Compositor Latency` values plus bounded plot-ready history for both metrics. -- [x] 1.2 Update compositor metric collection so the published histories are populated from the active - capture target only and reset cleanly when the target changes. - -## 2. Application and UI restoration - -- [x] 2.1 Thread the expanded runtime metrics snapshot through the application boundary into - `ImGuiLayer` without reintroducing any legacy timing path. -- [x] 2.2 Restore one live `Game FPS` plot beneath the existing `Game FPS` text readout in the - Application performance panel. -- [x] 2.3 Restore one live `Compositor Latency` plot beneath the existing latency text readout in the - Application performance panel. -- [x] 2.4 Verify the panel continues to omit legacy `Render` / `Source` labels, plots, and timing - buffers. - -## 3. Verification - -- [x] 3.1 Verify the implementation matches the `app-window` and `compositor-capture` delta specs and - keeps the compositor-sourced metrics path as the only source of truth. -- [x] 3.2 Verify `Game FPS` and `Compositor Latency` histories clear before accumulating samples for a - new capture target, using automated coverage when available or a named manual proof artifact when - fallback is required. -- [x] 3.3 Run `pixi run build -p debug` and address any compile or contract drift issues. -- [x] 3.4 Run `pixi run build -p asan`, `pixi run test -p asan`, and `pixi run build -p quality`. -- [x] 3.5 Run `pixi run test -p test` when the compositor/UI runtime is available; otherwise perform a - manual visible-panel validation, record prerequisites, target-switch observations, and attach proof - location for the fallback. diff --git a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/proposal.md b/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/proposal.md deleted file mode 100644 index dc1f8d60..00000000 --- a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/proposal.md +++ /dev/null @@ -1,83 +0,0 @@ -# Change: Add Aspect Ratio Display Modes - -## Status: In Progress - -**32/33 tasks complete** - -| Section | Progress | Notes | -|---------|----------|-------| -| Configuration Support | 6/6 | ScaleMode enum, integer_scale, TOML parsing | -| Viewport Calculation | 7/7 | All four modes implemented | -| Filter Chain Integration | 4/4 | SemanticBinder FinalViewportSize complete | -| OutputPass Rendering | 5/5 | Viewport/scissor with calculate_viewport() | -| Integration | 2/2 | Config wired through, resize handling | -| Manual Testing | 8/9 | Only scale_type=viewport test pending | - -## Why - -Currently, Goggles stretches the captured image to fill the entire window, ignoring the original aspect ratio. This distorts the image when the window aspect ratio differs from the source. Users need control over how the captured frame is scaled and positioned within the output window. - -## What Changes - -- Add four display modes via `scale_mode`: - - **Fit**: Scale image to fit entirely within the window (letterbox/pillarbox as needed) - - **Fill**: Scale image to fill the window completely (crop edges as needed) - - **Stretch**: Current behavior - force image to match window dimensions exactly - - **Integer**: Pixel-perfect integer scaling for retro content -- Add `integer_scale` setting (only applies when `scale_mode = "integer"`): - - `0` or `"auto"`: Maximum integer multiplier that fits in the window - - `1`: Original size (no scaling, "origin" mode) - - `2-8`: Fixed integer multiplier -- Calculate effective viewport dimensions based on scale mode for filter chain integration -- Modify `OutputPass` to calculate viewport/scissor based on selected mode - -## Configuration Design - -```toml -[render] -# Display scaling mode -# Options: "fit", "fill", "stretch", "integer" -scale_mode = "stretch" - -# Integer scaling multiplier (only used when scale_mode = "integer") -# 0 or "auto" = maximum that fits, 1 = original size, 2-8 = fixed multiplier -integer_scale = 0 -``` - -The `integer_scale` setting is intentionally only active when `scale_mode = "integer"` to avoid user confusion (e.g., "why doesn't stretch fill my screen?"). - -## Filter Chain Integration - -This change specifically affects the **final OutputPass** rendering, which occurs after the filter chain. The interaction with `scale_type = viewport` in RetroArch presets requires careful handling: - -### FinalViewportSize Semantic - -When a shader pass uses `scale_type = viewport`, it renders at `FinalViewportSize`. This semantic should represent the **effective content area**, not the raw swapchain size: - -| Scale Mode | Swapchain | Source | FinalViewportSize | -|------------|-----------|--------|-------------------| -| Stretch | 1920x1080 | 640x480 | 1920x1080 (full swapchain) | -| Fit | 1920x1080 | 640x480 (4:3) | 1440x1080 (letterboxed area) | -| Fill | 1920x1080 | 640x480 (4:3) | 1920x1440 (cropped, exceeds bounds) | -| Integer (auto) | 1920x1080 | 640x480 | 1280x960 (2x, max that fits) | -| Integer (1) | 1920x1080 | 640x480 | 640x480 (original size) | -| Integer (3) | 1920x1080 | 640x480 | 1920x1440 (3x, may exceed/clip) | - -### Design Decision - -- **Fit/Fill modes**: Use source aspect ratio to calculate effective viewport -- **Integer mode**: Use raw source dimensions × scale factor (ignores aspect correction for pixel purity) -- **Stretch mode**: FinalViewportSize = swapchain size (current behavior) - -This ensures shaders using `scale_type = viewport` render at the correct resolution for the selected display mode. - -## Impact - -- Affected specs: `render-pipeline` -- Affected code: - - `src/util/config.hpp` - add `ScaleMode` enum, `integer_scale` field - - `src/util/config.cpp` - parse new config options - - `src/render/chain/filter_chain.*` - calculate FinalViewportSize based on scale mode - - `src/render/chain/output_pass.cpp` - implement viewport/scissor positioning - - `config/goggles.template.toml` - add new settings with documentation; first-run bootstrap may - materialize them into `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` diff --git a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/specs/render-pipeline/spec.md b/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/specs/render-pipeline/spec.md deleted file mode 100644 index b5e777a7..00000000 --- a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/specs/render-pipeline/spec.md +++ /dev/null @@ -1,256 +0,0 @@ -# render-pipeline Delta - -## ADDED Requirements - -### Requirement: Aspect Ratio Display Modes - -The output pass SHALL support four display modes for scaling captured frames to the output window, controlled by configuration. - -#### Scenario: Fit mode scales image to fit within window - -- **GIVEN** scale mode is set to `fit` -- **AND** source image has aspect ratio different from window -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be calculated to show the entire image -- **AND** the image SHALL be centered in the window -- **AND** letterbox (horizontal bars) or pillarbox (vertical bars) SHALL fill unused areas with black - -#### Scenario: Fill mode scales image to cover entire window - -- **GIVEN** scale mode is set to `fill` -- **AND** source image has aspect ratio different from window -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be calculated to cover the entire window -- **AND** the image SHALL be centered -- **AND** portions of the image extending beyond window bounds SHALL be clipped by scissor - -#### Scenario: Stretch mode matches window dimensions exactly - -- **GIVEN** scale mode is set to `stretch` -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL cover the entire window -- **AND** the image SHALL be scaled to match window dimensions exactly -- **AND** aspect ratio distortion is acceptable - -#### Scenario: Integer mode with auto scale finds maximum fit - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `0` (auto) -- **AND** source is 640x480 and window is 1920x1080 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the maximum integer scale that fits SHALL be calculated (2x = 1280x960) -- **AND** the image SHALL be centered with black borders - -#### Scenario: Integer mode with fixed scale of 1 shows original size - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `1` -- **AND** source is 640x480 and window is 1920x1080 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be 640x480 (original size) -- **AND** the image SHALL be centered with black borders - -#### Scenario: Integer mode with fixed scale multiplies source dimensions - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `3` -- **AND** source is 640x480 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be 1920x1440 (3x source) -- **AND** portions exceeding window bounds SHALL be clipped by scissor - -#### Scenario: Same aspect ratio produces identical output for fit/fill/stretch - -- **GIVEN** source image and window have the same aspect ratio -- **WHEN** fit, fill, or stretch mode is used -- **THEN** the output SHALL be identical regardless of mode -- **AND** the image SHALL fill the entire window - -### Requirement: Scale Mode Configuration - -The application config SHALL include a setting to control the display scale mode. - -#### Scenario: Config field definition - -- **GIVEN** the `goggles::Config` struct -- **WHEN** `Config::Render` is defined -- **THEN** it SHALL include `ScaleMode scale_mode` field -- **AND** the default value SHALL be `ScaleMode::Stretch` - -#### Scenario: TOML parsing for fit mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "fit"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Fit` - -#### Scenario: TOML parsing for fill mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "fill"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Fill` - -#### Scenario: TOML parsing for stretch mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "stretch"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Stretch` - -#### Scenario: TOML parsing for integer mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "integer"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Integer` - -#### Scenario: Missing config field uses default - -- **GIVEN** `goggles.toml` does not contain `scale_mode` field -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL default to `ScaleMode::Stretch` - -#### Scenario: Invalid config value produces error - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "invalid_value"` -- **WHEN** `load_config()` is called -- **THEN** an error SHALL be returned -- **AND** the error message SHALL indicate the invalid value - -### Requirement: Integer Scale Configuration - -The application config SHALL include a setting to control the integer scaling multiplier when scale_mode is "integer". - -#### Scenario: Integer scale field definition - -- **GIVEN** the `goggles::Config` struct -- **WHEN** `Config::Render` is defined -- **THEN** it SHALL include `uint32_t integer_scale` field -- **AND** the default value SHALL be `0` (auto) - -#### Scenario: Integer scale only applies in integer mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "stretch"` and `integer_scale = 2` -- **WHEN** rendering occurs -- **THEN** the `integer_scale` value SHALL be ignored -- **AND** stretch mode behavior SHALL apply - -#### Scenario: TOML parsing for auto integer scale - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 0` -- **WHEN** `load_config()` is called -- **THEN** `config.render.integer_scale` SHALL be `0` - -#### Scenario: TOML parsing for fixed integer scale - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 3` -- **WHEN** `load_config()` is called -- **THEN** `config.render.integer_scale` SHALL be `3` - -#### Scenario: Integer scale validation - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 10` -- **WHEN** `load_config()` is called -- **THEN** an error SHALL be returned -- **AND** the error message SHALL indicate valid range is 0-8 - -### Requirement: FinalViewportSize Calculation - -The filter chain SHALL calculate `FinalViewportSize` based on the scale mode to ensure correct shader behavior when `scale_type = viewport` is used. - -#### Scenario: Stretch mode uses swapchain size - -- **GIVEN** scale mode is `stretch` -- **AND** swapchain size is 1920x1080 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1920, 1080) - -#### Scenario: Fit mode uses letterboxed effective area - -- **GIVEN** scale mode is `fit` -- **AND** swapchain size is 1920x1080 (16:9) -- **AND** source aspect ratio is 4:3 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1440, 1080) representing the effective content area -- **AND** shaders using `scale_type = viewport` SHALL render at this resolution - -#### Scenario: Fill mode uses scaled area exceeding bounds - -- **GIVEN** scale mode is `fill` -- **AND** swapchain size is 1920x1080 (16:9) -- **AND** source aspect ratio is 4:3 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1920, 1440) representing the full scaled content -- **AND** the OutputPass scissor SHALL clip to swapchain bounds - -#### Scenario: Integer mode uses source multiplied by scale factor - -- **GIVEN** scale mode is `integer` -- **AND** integer_scale is `2` -- **AND** source is 640x480 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1280, 960) -- **AND** shaders using `scale_type = viewport` SHALL render at this resolution - -#### Scenario: Integer mode auto calculates max scale - -- **GIVEN** scale mode is `integer` -- **AND** integer_scale is `0` (auto) -- **AND** source is 640x480 and swapchain is 1920x1080 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** max scale SHALL be min(floor(1920/640), floor(1080/480)) = min(3, 2) = 2 -- **AND** FinalViewportSize SHALL be (1280, 960) - -#### Scenario: SemanticBinder uses calculated FinalViewportSize - -- **GIVEN** a shader pass with `FinalViewportSize` semantic -- **WHEN** SemanticBinder populates push constants -- **THEN** `FinalViewportSize` SHALL reflect the calculated value based on scale mode -- **AND** NOT the raw swapchain dimensions (except in stretch mode) - -### Requirement: Viewport Calculation Utility - -The render subsystem SHALL provide a utility function to calculate scaled viewport parameters. - -#### Scenario: Calculate fit viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Fit` -- **THEN** result SHALL have width=1440, height=1080 -- **AND** offset_x=240, offset_y=0 (centered horizontally) - -#### Scenario: Calculate fill viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Fill` -- **THEN** result SHALL have width=1920, height=1440 -- **AND** offset_x=0, offset_y=-180 (centered, extends beyond bounds) - -#### Scenario: Calculate stretch viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Stretch` -- **THEN** result SHALL have width=1920, height=1080 -- **AND** offset_x=0, offset_y=0 - -#### Scenario: Calculate integer viewport with auto scale - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Integer` and integer_scale=0 -- **THEN** result SHALL have width=1280, height=960 (2x) -- **AND** offset_x=320, offset_y=60 (centered) - -#### Scenario: Calculate integer viewport with fixed scale - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Integer` and integer_scale=1 -- **THEN** result SHALL have width=640, height=480 -- **AND** offset_x=640, offset_y=300 (centered) - -### Requirement: PassContext Source Extent - -The PassContext struct SHALL include source image dimensions to enable aspect ratio calculations. - -#### Scenario: Source extent available for aspect ratio calculation - -- **GIVEN** a captured frame with known dimensions -- **WHEN** PassContext is created for OutputPass -- **THEN** `source_extent` SHALL contain the width and height of the source image -- **AND** the values SHALL be used for aspect ratio mode calculations diff --git a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/tasks.md b/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/tasks.md deleted file mode 100644 index 6d292e0c..00000000 --- a/openspec/changes/archive/2025-12-19-add-aspect-ratio-modes/tasks.md +++ /dev/null @@ -1,47 +0,0 @@ -# Tasks - -## 1. Configuration Support -- [x] 1.1 Add `ScaleMode` enum to `config.hpp` with values `Fit`, `Fill`, `Stretch`, `Integer` -- [x] 1.2 Add `scale_mode` field to `Config::Render` struct (default: `Stretch`) -- [x] 1.3 Add `integer_scale` field to `Config::Render` struct (default: `0` = auto) -- [x] 1.4 Update `config.cpp` to parse `scale_mode` from TOML -- [x] 1.5 Update `config.cpp` to parse `integer_scale` from TOML (validate 0-8 range) -- [x] 1.6 Add settings to `config/goggles.template.toml` with documentation; first-run bootstrap - may materialize them into `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` - -## 2. Viewport Calculation Utility -- [x] 2.1 Create `ScaledViewport` struct (offset_x, offset_y, width, height) -- [x] 2.2 Implement `calculate_viewport(source_extent, target_extent, scale_mode, integer_scale)` helper -- [x] 2.3 Implement Fit mode: scale to fit preserving aspect ratio, center -- [x] 2.4 Implement Fill mode: scale to fill preserving aspect ratio, center, may exceed bounds -- [x] 2.5 Implement Stretch mode: use target_extent directly (no offset) -- [x] 2.6 Implement Integer mode with auto (0): find max integer scale that fits, center -- [x] 2.7 Implement Integer mode with fixed scale (1-8): multiply source by scale, center - -## 3. Filter Chain Integration -- [x] 3.1 Pass `ScaleMode` and `integer_scale` to FilterChain -- [x] 3.2 Calculate `FinalViewportSize` based on scale mode and source dimensions -- [x] 3.3 Update SemanticBinder to use calculated FinalViewportSize -- [x] 3.4 Ensure passes with `scale_type = viewport` render at correct resolution - -## 4. OutputPass Viewport Rendering -- [x] 4.1 Add scale mode parameters to `OutputPass::record()` or `PassContext` -- [x] 4.2 Use `calculate_viewport()` to determine actual viewport position/size -- [x] 4.3 Set viewport to calculated values (may be offset from origin) -- [x] 4.4 Set scissor to swapchain bounds (clips overflow in Fill/Integer modes) -- [x] 4.5 Ensure clear color fills letterbox/pillarbox areas (black via clear) - -## 5. Integration -- [x] 5.1 Wire config settings through to filter chain and output pass -- [x] 5.2 Handle window resize: recalculate viewport on swapchain recreation - -## 6. Testing -- [X] 6.1 Manual test: Fit mode with 4:3 source in 16:9 window (letterbox) -- [X] 6.2 Manual test: Fit mode with 16:9 source in 4:3 window (pillarbox) -- [X] 6.3 Manual test: Fill mode crops correctly -- [X] 6.4 Manual test: Stretch mode maintains current behavior -- [X] 6.5 Manual test: Integer mode auto (0) finds max scale that fits -- [X] 6.6 Manual test: Integer mode with scale=1 shows original size centered -- [X] 6.7 Manual test: Integer mode with scale=2 shows 2x centered -- [ ] 6.8 Manual test: scale_type=viewport shader renders at correct FinalViewportSize (deferred: no shader uses FinalViewportSize) -- [X] 6.9 Manual test: Config parsing for all modes and integer_scale values diff --git a/openspec/changes/archive/2025-12-21-add-crt-royale-support/design.md b/openspec/changes/archive/2025-12-21-add-crt-royale-support/design.md deleted file mode 100644 index cb73eb3a..00000000 --- a/openspec/changes/archive/2025-12-21-add-crt-royale-support/design.md +++ /dev/null @@ -1,50 +0,0 @@ -## Context - -`crt-royale` is a 12-pass RetroArch preset that depends on multiple external mask textures, alias-based pass routing (e.g., `VERTICAL_SCANLINES`), and a large UBO of runtime parameters. Goggles currently binds a single source texture to every sampler and only populates an MVP UBO, so crt-royale cannot render correctly. - -## Goals / Non-Goals - -- Goals: - - Load and bind preset textures by name with correct sampler state. - - Route alias outputs to later passes and provide alias size push constants. - - Populate UBO parameters and apply preset overrides. -- Non-Goals: - - OriginalHistory / PassFeedback semantics (deferred). - - UI for tuning shader parameters at runtime (deferred). - -## Decisions - -- Decision: add a small texture-loading utility for PNG LUTs. - - Rationale: crt-royale LUTs are PNG assets; a lightweight loader is sufficient. - - Implementation: use `stb_image` (header-only) to decode, upload via staging buffer, and generate mipmaps when requested. - -- Decision: bind textures by name rather than by index. - - Rationale: RetroArch presets rely on named samplers (`Source`, `Original`, alias names, and LUT names). - - Implementation: create a per-pass binding table `{binding -> texture source}` derived from reflection + preset alias/texture registry. - -- Decision: sampler state is per-binding, not a single sampler per pass. - - Rationale: crt-royale needs `repeat` wrap and mipmaps for LUTs, while source inputs often clamp. - - Implementation: maintain a `SamplerKey` cache (filter, mipmap, wrap U/V/W) and assign per-binding samplers. - -- Decision: use reflection member names to populate UBO parameters. - - Rationale: crt-royale declares many parameters in `bind-shader-params.h` as UBO members. - - Implementation: build a `{name -> offset}` map from `UniformMember` and write values each frame or once after creation. - -## Risks / Trade-offs - -- PNG decoding adds a new dependency and CPU work during preset load. - - Mitigation: load once at preset initialization; cache textures for reuse. -- Per-binding samplers increase descriptor updates and resource count. - - Mitigation: cache samplers and reuse across bindings with identical state. - -## Migration Plan - -1. Add loader + GPU upload for preset textures. -2. Extend preset parsing for wrap modes. -3. Add alias routing + size push constants. -4. Map parameter defaults/overrides into UBO members. - -## Open Questions - -- Should we support non-PNG LUT formats now (e.g., JPG, BMP), or only PNG? -- Do we want optional shared texture cache across presets? diff --git a/openspec/changes/archive/2025-12-21-add-crt-royale-support/proposal.md b/openspec/changes/archive/2025-12-21-add-crt-royale-support/proposal.md deleted file mode 100644 index 87a8c5a5..00000000 --- a/openspec/changes/archive/2025-12-21-add-crt-royale-support/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -# Change: Add RetroArch CRT Royale Support - -## Why - -We need to support the RetroArch `crt-royale` preset. It relies on multi-pass aliases, external LUT textures, per-texture sampler state, and a large runtime-parameter UBO. The current filter chain only binds a single source texture and ignores preset textures, aliases, and UBO parameters, so crt-royale cannot render correctly. - -## Investigation Summary - -- Preset textures and aliases are parsed but never used in runtime execution (`src/render/chain/preset_parser.hpp`, `src/render/chain/preset_parser.cpp`, `src/render/chain/filter_chain.cpp`). -- `FilterPass` binds every reflected sampler to the same `source_view` with a single clamp/no-mip sampler (`src/render/chain/filter_pass.cpp`). -- Push constants only cover standard semantics; aliased `ALIASSize` inputs (e.g., `VERTICAL_SCANLINESSize`) are not populated (`src/render/chain/filter_pass.cpp`, `src/render/chain/semantic_binder.hpp`). -- UBO parameters are never populated beyond MVP, while crt-royale defines a large `global` UBO (`src/render/chain/filter_pass.cpp`, `src/render/shader/slang_reflect.cpp`). -- No existing image/PNG loader in `src/` for LUT textures (grep search for stb/image loaders returned none). - -## What Changes - -- Load and bind preset-defined textures (mask LUTs and other external assets) by name. -- Honor per-texture sampling flags (`*_linear`, `*_mipmap`, `*_wrap_mode`). -- Route pass outputs via preset `aliasN` names and bind alias textures in later passes. -- Provide `ALIASSize` push constants for aliased inputs. -- Populate parameter values into UBO members and push constants by name, applying preset parameter overrides. - -## Impact - -- Affected specs: `render-pipeline` (ADDED requirements for preset textures, alias routing, and parameter binding). -- Affected code: - - `src/render/chain/filter_chain.*` (load textures, alias map, parameter overrides) - - `src/render/chain/filter_pass.*` (per-binding sampler, texture routing, size push constants, UBO parameter writes) - - `src/render/chain/preset_parser.*` (wrap_mode parsing) - - `src/render/shader/slang_reflect.*` (UBO member metadata already available; used for name-to-offset mapping) -- Dependencies: add a PNG loader (likely `stb_image` or equivalent) for LUT assets. -- Verification: add/extend tests to confirm preset texture parsing + alias size binding + UBO parameter mapping. diff --git a/openspec/changes/archive/2025-12-21-add-crt-royale-support/specs/render-pipeline/spec.md b/openspec/changes/archive/2025-12-21-add-crt-royale-support/specs/render-pipeline/spec.md deleted file mode 100644 index a4842919..00000000 --- a/openspec/changes/archive/2025-12-21-add-crt-royale-support/specs/render-pipeline/spec.md +++ /dev/null @@ -1,48 +0,0 @@ -## ADDED Requirements - -### Requirement: Preset Texture Assets - -The filter chain SHALL load external textures listed in a RetroArch preset `textures` entry and bind them by name to matching sampler uniforms. - -#### Scenario: Mask LUTs loaded and bound -- **WHEN** a preset defining `textures = "mask_a;mask_b"` with paths for each name is loaded -- **THEN** each texture SHALL be decoded and uploaded to a GPU image -- **AND** each texture SHALL be bound to the sampler with the same name in the shader - -### Requirement: Preset Texture Sampling Overrides - -The filter chain SHALL honor per-texture sampling flags from the preset (`*_linear`, `*_mipmap`, `*_wrap_mode`). - -#### Scenario: Repeat + mipmapped mask texture -- **GIVEN** a preset sets `mask_grille_texture_large_wrap_mode = "repeat"` and `mask_grille_texture_large_mipmap = true` -- **WHEN** the preset is loaded -- **THEN** the bound sampler SHALL use repeat addressing and mipmapped sampling - -### Requirement: Alias Pass Routing - -The filter chain SHALL expose aliased pass outputs as named textures for subsequent passes, and SHALL provide `ALIASSize` push constants for aliased inputs. - -#### Scenario: Vertical scanline alias -- **GIVEN** pass 1 declares `alias1 = "VERTICAL_SCANLINES"` -- **WHEN** pass 7 samples a sampler named `VERTICAL_SCANLINES` -- **THEN** the bound image SHALL be the output of pass 1 -- **AND** `VERTICAL_SCANLINESSize` SHALL reflect the aliased texture size as vec4 - -### Requirement: Parameter Override Binding - -The filter chain SHALL apply preset parameter overrides and populate shader parameters by name into push constants or UBO members. - -#### Scenario: Override applied to UBO member -- **GIVEN** a shader defines parameter `mask_type` in its UBO -- **AND** the preset includes `mask_type = 2.0` -- **WHEN** the pass is recorded -- **THEN** the UBO member named `mask_type` SHALL be written with value `2.0` - -### Requirement: Pass Input Mipmap Control - -The filter chain SHALL honor `mipmap_inputN` when selecting sampler state for a pass input. - -#### Scenario: Mipmap input enabled -- **GIVEN** a preset sets `mipmap_input11 = true` -- **WHEN** pass 11 samples `Source` -- **THEN** the sampler bound to `Source` SHALL have mipmapping enabled diff --git a/openspec/changes/archive/2025-12-21-add-crt-royale-support/tasks.md b/openspec/changes/archive/2025-12-21-add-crt-royale-support/tasks.md deleted file mode 100644 index 88d1d711..00000000 --- a/openspec/changes/archive/2025-12-21-add-crt-royale-support/tasks.md +++ /dev/null @@ -1,8 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add PNG texture loader + GPU upload utility (staging + optional mipmap generation) -- [x] 1.2 Extend `PresetParser` to parse `*_wrap_mode` and store in `TextureConfig` -- [x] 1.3 Add texture registry to `FilterChain` and load preset textures at `load_preset()` -- [x] 1.4 Add alias routing and size map for `ALIASSize` push constants -- [x] 1.5 Update `FilterPass` to bind textures by name (Source/Original/aliases/LUTs) -- [x] 1.6 Populate UBO parameter values by reflection name, applying preset overrides -- [x] 1.7 Add/extend tests for texture parsing, alias size mapping, and UBO parameter binding diff --git a/openspec/changes/archive/2025-12-22-add-cli-args/proposal.md b/openspec/changes/archive/2025-12-22-add-cli-args/proposal.md deleted file mode 100644 index be7cca96..00000000 --- a/openspec/changes/archive/2025-12-22-add-cli-args/proposal.md +++ /dev/null @@ -1,22 +0,0 @@ -# Change: Add CLI Argument Parsing - -## Why -The Goggles application previously hardcoded the configuration path and lacked runtime overrides for shader presets. This limited usability for development and testing. - -## What Changes -- **CLI11 Integration**: Added `CLI11` as a modern, declarative command-line parser. -- **Header-Only Module**: Implemented parsing logic in `src/app/cli.hpp` to maintain a clean codebase. -- **Exception Safety**: Encapsulated CLI11's exception-based control flow (help/version/errors) within the library boundary, returning a policy-compliant `Result` to the application. -- **Key Options**: - - `--config`, `-c`: Override default configuration path. - - `--shader`, `-s`: Override shader preset. - - `--help`, `-h`: Automatic help generation. - - `--version`, `-v`: Display version info. - -## Impact -- **Affected specs**: `app-window` -- **Affected code**: - - `src/app/main.cpp` (simplified orchestration) - - `src/app/cli.hpp` (new parsing module) - - `cmake/Dependencies.cmake` (new dependency) - - `src/app/CMakeLists.txt` (linkage) \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-22-add-cli-args/specs/app-window/spec.md b/openspec/changes/archive/2025-12-22-add-cli-args/specs/app-window/spec.md deleted file mode 100644 index 5a88428b..00000000 --- a/openspec/changes/archive/2025-12-22-add-cli-args/specs/app-window/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -## ADDED Requirements -### Requirement: Command Line Interface -The application SHALL support command-line arguments to override default behavior and provide information without throwing exceptions into the main execution flow. - -#### Scenario: Display help -- **WHEN** the application is run with `--help` -- **THEN** it SHALL print usage information -- **AND** it SHALL exit with code 0 - -#### Scenario: Display version -- **WHEN** the application is run with `--version` -- **THEN** it SHALL print "Goggles v0.1.0" -- **AND** it SHALL exit with code 0 - -#### Scenario: Exception encapsulation -- **GIVEN** the application uses CLI11 for parsing -- **WHEN** `parse_cli` is called -- **THEN** it MUST catch all library exceptions internally -- **AND** it MUST return a `nonstd::expected` value-based result - -#### Scenario: Override shader preset -- **GIVEN** a valid `.slangp` file exists -- **WHEN** run with `--shader ` -- **THEN** the specified preset SHALL be loaded regardless of config file settings \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-22-add-cli-args/tasks.md b/openspec/changes/archive/2025-12-22-add-cli-args/tasks.md deleted file mode 100644 index 108ceb87..00000000 --- a/openspec/changes/archive/2025-12-22-add-cli-args/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add `CLI11` dependency via `CPM.cmake` -- [x] 1.2 Implement `goggles::app::parse_cli` in `cli.hpp` -- [x] 1.3 Encapsulate `CLI::ParseError` to ensure no exceptions propagate to core logic -- [x] 1.4 Integrate `CliResult` (nonstd::expected) into `main.cpp` -- [x] 1.5 Verify help and version flags exit with code 0 -- [x] 1.6 Verify invalid arguments trigger `EXIT_FAILURE` -- [x] 1.7 Clean up all narration comments per Policy C.7 -- [x] 1.8 Define project version in `CMakeLists.txt` and generate `version.hpp` via `configure_file` \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-22-add-version-management/proposal.md b/openspec/changes/archive/2025-12-22-add-version-management/proposal.md deleted file mode 100644 index 36795f7e..00000000 --- a/openspec/changes/archive/2025-12-22-add-version-management/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Change: Centralized Version Management System - -## Why - -Currently, version information is partially scattered: -- `CMakeLists.txt` defines `VERSION 0.1.0` in `project()` directive -- Compile definitions expose `GOGGLES_VERSION` string -- `vulkan_backend.cpp:160-162` hardcodes `VK_MAKE_VERSION(0, 1, 0)` independently - -This creates a maintenance risk: when updating the version, developers must remember to update hardcoded values in `vulkan_backend.cpp`, with no mechanism to prevent inconsistency. - -## What Changes - -- Add three new compile definitions: `GOGGLES_VERSION_MAJOR`, `GOGGLES_VERSION_MINOR`, `GOGGLES_VERSION_PATCH` -- Update `vulkan_backend.cpp` to use macros instead of hardcoded `0, 1, 0` -- Maintain `project(goggles VERSION x.y.z)` as single source of truth - -This establishes version propagation using CMake's built-in versioning with compile definitions, following the existing pattern already used for `GOGGLES_VERSION` and `GOGGLES_PROJECT_NAME`. - -## Impact - -- Affected specs: build-system (new capability) -- Affected code: - - `CMakeLists.txt` - Add 3 compile definitions for version components - - `src/render/backend/vulkan_backend.cpp` - Replace hardcoded `0, 1, 0` with macros -- Build impact: None (follows existing compile definition pattern) - -## Policy Compliance - -This change adheres to project policies: -- **Section C.7**: No new files, no comments needed (follows existing pattern) -- **Section D.2**: Vulkan version usage via `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, ...)` macros -- **Section B.1**: No changes to logging (already using macros) diff --git a/openspec/changes/archive/2025-12-22-add-version-management/specs/build-system/spec.md b/openspec/changes/archive/2025-12-22-add-version-management/specs/build-system/spec.md deleted file mode 100644 index 3fe4731f..00000000 --- a/openspec/changes/archive/2025-12-22-add-version-management/specs/build-system/spec.md +++ /dev/null @@ -1,70 +0,0 @@ -## ADDED Requirements - -### Requirement: Single Source of Truth for Version - -The build system SHALL maintain project version in a single authoritative location that automatically propagates to all code via compile definitions. - -#### Scenario: Version defined in CMake project directive - -- **GIVEN** the root `CMakeLists.txt` file -- **WHEN** the `project()` directive is invoked -- **THEN** the VERSION parameter SHALL specify the project version in `MAJOR.MINOR.PATCH` format -- **AND** this SHALL be the only location where version is manually maintained - -#### Scenario: CMake version variables available after project directive - -- **GIVEN** `project(goggles VERSION 0.1.0)` has been invoked -- **WHEN** CMake configuration proceeds -- **THEN** variables `PROJECT_VERSION`, `PROJECT_VERSION_MAJOR`, `PROJECT_VERSION_MINOR`, `PROJECT_VERSION_PATCH` SHALL be defined -- **AND** variable `PROJECT_NAME` SHALL contain the project name - -### Requirement: Version Component Compile Definitions - -The build system SHALL define preprocessor macros for individual version components. - -#### Scenario: Major minor patch macros defined - -- **GIVEN** project version is `0.1.0` -- **WHEN** CMake processes compile definitions -- **THEN** `GOGGLES_VERSION_MAJOR` SHALL be defined as `0` -- **AND** `GOGGLES_VERSION_MINOR` SHALL be defined as `1` -- **AND** `GOGGLES_VERSION_PATCH` SHALL be defined as `0` -- **AND** these SHALL be defined via `add_compile_definitions()` - -#### Scenario: Vulkan version compatibility - -- **GIVEN** `GOGGLES_VERSION_MAJOR`, `GOGGLES_VERSION_MINOR`, `GOGGLES_VERSION_PATCH` macros are defined -- **WHEN** Vulkan application info is populated -- **THEN** `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` SHALL compile without errors -- **AND** SHALL produce correct Vulkan version encoding - -### Requirement: No Hardcoded Version Values - -Source code SHALL NOT contain hardcoded version numbers independent of CMake project version. - -#### Scenario: Vulkan backend uses version macros - -- **GIVEN** `VulkanBackend::create_instance()` sets Vulkan application info -- **WHEN** `applicationVersion` and `engineVersion` are assigned -- **THEN** they SHALL use `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` -- **AND** SHALL NOT use hardcoded values like `VK_MAKE_VERSION(0, 1, 0)` - -#### Scenario: No hardcoded version strings in source - -- **GIVEN** the version management system is implemented -- **WHEN** searching source files with `rg "0\.1\.0" src/` -- **THEN** no hardcoded version strings SHALL be found in source files -- **AND** all version references SHALL use compile definition macros - -### Requirement: Version Change Propagation - -Changes to the project version SHALL automatically propagate to all code without manual updates. - -#### Scenario: Version update workflow - -- **GIVEN** project version is `0.1.0` and code uses `GOGGLES_VERSION_*` macros -- **WHEN** `project(goggles VERSION 0.2.0)` is modified in `CMakeLists.txt` -- **AND** rebuild is performed -- **THEN** all macros SHALL reflect version `0.2.0` after recompilation -- **AND** no manual code changes SHALL be required -- **AND** Vulkan application info SHALL show version `0.2.0` diff --git a/openspec/changes/archive/2025-12-22-add-version-management/tasks.md b/openspec/changes/archive/2025-12-22-add-version-management/tasks.md deleted file mode 100644 index dc1e1a3c..00000000 --- a/openspec/changes/archive/2025-12-22-add-version-management/tasks.md +++ /dev/null @@ -1,17 +0,0 @@ -## 1. CMake Configuration -- [x] 1.1 Add `add_compile_definitions(GOGGLES_VERSION_MAJOR=${PROJECT_VERSION_MAJOR})` after existing `GOGGLES_VERSION` definition -- [x] 1.2 Add `add_compile_definitions(GOGGLES_VERSION_MINOR=${PROJECT_VERSION_MINOR})` -- [x] 1.3 Add `add_compile_definitions(GOGGLES_VERSION_PATCH=${PROJECT_VERSION_PATCH})` -- [x] 1.4 Add comment noting `project(VERSION ...)` as single source of truth - -## 2. Code Migration -- [x] 2.1 Update `src/render/backend/vulkan_backend.cpp` line 160: replace `VK_MAKE_VERSION(0, 1, 0)` with `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` for `applicationVersion` -- [x] 2.2 Update `src/render/backend/vulkan_backend.cpp` line 162: replace `VK_MAKE_VERSION(0, 1, 0)` with `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` for `engineVersion` -- [x] 2.3 Search for any other hardcoded version strings with `rg "0\.1\.0" src/` - -## 3. Testing & Validation -- [x] 3.1 Clean build: `make clean && make dev` -- [x] 3.2 Verify build succeeds -- [x] 3.3 Run `./build/debug/bin/goggles --version` and verify correct version output -- [x] 3.4 Test version change: modify `CMakeLists.txt` VERSION to `0.2.0`, rebuild, verify propagation -- [x] 3.5 Restore original version diff --git a/openspec/changes/archive/2025-12-23-add-async-capture-worker/design.md b/openspec/changes/archive/2025-12-23-add-async-capture-worker/design.md deleted file mode 100644 index 3e1a6080..00000000 --- a/openspec/changes/archive/2025-12-23-add-async-capture-worker/design.md +++ /dev/null @@ -1,215 +0,0 @@ -# Design: Async Capture Worker Thread - -## Architecture - -``` -┌──────────────────────┐ ┌─────────────────────────┐ -│ Main Thread │ │ Worker Thread │ -│ (vkQueuePresent) │ │ │ -├──────────────────────┤ ├─────────────────────────┤ -│ 1. Submit GPU copy │ │ while (!shutdown) { │ -│ with timeline sem │ │ cv.wait(queue) │ -│ 2. dup(dmabuf_fd) │ │ item = queue.pop() │ -│ 3. queue.push({ │ ──────> │ WaitSemaphoresKHR() │ -│ device, │ │ send_texture(fd) │ -│ timeline_sem, │ │ close(dup_fd) │ -│ timeline_value, │ │ } │ -│ dup_fd, │ │ │ -│ metadata │ │ Drain queue on exit │ -│ }) │ │ │ -│ 4. notify_one() │ │ │ -│ 5. return │ │ │ -└──────────────────────┘ └─────────────────────────┘ -``` - -## Key Design Decisions - -### 1. Timeline Semaphores (Not Fences) -**Decision:** Use Vulkan timeline semaphores (`VK_SEMAPHORE_TYPE_TIMELINE`) instead of binary fences. - -**Rationale:** -- Single semaphore per swapchain instead of per-frame fences -- More efficient GPU synchronization with monotonic counter -- `WaitSemaphoresKHR` allows waiting on specific timeline value -- Cleaner resource management (no fence reset needed) - -**Implementation:** -```cpp -// Per-swapchain timeline semaphore -VkSemaphore timeline_sem = VK_NULL_HANDLE; -uint64_t frame_counter = 0; - -// Submit with timeline signal -uint64_t timeline_value = ++swap->frame_counter; -VkTimelineSemaphoreSubmitInfo timeline_submit{}; -timeline_submit.pSignalSemaphoreValues = &timeline_value; - -// Worker waits on specific value -VkSemaphoreWaitInfo wait_info{}; -wait_info.pSemaphores = &item.timeline_sem; -wait_info.pValues = &item.timeline_value; -funcs.WaitSemaphoresKHR(device, &wait_info, timeout); -``` - -### 2. Eager Worker Thread Initialization -**Decision:** Start worker thread in `CaptureManager` constructor, not lazily on first frame. - -**Rationale:** -- Simpler code path with predictable initialization -- Avoids thread spawn latency on first captured frame -- Worker thread idles efficiently via condition variable when queue empty - -### 3. Runtime Toggle for Fallback -**Decision:** Provide `GOGGLES_CAPTURE_ASYNC` environment variable to control async vs sync mode. - -**Rationale:** -- Safety mechanism if threading issues discovered in production -- Allows performance comparison between sync and async modes -- Standard pattern for Vulkan layer configuration - -**Implementation:** -```cpp -static bool should_use_async_capture() { - static const bool use_async = []() { - const char* env = std::getenv("GOGGLES_CAPTURE_ASYNC"); - return env == nullptr || std::strcmp(env, "0") != 0; - }(); - return use_async; -} -``` - -### 4. Lock-Free Queue (SPSCQueue) -**Decision:** Use existing `goggles::util::SPSCQueue` for main→worker communication. - -**Rationale:** -- Already implemented and tested in src/util/queues.hpp -- Lock-free for single producer (main thread) / single consumer (worker) -- No mutex contention on hot path (queue.push) - -**Enhancement:** Added `empty()` method for idiomatic queue checks. - -### 5. dup() File Descriptor -**Decision:** Duplicate the DMA-buf FD for each queued frame. - -**Rationale:** -- Original FD (swap->dmabuf_fd) has swapchain lifetime -- Worker thread may process frame after swapchain destroyed -- dup() creates independent FD with separate refcount - -### 6. Sync Fence Fallback -**Decision:** Fall back to per-swapchain fence if timeline semaphores unavailable. - -**Rationale:** -- Timeline semaphores require Vulkan 1.2 or `VK_KHR_timeline_semaphore` -- Graceful degradation ensures capture works on older drivers -- Sync fallback uses single fence per swapchain (not per-frame) - -## Data Structures - -### AsyncCaptureItem -```cpp -struct AsyncCaptureItem { - VkDevice device = VK_NULL_HANDLE; - VkSemaphore timeline_sem = VK_NULL_HANDLE; - uint64_t timeline_value = 0; - int dmabuf_fd = -1; - CaptureTextureData metadata{}; -}; -``` - -### SwapData Additions -```cpp -// Timeline semaphore for async capture -VkSemaphore timeline_sem = VK_NULL_HANDLE; -uint64_t frame_counter = 0; - -// Fence for sync mode fallback -VkFence sync_fence = VK_NULL_HANDLE; -``` - -### FrameData Changes -```cpp -struct FrameData { - VkCommandPool cmd_pool = VK_NULL_HANDLE; - VkCommandBuffer cmd_buffer = VK_NULL_HANDLE; - uint64_t timeline_value = 0; // Tracks which timeline value this frame signaled - bool cmd_buffer_busy = false; -}; -``` - -### CaptureManager State -```cpp -// Async worker state -util::SPSCQueue async_queue_{16}; -std::thread worker_thread_; -std::atomic shutdown_{false}; -std::mutex cv_mutex_; -std::condition_variable cv_; -``` - -## Thread Lifecycle - -### Startup (Eager) -Worker thread spawned in constructor if async mode enabled: -```cpp -CaptureManager::CaptureManager() { - if (should_use_async_capture()) { - shutdown_.store(false, std::memory_order_release); - worker_thread_ = std::thread(&CaptureManager::worker_func, this); - } -} -``` - -### Shutdown -Idempotent `shutdown()` method with compare-and-swap: -```cpp -void CaptureManager::shutdown() { - bool expected = false; - if (!shutdown_.compare_exchange_strong(expected, true, std::memory_order_release)) { - return; // Already shutdown - } - cv_.notify_one(); - if (worker_thread_.joinable()) { - worker_thread_.join(); - } -} -``` - -## Error Handling - -### Queue Full -```cpp -if (!async_queue_.try_push(std::move(item))) { - close(dup_fd); - // Wait synchronously to ensure GPU completes - funcs.WaitSemaphoresKHR(device, &wait_info, Time::infinite); - return; -} -``` - -### dup() Failure -```cpp -int dup_fd = dup(swap->dmabuf_fd); -if (dup_fd < 0) { - // Wait synchronously and skip IPC - funcs.WaitSemaphoresKHR(device, &wait_info, Time::infinite); - return; -} -``` - -### Timeline Semaphore Creation Failure -```cpp -if (res != VK_SUCCESS) { - swap->timeline_sem = VK_NULL_HANDLE; - // Fall back to sync fence - funcs.CreateFence(device, &fence_info, nullptr, &swap->sync_fence); -} -``` - -## Performance Characteristics - -- **Main thread latency**: O(1) - queue.push + notify -- **Worker latency**: GPU-bound (WaitSemaphoresKHR duration) -- **Queue contention**: None (lock-free SPSC) -- **CPU overhead**: Minimal (blocking wait, not polling) -- **Memory overhead**: ~2 MB (thread stack) + ~1.3 KB (queue) diff --git a/openspec/changes/archive/2025-12-23-add-async-capture-worker/proposal.md b/openspec/changes/archive/2025-12-23-add-async-capture-worker/proposal.md deleted file mode 100644 index 078f7938..00000000 --- a/openspec/changes/archive/2025-12-23-add-async-capture-worker/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Add Async Capture Worker - -## Why -The current capture implementation performs synchronous fence waits in the application's render thread during `vkQueuePresentKHR`, which can cause frame pacing variance that is perceptible to users in low-latency game streaming scenarios. - -## What Changes -- Add dedicated worker thread using timeline semaphores for async GPU synchronization -- Move fence waiting and IPC operations off the render thread -- Add `GOGGLES_CAPTURE_ASYNC` environment variable for runtime toggle (default: enabled) -- Replace per-frame fences with single timeline semaphore per swapchain -- Add `empty()` method to `SPSCQueue` for idiomatic queue checks -- Add `shutdown()` method to `CaptureManager` for proper cleanup in shared library context - -## Impact -- Affected specs: `vk-layer-capture` -- Affected code: `src/capture/vk_layer/vk_capture.cpp`, `src/capture/vk_layer/vk_capture.hpp`, `src/util/queues.hpp` diff --git a/openspec/changes/archive/2025-12-23-add-async-capture-worker/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2025-12-23-add-async-capture-worker/specs/vk-layer-capture/spec.md deleted file mode 100644 index 170c49ac..00000000 --- a/openspec/changes/archive/2025-12-23-add-async-capture-worker/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,134 +0,0 @@ -# vk-layer-capture Specification Delta - -## ADDED Requirements - -### Requirement: Async Mode Configuration -The layer SHALL provide runtime control over async vs synchronous capture mode. - -#### Scenario: Default async mode -- **GIVEN** `GOGGLES_CAPTURE_ASYNC` environment variable is not set -- **WHEN** the layer initializes -- **THEN** async worker mode SHALL be enabled by default - -#### Scenario: Explicit async enable -- **GIVEN** `GOGGLES_CAPTURE_ASYNC=1` is set -- **WHEN** the layer initializes -- **THEN** async worker mode SHALL be enabled - -#### Scenario: Synchronous fallback -- **GIVEN** `GOGGLES_CAPTURE_ASYNC=0` is set -- **WHEN** the layer initializes -- **THEN** the layer SHALL use synchronous fence wait -- **AND** SHALL NOT spawn worker thread - -### Requirement: Timeline Semaphore Synchronization -The layer SHALL use Vulkan timeline semaphores for async GPU synchronization when available. - -#### Scenario: Timeline semaphore creation -- **WHEN** export image is initialized for a swapchain -- **THEN** the layer SHALL create a timeline semaphore with `VK_SEMAPHORE_TYPE_TIMELINE` -- **AND** initialize the timeline value to 0 - -#### Scenario: Timeline semaphore signaling -- **WHEN** `vkQueuePresentKHR` submits the copy command in async mode -- **THEN** the layer SHALL increment the per-swapchain frame counter -- **AND** signal the timeline semaphore with the new value via `VkTimelineSemaphoreSubmitInfo` - -#### Scenario: Timeline semaphore waiting -- **WHEN** the worker thread processes a queued item -- **THEN** the worker SHALL call `vkWaitSemaphoresKHR` with the item's timeline value -- **AND** proceed with IPC only after the semaphore reaches that value - -#### Scenario: Timeline semaphore fallback -- **WHEN** timeline semaphore creation fails -- **THEN** the layer SHALL fall back to per-swapchain binary fence -- **AND** use synchronous `vkWaitForFences` in the capture path - -### Requirement: Async Frame Processing -The layer SHALL use a dedicated worker thread to perform semaphore waiting and IPC operations off the render thread. - -#### Scenario: Worker thread initialization -- **WHEN** `CaptureManager` is constructed with async mode enabled -- **THEN** the layer SHALL spawn a worker thread eagerly -- **AND** initialize a lock-free SPSC queue with capacity of 16 - -#### Scenario: Frame enqueuing on render thread -- **WHEN** `vkQueuePresentKHR` submits the copy command in async mode -- **THEN** the layer SHALL duplicate the DMA-BUF file descriptor via `dup()` -- **AND** push an item containing {device, timeline_sem, timeline_value, dup_fd, metadata} to the queue -- **AND** return immediately without blocking on the semaphore - -#### Scenario: Frame processing on worker thread -- **WHEN** the worker thread receives a queued item -- **THEN** the worker SHALL call `vkWaitSemaphoresKHR` with blocking wait -- **AND** send the texture via IPC when the semaphore signals -- **AND** close the duplicated file descriptor - -#### Scenario: Queue overflow handling -- **WHEN** the queue is full and a new frame is submitted -- **THEN** the layer SHALL drop the current frame -- **AND** close the duplicated file descriptor immediately -- **AND** wait synchronously on the timeline semaphore to ensure GPU completion - -### Requirement: Worker Thread Lifecycle -The layer SHALL manage worker thread lifecycle to ensure clean startup and shutdown. - -#### Scenario: Graceful shutdown -- **WHEN** `CaptureManager::shutdown()` is called -- **THEN** the layer SHALL set a shutdown flag atomically -- **AND** notify the worker thread via condition variable -- **AND** join the worker thread before the method returns - -#### Scenario: Idempotent shutdown -- **WHEN** `shutdown()` is called multiple times -- **THEN** only the first call SHALL perform shutdown operations -- **AND** subsequent calls SHALL return immediately - -#### Scenario: Queue drain on shutdown -- **WHEN** the worker thread receives shutdown signal -- **THEN** the worker SHALL process all remaining queued items -- **AND** close all file descriptors before exiting - -### Requirement: Resource Lifetime Management -The layer SHALL ensure file descriptor and Vulkan handle lifetimes are independent between main and worker threads. - -#### Scenario: Independent file descriptor lifetime -- **WHEN** enqueueing a frame for async processing -- **THEN** the layer SHALL use `dup()` to create an independent FD -- **AND** the worker thread SHALL close the dup'd FD after use -- **AND** the original FD lifetime SHALL remain tied to swapchain lifetime - -## MODIFIED Requirements - -### Requirement: GPU Frame Copy -The layer SHALL intercept `vkQueuePresentKHR` and perform GPU-to-GPU copy to the export image. - -#### Scenario: Frame copy command recording -- **WHEN** `vkQueuePresentKHR` is called -- **THEN** the layer SHALL record a command buffer with `vkCmdCopyImage` -- **AND** include image memory barriers for layout transitions -- **AND** copy from `PRESENT_SRC_KHR` layout to export image in `TRANSFER_DST_OPTIMAL` - -#### Scenario: Async submission with timeline semaphore -- **WHEN** the copy command is submitted in async mode -- **THEN** the layer SHALL signal the timeline semaphore with the frame's timeline value -- **AND** enqueue the frame metadata to the worker thread -- **AND** return immediately to the application without blocking - -#### Scenario: Sync submission with fence -- **WHEN** the copy command is submitted in sync mode -- **THEN** the layer SHALL submit with the per-swapchain sync fence -- **AND** wait on the fence before sending via IPC -- **AND** reset the fence for reuse - -### Requirement: Layer Logging Constraints -The layer SHALL follow project logging policies for capture layer code. - -#### Scenario: Minimal hot-path logging -- **WHEN** `vkQueuePresentKHR` executes -- **THEN** no logging SHALL occur at info level or below -- **AND** only error/critical conditions MAY be logged - -#### Scenario: Initialization logging -- **WHEN** `vkCreateInstance` or `vkCreateDevice` is hooked -- **THEN** the layer MAY log at info level with `[goggles_vklayer]` prefix diff --git a/openspec/changes/archive/2025-12-23-add-async-capture-worker/tasks.md b/openspec/changes/archive/2025-12-23-add-async-capture-worker/tasks.md deleted file mode 100644 index d6a67e1e..00000000 --- a/openspec/changes/archive/2025-12-23-add-async-capture-worker/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -# Tasks: add-async-capture-worker - -## Phase 1: Core Implementation - -- [x] 1.1 Add `GOGGLES_CAPTURE_ASYNC` environment variable check -- [x] 1.2 Add `AsyncCaptureItem` struct with timeline semaphore fields -- [x] 1.3 Add timeline semaphore to `SwapData` struct -- [x] 1.4 Add sync fence fallback to `SwapData` struct -- [x] 1.5 Remove per-frame fences and semaphores from `FrameData` -- [x] 1.6 Add `timeline_value` tracking to `FrameData` -- [x] 1.7 Add worker thread state to `CaptureManager` -- [x] 1.8 Implement `worker_func()` with `WaitSemaphoresKHR` -- [x] 1.9 Implement eager worker thread startup in constructor -- [x] 1.10 Create timeline semaphore in `init_export_image()` -- [x] 1.11 Add sync fence fallback in `init_export_image()` -- [x] 1.12 Modify `capture_frame()` async path with timeline semaphore signaling -- [x] 1.13 Modify `capture_frame()` sync fallback path with per-swapchain fence -- [x] 1.14 Update `destroy_frame_resources()` to use timeline semaphore wait - -## Phase 2: Shutdown & Cleanup - -- [x] 2.1 Implement idempotent `shutdown()` method with compare-and-swap -- [x] 2.2 Implement destructor delegating to `shutdown()` -- [x] 2.3 Add queue drain logic on worker shutdown -- [x] 2.4 Clean up timeline semaphore in `cleanup_swap_data()` -- [x] 2.5 Clean up sync fence in `cleanup_swap_data()` -- [x] 2.6 Add `empty()` method to `SPSCQueue` - -## Phase 3: Testing & Validation - -- [X] 3.1 Manual testing: basic capture (async mode) -- [X] 3.2 Manual testing: basic capture (sync mode fallback) -- [X] 3.3 Manual testing: rapid swapchain recreation -- [X] 3.4 Manual testing: prolonged capture -- [X] 3.5 Manual testing: shutdown stress - -## Phase 4: Documentation - -- [x] 4.1 Update vk-layer-capture spec delta diff --git a/openspec/changes/archive/2025-12-23-add-tracy-profiling/design.md b/openspec/changes/archive/2025-12-23-add-tracy-profiling/design.md deleted file mode 100644 index 6d238779..00000000 --- a/openspec/changes/archive/2025-12-23-add-tracy-profiling/design.md +++ /dev/null @@ -1,139 +0,0 @@ -## Context - -Goggles is a real-time frame capture and post-processing application with strict latency requirements (<16.6ms per frame for 60fps). Understanding where time is spent in the render loop, filter chain, and capture pipeline is essential for optimization. Current debugging relies on log timestamps and manual analysis, which is insufficient for frame-level profiling. - -**Stakeholders:** Developers optimizing render performance, maintainers debugging latency issues. - -## Goals / Non-Goals - -**Goals:** -- Provide zero-overhead profiling when disabled (compile-time elimination) -- Enable detailed frame-by-frame timing analysis via Tracy UI -- Abstract profiling behind macros to allow future backend swaps (chrono, GPU timestamps) -- Instrument all performance-critical code paths identified in the codebase analysis - -**Non-Goals:** -- GPU profiling via Vulkan timestamp queries (future work) -- Automated performance regression testing -- Production telemetry or metrics collection - -## Decisions - -### Decision 1: Use Tracy via CPM.cmake - -**What:** Integrate Tracy v0.11.1+ using CPM.cmake with `TRACY_ENABLE` compile definition controlled by `ENABLE_PROFILING` CMake option. - -**Why:** -- Tracy is the industry standard for game/graphics profiling -- Header-only core with minimal integration overhead -- Excellent Vulkan support (GPU zones, memory tracking) -- CPM.cmake aligns with project dependency policy (Section G) - -**Alternatives considered:** -- Optick: Less actively maintained, similar feature set -- Superluminal: Windows-only -- Manual chrono timing: Insufficient visibility, no UI - -### Decision 2: Macro Abstraction Layer - -**What:** Create `src/util/profiling.hpp` with macros that wrap Tracy calls: - -```cpp -// When ENABLE_PROFILING is ON: -#define GOGGLES_PROFILE_SCOPE(name) ZoneScopedN(name) -#define GOGGLES_PROFILE_FUNCTION() ZoneScoped -#define GOGGLES_PROFILE_FRAME(name) FrameMarkNamed(name) -#define GOGGLES_PROFILE_TAG(text) ZoneText(text, strlen(text)) -#define GOGGLES_PROFILE_VALUE(name, v) TracyPlot(name, v) -// Note: GOGGLES_PROFILE_BEGIN/END not implemented - scoped zones preferred (see tasks.md 2.5) - -// When ENABLE_PROFILING is OFF: -// All macros expand to nothing (zero overhead) -``` - -**Why:** -- Allows swapping profiling backend without touching instrumented code -- Enables lightweight `std::chrono` fallback for environments without Tracy server -- Follows project pattern established by `GOGGLES_TRY`/`GOGGLES_MUST` macros - -### Decision 3: clang-tidy Suppression for Profiling Macros - -**What:** Add `NOLINTBEGIN/NOLINTEND` blocks around macro definitions in `profiling.hpp` to suppress: -- `cppcoreguidelines-macro-usage` - Macros are intentional for zero-overhead -- `bugprone-macro-parentheses` - Tracy macros have specific syntax requirements - -**Why:** Project policy requires clean clang-tidy, but profiling macros are an accepted exception similar to `error.hpp` pattern. - -### Decision 4: Profiling Points Selection - -Based on codebase analysis, instrument these critical paths: - -**Main Loop (`src/app/main.cpp`):** -- Frame boundary: `GOGGLES_PROFILE_FRAME("Main")` at loop start -- Event processing scope -- Capture poll scope -- Render/clear call scope - -**Vulkan Backend (`src/render/backend/vulkan_backend.cpp`):** -- `render_frame()` - Full frame render -- `acquire_next_image()` - Swapchain acquire -- `record_render_commands()` - Command buffer recording -- `submit_and_present()` - Queue submit and present -- `import_dmabuf()` - DMA-BUF import (potentially expensive) -- `create_swapchain()` - Swapchain creation (infrequent but heavy) -- `recreate_swapchain()` - Resize handling - -**Filter Chain (`src/render/chain/filter_chain.cpp`):** -- `record()` - Full chain execution -- `ensure_framebuffers()` - Framebuffer management -- `load_preset()` - Preset loading (startup only) - -**Filter Pass (`src/render/chain/filter_pass.cpp`):** -- `record()` - Per-pass execution with pass index tag -- `init()` - Pass initialization -- `create_pipeline()` - Pipeline creation (startup only) -- `update_descriptor()` - Descriptor updates - -**Shader Runtime (`src/render/shader/shader_runtime.cpp`):** -- `compile_shader()` - Overall compilation -- `compile_retroarch_shader()` - RetroArch shader path -- `compile_glsl_with_reflection()` - GLSL compilation -- `load_cached_spirv()` / `save_cached_spirv()` - Cache I/O - -**Capture Receiver (`src/capture/capture_receiver.cpp`):** -- `poll_frame()` - Frame reception -- `init()` - Initialization - -**Capture Layer (`src/capture/vk_layer/vk_capture.cpp`):** -- `on_present()` - Present interception (minimal instrumentation) -- `capture_frame()` - Frame capture -- `record_copy_commands()` - Copy command recording - -**Note:** Capture layer instrumentation must be minimal due to performance constraints (Project Policy B.4). - -## Risks / Trade-offs - -| Risk | Mitigation | -|------|------------| -| Tracy overhead in hot paths | Compile-time elimination when disabled; minimal zones in vk_layer | -| Macro proliferation | Limited to profiling.hpp; follows established error.hpp pattern | -| Version compatibility | Pin Tracy version in CPM; test with CI | -| Binary size increase | Tracy client adds ~100KB; acceptable for debug/profile builds | - -## Migration Plan - -1. Add Tracy dependency (disabled by default) -2. Create profiling.hpp with macro definitions -3. Add instrumentation to identified code paths -4. Update build presets with `profile` preset -5. Document usage in README or docs/ - -**Rollback:** Remove `ENABLE_PROFILING` option usage; profiling code compiles to nothing. - -## Open Questions - -1. Should GPU zones (`TracyVkZone`) be included in initial implementation or deferred? - - **Recommendation:** Defer to follow-up change. Requires command buffer integration. - -2. Should capture layer use Tracy or remain uninstrumented for minimal overhead? - - **Recommendation:** Minimal instrumentation only (`on_present`, `capture_frame`). Skip hot paths per policy B.4. diff --git a/openspec/changes/archive/2025-12-23-add-tracy-profiling/proposal.md b/openspec/changes/archive/2025-12-23-add-tracy-profiling/proposal.md deleted file mode 100644 index a15dd4e2..00000000 --- a/openspec/changes/archive/2025-12-23-add-tracy-profiling/proposal.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change: Add Tracy Profiler Integration - -## Why - -Real-time performance analysis is critical for a frame-capture application targeting sub-16ms latency. Tracy provides frame-level visibility into CPU/GPU timing, helping identify bottlenecks in the render loop, filter chain execution, and capture pipeline without the overhead of heavyweight profilers. - -## What Changes - -- **New dependency:** Tracy profiler via CPM.cmake with `ENABLE_PROFILING` CMake option -- **New header:** `src/util/profiling.hpp` with abstracted macros that wrap Tracy (or fallback to no-op/chrono) -- **Instrumentation:** Add profiling zones to performance-critical code paths across all modules -- **clang-tidy:** Suppress macro-related warnings only for profiling macros - -## Impact - -- Affected specs: New `profiling` capability -- Affected code: - - `cmake/Dependencies.cmake` - Tracy dependency - - `cmake/CompilerConfig.cmake` - `ENABLE_PROFILING` option - - `src/util/profiling.hpp` - Profiling macro header (new file) - - `src/util/CMakeLists.txt` - Link Tracy - - `src/app/main.cpp` - Frame marks, main loop profiling - - `src/render/backend/vulkan_backend.cpp` - Render pipeline profiling - - `src/render/chain/filter_chain.cpp` - Filter chain profiling - - `src/render/chain/filter_pass.cpp` - Per-pass profiling - - `src/render/shader/shader_runtime.cpp` - Shader compilation profiling - - `src/capture/capture_receiver.cpp` - Capture receive profiling - - `src/capture/vk_layer/vk_capture.cpp` - Capture layer profiling (minimal, performance-critical) diff --git a/openspec/changes/archive/2025-12-23-add-tracy-profiling/specs/profiling/spec.md b/openspec/changes/archive/2025-12-23-add-tracy-profiling/specs/profiling/spec.md deleted file mode 100644 index 275e0852..00000000 --- a/openspec/changes/archive/2025-12-23-add-tracy-profiling/specs/profiling/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -## ADDED Requirements - -### Requirement: Profiling Infrastructure - -The system SHALL provide a compile-time toggleable profiling infrastructure via a CMake option `ENABLE_PROFILING`. - -#### Scenario: Profiling disabled (default) - -- **WHEN** `ENABLE_PROFILING` is `OFF` (default) -- **THEN** all profiling macros SHALL expand to no-ops -- **AND** no Tracy symbols or overhead SHALL be present in the binary - -#### Scenario: Profiling enabled - -- **WHEN** `ENABLE_PROFILING` is `ON` -- **THEN** profiling macros SHALL emit Tracy instrumentation -- **AND** the application SHALL be connectable to the Tracy profiler UI - -### Requirement: Profiling Macro API - -The system SHALL provide profiling macros in `src/util/profiling.hpp` that abstract the underlying profiler implementation. - -#### Scenario: Scoped zone profiling - -- **WHEN** `GOGGLES_PROFILE_SCOPE(name)` is used -- **THEN** a named profiling zone SHALL be created that ends when the scope exits - -#### Scenario: Function profiling - -- **WHEN** `GOGGLES_PROFILE_FUNCTION()` is used -- **THEN** a profiling zone named after the enclosing function SHALL be created - -#### Scenario: Frame boundary marking - -- **WHEN** `GOGGLES_PROFILE_FRAME(name)` is used -- **THEN** a frame boundary marker SHALL be emitted for frame-rate analysis - -> **Note:** Manual zone control via `GOGGLES_PROFILE_BEGIN/END` is not implemented. Scoped zones (`GOGGLES_PROFILE_SCOPE`, `GOGGLES_PROFILE_FUNCTION`) are used instead for RAII safety. See tasks.md item 2.5. - -#### Scenario: Zone annotation - -- **WHEN** `GOGGLES_PROFILE_TAG(text)` is used within a zone -- **THEN** the zone SHALL be annotated with the provided text - -#### Scenario: Numeric value plotting - -- **WHEN** `GOGGLES_PROFILE_VALUE(name, value)` is used -- **THEN** the numeric value SHALL be recorded for time-series visualization - -### Requirement: Render Pipeline Instrumentation - -The system SHALL instrument performance-critical render pipeline functions with profiling zones. - -#### Scenario: Frame render profiling - -- **WHEN** `VulkanBackend::render_frame()` executes -- **THEN** profiling data SHALL capture the full frame render duration - -#### Scenario: Filter chain profiling - -- **WHEN** `FilterChain::record()` executes -- **THEN** profiling data SHALL capture the filter chain execution duration - -#### Scenario: Per-pass profiling - -- **WHEN** `FilterPass::record()` executes -- **THEN** profiling data SHALL capture individual pass durations with pass identification - -### Requirement: Shader Compilation Instrumentation - -The system SHALL instrument shader compilation functions with profiling zones. - -#### Scenario: Shader compilation profiling - -- **WHEN** `ShaderRuntime::compile_shader()` executes -- **THEN** profiling data SHALL capture compilation duration - -#### Scenario: Cache operation profiling - -- **WHEN** SPIR-V cache load or save operations execute -- **THEN** profiling data SHALL capture I/O duration - -### Requirement: Capture Pipeline Instrumentation - -The system SHALL instrument capture pipeline functions with minimal profiling to avoid performance impact. - -#### Scenario: Frame capture profiling - -- **WHEN** `CaptureManager::capture_frame()` executes -- **THEN** profiling data SHALL capture capture operation duration - -#### Scenario: Capture layer hot path protection - -- **WHEN** `vkQueuePresentKHR` hook executes -- **THEN** profiling overhead SHALL be minimal (entry marker only, no nested zones) - -### Requirement: CMake Build Preset - -The system SHALL provide a CMake preset for profiling builds. - -#### Scenario: Profile preset usage - -- **WHEN** building with `cmake --preset profile` -- **THEN** a Release build with profiling enabled SHALL be produced diff --git a/openspec/changes/archive/2025-12-23-add-tracy-profiling/tasks.md b/openspec/changes/archive/2025-12-23-add-tracy-profiling/tasks.md deleted file mode 100644 index 3a855a76..00000000 --- a/openspec/changes/archive/2025-12-23-add-tracy-profiling/tasks.md +++ /dev/null @@ -1,113 +0,0 @@ -## 1. CMake Integration - -- [x] 1.1 Add `ENABLE_PROFILING` option to `cmake/CompilerConfig.cmake` (OFF by default) -- [x] 1.2 Add Tracy dependency to `cmake/Dependencies.cmake` via CPM.cmake -- [x] 1.3 Configure Tracy with `TRACY_ENABLE` definition when `ENABLE_PROFILING=ON` -- [x] 1.4 Create `goggles_profiling_options` interface library for propagating Tracy settings -- [x] 1.5 Add `profile` preset to `CMakePresets.json` (Release with profiling enabled) - -## 2. Profiling Header - -- [x] 2.1 Create `src/util/profiling.hpp` with macro definitions -- [x] 2.2 Implement `GOGGLES_PROFILE_SCOPE(name)` - Named scoped zone -- [x] 2.3 Implement `GOGGLES_PROFILE_FUNCTION()` - Auto-named function zone -- [x] 2.4 Implement `GOGGLES_PROFILE_FRAME(name)` - Frame boundary marker -- [x] 2.5 Implement `GOGGLES_PROFILE_BEGIN(name)` / `GOGGLES_PROFILE_END()` - Manual zone pair (not implemented - not needed per design) -- [x] 2.6 Implement `GOGGLES_PROFILE_TAG(text)` - Zone text annotation -- [x] 2.7 Implement `GOGGLES_PROFILE_VALUE(name, value)` - Numeric plot value -- [x] 2.8 Add clang-tidy NOLINT suppressions for macro definitions -- [x] 2.9 Ensure all macros expand to no-op when `ENABLE_PROFILING=OFF` -- [x] 2.10 Update `src/util/CMakeLists.txt` to link Tracy conditionally - -## 3. Main Application Instrumentation - -- [x] 3.1 Add `GOGGLES_PROFILE_FRAME("Main")` at main loop start in `src/app/main.cpp` -- [x] 3.2 Add `GOGGLES_PROFILE_SCOPE("EventProcessing")` around SDL event polling -- [x] 3.3 Add `GOGGLES_PROFILE_SCOPE("CaptureReceive")` around `capture_receiver.poll_frame()` -- [x] 3.4 Add `GOGGLES_PROFILE_SCOPE("RenderFrame")` / `GOGGLES_PROFILE_SCOPE("RenderClear")` around render calls -- [x] 3.5 Add `GOGGLES_PROFILE_SCOPE("HandleResize")` around resize handling - -## 4. Vulkan Backend Instrumentation - -- [x] 4.1 Add `GOGGLES_PROFILE_FUNCTION()` to `VulkanBackend::render_frame()` -- [x] 4.2 Add `GOGGLES_PROFILE_FUNCTION()` to `VulkanBackend::render_clear()` -- [x] 4.3 Add `GOGGLES_PROFILE_SCOPE("AcquireImage")` in `acquire_next_image()` -- [x] 4.4 Add `GOGGLES_PROFILE_SCOPE("RecordCommands")` in `record_render_commands()` -- [x] 4.5 Add `GOGGLES_PROFILE_SCOPE("SubmitPresent")` in `submit_and_present()` -- [x] 4.6 Add `GOGGLES_PROFILE_FUNCTION()` to `import_dmabuf()` -- [x] 4.7 Add `GOGGLES_PROFILE_FUNCTION()` to `create_swapchain()` -- [x] 4.8 Add `GOGGLES_PROFILE_FUNCTION()` to `recreate_swapchain()` -- [x] 4.9 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` -- [x] 4.10 Add `GOGGLES_PROFILE_FUNCTION()` to `init_filter_chain()` -- [x] 4.11 Add `GOGGLES_PROFILE_FUNCTION()` to `load_shader_preset()` - -## 5. Filter Chain Instrumentation - -- [x] 5.1 Add `GOGGLES_PROFILE_FUNCTION()` to `FilterChain::record()` -- [x] 5.2 Add `GOGGLES_PROFILE_SCOPE("EnsureFramebuffers")` in `ensure_framebuffers()` -- [x] 5.3 Add `GOGGLES_PROFILE_FUNCTION()` to `load_preset()` -- [x] 5.4 Add `GOGGLES_PROFILE_FUNCTION()` to `handle_resize()` -- [x] 5.5 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` -- [x] 5.6 Add `GOGGLES_PROFILE_SCOPE("LoadPresetTextures")` in `load_preset_textures()` - -## 6. Filter Pass Instrumentation - -- [x] 6.1 Add `GOGGLES_PROFILE_FUNCTION()` to `FilterPass::record()` -- [x] 6.2 Add pass index tag using `GOGGLES_PROFILE_TAG()` in `record()` (skipped - pass index not available in record()) -- [x] 6.3 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` -- [x] 6.4 Add `GOGGLES_PROFILE_SCOPE("CreatePipeline")` in `create_pipeline()` -- [x] 6.5 Add `GOGGLES_PROFILE_SCOPE("UpdateDescriptor")` in `update_descriptor()` -- [x] 6.6 Add `GOGGLES_PROFILE_SCOPE("BuildPushConstants")` in `build_push_constants()` - -## 7. Shader Runtime Instrumentation - -- [x] 7.1 Add `GOGGLES_PROFILE_FUNCTION()` to `ShaderRuntime::compile_shader()` -- [x] 7.2 Add `GOGGLES_PROFILE_FUNCTION()` to `compile_retroarch_shader()` -- [x] 7.3 Add `GOGGLES_PROFILE_SCOPE("CompileGlslWithReflection")` in `compile_glsl_with_reflection()` -- [x] 7.4 Add `GOGGLES_PROFILE_SCOPE("CompileSlang")` in `compile_slang()` (replaced LoadCachedSpirv - more relevant) -- [x] 7.5 Add `GOGGLES_PROFILE_SCOPE("CompileGlsl")` in `compile_glsl()` (replaced SaveCachedSpirv - more relevant) -- [x] 7.6 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` - -## 8. Capture Receiver Instrumentation - -- [x] 8.1 Add `GOGGLES_PROFILE_FUNCTION()` to `CaptureReceiver::poll_frame()` -- [x] 8.2 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` (skipped - init() is simple socket setup) - -## 9. Capture Layer Instrumentation - -- [x] 9.1 Add `GOGGLES_PROFILE_FUNCTION()` to `CaptureManager::on_present()` -- [x] 9.2 Add `GOGGLES_PROFILE_FUNCTION()` to `capture_frame()` -- [x] 9.3 Add `GOGGLES_PROFILE_FUNCTION()` to `record_copy_commands()` -- [x] 9.4 Add `GOGGLES_PROFILE_FUNCTION()` to `worker_func()` -- [x] 9.5 Add `GOGGLES_PROFILE_FUNCTION()` to `init_export_image()` -- [x] 9.6 Add `GOGGLES_PROFILE_FUNCTION()` to `init_sync_primitives()` -- [x] 9.7 Add `GOGGLES_PROFILE_FUNCTION()` to `LayerSocketClient::connect()` -- [x] 9.8 Add `GOGGLES_PROFILE_FUNCTION()` to `send_texture()` -- [x] 9.9 Add `GOGGLES_PROFILE_FUNCTION()` to `poll_control()` -- [x] 9.10 Update `src/capture/CMakeLists.txt` to enable profiling for `goggles_vklayer` - -## 10. Output Pass Instrumentation - -- [x] 10.1 Add `GOGGLES_PROFILE_FUNCTION()` to `OutputPass::record()` -- [x] 10.2 Add `GOGGLES_PROFILE_FUNCTION()` to `init()` - -## 11. Preset Parser Instrumentation - -- [x] 11.1 Add `GOGGLES_PROFILE_FUNCTION()` to `PresetParser::load()` - -## 12. Texture Loader Instrumentation - -- [x] 12.1 Add `GOGGLES_PROFILE_FUNCTION()` to `TextureLoader::load_from_file()` - -## 13. Verification - -- [x] 13.1 Build with `ENABLE_PROFILING=OFF` - Verified zero overhead (debug build succeeds, 21/21 targets) -- [x] 13.2 Build with `ENABLE_PROFILING=ON` - Verified Tracy links correctly (profile build succeeds, 509/509 targets) -- [x] 13.3 Run with Tracy server connected - Verify zones appear (not tested in automation) -- [x] 13.4 Run clang-tidy - No new warnings from profiling code (part of build process) -- [x] 13.5 Run test suite - Verified no regressions (100% tests passed in both debug and profile builds) - -## 14. Documentation - -- [x] 14.1 Update `openspec/project.md` dependencies section with Tracy -- [x] 14.2 Add profiling usage notes to docs/ if appropriate (skipped - profiling.hpp comments sufficient) diff --git a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/design.md b/openspec/changes/archive/2025-12-25-add-wsi-virtualization/design.md deleted file mode 100644 index 99f4747c..00000000 --- a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/design.md +++ /dev/null @@ -1,77 +0,0 @@ -## Context - -The Goggles Vulkan layer intercepts frame presentation to capture and share frames via DMA-BUF. Currently, target applications still create their own windows and display independently. WSI virtualization intercepts all window system integration calls to create a fully virtual display path. - -## Goals / Non-Goals - -**Goals:** -- Virtual surface creation (no real window) -- Virtual swapchain with DMA-BUF exportable images -- Compatible with existing capture pipeline -- Linux platform support (X11, Wayland, XCB) - -**Non-Goals:** -- Input forwarding (separate proposal) -- Windows/macOS support -- VR/XR surface virtualization - -## Decisions - -**Decision: Use synthetic handles for virtual objects** - -Virtual surfaces and swapchains use incrementing uint64 addresses cast to Vulkan handles. This avoids conflicts with real driver handles. - -```cpp -uint64_t next_handle_ = 0x1000; -VkSurfaceKHR handle = reinterpret_cast(next_handle_++); -``` - -**Decision: Reuse existing DMA-BUF export logic** - -Virtual swapchain images are created the same way as the current export image in `vk_capture.cpp`. The `VkExternalMemoryImageCreateInfo` chain with DMA-BUF handle type is already proven. - -**Decision: Fixed virtual capabilities** - -Return sensible defaults for surface queries: -- Image count: 2-3 -- Formats: B8G8R8A8_SRGB, B8G8R8A8_UNORM -- Present modes: FIFO, IMMEDIATE -- Extent: From environment variable or default 1920x1080 - -## Risks / Trade-offs - -**Risk:** Applications may query unsupported features -- Mitigation: Return comprehensive capability sets covering common use cases - -**Risk:** Validation layers may flag virtual handles -- Mitigation: Document that validation should be disabled in WSI proxy mode - -**Trade-off:** Virtual swapchain images require DMA-BUF export capability -- All modern Linux drivers support this; older drivers will fail gracefully - -## Architecture - -``` -vkCreate*SurfaceKHR - └─ [WSI_PROXY mode] → WsiVirtualizer::create_surface() - └─ Returns synthetic VkSurfaceKHR - -vkGetPhysicalDeviceSurface*KHR - └─ [virtual surface] → Return fixed capabilities - -vkCreateSwapchainKHR - └─ [virtual surface] → WsiVirtualizer::create_swapchain() - ├─ Create N VkImage with DMA-BUF export - ├─ Export each as DMA-BUF fd - └─ Return synthetic VkSwapchainKHR - -vkAcquireNextImageKHR - └─ [virtual swapchain] → Return next index (round-robin) - -vkQueuePresentKHR - └─ [virtual swapchain] → Send DMA-BUF fd to Goggles app -``` - -## Open Questions - -None - design is straightforward extension of existing capture mechanism. \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/proposal.md b/openspec/changes/archive/2025-12-25-add-wsi-virtualization/proposal.md deleted file mode 100644 index cc970de7..00000000 --- a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/proposal.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change: Add WSI Virtualization Layer - -## Why - -Goggles currently captures frames from target applications but the target app still creates and displays its own window. Users want the ability to have Goggles' SDL window act as a proxy, completely replacing the target application's display. This enables use cases like: -- Running games without their own window (headless capture) -- Remote game streaming where only the viewer displays -- CRT shader processing without duplicate windows - -## What Changes - -- **ADDED:** WSI virtualization mode activated via `GOGGLES_WSI_PROXY=1` environment variable -- **ADDED:** Virtual surface creation replacing platform-specific surfaces (X11/Wayland/XCB) -- **ADDED:** Virtual swapchain with DMA-BUF exportable images -- **ADDED:** Surface capability/format/present mode query virtualization -- **MODIFIED:** Existing `vkQueuePresentKHR` hook to work with virtual swapchains - -## Impact - -- Affected specs: `vk-layer-capture` -- Affected code: `src/capture/vk_layer/` (new files + hook modifications) -- No external dependencies added -- Linux only (X11 + Wayland + XCB surfaces) \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2025-12-25-add-wsi-virtualization/specs/vk-layer-capture/spec.md deleted file mode 100644 index d3471c83..00000000 --- a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,159 +0,0 @@ -## ADDED Requirements - -### Requirement: WSI Proxy Mode Configuration - -The layer SHALL provide a WSI proxy mode that virtualizes all window system integration calls. - -#### Scenario: WSI proxy mode activation - -- **GIVEN** `GOGGLES_WSI_PROXY=1` environment variable is set -- **AND** `GOGGLES_CAPTURE=1` is also set -- **WHEN** the layer initializes -- **THEN** WSI proxy mode SHALL be enabled -- **AND** all surface creation calls SHALL be intercepted - -#### Scenario: WSI proxy mode disabled by default - -- **GIVEN** `GOGGLES_WSI_PROXY` environment variable is not set -- **WHEN** the layer initializes -- **THEN** WSI proxy mode SHALL be disabled -- **AND** surface creation calls SHALL pass through to the driver - -### Requirement: Virtual Surface Resolution Configuration - -The layer SHALL allow configuring the virtual surface resolution via environment variables. - -#### Scenario: Default resolution - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_WIDTH` and `GOGGLES_HEIGHT` are not set -- **WHEN** a virtual surface is created -- **THEN** the surface SHALL have resolution 1920x1080 - -#### Scenario: Custom resolution - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_WIDTH` is set to a positive integer -- **AND** `GOGGLES_HEIGHT` is set to a positive integer -- **WHEN** a virtual surface is created -- **THEN** the surface SHALL have the specified resolution - -### Requirement: Virtual Surface Creation - -The layer SHALL intercept platform-specific surface creation calls and return virtual surfaces when WSI proxy mode is enabled. - -#### Scenario: X11 surface virtualization - -- **GIVEN** WSI proxy mode is enabled -- **WHEN** the application calls `vkCreateXlibSurfaceKHR` -- **THEN** the layer SHALL NOT create a real X11 surface -- **AND** SHALL return a synthetic VkSurfaceKHR handle -- **AND** no window SHALL be displayed - -#### Scenario: Wayland surface virtualization - -- **GIVEN** WSI proxy mode is enabled -- **WHEN** the application calls `vkCreateWaylandSurfaceKHR` -- **THEN** the layer SHALL NOT create a real Wayland surface -- **AND** SHALL return a synthetic VkSurfaceKHR handle - -#### Scenario: XCB surface virtualization - -- **GIVEN** WSI proxy mode is enabled -- **WHEN** the application calls `vkCreateXcbSurfaceKHR` -- **THEN** the layer SHALL NOT create a real XCB surface -- **AND** SHALL return a synthetic VkSurfaceKHR handle - -### Requirement: Virtual Surface Capability Queries - -The layer SHALL return valid capability information for virtual surfaces. - -#### Scenario: Surface capabilities query - -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkGetPhysicalDeviceSurfaceCapabilitiesKHR` -- **THEN** the layer SHALL return capabilities with: - - `minImageCount` of 2 - - `maxImageCount` of 3 - - `currentExtent` matching configured resolution - - `supportedUsageFlags` including `VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT` and `VK_IMAGE_USAGE_TRANSFER_SRC_BIT` - -#### Scenario: Surface formats query - -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkGetPhysicalDeviceSurfaceFormatsKHR` -- **THEN** the layer SHALL return at least `VK_FORMAT_B8G8R8A8_SRGB` and `VK_FORMAT_B8G8R8A8_UNORM` - -#### Scenario: Present modes query - -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkGetPhysicalDeviceSurfacePresentModesKHR` -- **THEN** the layer SHALL return `VK_PRESENT_MODE_FIFO_KHR` and `VK_PRESENT_MODE_IMMEDIATE_KHR` - -#### Scenario: Surface support query - -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkGetPhysicalDeviceSurfaceSupportKHR` -- **THEN** the layer SHALL return `VK_TRUE` for all queue families with graphics capability - -### Requirement: Virtual Swapchain Creation - -The layer SHALL create DMA-BUF exportable images for virtual swapchains. - -#### Scenario: Virtual swapchain creation - -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkCreateSwapchainKHR` with that surface -- **THEN** the layer SHALL create VkImages with `VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT` -- **AND** SHALL export DMA-BUF file descriptors for each image -- **AND** SHALL return a synthetic VkSwapchainKHR handle - -#### Scenario: Swapchain image query - -- **GIVEN** a virtual swapchain exists -- **WHEN** the application calls `vkGetSwapchainImagesKHR` -- **THEN** the layer SHALL return the DMA-BUF exportable images - -#### Scenario: Next image acquisition - -- **GIVEN** a virtual swapchain exists -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL return the next available image index -- **AND** signal the provided semaphore or fence immediately - -### Requirement: Virtual Swapchain Presentation - -The layer SHALL send DMA-BUF frames to the Goggles application on present. - -#### Scenario: Virtual present - -- **GIVEN** a virtual swapchain exists -- **WHEN** the application calls `vkQueuePresentKHR` -- **THEN** the layer SHALL send the presented image's DMA-BUF fd to Goggles -- **AND** SHALL NOT present to any physical display -- **AND** SHALL return `VK_SUCCESS` - -### Requirement: Virtual Swapchain Frame Rate Limiting - -The layer SHALL provide frame rate limiting for virtual swapchains to prevent runaway frame rates. - -#### Scenario: Default frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT` environment variable is not set -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL limit acquisition rate to 60 FPS - -#### Scenario: Custom frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT` is set to a positive integer -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL limit acquisition rate to the specified FPS - -#### Scenario: Disable frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT=0` is set -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL NOT limit acquisition rate \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/tasks.md b/openspec/changes/archive/2025-12-25-add-wsi-virtualization/tasks.md deleted file mode 100644 index 16dfb126..00000000 --- a/openspec/changes/archive/2025-12-25-add-wsi-virtualization/tasks.md +++ /dev/null @@ -1,51 +0,0 @@ -## 1. Core Infrastructure - -- [x] 1.1 Create `wsi_virtual.hpp` with VirtualSurface/VirtualSwapchain structs -- [x] 1.2 Create `wsi_virtual.cpp` with WsiVirtualizer implementation -- [x] 1.3 Add `should_use_wsi_proxy()` config function -- [x] 1.4 Add `GOGGLES_WIDTH`/`GOGGLES_HEIGHT` resolution config (default 1920x1080) - -## 2. Dispatch Table Updates - -- [x] 2.1 Add surface function pointers to `VkInstFuncs` -- [x] 2.2 Add swapchain function pointers to `VkDeviceFuncs` - -## 3. Surface Hooks - -- [x] 3.1 Implement `Goggles_CreateXlibSurfaceKHR` -- [x] 3.2 Implement `Goggles_CreateWaylandSurfaceKHR` -- [x] 3.3 Implement `Goggles_CreateXcbSurfaceKHR` -- [x] 3.4 Implement `Goggles_DestroySurfaceKHR` - -## 4. Surface Query Hooks - -- [x] 4.1 Implement `Goggles_GetPhysicalDeviceSurfaceCapabilitiesKHR` -- [x] 4.2 Implement `Goggles_GetPhysicalDeviceSurfaceFormatsKHR` -- [x] 4.3 Implement `Goggles_GetPhysicalDeviceSurfacePresentModesKHR` -- [x] 4.4 Implement `Goggles_GetPhysicalDeviceSurfaceSupportKHR` - -## 5. Swapchain Hooks - -- [x] 5.1 Modify `Goggles_CreateSwapchainKHR` for virtual surfaces -- [x] 5.2 Implement `Goggles_GetSwapchainImagesKHR` -- [x] 5.3 Implement `Goggles_AcquireNextImageKHR` - -## 6. Hook Registration - -- [x] 6.1 Register surface hooks in `Goggles_GetInstanceProcAddr` -- [x] 6.2 Register swapchain hooks in `Goggles_GetDeviceProcAddr` - -## 7. Build Configuration - -- [x] 7.1 Add platform defines to headers -- [x] 7.2 Add new source files to CMakeLists.txt - -## 8. Testing - -- [x] 8.1 Test with vkcube: `GOGGLES_CAPTURE=1 GOGGLES_WSI_PROXY=1 vkcube` -- [x] 8.2 Verify no window appears for target app - -## 9. Frame Rate Limiting - -- [x] 9.1 Add `GOGGLES_FPS_LIMIT` config (default 60) -- [x] 9.2 Implement rate limiting in `acquire_next_image` \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/proposal.md b/openspec/changes/archive/2025-12-25-optimize-capture-present-check/proposal.md deleted file mode 100644 index 4c724a48..00000000 --- a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/proposal.md +++ /dev/null @@ -1,29 +0,0 @@ -# Optimize Capture Present Check - -## Summary -Reorder the logic in `CaptureManager::on_present` to check and attempt the IPC socket connection *before* performing expensive resource initialization. - -## Motivation -Currently, `CaptureManager::on_present` checks if the export image is initialized and initializes it if necessary (creating images, allocating memory, exporting file descriptors) *before* checking if the capture layer is actually connected to the viewer application. - -If the Goggles viewer is not running, the layer still incurs the overhead of: -1. Querying DRM modifiers. -2. Creating a `VkImage`. -3. Allocating `VkDeviceMemory`. -4. Exporting a DMA-BUF file descriptor. -5. Creating semaphores and fences. -6. Creating command pools and buffers. - -This logic is wasteful for games running with the layer enabled but without an active viewer. By moving the connection check to the start of `on_present`, we can skip all this work when it's not needed. - -## Technical Design -The `CaptureManager::on_present` method in `src/capture/vk_layer/vk_capture.cpp` will be modified to: -1. Perform the socket connection check (and attempt to connect) immediately after acquiring the lock and finding the swapchain data. -2. If the socket is not connected (and cannot connect), return early. -3. Only proceed to `init_export_image` and `capture_frame` if the socket is connected. - -## Drawbacks -* None. This is a pure optimization. - -## Alternatives -* None. The current behavior is demonstrably inefficient. diff --git a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2025-12-25-optimize-capture-present-check/specs/vk-layer-capture/spec.md deleted file mode 100644 index 6bd06b68..00000000 --- a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,11 +0,0 @@ -# vk-layer-capture Specification - -## MODIFIED Requirements -### Requirement: Unix Socket IPC -The layer SHALL communicate with the Goggles application via Unix domain socket to transfer DMA-BUF file descriptors. - -#### Scenario: Early Connection Check -- **WHEN** `on_present` is called -- **THEN** the layer SHALL check the socket connection status first -- **AND** if not connected and connection attempt fails, return immediately -- **AND** skip export image initialization and frame capture \ No newline at end of file diff --git a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/tasks.md b/openspec/changes/archive/2025-12-25-optimize-capture-present-check/tasks.md deleted file mode 100644 index a6efbf26..00000000 --- a/openspec/changes/archive/2025-12-25-optimize-capture-present-check/tasks.md +++ /dev/null @@ -1,5 +0,0 @@ -# Tasks - -- [x] Refactor `CaptureManager::on_present` to check connection before resource init @src/capture/vk_layer/vk_capture.cpp -- [x] Verify that starting a game without the viewer does not trigger export image creation logs -- [x] Verify that starting the viewer later correctly initiates capture diff --git a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/proposal.md b/openspec/changes/archive/2025-12-30-improve-pixi-workflow/proposal.md deleted file mode 100644 index d18c1a7a..00000000 --- a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/proposal.md +++ /dev/null @@ -1,38 +0,0 @@ -# Change: Improve Pixi Workflow for Worktrees - -## Why -Local pixi-build packages require 300MB+ source downloads per worktree because pixi-build caches sources in `.pixi/build/` (per-workspace), not globally. The `vulkansdk` package was unnecessarily large and could be replaced with minimal conda-forge packages. The `slang-shaders` package is intentionally kept local for independent version control. - -Additionally, pre-commit hook installation failed in worktrees due to scripts checking `[[ -d .git ]]` which fails when `.git` is a file (worktree behavior). - -## What Changes - -### 1. Replace local `vulkansdk` with conda-forge packages -- **BREAKING**: Remove `packages/vulkansdk/` local package -- Add conda-forge dependency: `vulkan-validation-layers` (headers already present via `libvulkan-headers`) -- Add activation environment variables: `VULKAN_SDK` and `VK_ADD_LAYER_PATH` in pixi.toml -- **Note**: Shader compilation uses Slang, not glslang/shaderc, so those packages are not needed - -### 2. Keep `slang-shaders` as local package -- Slang shader compiler is intentionally managed as a local package for independent version control -- **Note**: Slang is not available on conda-forge (the "slang" package there is S-Lang interpreter, GPL) -- This allows the project to control Slang updates independently from other dependencies -- Workaround for worktrees: symlink `.pixi` directory or use `detached-environments = true` in user config - -### 3. Fix pre-commit hook for worktrees (already committed) -- Use `git rev-parse --is-inside-work-tree` instead of `[[ -d .git ]]` -- Use `git rev-parse --git-path hooks` to find hooks directory -- Handle `core.hooksPath` config - -### 4. Remove IDE setup scripts (already committed) -- Removed `scripts/setup-ide.sh` -- Removed `scripts/check-ide-setup.sh` -- Pre-commit hook is the enforcement mechanism - -## Impact -- Affected specs: `dependency-management` -- Affected code: `pixi.toml`, `packages/vulkansdk/`, `scripts/` -- Benefits: - - Faster worktree setup (reduced downloads from 313MB to 6MB for Vulkan components) - - Slang remains independently managed for version control flexibility - - Improved pre-commit hook compatibility with worktrees diff --git a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/specs/dependency-management/spec.md b/openspec/changes/archive/2025-12-30-improve-pixi-workflow/specs/dependency-management/spec.md deleted file mode 100644 index 492d962b..00000000 --- a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/specs/dependency-management/spec.md +++ /dev/null @@ -1,86 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Pixi as Primary Dependency Manager - -The project SHALL use [Pixi](https://pixi.sh) as the primary dependency manager for system libraries, toolchains, and build tools. - -#### Scenario: Fresh environment setup -- **GIVEN** a clean checkout of the repository -- **WHEN** `pixi install` is run -- **THEN** all system dependencies SHALL be installed from conda-forge -- **AND** the environment SHALL be reproducible via `pixi.lock` - -#### Scenario: Git worktree setup -- **GIVEN** an existing checkout with `pixi install` completed -- **WHEN** a new git worktree is created -- **THEN** `pixi install` SHALL complete without re-downloading source tarballs for conda-forge packages -- **AND** detached environments SHALL be shared via `~/.pixi/envs/` (when `detached-environments = true` in user config) - -#### Scenario: Pixi-managed dependencies -- **GIVEN** the `pixi.toml` configuration -- **THEN** the following categories SHALL be managed by Pixi: - - Clang toolchain (clang, clang++, compiler-rt, libcxx) - - Build tools (cmake, ninja, ccache) - - System libraries (SDL3, CLI11, Vulkan headers, Vulkan validation layers) - - X11/Wayland/audio libraries - -### Requirement: Pixi for C++ Libraries (Primary) - -The build system SHALL consume C++ libraries from Pixi packages (including source-built recipes) as the primary source. - -#### Scenario: Pixi-managed C++ libraries -- **GIVEN** the `pixi.toml` configuration -- **THEN** the following libraries SHALL be provided by Pixi packages (built from source where applicable): - - expected-lite (error handling) - - spdlog (logging) - - toml11 (configuration) - - Catch2 (testing) - - stb (image loading) - - BS_thread_pool (concurrency) - - slang-shaders (Slang shader compiler) - intentionally managed as local package for independent version control - - Tracy (profiling, optional) - -#### Scenario: Pixi package discovery -- **GIVEN** `CPM_USE_LOCAL_PACKAGES=ON` is set during CMake configure -- **WHEN** `find_package()` is invoked for the above libraries -- **THEN** the Pixi-provided packages SHALL be found without CPM downloads -- **NOTE**: Slang shader compiler is intentionally managed as a local pixi-build package for independent version control -- **RATIONALE**: This allows the project to control Slang updates independently from conda-forge package updates - -## ADDED Requirements - -### Requirement: Worktree-Compatible Hook Installation - -The pre-commit hook installation SHALL work correctly in git worktrees. - -#### Scenario: Hook installation in worktree -- **GIVEN** a git worktree created from the main repository -- **WHEN** `pixi run init` is executed -- **THEN** the pre-commit hook SHALL be installed to the correct hooks directory -- **AND** the script SHALL use `git rev-parse --git-path hooks` to locate the hooks directory - -#### Scenario: Hook installation with custom hooksPath -- **GIVEN** `core.hooksPath` is configured in git config -- **WHEN** `pixi run init` is executed -- **THEN** the pre-commit hook SHALL be installed to the configured hooksPath - -### Requirement: Vulkan Components from conda-forge - -Vulkan headers and validation layers SHALL be sourced from conda-forge packages instead of local pixi-build packages. - -#### Scenario: Vulkan package availability -- **GIVEN** the `pixi.toml` configuration -- **THEN** the following Vulkan components SHALL be provided by conda-forge: - - `libvulkan-headers = "1.4.328.*"` - Vulkan API headers - - `vulkan-validation-layers = "1.4.328.*"` - Debug validation layers for development - -#### Scenario: Validation layer activation -- **GIVEN** the pixi environment is activated -- **WHEN** a Vulkan application runs with validation enabled -- **THEN** `VK_ADD_LAYER_PATH` SHALL point to `$CONDA_PREFIX/share/vulkan/explicit_layer.d` -- **AND** `VULKAN_SDK` SHALL be set to `$CONDA_PREFIX` - -#### Rationale -- The project uses Slang for shader compilation (via `slang-shaders` local package) -- glslang, shaderc, and spirv-cross are not required dependencies -- Only validation layers are needed for development and debugging diff --git a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/tasks.md b/openspec/changes/archive/2025-12-30-improve-pixi-workflow/tasks.md deleted file mode 100644 index 79beaf49..00000000 --- a/openspec/changes/archive/2025-12-30-improve-pixi-workflow/tasks.md +++ /dev/null @@ -1,25 +0,0 @@ -## 1. Pre-commit Hook Fix (Completed) -- [x] 1.1 Update `scripts/install-pre-commit-hook.sh` to use `git rev-parse` -- [x] 1.2 Update `scripts/ensure-init.sh` to use `git rev-parse` -- [x] 1.3 Update `scripts/pre-commit-format.sh` to use `pixi run` for tools -- [x] 1.4 Remove `scripts/setup-ide.sh` -- [x] 1.5 Remove `scripts/check-ide-setup.sh` -- [x] 1.6 Simplify `scripts/init-format-setup.sh` -- [x] 1.7 Remove `setup-ide` task from `pixi.toml` - -## 2. Replace vulkansdk with conda-forge -- [x] 2.1 Identify required components from vulkansdk -- [x] 2.2 Add conda-forge packages to pixi.toml dependencies -- [x] 2.3 Add activation script for VK_ADD_LAYER_PATH in pixi.toml -- [x] 2.4 Remove `packages/vulkansdk/` directory -- [x] 2.5 Update pixi.toml to remove vulkansdk path dependency -- [x] 2.6 Test build and validation layers work - -## 3. Evaluate slang-shaders -- [x] 3.1 Check if Slang is available on conda-forge - - Result: NOT available. conda-forge "slang" is S-Lang (GPL interpreter), not Slang shader compiler -- [x] 3.2 Keep local package, document workaround (symlink `.pixi` for worktrees) - -## 4. Documentation -- [x] 4.1 Worktree limitation documented in proposal.md -- [x] 4.2 Slang not on conda-forge - local package remains diff --git a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/design.md b/openspec/changes/archive/2026-01-02-refactor-factory-pattern/design.md deleted file mode 100644 index 1d27191e..00000000 --- a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/design.md +++ /dev/null @@ -1,220 +0,0 @@ -# Design: Factory Pattern Refactoring - -## Context -The codebase currently uses two-phase initialization (constructor + `init()`) for 7+ major classes. This pattern was common in older C++ codebases but violates modern RAII principles and the project's error handling policies. The InputForwarder refactoring (completed) demonstrated the viability of using static factory methods with `ResultPtr` to eliminate uninitialized states. - -## Goals / Non-Goals - -### Goals -- Eliminate possibility of using uninitialized objects -- Enforce single-phase initialization across render and capture subsystems -- Provide consistent error handling using `ResultPtr` pattern -- Maintain or improve error message clarity -- Ensure proper cleanup on initialization failures - -### Non-Goals -- Changing the internal implementation logic of initialization -- Refactoring classes that don't use two-phase initialization -- Refactoring optional/lazy-initialized components that can validly exist in inactive states -- Modifying Vulkan layer code (uses C API, exempt from policy) -- Performance optimization (maintain current performance characteristics) - -**Scope Clarification**: This refactoring targets major subsystem classes (backend, chains, passes, runtimes) that must be fully initialized before use. Classes like FrameHistory that implement optional features and can validly exist in an "inactive" state (depth=0) are excluded from this refactoring, as their two-phase initialization is intentional and appropriate for their use case. - -## Decisions - -### Factory Method Signature -Use `static auto create(...) -> ResultPtr` for all factory methods: -- Static method allows private constructor enforcement -- `ResultPtr` is type alias for `Result>` -- Consistent with project's Result-based error handling -- `std::unique_ptr` provides clear ownership semantics - -**Alternative considered:** Pass-by-value factories returning `Result`: -- Rejected: Large objects (VulkanBackend) expensive to copy -- Rejected: Doesn't prevent uninitialized state if caller forgets to check Result - -### Initialization Order -Bottom-up refactoring following dependency chain: -1. Leaf dependencies first (ShaderRuntime, Framebuffer) -2. Mid-level components (FilterPass, OutputPass) -3. Composition classes (FilterChain) -4. Root dependencies last (VulkanBackend, CaptureReceiver) - -**Rationale:** Each refactored class can be tested independently before refactoring its dependents. - -### Error Propagation -Use `GOGGLES_TRY()` macro where applicable for clean error propagation: -```cpp -auto FilterChain::create() -> ResultPtr { - auto chain = std::unique_ptr(new FilterChain()); - - // Use GOGGLES_TRY for nested factory calls - chain->m_runtime = GOGGLES_TRY(ShaderRuntime::create(...)); - - // Use make_result_ptr_error for direct errors - if (some_condition) { - return make_result_ptr_error(ErrorCode::..., "message"); - } - - return make_result_ptr(std::move(chain)); -} -``` - -**Alternative considered:** Manual error checking and propagation: -- Rejected: More verbose, error-prone -- `GOGGLES_TRY` already established in project, familiar to contributors - -### Handling CaptureReceiver's bool init() -Convert from `bool init()` to `Result` internally, then wrap in factory: -```cpp -auto CaptureReceiver::create() -> ResultPtr { - auto receiver = std::unique_ptr(new CaptureReceiver()); - - if (!receiver->internal_init()) { - return make_result_ptr_error( - ErrorCode::capture_init_failed, "Failed to initialize capture"); - } - - return make_result_ptr(std::move(receiver)); -} -``` - -**Alternative considered:** Refactor internal implementation to use Result: -- Deferred: Broader change, can be done in follow-up if needed -- Current approach minimizes diff size - -### Constructor Visibility -All constructors moved to private section: -- Prevents direct construction, enforces factory usage -- Move/copy operations remain deleted (unchanged) -- Destructor remains public (required for unique_ptr) - -## VulkanBackend Complexity - -VulkanBackend has the most complex initialization: -- 128+ lines of init logic -- 15+ private create_* helper methods -- Dependencies on FilterChain and OutputPass -- Multiple failure points requiring cleanup - -Strategy: -1. Refactor dependencies first (FilterChain, OutputPass) -2. Convert VulkanBackend's create_* methods to return Result types -3. Use GOGGLES_TRY for each create_* call in factory method -4. Rely on RAII (vk::Unique*) for automatic cleanup on early return - -Example transformation: -```cpp -// Before -auto VulkanBackend::init(...) -> Result { - auto device_result = create_device(); - if (!device_result) return device_result; - - auto swapchain_result = create_swapchain(); - if (!swapchain_result) return swapchain_result; - - // ... 10+ more calls - return {}; -} - -// After -auto VulkanBackend::create(...) -> ResultPtr { - auto backend = std::unique_ptr(new VulkanBackend()); - - GOGGLES_TRY(backend->create_device()); - GOGGLES_TRY(backend->create_swapchain()); - // ... 10+ more calls - - return make_result_ptr(std::move(backend)); -} -``` - -## Risks / Trade-offs - -### Risk: Breaking API Changes -- **Mitigation:** All usage sites must be updated in same change -- **Mitigation:** Compilation errors will catch missed sites -- **Trade-off:** Short-term churn for long-term type safety - -### Risk: Complex Initialization Failures -VulkanBackend and FilterChain have complex initialization with multiple failure points. -- **Mitigation:** RAII handles cleanup automatically (vk::Unique* types) -- **Mitigation:** Test failure paths with AddressSanitizer -- **Trade-off:** More complex factory methods, but safer than current pattern - -### Risk: Initialization Order Dependencies -Some classes may have hidden dependencies on initialization order. -- **Mitigation:** Bottom-up refactoring exposes dependencies early -- **Mitigation:** Comprehensive testing after each class refactoring -- **Trade-off:** Slower incremental approach, but reduces risk - -### Risk: Error Message Quality -Factory failures need clear error messages for debugging. -- **Mitigation:** Preserve existing error messages during refactoring -- **Mitigation:** Test error paths to verify messages are actionable -- **Trade-off:** None - factory pattern doesn't affect error messages - -## Migration Plan - -### Phase 1: Leaf Dependencies (ShaderRuntime, Framebuffer) -- Low risk, simple initialization logic -- Test independently before proceeding -- Establishes pattern for subsequent classes - -### Phase 2: Mid-Level Components (FilterPass, OutputPass) -- Medium complexity, well-encapsulated -- Update FilterChain usage of ShaderRuntime -- Verify error propagation works correctly - -### Phase 3: Composition Classes (FilterChain) -- Higher complexity due to dynamic FilterPass creation -- Depends on Phase 2 completeness -- Critical for VulkanBackend refactoring - -### Phase 4: Root Dependencies (VulkanBackend, CaptureReceiver) -- Highest complexity and risk -- Depends on Phases 1-3 completeness -- Most impactful change for main.cpp - -### Rollback Strategy -Each phase can be committed independently. If issues arise: -1. Revert most recent commit -2. Fix identified issues -3. Re-apply with additional testing - -## Open Questions - -### Q: Should we refactor all classes in single PR or multiple PRs? -**Recommendation:** Single PR per phase (4 PRs total) -- Phase 1 PR: ShaderRuntime + Framebuffer -- Phase 2 PR: FilterPass + OutputPass -- Phase 3 PR: FilterChain -- Phase 4 PR: VulkanBackend + CaptureReceiver - -**Rationale:** Smaller PRs easier to review, can pause if issues found - -### Q: Do we need backward compatibility shims? -**Answer:** No - this is internal API, no external consumers -- All usage sites are in-tree -- Compilation errors will catch all usages -- No runtime compatibility needed - -### Q: Should we add tests for factory failures? -**Answer:** Yes - add unit tests for each factory's error paths -- Test invalid parameters -- Test resource allocation failures (where mockable) -- Test cleanup on partial initialization -- Use AddressSanitizer to verify no leaks - -### Q: What about classes with optional initialization? -Some classes (like CaptureReceiver) allow init to fail without aborting. -**Answer:** Factory returns ResultPtr, caller checks and handles gracefully: -```cpp -auto receiver_result = CaptureReceiver::create(); -if (!receiver_result) { - GOGGLES_LOG_WARN("Capture disabled: {}", receiver_result.error().message); - // Continue without capture -} -``` -Pattern already used in main.cpp for InputForwarder (see lines 226-233). diff --git a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/proposal.md b/openspec/changes/archive/2026-01-02-refactor-factory-pattern/proposal.md deleted file mode 100644 index cf914b42..00000000 --- a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/proposal.md +++ /dev/null @@ -1,42 +0,0 @@ -# Change: Refactor Two-Phase Initialization to Factory Pattern - -## Why -Multiple classes use two-phase initialization (constructor + `init()`), which allows objects to exist in uninitialized states. This pattern is error-prone and violates RAII principles for classes that must be fully initialized before use. The InputForwarder refactoring demonstrated that using static factory methods with `ResultPtr` provides better type safety and prevents usage of uninitialized objects. - -**Note**: This refactoring targets major subsystem classes that require full initialization. Classes with optional/lazy initialization semantics (like FrameHistory, which can validly exist with depth=0) may retain two-phase init where appropriate. - -## What Changes -- Refactor VulkanBackend to use factory pattern -- Refactor FilterChain to use factory pattern -- Refactor FilterPass to use factory pattern -- Refactor ShaderRuntime to use factory pattern -- Refactor Framebuffer to use factory pattern -- Refactor OutputPass to use factory pattern -- Refactor CaptureReceiver to use factory pattern - -Each class will: -- Add `static auto create() -> ResultPtr;` factory method -- Move constructor to private section -- Remove public `init()` method -- Inline initialization logic into factory method -- Update all usage sites to use factory pattern - -## Impact -- Affected code: - - `src/render/backend/vulkan_backend.{hpp,cpp}` - - `src/render/chain/filter_chain.{hpp,cpp}` - - `src/render/chain/filter_pass.{hpp,cpp}` - - `src/render/shader/shader_runtime.{hpp,cpp}` - - `src/render/chain/framebuffer.{hpp,cpp}` - - `src/render/chain/output_pass.{hpp,cpp}` - - `src/capture/capture_receiver.{hpp,cpp}` - - `src/app/main.cpp` and other usage sites -- Benefits: - - Eliminates possibility of uninitialized object usage - - Enforces single-phase initialization - - Consistent error handling pattern across codebase - - Better alignment with RAII and project policies -- Risks: - - Breaking API changes require updating all call sites - - Complex initialization in VulkanBackend may require careful refactoring - - Some classes may have interdependencies requiring initialization order changes diff --git a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/specs/object-lifecycle/spec.md b/openspec/changes/archive/2026-01-02-refactor-factory-pattern/specs/object-lifecycle/spec.md deleted file mode 100644 index 7ba1007b..00000000 --- a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/specs/object-lifecycle/spec.md +++ /dev/null @@ -1,61 +0,0 @@ -## ADDED Requirements - -### Requirement: Factory Pattern for Complex Initialization -Major subsystem classes with fallible initialization SHALL use static factory methods returning `ResultPtr` instead of two-phase initialization (constructor + `init()`). - -**Applicability**: This requirement applies to core subsystem classes (backends, chains, passes, runtimes, receivers) that must be fully initialized before use. It does NOT apply to: -- Optional/lazy-initialized components that can validly exist in inactive states -- C API boundary code (Vulkan layer) -- Utility classes where default construction is semantically valid - -#### Scenario: Successful initialization -- **WHEN** a factory method is called with valid parameters -- **THEN** the method SHALL return a `ResultPtr` containing a fully initialized object -- **AND** the object SHALL be ready for immediate use without additional initialization calls - -#### Scenario: Initialization failure -- **WHEN** initialization fails for any reason -- **THEN** the factory method SHALL return an error using `make_result_ptr_error()` -- **AND** the error SHALL include a clear error code and descriptive message -- **AND** all partially allocated resources SHALL be cleaned up automatically - -#### Scenario: Constructor visibility -- **WHEN** a class uses factory pattern -- **THEN** the constructor SHALL be private -- **AND** direct construction SHALL be prevented at compile time - -### Requirement: ResultPtr Type Alias -The codebase SHALL provide a `ResultPtr` type alias for `Result>` to simplify factory method signatures. - -#### Scenario: Type alias usage -- **WHEN** declaring a factory method -- **THEN** the return type SHALL use `ResultPtr` instead of `Result>` -- **AND** the code SHALL use `make_result_ptr()` for success cases -- **AND** the code SHALL use `make_result_ptr_error()` for error cases - -### Requirement: Classes Using Factory Pattern -The following classes SHALL use factory pattern instead of two-phase initialization: -- VulkanBackend -- FilterChain -- FilterPass -- ShaderRuntime -- Framebuffer -- OutputPass -- CaptureReceiver -- InputForwarder (already implemented) - -#### Scenario: Factory method signature -- **WHEN** implementing a factory method -- **THEN** it SHALL be declared as `[[nodiscard]] static auto create(...) -> ResultPtr;` -- **AND** it SHALL accept all necessary initialization parameters -- **AND** it SHALL use `GOGGLES_TRY()` for nested factory calls where applicable - -### Requirement: Two-Phase Initialization SHALL NOT Be Used for Major Subsystems -Major subsystem classes (backends, chains, passes, runtimes, receivers) SHALL NOT use two-phase initialization (constructor + `init()`) pattern, as it allows objects to exist in uninitialized states, violating RAII principles. - -**Exception**: Two-phase initialization MAY be used for optional/lazy-initialized components where an "inactive" or "uninitialized" state is a valid operational mode (e.g., components that can be disabled or have zero-state configurations). - -#### Scenario: Prohibited two-phase initialization -- **WHEN** implementing a major subsystem class -- **THEN** the class SHALL NOT expose a public `init()` method -- **AND** the class SHALL use factory pattern instead diff --git a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/tasks.md b/openspec/changes/archive/2026-01-02-refactor-factory-pattern/tasks.md deleted file mode 100644 index c4589085..00000000 --- a/openspec/changes/archive/2026-01-02-refactor-factory-pattern/tasks.md +++ /dev/null @@ -1,89 +0,0 @@ -# Implementation Tasks - -## 1. ShaderRuntime Refactoring -- [x] 1.1 Add `static auto create() -> ResultPtr;` to header -- [x] 1.2 Move constructor to private section -- [x] 1.3 Remove public `init()` method from header -- [x] 1.4 Implement `create()` factory method in cpp file -- [x] 1.5 Update usage sites in FilterChain -- [x] 1.6 Test shader loading and runtime initialization - -## 2. Framebuffer Refactoring -- [x] 2.1 Add `static auto create() -> ResultPtr;` to header -- [x] 2.2 Move constructor to private section -- [x] 2.3 Remove public `init()` method from header -- [x] 2.4 Implement `create()` factory method in cpp file -- [x] 2.5 Update usage sites in FilterPass and OutputPass -- [x] 2.6 Test framebuffer creation and resize handling - -## 3. OutputPass Refactoring -- [x] 3.1 Add `static auto create() -> ResultPtr;` to header -- [x] 3.2 Move constructor to private section -- [x] 3.3 Remove public `init()` method from header -- [x] 3.4 Implement `create()` factory method in cpp file -- [x] 3.5 Update usage sites in VulkanBackend -- [x] 3.6 Test output pass creation and rendering - -## 4. FilterPass Refactoring -- [x] 4.1 Analyze complex initialization logic (147 lines) -- [x] 4.2 Add `static auto create() -> ResultPtr;` to header -- [x] 4.3 Move constructor to private section -- [x] 4.4 Remove public `init()` method from header -- [x] 4.5 Implement `create()` factory method in cpp file -- [x] 4.6 Update usage sites in FilterChain -- [x] 4.7 Test filter pass creation and rendering - -## 5. FilterChain Refactoring -- [x] 5.1 Analyze dynamic FilterPass creation pattern -- [x] 5.2 Add `static auto create() -> ResultPtr;` to header -- [x] 5.3 Move constructor to private section -- [x] 5.4 Remove public `init()` method from header -- [x] 5.5 Implement `create()` factory method in cpp file -- [x] 5.6 Update FilterPass factory usage within FilterChain -- [x] 5.7 Update usage sites in VulkanBackend -- [x] 5.8 Test filter chain creation and pipeline execution - -## 6. CaptureReceiver Refactoring -- [x] 6.1 Convert from `bool init()` to Result-based factory -- [x] 6.2 Add `static auto create() -> ResultPtr;` to header -- [x] 6.3 Move constructor to private section -- [x] 6.4 Remove public `init()` method from header -- [x] 6.5 Implement `create()` factory method in cpp file -- [x] 6.6 Update usage in main.cpp -- [x] 6.7 Test capture initialization and frame reception - -## 7. VulkanBackend Refactoring -- [x] 7.1 Analyze complex initialization logic (128+ lines, 15+ create methods) -- [x] 7.2 Add `static auto create() -> ResultPtr;` to header -- [x] 7.3 Move constructor to private section -- [x] 7.4 Remove public `init()` method from header -- [x] 7.5 Implement `create()` factory method in cpp file -- [x] 7.6 Handle dependencies (FilterChain, OutputPass, Framebuffer) -- [x] 7.7 Update usage in main.cpp -- [x] 7.8 Test Vulkan initialization and rendering pipeline -- [x] 7.9 Test error handling and cleanup on initialization failure - -## 8. Testing and Validation -- [x] 8.1 Run all existing tests with new factory pattern -- [x] 8.2 Test initialization failure paths -- [x] 8.3 Test proper cleanup on factory failures -- [x] 8.4 Verify error messages are clear -- [x] 8.5 Test all public API surfaces still work correctly -- [x] 8.6 Run with AddressSanitizer to detect memory issues - -**Note**: Vulkan-dependent classes (Framebuffer, FilterPass, OutputPass, FilterChain, VulkanBackend) -require device setup for comprehensive initialization failure testing and are deferred for future test infrastructure work. - -## 9. Documentation -- [x] 9.1 Update code comments if needed (following minimal comment policy) -- [x] 9.2 Update design documentation if initialization patterns have changed - -## Implementation Order Rationale -Classes are ordered by dependency chain: -1. ShaderRuntime - Leaf dependency, simple pimpl pattern -2. Framebuffer - Used by passes, manageable complexity -3. OutputPass - Depends on Framebuffer -4. FilterPass - Depends on Framebuffer, moderate complexity -5. FilterChain - Depends on FilterPass, dynamic creation pattern -6. CaptureReceiver - Independent, bool to Result conversion -7. VulkanBackend - Root dependency, most complex, depends on FilterChain and OutputPass diff --git a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/design.md b/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/design.md deleted file mode 100644 index 3ec57168..00000000 --- a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/design.md +++ /dev/null @@ -1,309 +0,0 @@ -# Design: Input Forwarding Architecture - -## Overview - -Input forwarding enables users to control captured Vulkan applications by pressing keys in the Goggles viewer window. The solution uses a nested XWayland server (via headless wlroots compositor) combined with XTest injection to generate real X11 protocol events that bypass synthetic input filters. - -## Architecture Layers - -### Layer 1: SDL Event Source (Existing) - -``` -User presses W key - ↓ -SDL3 generates SDL_EVENT_KEY_DOWN - ↓ -main.cpp event loop receives event -``` - -**No changes to SDL integration.** - -### Layer 2: Input Forwarding Module (New) - -```cpp -namespace goggles::input { - -class InputForwarder { -public: - [[nodiscard]] static auto create() -> ResultPtr; - [[nodiscard]] auto forward_key(const SDL_KeyboardEvent& event) -> Result; - [[nodiscard]] auto display_number() const -> int; - -private: - struct Impl; - std::unique_ptr m_impl; -}; - -} // namespace goggles::input -``` - -**`InputForwarder::Impl`** contains: -- `XWaylandServer` instance (compositor thread, wlroots objects) -- X11 `Display*` connection to nested XWayland -- SDL → Linux keycode → X11 keycode translation map - -**Responsibilities**: -- Start headless Wayland compositor on wayland-N socket -- Spawn XWayland process connected to compositor (auto-select :1, :2, ...) -- Open X11 client connection to :N -- Translate SDL scancodes to X11 keycodes -- Call `XTestFakeKeyEvent()` / `XTestFakeButtonEvent()` / `XTestFakeMotionEvent()` to inject input -- Expose the selected DISPLAY number so the target can be launched with `DISPLAY=:N` - -### Layer 3: XWayland Server (New) - -```cpp -namespace goggles::input { - -class XWaylandServer { -public: - [[nodiscard]] auto start() -> Result; // Returns DISPLAY number - void stop(); - -private: - struct wlr_backend* m_backend; - struct wlr_compositor* m_compositor; - struct wlr_seat* m_seat; - struct wlr_xwayland* m_xwayland; - struct wl_display* m_display; - struct wl_event_loop* m_event_loop; - std::jthread m_compositor_thread; -}; - -} // namespace goggles::input -``` - -**Responsibilities**: -- Create headless wlroots backend (`wlr_headless_backend_create`) -- Create minimal Wayland compositor (no rendering, just protocol infrastructure) -- Start XWayland server via `wlr_xwayland_create()` -- Run Wayland event loop in dedicated thread -- Clean up all wlroots resources on shutdown - -**Why headless backend?** -- No GPU/display conflicts with host compositor -- No actual frame composition (XWayland only needs Wayland socket) -- Lightweight: ~1 MB memory, <1% CPU - -### Layer 4: Target Launch Environment - -Input forwarding relies on the target application connecting to the nested XWayland server. This requires launching the target with `DISPLAY=:N` set before any windowing happens (where `N` is the display chosen by the viewer). - -The capture layer does not participate in selecting or retargeting `DISPLAY`. - -## Data Flow: Keyboard Event Path - -``` -1. User presses W in Goggles window - ↓ -2. SDL3 generates SDL_EVENT_KEY_DOWN (scancode=26) - ↓ -3. main.cpp calls input_forwarder.forward_key(event) - ↓ -4. InputForwarder::forward_key(): - - Translate scancode: SDL 26 → Linux KEY_W (17) → X11 25 - - Call XTestFakeKeyEvent(display_to_nested, 25, True, CurrentTime) - - Call XFlush(display_to_nested) - ↓ -5. XWayland receives XTest request on Xorg internal API - ↓ -6. XWayland generates X11 KeyPress event on wire protocol - ↓ -7. Captured app's XNextEvent() / xcb_poll_for_event() receives KeyPress - ↓ -8. App processes input (indistinguishable from physical keyboard) -``` - -**Latency**: ~1-2ms (SDL → XTest → XWayland → App) - -## Threading Model - -### Main Thread (Existing) -- SDL event loop -- Vulkan rendering -- `InputForwarder::forward_key()` calls (non-blocking X11 send) - -### Compositor Thread (New) -- Runs `wl_display_run()` event loop -- Handles XWayland lifecycle events -- Processes Wayland protocol requests (minimal, XWayland only) - -**Synchronization**: None required (X11 connection thread-safe for write) - -## Error Handling Strategy - -### InputForwarder::create() - -```cpp -auto InputForwarder::create() -> ResultPtr { - auto forwarder = std::unique_ptr(new InputForwarder()); - - auto start_result = forwarder->m_impl->server.start(); - if (!start_result) { - return make_result_ptr_error(start_result.error().code, - start_result.error().message); - } - - auto x11_display = XOpenDisplay(":N"); // N = selected display number - if (!x11_display) { - forwarder->m_impl->server.stop(); - return make_result_ptr_error( - ErrorCode::input_init_failed, "Failed to connect to nested XWayland"); - } - forwarder->m_impl->x11_display = x11_display; - - return make_result_ptr(std::move(forwarder)); -} -``` - -**Error codes**: -- `ErrorCode::input_init_failed` - XWayland start failed or X11 connection failed -- `ErrorCode::input_socket_send_failed` - reserved for future IPC-related failures (not currently used) - -### XWaylandServer::start() - -```cpp -auto XWaylandServer::start() -> Result { - // Try :1, :2, :3... until successful - for (int display_num = 1; display_num < 10; ++display_num) { - auto socket_name = fmt::format("wayland-{}", display_num); - const char* socket = wl_display_add_socket(m_display, socket_name.c_str()); - - if (socket) { - // Success, start XWayland on :N - m_xwayland = wlr_xwayland_create(m_display, m_compositor, false); - if (!m_xwayland) { - return make_error(ErrorCode::input_init_failed, - "XWayland creation failed"); - } - - return ok(display_num); - } - } - - return make_error(ErrorCode::input_init_failed, - "No available DISPLAY numbers (1-9 all bound)"); -} -``` - -### Graceful Degradation - -If `InputForwarder::create()` fails: -1. Log error with `GOGGLES_LOG_ERROR` -2. Continue app startup without input forwarding -3. User can still view captured frames, just can't control app -4. `forward_key()` becomes no-op (early return if not initialized) - -## Resource Management - -### RAII Ownership - -```cpp -struct InputForwarder::Impl { - XWaylandServer server; // RAII: stops in destructor - Display* x11_display = nullptr; // Closed in destructor via XCloseDisplay - std::unique_ptr keymap; // RAII - - ~Impl() { - if (x11_display) { - XCloseDisplay(x11_display); - } - // server.~XWaylandServer() called automatically, stops compositor - } -}; - -struct XWaylandServer { - struct wl_display* m_display = nullptr; - struct wlr_backend* m_backend = nullptr; - // ... other wlroots objects - std::jthread m_compositor_thread; // Joins automatically in destructor - - ~XWaylandServer() { - stop(); - } - - void stop() { - if (m_display) { - wl_display_terminate(m_display); // Stops event loop - } - // Thread joins automatically via std::jthread - - // Clean up wlroots in correct order - if (m_xwayland) wlr_xwayland_destroy(m_xwayland); - if (m_seat) wlr_seat_destroy(m_seat); - if (m_compositor) wlr_compositor_destroy(m_compositor); - if (m_backend) wlr_backend_destroy(m_backend); - if (m_display) wl_display_destroy(m_display); - } -}; -``` - -**No raw pointers in public API**. All resources owned via RAII. - -## Performance Characteristics - -### Memory -- XWaylandServer: ~1 MB (wlroots + XWayland process) -- InputForwarder: ~4 KB (PIMPL + X11 connection) -- Total overhead: ~1 MB - -### CPU -- Compositor thread: <0.5% (event loop mostly idle) -- forward_key(): <10 μs per call (XTest + XFlush) - -### Latency -- SDL event → XTest → XWayland → App: ~1-2 ms - -## Testing Strategy - -### Unit Tests (Deferred) -- Keycode translation (SDL → Linux → X11) -- Error handling paths - -### Integration Tests (Manual) -1. Start Goggles -2. Start test app with `GOGGLES_CAPTURE=1` -3. Press keys and use the mouse in the Goggles window -4. Verify the test app prints corresponding `[Input] ...` events - -### Compatibility Testing -- X11 native apps (test_app) -- Wine/DXVK apps (Resident Evil 4) -- Different keyboard layouts (US, DE, etc.) - -## Future Enhancements - -### Phase 2: Mouse Coordinate Mapping & Constraints -- Map viewer coordinates to captured app coordinates (scale/offset) -- Optional pointer confinement and relative mouse mode support - -### Phase 3: Wayland Native Apps -- Replace XTest with libei (Wayland input injection) -- Requires different protocol (ei_seat, ei_keyboard) - -### Phase 4: Multi-App Focus -- Track multiple captured app windows -- Route input based on focus policy (user-selectable) - -## Comparison with Alternatives - -| Approach | Pros | Cons | Verdict | -|----------|------|------|---------| -| **XTest → XWayland** (chosen) | Real X11 events, Wine compatible, no special perms | Requires wlroots dep | ✅ Best | -| uinput | Direct kernel injection | Needs root/groups, Wine filters | ❌ Rejected | -| Cage | Full compositor | Complex, unneeded display output | ❌ Rejected | -| SDL forwarding | No deps | No way to inject cross-DISPLAY | ❌ Impossible | - -## Dependencies Justification - -| Dependency | Why Required | Size Impact | -|------------|--------------|-------------| -| wlroots | Only library providing headless Wayland compositor + XWayland integration | ~500 KB lib | -| wayland-server | Protocol implementation (wlroots dependency) | ~200 KB lib | -| xkbcommon | Keyboard layout handling (wlroots dependency) | ~300 KB lib | -| libX11 | X11 client (connect to XWayland) | Already installed | -| libXtst | XTest extension | ~20 KB lib | - -**Total new deps**: wlroots + wayland-server + xkbcommon = ~1 MB - -All are system packages, no build-time compilation required. diff --git a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/proposal.md b/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/proposal.md deleted file mode 100644 index 63614bfa..00000000 --- a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/proposal.md +++ /dev/null @@ -1,152 +0,0 @@ -# Proposal: Add Input Forwarding - -## Why - -When Goggles captures frames from a Vulkan application via the layer, users cannot control the captured application by pressing keys in the Goggles viewer window. Input events go to the Goggles window instead of the captured application. - -Standard input forwarding approaches fail: - -1. **Synthetic input injection** (uinput, XTest to host DISPLAY) is filtered by many applications, especially Wine/DXVK -2. **Focusing the captured app window** breaks the seamless viewing experience and may not work if the app is headless - -Users currently have no way to interact with captured applications while viewing through Goggles. - -## Proposed Solution - -Introduce an input forwarding module (`src/input/`) that creates a nested XWayland server for captured applications and forwards key + mouse events from the Goggles SDL window via XTest injection into the nested server. - -### Architecture - -```text -┌─────────────────────────────────────────┐ -│ Goggles (DISPLAY=:0) │ -│ │ -│ ┌────────────┐ ┌──────────────┐ │ -│ │ SDL Window │─────▶│ InputForwarder│ │ -│ │ (viewer) │ keys │ (src/input/) │ │ -│ └────────────┘ └────────┬──────┘ │ -│ │ │ -│ ┌────────────▼──────┐ │ -│ │ XWaylandServer │ │ -│ │ (headless wlroots)│ │ -│ └────────────┬──────┘ │ -│ │ │ -│ Creates DISPLAY=:N │ -│ │ │ -│ ┌────────────▼──────┐ │ -│ │ X11 connection │ │ -│ │ XTestFakeKeyEvent │ │ -│ └────────────┬──────┘ │ -└──────────────────────────────┼─────────┘ - │ XTest - ▼ -┌─────────────────────────────────────────┐ -│ Captured App (DISPLAY=:N) │ -│ Receives real X11 KeyPress events │ -└─────────────────────────────────────────┘ -``` - -**Key insight**: XTest injection into XWayland generates real X11 protocol events (KeyPress/KeyRelease), indistinguishable from physical keyboard, bypassing synthetic event filters. - -### Components - -1. **`src/input/xwayland_server.cpp/hpp`**: Manages headless Wayland compositor (wlroots) and spawns XWayland process -2. **`src/input/input_forwarder.cpp/hpp`**: Public API class (PIMPL pattern), forwards SDL key + mouse events via XTest -3. **Configuration/CLI**: Input forwarding is opt-in in the viewer; the target app must be launched inside the nested XWayland session (`DISPLAY=:N`) - -### Integration Points - -- **`src/app/main.cpp`**: Optionally initialize `InputForwarder` (via `create()`) and forward SDL input events via XTest -- **`docs/input_forwarding.md`**: Document how to launch the target inside the nested XWayland session (`DISPLAY=:N`) - -## Benefits - -- **Seamless UX**: Users control captured apps by pressing keys in viewer window -- **Wine/DXVK compatible**: XTest → XWayland generates real X11 events, not filtered -- **Opt-in**: Disabled by default; enable via config or `--input-forwarding` -- **Minimal deps**: System packages only (wlroots, wayland-server, xkbcommon, libX11, libXtst) -- **Basic mouse support**: Button/motion/wheel events are forwarded (coordinate mapping is currently 1:1 with the viewer window) - -## Non-Goals - -- Accurate mouse coordinate mapping / scaling between viewer and captured app -- Pointer confinement / relative mouse mode support -- Wayland native app support (X11-only for now) -- Display/composition (XWayland used only as input server) -- Multiple app focus management - -## Alternatives Considered - -### uinput Injection -**Rejected**: Requires root/uinput group, filtered by Wine - -### Cage Compositor -**Rejected**: Full compositor with display output we don't need, complex integration - -### Direct SDL Input Forwarding -**Rejected**: No way to inject into different DISPLAY without XTest - -## What Changes - -### Configuration / CLI -- New config option: `input.forwarding` (default: false) -- New CLI flag: `--input-forwarding` (overrides config to enable) -- Input forwarding only works for targets launched with `DISPLAY=:N` (nested XWayland) -- Wayland input forwarding is not supported yet - -### Public API -- New class `goggles::input::InputForwarder` (PIMPL pattern) - - `create() -> ResultPtr` - - `forward_key(const SDL_KeyboardEvent&) -> Result` - - `forward_mouse_button(const SDL_MouseButtonEvent&) -> Result` - - `forward_mouse_motion(const SDL_MouseMotionEvent&) -> Result` - - `forward_mouse_wheel(const SDL_MouseWheelEvent&) -> Result` - - `display_number() -> int` -- New class `goggles::input::XWaylandServer` (internal) - - `start() -> Result` - - `stop() -> void` - -### Error Codes -- `ErrorCode::input_init_failed` - XWayland/compositor startup failure -- `ErrorCode::input_socket_send_failed` - IPC message send failure - -### Files Added -- `src/input/input_forwarder.cpp/hpp` -- `src/input/xwayland_server.cpp/hpp` - -### Files Modified -- `src/app/main.cpp` - InputForwarder instantiation -- `src/app/cli.hpp` - `--input-forwarding` flag -- `src/util/config.*` - `input.forwarding` config option -- `docs/input_forwarding.md` - usage instructions - -## Dependencies - -All system packages (no CPM changes required): - -| Package | Version | Purpose | -|---------|---------|---------| -| wlroots | 0.18 | Headless Wayland compositor | -| wayland-server | Latest | Wayland protocol server | -| xkbcommon | Latest | Keyboard keymap handling | -| libX11 | Latest | X11 client (connect to nested XWayland) | -| libXtst | Latest | XTest extension for input injection | - -SDL3 already in project. - -## Risks and Mitigations - -| Risk | Mitigation | -|------|------------| -| DISPLAY conflict (socket already bound) | Auto-select :1, :2, :3... until successful | -| XWayland startup failure | Propagate error via `Result`, log failure, disable input forwarding | -| Memory leak in compositor thread | RAII wrappers for all wlroots objects, explicit cleanup | -| Target launched outside nested XWayland | Document requirement: target must be launched with `DISPLAY=:N` | - -## Success Criteria - -- User presses W/A/S/D in Goggles window → captured app receives KeyPress events -- Works with Wine/DXVK applications (tested with RE4) -- Enabled explicitly (config/CLI); default behavior remains unchanged when disabled -- Clean shutdown (no segfaults, proper resource cleanup) -- Passes `openspec validate add-input-forwarding --strict` diff --git a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/specs/input-forwarding/spec.md deleted file mode 100644 index b301e80e..00000000 --- a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/specs/input-forwarding/spec.md +++ /dev/null @@ -1,209 +0,0 @@ -# input-forwarding Specification - -## Purpose - -Defines the input forwarding module that enables users to control captured Vulkan applications by pressing keys and using the mouse in the Goggles viewer window. The module creates a nested XWayland server and forwards input events via XTest injection. - -## ADDED Requirements - -### Requirement: XWayland Server Lifecycle - -The input forwarding module SHALL create a headless Wayland compositor with nested XWayland server for captured applications to connect to. - -#### Scenario: Automatic DISPLAY selection -- **WHEN** `InputForwarder::create()` is called -- **THEN** the system SHALL attempt to create Wayland sockets on wayland-1, wayland-2, etc. in sequence -- **AND** start XWayland on the first available DISPLAY (:1, :2, ...) -- **AND** expose the selected DISPLAY number via `InputForwarder::display_number()` - -#### Scenario: XWayland startup -- **WHEN** a Wayland socket is successfully created on wayland-N -- **THEN** the system SHALL call `wlr_xwayland_create()` to spawn XWayland process -- **AND** XWayland SHALL listen on DISPLAY :N -- **AND** the Wayland event loop SHALL run in a dedicated `std::jthread` - -#### Scenario: Compositor thread lifecycle -- **WHEN** `XWaylandServer::start()` succeeds -- **THEN** a compositor thread SHALL be spawned running `wl_display_run()` -- **AND** the thread SHALL automatically join in `XWaylandServer::~XWaylandServer()` -- **AND** the event loop SHALL terminate via `wl_display_terminate()` before thread join - -#### Scenario: Resource cleanup order -- **WHEN** `XWaylandServer::stop()` or destructor is called -- **THEN** wlroots resources SHALL be destroyed in reverse creation order -- **AND** XWayland SHALL be destroyed before compositor -- **AND** compositor SHALL be destroyed before backend -- **AND** backend SHALL be destroyed before display - -### Requirement: Keyboard Event Forwarding - -The input forwarding module SHALL translate SDL keyboard events to X11 KeyPress/KeyRelease events and inject them into the nested XWayland via XTest. - -#### Scenario: SDL to X11 keycode translation -- **WHEN** `InputForwarder::forward_key()` receives an SDL_KeyboardEvent -- **THEN** the SDL scancode SHALL be translated to Linux keycode (e.g. SDL_SCANCODE_W → KEY_W = 17) -- **AND** the Linux keycode SHALL be translated to X11 keycode (+8 offset, e.g. 17 → 25) -- **AND** unknown scancodes SHALL be skipped without error - -#### Scenario: XTest key injection -- **WHEN** a valid X11 keycode is obtained -- **THEN** `XTestFakeKeyEvent(display, keycode, is_press, CurrentTime)` SHALL be called -- **AND** `XFlush(display)` SHALL be called to send the event immediately - -#### Scenario: Press and release events -- **WHEN** `SDL_EVENT_KEY_DOWN` is received -- **THEN** `XTestFakeKeyEvent(..., True, ...)` SHALL be called (key press) -- **WHEN** `SDL_EVENT_KEY_UP` is received -- **THEN** `XTestFakeKeyEvent(..., False, ...)` SHALL be called (key release) - -### Requirement: X11 Connection Management - -The input forwarding module SHALL maintain an X11 client connection to the nested XWayland server. - -#### Scenario: X11 connection establishment -- **WHEN** XWayland server has started on :N -- **THEN** `XOpenDisplay(":N")` SHALL be called to establish X11 client connection -- **AND** if connection fails, `InputForwarder::create()` SHALL return an error result -- **AND** XWaylandServer SHALL be stopped and cleaned up on connection failure - -#### Scenario: X11 connection cleanup -- **WHEN** `InputForwarder::~InputForwarder()` is called -- **THEN** `XCloseDisplay()` SHALL be called if connection is open -- **AND** the connection SHALL be closed before XWaylandServer is stopped - -### Requirement: Expose Selected DISPLAY Number - -The input forwarding module SHALL expose the selected nested DISPLAY number so the target application can be launched inside the nested XWayland session. - -#### Scenario: Query DISPLAY after init -- **WHEN** `InputForwarder::create()` succeeds -- **THEN** `InputForwarder::display_number()` SHALL return a positive integer N -- **AND** the application SHALL be able to report N to the user (e.g. via logs) - -### Requirement: Mouse Event Forwarding (Basic) - -The input forwarding module SHALL forward basic mouse input into the nested XWayland server via XTest. - -#### Scenario: Mouse button injection -- **WHEN** `InputForwarder::forward_mouse_button()` receives an SDL_MouseButtonEvent -- **THEN** `XTestFakeButtonEvent(display, button, is_press, CurrentTime)` SHALL be called -- **AND** `XFlush(display)` SHALL be called to send the event immediately - -#### Scenario: Mouse motion injection -- **WHEN** `InputForwarder::forward_mouse_motion()` receives an SDL_MouseMotionEvent -- **THEN** `XTestFakeMotionEvent(display, 0, x, y, CurrentTime)` SHALL be called using the SDL event coordinates -- **AND** `XFlush(display)` SHALL be called to send the event immediately - -#### Scenario: Mouse wheel injection -- **WHEN** `InputForwarder::forward_mouse_wheel()` receives an SDL_MouseWheelEvent -- **THEN** wheel input SHALL be translated into X11 button events (4/5 for vertical, 6/7 for horizontal) -- **AND** a press+release pair SHALL be sent via `XTestFakeButtonEvent` - -### Requirement: Error Handling and Graceful Degradation - -The input forwarding module SHALL handle initialization failures gracefully and allow the application to continue without input forwarding. - -#### Scenario: XWayland start failure -- **WHEN** all DISPLAY numbers 1-9 are already bound -- **THEN** `XWaylandServer::start()` SHALL return `Result` error with `ErrorCode::input_init_failed` -- **AND** `InputForwarder::create()` SHALL propagate the error to the caller -- **AND** the application SHALL log the error and continue without input forwarding - -#### Scenario: X11 connection failure -- **WHEN** `XOpenDisplay(":N")` returns NULL -- **THEN** `InputForwarder::create()` SHALL stop the XWaylandServer -- **AND** return an error result with `ErrorCode::input_init_failed` -- **AND** clean up all allocated resources via RAII - -#### Scenario: Forward key when not initialized -- **WHEN** `InputForwarder::forward_key()` is called but `InputForwarder::create()` failed or was not called -- **THEN** the function SHALL return immediately without error (no-op) -- **AND** no logging SHALL occur (avoid log spam) - -### Requirement: PIMPL Pattern for Implementation Hiding - -The input forwarding public API SHALL use the PIMPL idiom to hide wlroots and X11 implementation details from public headers. - -#### Scenario: Forward declaration in public header -- **WHEN** `input_forwarder.hpp` is included by application code -- **THEN** the header SHALL NOT include wlroots or X11 headers directly -- **AND** SHALL forward-declare `struct InputForwarder::Impl` -- **AND** store `std::unique_ptr m_impl` as the only data member - -#### Scenario: Implementation details in .cpp -- **WHEN** `input_forwarder.cpp` is compiled -- **THEN** `struct InputForwarder::Impl` SHALL be defined with full wlroots and X11 types -- **AND** SHALL own `XWaylandServer` instance -- **AND** SHALL own `Display* x11_display` for XTest injection - -### Requirement: Integration with Main Application - -The input forwarding module SHALL integrate with the existing main application event loop and lifecycle. - -#### Scenario: Initialization in main -- **WHEN** `run_app()` initializes subsystems -- **THEN** the application SHALL only initialize input forwarding when explicitly enabled by user configuration or CLI -- **AND** `InputForwarder::create()` SHALL be called after SDL initialization -- **AND** failures SHALL be logged and the application SHALL continue without input forwarding - -#### Scenario: Event forwarding in main loop -- **WHEN** the main event loop receives `SDL_EVENT_KEY_DOWN` or `SDL_EVENT_KEY_UP` -- **THEN** `InputForwarder::forward_key(event.key)` SHALL be called -- **AND** the return value SHALL be checked (propagate errors to log) - -#### Scenario: Shutdown order -- **WHEN** the application shuts down -- **THEN** `InputForwarder` SHALL be destroyed after capture receiver stops -- **AND** the X11 connection SHALL be closed before XWaylandServer stops -- **AND** the compositor thread SHALL join before main thread exits - -### Requirement: Wayland Input Forwarding Not Supported (Temporary) - -The input forwarding module SHALL NOT claim to support input injection into Wayland-native clients. - -#### Scenario: Wayland-native target -- **GIVEN** a target application uses a Wayland backend -- **WHEN** the user attempts to use input forwarding -- **THEN** input forwarding is not supported for that target (X11-only) - -### Requirement: Keycode Translation Map - -The input forwarding module SHALL maintain a translation map from SDL scancodes to Linux keycodes. - -#### Scenario: Common key mappings -- **GIVEN** SDL_SCANCODE_W (26) -- **THEN** SHALL translate to KEY_W (17) -- **GIVEN** SDL_SCANCODE_A (4) -- **THEN** SHALL translate to KEY_A (30) -- **GIVEN** SDL_SCANCODE_ESCAPE (41) -- **THEN** SHALL translate to KEY_ESC (1) - -#### Scenario: Unmapped scancode -- **WHEN** an SDL scancode has no Linux keycode mapping -- **THEN** `forward_key()` SHALL return immediately (no-op) -- **AND** no error SHALL be logged - -#### Scenario: X11 keycode offset -- **WHEN** translating Linux keycode to X11 keycode -- **THEN** 8 SHALL be added to the Linux keycode -- **AND** the resulting X11 keycode SHALL be passed to XTest - -### Requirement: Namespace and Module Placement - -The input forwarding module SHALL follow project conventions for namespace and directory structure. - -#### Scenario: Namespace hierarchy -- **GIVEN** the input forwarding module -- **THEN** classes SHALL be in `goggles::input` namespace -- **AND** SHALL NOT use `using namespace` in headers - -#### Scenario: File location -- **GIVEN** the input forwarding module -- **THEN** source files SHALL be placed in `src/input/` directory -- **AND** public headers SHALL use `snake_case.hpp` naming -- **AND** implementation files SHALL use `snake_case.cpp` naming - -#### Scenario: Header includes -- **WHEN** including input forwarding headers -- **THEN** `#pragma once` SHALL be used (no include guards) -- **AND** include order SHALL follow project policy (self, C++ std, third-party, project) diff --git a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/tasks.md b/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/tasks.md deleted file mode 100644 index 8697345f..00000000 --- a/openspec/changes/archive/2026-01-04-add-input-forwarding-x11/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add `goggles_input` module (`src/input/`) with wlroots headless compositor + XWayland. -- [x] 1.2 Implement `InputForwarder` (PIMPL) using X11 XTest for key + mouse injection. -- [x] 1.3 Integrate input forwarding into `src/app/main.cpp` SDL event loop (keys/buttons/motion/wheel). -- [x] 1.4 Add `goggles_input_test` manual test app. -- [x] 1.5 Make input forwarding opt-in via config and CLI (default disabled). -- [x] 1.6 Update docs: target must be launched with `DISPLAY=:N` (nested XWayland); Wayland input forwarding not supported yet. diff --git a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/design.md b/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/design.md deleted file mode 100644 index a1eea036..00000000 --- a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/design.md +++ /dev/null @@ -1,47 +0,0 @@ -# Design: Refactor App Main Orchestration - -## Goals -- Make app startup/shutdown and per-frame orchestration explicit and maintainable. -- Keep behavior stable while moving logic out of `src/app/main.cpp`. -- Keep an “escape hatch” for future SDL app-callback entrypoints by exposing `handle_event()` + `tick_frame()` style APIs. - -## Components - -### `goggles::app::SdlPlatform` -Responsible for SDL initialization and window ownership. -- Owns `SDL_Init`/`SDL_Quit` lifetime via RAII. -- Owns `SDL_Window` creation/destruction. -- Provides non-owning access to `SDL_Window*` for subsystem factories. - -### `goggles::app::UiController` -Responsible for ImGui integration glue and UI state reconciliation. -- Creates and owns `goggles::ui::ImGuiLayer` (optional; can be disabled). -- Maintains UI-only state that should not leak into the main loop (e.g., previous shader enabled state). -- Bridges UI events/state into `goggles::render::VulkanBackend` actions (reload preset, toggle shader, reset parameters). - -### `goggles::app::Application` -Responsible for orchestration and app state. -- Owns major subsystems and optional components: - - `VulkanBackend` - - optional `CaptureReceiver` (viewer-only mode when unavailable) - - optional `InputForwarder` - - `UiController` (optional UI) -- Implements a stable driving surface: - - `handle_event(const SDL_Event&)` - - `tick_frame()` - - `is_running()` - -## Per-frame Phase Order (Invariant) -The refactor preserves the existing phase order to avoid subtle regressions: -1. Poll/process SDL events (ImGui first, then app hotkeys, then input forwarding if not captured) -2. Handle resize requests (event-driven and/or swapchain-driven) -3. Poll capture receiver and import/refresh sync semaphores when updated -4. Reconcile UI state into backend (preset reload, shader enabled) -5. Render either captured frame or clear; render UI via backend callback when enabled -6. Refresh UI parameter state when the filter chain swaps - -## Future SDL App-Callback Wiring (Non-Goal) -This change does not adopt SDL app callbacks, but the `Application` surface is designed so a future adapter can forward: -- SDL events → `handle_event()` -- per-frame iterate → `tick_frame()` - diff --git a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/proposal.md b/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/proposal.md deleted file mode 100644 index 1534bdda..00000000 --- a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/proposal.md +++ /dev/null @@ -1,47 +0,0 @@ -# Change: Refactor App Main Orchestration - -## Why -`src/app/main.cpp` currently mixes platform setup (SDL), subsystem initialization (Vulkan, UI, capture, input forwarding), per-frame orchestration, and shutdown logic in one large translation unit. This makes the entrypoint hard to maintain, hard to test, and risky to evolve (any feature tends to grow `main.cpp` further). - -This change refactors the app entrypoint into a small set of focused components while preserving existing behavior and allowing small UX improvements (e.g., reducing duplicated resize handling) without introducing functional regressions. - -## What Changes -- Introduce a dedicated app orchestrator (`goggles::app::Application`) that owns app state and coordinates: - - SDL event handling (including F1 UI toggle, quit, resize) - - optional capture polling + sync semaphore import - - optional input forwarding with ImGui capture rules - - UI state reconciliation and UI rendering integration - - render/clear submission and resize recovery -- Introduce a minimal SDL platform wrapper for RAII lifetime management of: - - `SDL_Init` / `SDL_Quit` - - `SDL_Window` creation/destruction -- Introduce a UI controller to isolate ImGui-related glue: - - preset discovery/catalog - - filter-chain parameter ↔ UI state synchronization - - applying UI actions to `VulkanBackend` (shader enable, preset reload, parameter reset) -- Reduce `src/app/main.cpp` to composition and top-level error boundary only. -- Keep the code structured so migrating to SDL3’s app-callback style later would be a wiring change (core logic exposes `handle_event()` and `tick_frame()` style methods). - -## Impact -- Affected specs: `app-window`, `object-lifecycle` -- Affected code: - - `src/app/main.cpp` - - `src/app/` (new: `application.*`, `sdl_platform.*`, `ui_controller.*`) - -## Non-Goals -- No new features or behavior changes in capture, render, or shader pipeline. -- No switch to SDL app-callback entrypoints in this change. -- No rework of VulkanBackend, CaptureReceiver, InputForwarder, or ImGuiLayer public APIs unless strictly required for extraction. - -## Success Criteria -- `src/app/main.cpp` no longer contains per-frame orchestration logic or large helper blocks (UI glue, event routing, etc.). -- Initialization and shutdown paths have a single ownership model (RAII) with no duplicated cleanup sequences. -- Existing user-visible behaviors remain intact (window creation, quit behavior, F1 toggles UI visibility, optional capture/input forwarding). -- Error handling follows `docs/project_policies.md` (expected/Result-based, log once at boundaries). - -## Risks & Mitigations -- **Risk:** Behavior regressions due to event ordering changes. - - **Mitigation:** Keep the per-frame phase order identical during migration; document invariants in tasks and validate manually. -- **Risk:** Duplicate logging or silent failures introduced while moving code. - - **Mitigation:** Enforce “log once at subsystem boundaries” during extraction; prefer propagating `Result` instead of converting to booleans. - diff --git a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/app-window/spec.md b/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/app-window/spec.md deleted file mode 100644 index 1cd8dcec..00000000 --- a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/app-window/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -# app-window Spec Delta - -## ADDED Requirements - -### Requirement: SDL Resource Ownership via RAII - -The application SHALL manage SDL initialization and the SDL window lifetime via RAII wrappers within the app module to ensure SDL resources are cleaned up on all exit paths, including early returns due to initialization failures. - -#### Scenario: Window creation failure cleanup -- **GIVEN** SDL3 initializes successfully -- **WHEN** window creation fails -- **THEN** an error SHALL be logged -- **AND** SDL3 resources SHALL be cleaned up before exit - -### Requirement: Orchestrated Event Loop Boundary - -The application SHALL encapsulate window event handling and per-frame orchestration behind a dedicated component (e.g., `goggles::app::Application`), keeping `src/app/main.cpp` limited to composition and top-level error handling. - -#### Scenario: Quit event exits orchestration -- **GIVEN** the window is open -- **WHEN** the user closes the window (X button or Alt+F4) -- **THEN** the orchestrator SHALL stop the event loop -- **AND** SDL3 resources SHALL be cleaned up - diff --git a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/object-lifecycle/spec.md b/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/object-lifecycle/spec.md deleted file mode 100644 index 9cf9f1be..00000000 --- a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/specs/object-lifecycle/spec.md +++ /dev/null @@ -1,34 +0,0 @@ -# object-lifecycle Spec Delta - -## ADDED Requirements - -### Requirement: App Orchestrator Uses Factory Pattern - -The app orchestrator component (e.g., `goggles::app::Application`) SHALL use the factory pattern (`create(...) -> ResultPtr`) for fallible initialization and SHALL NOT expose two-phase initialization. - -#### Scenario: Orchestrator initialization failure -- **WHEN** orchestrator initialization fails at any step -- **THEN** `create(...)` SHALL return an error `ResultPtr` -- **AND** any partially acquired resources SHALL be cleaned up automatically via RAII - -## MODIFIED Requirements - -### Requirement: Classes Using Factory Pattern - -The following classes SHALL use factory pattern instead of two-phase initialization: -- VulkanBackend -- FilterChain -- FilterPass -- ShaderRuntime -- Framebuffer -- OutputPass -- CaptureReceiver -- InputForwarder (already implemented) -- Application (app orchestrator) - -#### Scenario: Factory method signature -- **WHEN** implementing a factory method -- **THEN** it SHALL be declared as `[[nodiscard]] static auto create(...) -> ResultPtr;` -- **AND** it SHALL accept all necessary initialization parameters -- **AND** it SHALL use `GOGGLES_TRY()` for nested factory calls where applicable - diff --git a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/tasks.md b/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/tasks.md deleted file mode 100644 index 24266d3a..00000000 --- a/openspec/changes/archive/2026-01-08-refactor-app-main-orchestration/tasks.md +++ /dev/null @@ -1,37 +0,0 @@ -# Tasks: Refactor App Main Orchestration - -## 1. Preparation (no behavior change) -- [x] Confirm current invariants in `src/app/main.cpp` (event priority, resize handling, UI toggle, input forwarding capture rules). -- [x] Confirm all new fallible init uses `Result`/`ResultPtr` per `docs/project_policies.md`. - -## 2. Extract UI Glue (behavior-preserving move) -- [x] Create `src/app/ui_controller.hpp/.cpp` and move ImGui/preset/parameter glue out of `main.cpp`. -- [x] Eliminate hidden static UI state by storing prior UI state in the controller. -- [x] Hide SDL/ImGui header dependencies (forward decls + out-of-line definitions). - -## 3. Add SDL Platform RAII (reduce cleanup duplication) -- [x] Create `src/app/sdl_platform.hpp/.cpp` for SDL init/quit and window lifetime management. -- [x] Replace early-return cleanup branches with RAII-managed teardown. -- [x] Use a `CreateInfo` struct for SDL platform/window creation to avoid boolean/positional arg churn. - -## 4. Introduce `Application` Orchestrator (explicit state + per-frame phases) -- [x] Create `src/app/application.hpp/.cpp` with `create(...) -> ResultPtr`. -- [x] Move ownership of window/backend/ui/capture/input into `Application`. -- [x] Implement `handle_event(const SDL_Event&)` and `tick_frame()` preserving current phase order: - - events → resize flagging → capture poll → semaphore import → UI begin/end → render/clear → resize recovery → UI parameter refresh on chain swap - -## 5. Shrink Entrypoint -- [x] Update `src/app/main.cpp` to only compose config/CLI and drive the loop via `Application`. -- [x] Keep top-level exception boundary in `main()` (no exceptions for expected failures). - -## 6. Specs -- [x] Add spec deltas for `app-window` and `object-lifecycle` reflecting the new orchestration and RAII ownership requirements. - -## 7. Validation -- [x] Build (debug + ASAN preset if available). -- [x] Manual checks: - - window opens and closes cleanly; quit exits loop and cleans SDL resources - - F1 toggles ImGui visibility - - shader preset loads (config + CLI override) - - viewer-only mode works when capture receiver fails - - input forwarding only forwards when ImGui does not capture input diff --git a/openspec/changes/archive/2026-01-10-add-appimage-packaging/design.md b/openspec/changes/archive/2026-01-10-add-appimage-packaging/design.md deleted file mode 100644 index bf4db9c6..00000000 --- a/openspec/changes/archive/2026-01-10-add-appimage-packaging/design.md +++ /dev/null @@ -1,79 +0,0 @@ -# Design: AppImage Packaging + Vulkan Layer Self-Install - -## Goals - -- Work on arbitrary distros without requiring root installation. -- Work on Steam Deck (Steam Runtime / pressure-vessel). -- Make Vulkan layer injection robust even when Steam sanitizes Vulkan-layer environment variables. -- Keep user UX simple: - - Steam launch option: `goggles -- %command%` - - No manual “copy manifest into implicit_layer.d” step. - -## Key Observation (Gamescope Pattern) - -Gamescope ships a Vulkan implicit layer manifest in the standard loader search path and gates activation behind a dedicated environment variable set by its launcher. This avoids needing to set `VK_LAYER_PATH` in the environment. - -Goggles already uses the same gating strategy (`enable_environment` uses `GOGGLES_CAPTURE=1`). The missing piece is making the manifest+layer library reliably discoverable across Steam runtimes. - -## Approach - -### 1) Distribution Artifact: AppImage - -The AppImage contains: -- `goggles` viewer executable (64-bit). -- Resources needed by the viewer (config defaults, shaders if bundled, etc.). - -The AppImage entrypoint (AppRun) is the user-facing `goggles` command. - -### 2) One-time user-level layer install (idempotent) - -Because an AppImage’s mounted runtime path is not stable across runs, the Vulkan loader cannot reliably load a layer library from inside the AppImage via a manifest that points into the mounted image. - -Therefore, on first run (and on version update), we install to stable user paths: - -- Data root: `${XDG_DATA_HOME:-$HOME/.local/share}` -- Manifests: - - `${data_root}/vulkan/implicit_layer.d/goggles_layer_x86_64.json` - - `${data_root}/vulkan/implicit_layer.d/goggles_layer_i386.json` -- Layer libraries: - - `${data_root}/goggles/vulkan-layers//x86_64/libgoggles_vklayer.so` - - `${data_root}/goggles/vulkan-layers//i386/libgoggles_vklayer.so` -- Version marker: - - `${data_root}/goggles/vulkan-layers//.installed` - -Manifests use absolute `library_path` to point at the installed library path. - -### 3) Steam-safe injection model - -We do not rely on Vulkan-layer path environment variables. - -Instead: -- The Vulkan loader discovers the implicit layer via the user manifest directory. -- The layer is activated only when `GOGGLES_CAPTURE=1` is set for the game process. -- The Goggles wrapper ensures `GOGGLES_CAPTURE=1` is set when it spawns the target command (including `%command%` in Steam). - -### 4) Multi-arch behavior (Proton / 32-bit) - -We install both 64-bit and 32-bit manifests + libraries. - -Expected behavior: -- Native 64-bit Vulkan apps load `VK_LAYER_goggles_capture_64` when `GOGGLES_CAPTURE=1`. -- 32-bit apps (including Wine/DXVK) load `VK_LAYER_goggles_capture_32` when `GOGGLES_CAPTURE=1`. - -The manifests must be valid for the Vulkan loader used inside Steam Runtime containers and must reference libraries on paths visible inside the container (home is typically bind-mounted). - -## Failure Modes + Mitigations - -- **Steam runtime strips `VK_LAYER_PATH`**: Not relevant; we rely on implicit layer discovery. -- **Steam runtime does not see the installed library path**: Choose install location under `$HOME`/`XDG_DATA_HOME`, which Steam generally bind-mounts. -- **Layer install races (two launches)**: Make self-install idempotent and atomic (write temp files then rename). -- **Updates leave stale manifests**: Install manifests as part of version update; optionally keep only one “active” version by rewriting manifests to the newest installed version. - -## Alternatives Considered - -### Alternative A: Run without self-install by setting `XDG_DATA_DIRS` from AppRun - -This can work on some systems, but it is not guaranteed under Steam/pressure-vessel and still faces the “library inside AppImage mount” instability unless the manifest can reference a stable path. - -Given the preference for a one-time self-install, we choose self-install as the primary path. - diff --git a/openspec/changes/archive/2026-01-10-add-appimage-packaging/proposal.md b/openspec/changes/archive/2026-01-10-add-appimage-packaging/proposal.md deleted file mode 100644 index cf6ab495..00000000 --- a/openspec/changes/archive/2026-01-10-add-appimage-packaging/proposal.md +++ /dev/null @@ -1,51 +0,0 @@ -# add-appimage-packaging - -## Why - -Goggles relies on an implicit Vulkan capture layer (32-bit + 64-bit) that must be discoverable by the Vulkan loader for Steam/Proton workloads. Steam runtimes commonly sanitize Vulkan layer environment variables (e.g., `VK_LAYER_PATH`), which makes “environment-only injection” unreliable. - -We need a packaging approach that works on arbitrary Linux distros and Steam Deck, minimizes user setup, and supports the preferred Steam launch pattern: `goggles -- %command%`. - -## What Changes - -- Add an AppImage-based distribution for Goggles (viewer + resources). -- Add a one-time, user-level installation flow for Vulkan implicit layer manifests and layer shared libraries (both 64-bit and 32-bit). -- Ensure Steam/Proton compatibility by avoiding reliance on `VK_LAYER_PATH`/`VK_ADD_LAYER_PATH` for layer discovery. -- Provide a stable, documented wrapper behavior for Steam launch options (`goggles -- %command%`) that guarantees `GOGGLES_CAPTURE=1` is set for the launched game process. - -## How (High-Level) - -- Package the Goggles viewer and assets as an AppImage entrypoint. -- On first run (and on version change), the AppImage entrypoint performs an idempotent “self-install” into the user data directory: - - Install Vulkan implicit layer manifests into `${XDG_DATA_HOME:-$HOME/.local/share}/vulkan/implicit_layer.d/`. - - Install layer shared libraries into `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/vulkan-layers//{x86_64,i386}/`. - - Generate manifests whose `library_path` points at the installed libraries using absolute paths. -- Runtime injection uses the existing manifest gating approach: - - The implicit layer exists system-wide/user-wide, but it only activates when `GOGGLES_CAPTURE=1` is present (analogous to how Gamescope gates its WSI layer behind `ENABLE_GAMESCOPE_WSI=1`). - -## Impact - -- New spec: - - `packaging` (AppImage distribution + Vulkan layer self-install + Steam compatibility) -- Existing specs referenced (no functional changes required by this proposal): - - `vk-layer-capture` (layer naming, multi-arch manifests, enable env behavior) - - `build-system` / `dependency-management` (Pixi-driven builds and reproducibility) -- Expected affected code areas (implementation phase): - - `scripts/` (new packaging tasks) - - AppImage entrypoint/wrapper (install + launch orchestration) - - Documentation (`README.md`, packaging/Steam guide) - -## Non-Goals - -- System-wide installation (root-required) of manifests or libraries. -- Depending on `VK_LAYER_PATH`, `VK_ADD_LAYER_PATH`, or Steam runtime overrides for layer discovery. -- Introducing a new Vulkan layer mechanism (explicit layers or manual `vkCreateInstance` interposition). - -## Open Questions - -- Installation location and versioning: - - **Decision**: Use a versioned install root: `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/vulkan-layers//...`. -- Uninstall UX: - - **Decision**: Provide an AppImage wrapper flag `--uninstall-layer` to remove the installed layer for the current AppImage version. -- Updates: - - **Decision**: Refresh manifests on launch to ensure they point at the active version directory. diff --git a/openspec/changes/archive/2026-01-10-add-appimage-packaging/specs/packaging/spec.md b/openspec/changes/archive/2026-01-10-add-appimage-packaging/specs/packaging/spec.md deleted file mode 100644 index 1d03c09e..00000000 --- a/openspec/changes/archive/2026-01-10-add-appimage-packaging/specs/packaging/spec.md +++ /dev/null @@ -1,72 +0,0 @@ -## ADDED Requirements - -### Requirement: AppImage Distribution - -The project SHALL provide an AppImage distribution artifact for the Goggles viewer that runs on arbitrary Linux distributions without requiring root installation. - -#### Scenario: AppImage starts viewer -- **GIVEN** the user has downloaded the Goggles AppImage -- **WHEN** the user executes the AppImage -- **THEN** the Goggles viewer SHALL start successfully - -### Requirement: User-Level Vulkan Layer Self-Install - -The AppImage entrypoint SHALL support a one-time, user-level installation of Vulkan implicit layer manifests and layer shared libraries for both 64-bit and 32-bit architectures. - -The installation SHALL target: -- `${XDG_DATA_HOME:-$HOME/.local/share}/vulkan/implicit_layer.d/` for manifests -- `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/vulkan-layers//...` for layer shared libraries - -#### Scenario: First run installs manifests and libraries -- **GIVEN** no Goggles layer manifests exist in the user implicit layer directory -- **WHEN** the user runs the AppImage -- **THEN** the manifests for i386 and x86_64 SHALL be installed into the implicit layer directory -- **AND** the corresponding layer libraries SHALL be installed into the Goggles user data directory - -#### Scenario: Subsequent runs are idempotent -- **GIVEN** the manifests and layer libraries are already installed for the current version -- **WHEN** the user runs the AppImage again -- **THEN** the install step SHALL NOT corrupt or partially rewrite installed files -- **AND** the viewer launch SHALL proceed normally - -### Requirement: Steam-Safe Layer Activation - -The packaged launch flow SHALL NOT rely on `VK_LAYER_PATH` or `VK_ADD_LAYER_PATH` to activate the capture layer. - -Instead, layer activation SHALL depend on: -- implicit layer discovery via installed manifests -- the existing enable environment variable `GOGGLES_CAPTURE=1` - -#### Scenario: Steam runtime sanitizes Vulkan layer env vars -- **GIVEN** a Steam runtime environment that strips `VK_LAYER_PATH` / `VK_ADD_LAYER_PATH` -- **WHEN** a game is launched through Goggles with `GOGGLES_CAPTURE=1` -- **THEN** the Vulkan loader SHALL still discover the Goggles layer via the implicit manifest search path -- **AND** the layer SHALL load successfully - -### Requirement: Steam Launch UX Compatibility - -The packaging SHALL support Steam launch options of the form `goggles -- %command%`. - -#### Scenario: Launch option passthrough -- **GIVEN** Steam is configured with launch options `goggles -- %command%` -- **WHEN** Steam launches the game -- **THEN** Goggles SHALL execute the target command exactly as provided by Steam -- **AND** set `GOGGLES_CAPTURE=1` for the spawned game process - -### Requirement: Packaged Assets Are Not CWD-Dependent - -The packaged runtime SHALL locate shipped assets (configuration and shaders) without relying on the current working directory. - -#### Scenario: AppImage provides a stable resource root -- **GIVEN** the Goggles AppImage is executed from an arbitrary working directory -- **WHEN** the viewer loads its default configuration, UI font asset, and default shader preset -- **THEN** the viewer SHALL locate shipped assets via a stable resource root (e.g. provided by the AppImage wrapper) -- **AND** it SHALL NOT require `./shaders` to exist in the working directory - -### Requirement: Optional Shader Pack Install Location - -The packaging SHALL provide a way to install/update the full RetroArch shader pack (slang-shaders) into a stable user location without requiring Pixi. - -#### Scenario: Shader pack is fetched into XDG data -- **WHEN** the user invokes the AppImage shader fetch/update flow -- **THEN** the shader pack SHALL be installed under `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/shaders/retroarch/` diff --git a/openspec/changes/archive/2026-01-10-add-appimage-packaging/tasks.md b/openspec/changes/archive/2026-01-10-add-appimage-packaging/tasks.md deleted file mode 100644 index 02098388..00000000 --- a/openspec/changes/archive/2026-01-10-add-appimage-packaging/tasks.md +++ /dev/null @@ -1,45 +0,0 @@ -## 1. Proposal Refinement - -- [x] 1.1 Confirm install paths (`XDG_DATA_HOME` fallback behavior) -- [x] 1.2 Decide versioning strategy for installed layer libs (versioned vs in-place) -- [x] 1.3 Decide uninstall UX (command vs docs-only) - -## 2. Build + Staging - -- [x] 2.1 Define a Pixi task to build release viewer + both layer arches -- [x] 2.2 Add a staging step that collects artifacts into an AppDir layout -- [x] 2.3 Ensure staged manifests reference installed user paths (not build paths) - -## 3. AppImage Entrypoint (Wrapper) - -- [x] 3.1 Implement idempotent self-install with atomic writes (temp + rename) -- [x] 3.2 Install manifests into `${XDG_DATA_HOME:-$HOME/.local/share}/vulkan/implicit_layer.d/` -- [x] 3.3 Install layer libs into `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/vulkan-layers//...` -- [x] 3.4 Add a `--self-install-only` mode for debugging -- [x] 3.5 Add an optional uninstall path if chosen in 1.3 - -## 4. Steam/Proton Compatibility - -- [x] 4.1 Ensure `goggles -- %command%` launch flow sets `GOGGLES_CAPTURE=1` for the spawned command -- [x] 4.2 Ensure 32-bit layer is installed and discoverable for Proton games -- [x] 4.3 Document known Steam Runtime caveats (container visibility, permissions) - -## 5. Documentation - -- [x] 5.1 Update `README.md` with AppImage install/run instructions -- [x] 5.2 Add a dedicated doc page for Steam setup + troubleshooting - -## 6. Validation - -- [x] 6.1 Check: Vulkan loader (64-bit) discovers `VK_LAYER_goggles_capture_64` -- [x] 6.2 Check: i386 manifest + library install paths are created and consistent -- [x] 6.3 Check: Steam/Deck guidance documented for external validation -- [x] 6.4 Add a lightweight diagnostic command to print detected install state - -## 7. Assets + Shader Packs - -- [x] 7.1 Ensure packaged runtime uses a stable shader base directory (not CWD-dependent) -- [x] 7.2 Add a non-Pixi shader pack fetch/update flow for AppImage users -- [x] 7.3 Document where assets/shaders live (AppImage vs XDG) -- [x] 7.4 Ensure ImGui preset catalog scans XDG/AppImage shader roots (not CWD-dependent) -- [x] 7.5 Fix AppRun shader-pack detection output (`--print-install-state`) diff --git a/openspec/changes/archive/2026-01-10-add-debug-overlay/proposal.md b/openspec/changes/archive/2026-01-10-add-debug-overlay/proposal.md deleted file mode 100644 index 3895e1de..00000000 --- a/openspec/changes/archive/2026-01-10-add-debug-overlay/proposal.md +++ /dev/null @@ -1,28 +0,0 @@ -# Add Debug Overlay - -## Summary - -Add a simple debug overlay to ImGuiLayer displaying FPS, frame time, and a frame time history graph for both render and source (capture) frames. - -## Problem - -No runtime visibility into performance metrics. Users cannot see FPS or frame timing information while using the application. - -## Solution - -Add a compact debug overlay window using ImGui: -- Render FPS and frame time -- Source (capture) FPS and frame time -- Frame time history graph (PlotLines) - -## Implementation - -1. Add frame time tracking to ImGuiLayer (ring buffer for render and source) -2. Add `notify_source_frame()` method called when new capture frame arrives -3. Add `draw_debug_overlay()` method -4. Wire through UiController and Application - -## Non-goals - -- GPU timing (requires Vulkan timestamp queries) -- Memory usage tracking \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-add-debug-overlay/tasks.md b/openspec/changes/archive/2026-01-10-add-debug-overlay/tasks.md deleted file mode 100644 index b46b6b16..00000000 --- a/openspec/changes/archive/2026-01-10-add-debug-overlay/tasks.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tasks - -- [x] Add frame time ring buffer to ImGuiLayer (render and source) -- [x] Add frame time tracking in begin_frame() -- [x] Add notify_source_frame() method for source FPS tracking -- [x] Add draw_debug_overlay() method with FPS/frame time/graph -- [x] Wire notify_source_frame through UiController -- [x] Call notify_source_frame from Application on new frame (using frame_number) -- [x] Fix vk_layer to use CaptureFrameMetadata with frame_number -- [x] Add separate plots for render and source frame times -- [x] Remove verbose DMA-BUF log -- [x] Add F2 keybind to toggle debug overlay independently -- [x] Test overlay display \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-add-shader-batch-test/proposal.md b/openspec/changes/archive/2026-01-10-add-shader-batch-test/proposal.md deleted file mode 100644 index 009eca87..00000000 --- a/openspec/changes/archive/2026-01-10-add-shader-batch-test/proposal.md +++ /dev/null @@ -1,41 +0,0 @@ -# Change: Add Shader Validation & Testing - -## Why - -Per ROADMAP.md Phase 1, we need to prevent regressions in the filter chain when adding new features. With 1870+ RetroArch shader presets, manual testing is impractical. - -## What - -Implement the Shader Validation & Testing items from ROADMAP.md: - -1. Validate shader compilation for all implemented presets -2. Catch SPIR-V compilation errors early -3. Report shader compilation failures with diagnostics -4. Automated test runner for shader validation -5. Integration with existing test infrastructure (Catch2) - -Future (out of scope for this change): -- Golden image generation for reference outputs -- Comparison against golden images -- Automated regression detection - -## Scope - -### In Scope -- Test harness binary for batch shader validation -- Shell script wrapper (`scripts/test_shaders.sh`) -- JSON output for CI integration -- Catch2 integration for preset parsing tests -- Diagnostic output on compilation failures - -### Out of Scope -- GPU rendering / visual regression testing -- Golden image infrastructure -- Performance benchmarking - -## Success Criteria - -1. `scripts/test_shaders.sh` tests all presets -2. Results in `build/shader_test_results.json` -3. CI integration with regression detection -4. Per-category filtering (e.g., `test_shaders.sh crt`) \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-add-shader-batch-test/specs/shader-testing/spec.md b/openspec/changes/archive/2026-01-10-add-shader-batch-test/specs/shader-testing/spec.md deleted file mode 100644 index daf7e5a9..00000000 --- a/openspec/changes/archive/2026-01-10-add-shader-batch-test/specs/shader-testing/spec.md +++ /dev/null @@ -1,27 +0,0 @@ -# Shader Testing - -## ADDED Requirements - -### Requirement: Batch shader preset testing -The system SHALL provide a batch testing tool that validates all RetroArch shader presets for parsing and compilation compatibility. - -#### Scenario: Run batch test on all presets -Given the shaders/retroarch directory contains .slangp files -When `scripts/test_shaders.sh` is executed -Then each preset is tested for parse and compile success -And results are written to build/shader_test_results.json -And exit code is 0 if no regressions, non-zero otherwise - -#### Scenario: Filter by category -Given a category argument is provided (e.g., "crt") -When `scripts/test_shaders.sh crt` is executed -Then only presets under shaders/retroarch/crt/ are tested - -### Requirement: Machine-readable results -The batch test tool SHALL output results in JSON format containing per-preset status and summary statistics. - -#### Scenario: JSON output format -Given batch tests have completed -When results are written to build/shader_test_results.json -Then each preset entry includes: path, parse_ok, compile_ok, error (if any) -And summary includes: total, passed, failed, skipped counts \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-add-shader-batch-test/tasks.md b/openspec/changes/archive/2026-01-10-add-shader-batch-test/tasks.md deleted file mode 100644 index a3935034..00000000 --- a/openspec/changes/archive/2026-01-10-add-shader-batch-test/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -## 1. Shader Compilation Test - -- [x] 1.1 Create `scripts/generate_shader_report.sh` script -- [x] 1.2 Find all .slangp presets in shaders/retroarch/ -- [x] 1.3 Test each preset: parse + compile passes -- [x] 1.4 Output pass/fail summary -- [x] 1.5 Dynamic category discovery (all categories tested) \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/design.md b/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/design.md deleted file mode 100644 index 0b6b437f..00000000 --- a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/design.md +++ /dev/null @@ -1,357 +0,0 @@ -# Design: Wayland Native Input Forwarding - -## Context - -Goggles provides input forwarding to captured applications by running a nested compositor (headless wlroots) with XWayland. The current implementation injects input via XTest to the XWayland server, which works but: - -1. Bypasses wlroots' seat/focus management system -2. Requires maintaining a separate X11 client connection -3. Cannot support Wayland-native apps -4. Adds X11/XTest as dependencies - -Investigation revealed that `wlr_xwayland_set_seat()` connects XWayland to the compositor's seat, allowing the wlr_xwm (X Window Manager) to automatically translate `wlr_seat_*` input events to X11 events. This enables a **unified input path** for both Wayland and X11 apps. - -### Constraints - -- **Thread safety**: wlr_seat calls must execute on the compositor thread (running `wl_display_run`) -- **Single-app focus**: MVP targets single captured application (no multi-window focus management) -- **Coordinate mapping**: Not implemented; document as limitation -- **Backward compatibility**: X11 apps must continue working (via wlr_xwm translation) -- **XWayland lifecycle**: XWayland surface destroy signals fire unexpectedly during normal X11 operation; must NOT be used for focus cleanup - -## Goals / Non-Goals - -**Goals:** -- Unified input path using `wlr_seat_*` for both X11 and Wayland apps -- Remove X11/XTest dependencies from input forwarding -- Support single focused application (auto-focus first connected client) -- Simpler codebase with single code path - -**Non-Goals:** -- Multi-application focus management -- Coordinate scaling/mapping between viewer and target -- Touch input support -- Clipboard/data device integration - -## Architecture - -### Previous Data Flow (XTest - Being Removed) - -``` -SDL Event (main thread) - | - v -InputForwarder::forward_key() - | - v -sdl_to_linux_keycode() -> linux_to_x11_keycode() - | - v -XTestFakeKeyEvent(x11_display, keycode, pressed) - | - v -XWayland receives XTest request - | - v -X11 app receives KeyPress/KeyRelease -``` - -**Problems**: Dual dependencies, bypasses seat, doesn't support Wayland apps. - -### New Data Flow (Unified wlr_seat) - -``` -SDL Event (main thread) - | - v -InputForwarder::forward_key() - | - v -sdl_to_linux_keycode() - | - v -Write to eventfd (queue event) - | - [Thread boundary - main -> compositor] - | - v -Compositor thread reads event - | - v -wlr_seat_keyboard_notify_key(seat, time, linux_keycode, state) - | - +--[Wayland surface focused] - | | - | v - | wl_keyboard.key sent to Wayland client - | - +--[XWayland surface focused] - | - v - wlr_xwm translates to X11 KeyPress - | - v - Xwayland server delivers to X11 app -``` - -**Benefits**: Single code path, no X11 deps, proper seat/focus management. - -### Surface Lifecycle - -#### Native Wayland (xdg_toplevel) - -``` -new_toplevel signal - | - v -handle_new_xdg_toplevel() - | - v -Register commit listener on surface - | - v -[commit signal] -> handle_xdg_surface_commit() - | - v -Check toplevel->base->initialized - | - v -Register map listener - | - v -[map signal] -> handle_xdg_surface_map() - | - v -Add to m_surfaces, register destroy listener - | - v -Focus if no current focus OR current focus is stale XWayland - | - v -[destroy signal] -> handle_xdg_surface_destroy() - | - v -Remove from m_surfaces, clear focus if was focused -``` - -#### XWayland (X11 apps) - -``` -new_surface signal - | - v -handle_new_xwayland_surface() - | - v -Register associate listener (waits for xsurface->surface to be valid) - | - v -[associate signal] -> handle_xwayland_surface_associate() - | - v -Register map listener - | - v -[map signal] -> handle_xwayland_surface_map() - | - v -Focus if no surface currently focused -``` - -**Note**: XWayland surfaces are NOT tracked in m_surfaces and NO destroy listener is registered. XWayland destroy signals fire at unpredictable times (e.g., during window operations), breaking X11 input forwarding. - -### Focus Management - -Focus uses **mutual exclusion** between native Wayland and XWayland: - -- `m_focused_surface`: Always points to the currently focused wlr_surface (or nullptr) -- `m_focused_xsurface`: Points to the XWayland surface if focus is XWayland (or nullptr) - -**Key invariants**: -1. If native Wayland has focus: `m_focused_surface != nullptr`, `m_focused_xsurface == nullptr` -2. If XWayland has focus: `m_focused_surface == xsurface->surface`, `m_focused_xsurface != nullptr` -3. No focus: both are nullptr - -**Stale pointer handling**: When switching from XWayland to native Wayland: -1. `m_focused_xsurface` may be dangling (X11 app exited, wlroots freed the memory) -2. `focus_surface()` clears `m_focused_xsurface = nullptr` BEFORE any wlroots API calls -3. This prevents crashes when wlroots internally tries to send leave events - -### Component Changes - -#### CompositorServer (renamed from XWaylandServer) - -```cpp -namespace goggles::input { - -class CompositorServer { -public: - ~CompositorServer(); - - CompositorServer(const CompositorServer&) = delete; - CompositorServer& operator=(const CompositorServer&) = delete; - CompositorServer(CompositorServer&&) = delete; - CompositorServer& operator=(CompositorServer&&) = delete; - - [[nodiscard]] static auto create() -> ResultPtr; - - [[nodiscard]] auto x11_display() const -> std::string; // ":1" - [[nodiscard]] auto wayland_display() const -> std::string; // "wayland-1" - - void inject_key(uint32_t linux_keycode, bool pressed); - void inject_pointer_motion(double sx, double sy); - void inject_pointer_button(uint32_t button, bool pressed); - void inject_pointer_axis(double value, bool horizontal); - -private: - CompositorServer() = default; - - wl_display* m_display = nullptr; - wl_event_loop* m_event_loop = nullptr; - wlr_backend* m_backend = nullptr; - wlr_renderer* m_renderer = nullptr; - wlr_allocator* m_allocator = nullptr; - wlr_compositor* m_compositor = nullptr; - wlr_xdg_shell* m_xdg_shell = nullptr; - wlr_seat* m_seat = nullptr; - wlr_xwayland* m_xwayland = nullptr; - wlr_keyboard* m_virtual_keyboard = nullptr; - - util::UniqueFd m_event_fd; - std::vector m_surfaces; - wlr_surface* m_focused_surface = nullptr; - int m_display_number = -1; - - wl_listener m_new_xdg_toplevel{}; - wl_listener m_new_xwayland_surface{}; - - std::jthread m_compositor_thread; -}; - -} // namespace goggles::input -``` - -#### InputForwarder (Simplified) - -```cpp -namespace goggles::input { - -struct InputForwarder::Impl { - std::unique_ptr server; -}; - -auto InputForwarder::create() -> ResultPtr { - auto forwarder = std::unique_ptr(new InputForwarder()); - forwarder->m_impl->server = GOGGLES_TRY(CompositorServer::create()); - return make_result_ptr(std::move(forwarder)); -} - -auto InputForwarder::forward_key(const SDL_KeyboardEvent& event) -> Result { - auto linux_keycode = sdl_to_linux_keycode(event.scancode); - if (linux_keycode == 0) return {}; - - m_impl->server->inject_key(linux_keycode, event.down); - return {}; -} - -} // namespace goggles::input -``` - -## Decisions - -### Decision 1: Replace XTest with wlr_seat for XWayland - -**Choice**: Use `wlr_xwayland_set_seat()` + `wlr_seat_*` APIs for X11 apps - -**Rationale**: -- wlr_xwm automatically translates seat events to X11 events -- Eliminates X11/XTest dependencies -- Single code path for all apps -- Proper integration with wlroots seat/focus system - -**Alternatives considered**: -- Keep XTest alongside wlr_seat (dual path) - rejected as unnecessary complexity -- Use XTest for X11 and wlr_seat for Wayland only - rejected, more code to maintain - -### Decision 2: SPSCQueue + eventfd for cross-thread marshaling - -**Choice**: Use `goggles::util::SPSCQueue` + eventfd for wl_event_loop wakeup - -**Rationale**: -- Project policy requires `SPSCQueue` for inter-thread communication -- Lock-free, wait-free guarantees (per project threading policy) -- eventfd solely for waking wl_event_loop (single byte notification) -- No mutex contention, predictable latency - -### Decision 3: Auto-focus first connected surface - -**Choice**: Automatically focus the first surface (xdg_toplevel or XWayland) that connects - -**Rationale**: -- MVP targets single-app use case -- Avoids complex focus management -- User explicitly launches target with appropriate env vars - -## Thread Model - -``` -Main Thread Compositor Thread ------------ ----------------- -SDL event loop wl_display_run() event loop - | | - v | -forward_key() | - | | - v | -server.inject_key() | - | | - v | -write(eventfd, event) | - v - wl_event_loop wakes - | - v - handle_input_event() - | - v - wlr_seat_keyboard_notify_key() - | - +--------------+---------------+ - | | - v v - [Wayland surface] [XWayland surface] - | | - v v - wl_keyboard.key wlr_xwm -> X11 KeyPress - | | - v v - Wayland Client X11 App -``` - -## Risks / Trade-offs - -| Risk | Mitigation | -|------|------------| -| wlr_xwm translation issues | Tested in many compositors; well-proven path | -| Event marshaling latency | eventfd is low-latency (~microseconds); acceptable | -| Modifier state desync | Track modifier state in CompositorServer | -| Coordinate mismatch | Document as limitation; future coordinate mapping task | -| XWayland destroy signal issues | Do NOT use destroy listeners for XWayland; use mutual exclusion focus model | -| Stale XWayland pointers | Clear m_focused_xsurface before wlroots API calls in focus_surface() | - -## Migration Plan - -1. Add `wlr_xwayland_set_seat()` call to connect XWayland to seat -2. Add virtual keyboard and surface tracking -3. Add eventfd marshaling for thread-safe input injection -4. Implement `inject_*` methods using `wlr_seat_*` APIs -5. Update InputForwarder to use new inject methods -6. Remove X11/XTest code and dependencies -7. Update documentation - -**Rollback**: Revert to XTest-based approach if wlr_xwm has issues (unlikely). - -## Open Questions - -1. ~~Should we keep XTest as fallback?~~ **No** - wlr_xwm is proven, adds complexity -2. How to handle keyboard layout/keymap? **Use xkb_keymap_new_from_names with defaults** diff --git a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/proposal.md b/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/proposal.md deleted file mode 100644 index daeedcce..00000000 --- a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/proposal.md +++ /dev/null @@ -1,60 +0,0 @@ -# Change: Add Wayland Native Input Forwarding - -## Why - -The current input forwarding implementation only supports X11 applications via XTest injection to XWayland. Wayland-native applications connecting to the nested compositor cannot receive input events. Additionally, the XTest approach bypasses wlroots' seat/focus management system and requires maintaining a separate X11 connection. - -## What Changes - -- **BREAKING**: Replace XTest injection with unified `wlr_seat_*` APIs for both X11 and Wayland apps (removes `display_number()`, adds `x11_display()`/`wayland_display()`) -- Connect XWayland to seat via `wlr_xwayland_set_seat()` so wlr_xwm handles X11 translation -- Track surfaces from both xdg_shell (Wayland) and XWayland clients -- Add virtual keyboard device with xkb keymap for proper key event delivery -- Add pointer capability to seat for mouse event delivery -- Implement thread-safe input event marshaling from main thread to compositor thread -- Rename `XWaylandServer` to `CompositorServer` to reflect its broader role -- **BREAKING**: Remove X11/XTest dependencies from input forwarding (removes libX11/libXtst link targets from build) - -## Old Code Cleanup - -Since the project is pre-release, we will completely remove the XTest-based implementation rather than maintaining dual paths: - -**Files to rename:** -- `src/input/xwayland_server.hpp` -> `compositor_server.hpp` -- `src/input/xwayland_server.cpp` -> `compositor_server.cpp` - -**Code to remove from `input_forwarder.cpp`:** -- `#include ` and `#include ` -- `Display* x11_display` member and `XOpenDisplay()`/`XCloseDisplay()` calls -- `linux_to_x11_keycode()` function (wlr_xwm handles X11 keycode translation) -- `XTestFakeKeyEvent()`, `XTestFakeButtonEvent()`, `XTestFakeMotionEvent()` calls -- `XFlush()` calls - -**Dependencies to remove from `src/input/CMakeLists.txt`:** -- `X11::X11` link target -- `X11::Xtst` link target -- Any `find_package(X11)` if local to this module - -**Archive for reference:** -- The old XTest design is preserved in `openspec/changes/archive/2026-01-04-add-input-forwarding-x11/` - -**Test app updates (`tests/input/goggles_input_test.cpp`):** -- Currently hardcodes `setenv("SDL_VIDEODRIVER", "x11", 1)` -- Create two separate test binaries for separation of concerns: - - `goggles_manual_input_x11` - manual X11/XWayland input probe via wlr_seat -> wlr_xwm path - - `goggles_manual_input_wayland` - manual native Wayland input probe via wlr_seat path -- Each binary sets appropriate `SDL_VIDEODRIVER` (`x11` or `wayland`) -- Also log `WAYLAND_DISPLAY` in addition to `DISPLAY` - -## Impact - -- New spec: `input-forwarding` capability -- Affected code: - - `src/input/xwayland_server.*` -> `compositor_server.*` (rename + refactor) - - `src/input/input_forwarder.hpp` (minimal changes) - - `src/input/input_forwarder.cpp` (major refactor - remove X11, add wlr_seat) - - `src/input/CMakeLists.txt` (remove X11/XTest deps) - - `tests/input/goggles_input_test.cpp` -> split into `_x11` and `_wayland` variants - - `tests/input/CMakeLists.txt` (build both test binaries) - - `docs/input_forwarding.md` (update architecture) -- Removed dependencies: `libX11`, `libXtst` (from input forwarding module) diff --git a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/specs/input-forwarding/spec.md deleted file mode 100644 index ec6f2cbf..00000000 --- a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/specs/input-forwarding/spec.md +++ /dev/null @@ -1,157 +0,0 @@ -## ADDED Requirements - -### Requirement: Input Forwarding Infrastructure - -The system SHALL provide a compositor server that supports both XWayland (for X11 apps) and native Wayland clients using a unified `wlr_seat` input path. - -The compositor server SHALL: -- Create a headless wlroots backend -- Bind a Wayland socket for client connections -- Start XWayland server for X11 application support -- Create a wlr_seat with keyboard and pointer capabilities -- Connect XWayland to the seat via `wlr_xwayland_set_seat()` -- Run the compositor event loop on a dedicated thread - -#### Scenario: Compositor initializes with unified input -- **WHEN** the input forwarding system starts -- **THEN** a Wayland socket is created (wayland-N) -- **AND** an XWayland server is started (DISPLAY :N) -- **AND** XWayland is connected to the seat for automatic input translation -- **AND** a seat with keyboard and pointer capabilities is available - -### Requirement: Surface Tracking - -The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients. - -The compositor server SHALL: -- Listen for `new_toplevel` signals from xdg_shell -- Listen for `new_surface` signals from XWayland -- Maintain a unified list of active surfaces (Wayland surfaces only) -- Register destroy listeners for Wayland surfaces only -- Auto-focus the first connected surface (single-app model) -- Use mutual exclusion to manage focus between Wayland and XWayland surfaces - -**Important**: XWayland surfaces SHALL NOT use destroy listeners. XWayland destroy signals fire at unpredictable times during normal X11 operation, causing input forwarding failures. Instead, stale XWayland pointers are cleared during focus transitions. - -#### Scenario: Wayland client connects and receives focus -- **WHEN** a Wayland client creates an xdg_toplevel -- **THEN** the surface is tracked by the compositor -- **AND** if no surface was previously focused, the new surface receives keyboard and pointer focus -- **AND** if an XWayland surface had focus, the Wayland surface steals focus (XWayland pointer may be stale) - -#### Scenario: XWayland client connects and receives focus -- **WHEN** an X11 app creates a window via XWayland -- **THEN** the XWayland surface is tracked by the compositor (not in m_surfaces list) -- **AND** if no surface was previously focused, the surface receives keyboard and pointer focus -- **AND** if a Wayland surface already has focus, the XWayland surface does NOT steal focus - -#### Scenario: Wayland client disconnects -- **WHEN** a tracked Wayland surface is destroyed -- **THEN** the surface is removed from tracking via destroy listener -- **AND** if it was focused, focus is cleared - -#### Scenario: XWayland client disconnects -- **WHEN** an X11 app exits -- **THEN** no destroy listener fires (by design) -- **AND** m_focused_xsurface becomes a dangling pointer -- **AND** when a new surface gains focus, stale XWayland pointers are cleared safely - -### Requirement: Unified Keyboard Input - -The system SHALL forward keyboard events to the focused surface using `wlr_seat_keyboard_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_keyboard_enter()` on surface focus -- Use `wlr_seat_keyboard_notify_key()` for key events -- Use `wlr_seat_keyboard_notify_modifiers()` for modifier state -- Marshal events from main thread to compositor thread via eventfd - -The wlr_xwm SHALL automatically translate keyboard events to X11 for XWayland surfaces. - -#### Scenario: Key event forwarded to Wayland client -- **WHEN** user presses a key in the viewer window -- **AND** a Wayland surface has keyboard focus -- **THEN** the key event is delivered via wl_keyboard.key protocol - -#### Scenario: Key event forwarded to X11 client -- **WHEN** user presses a key in the viewer window -- **AND** an XWayland surface has keyboard focus -- **THEN** wlr_xwm translates the event to X11 KeyPress/KeyRelease -- **AND** the X11 app receives the event - -### Requirement: Unified Pointer Input - -The system SHALL forward pointer events (motion, button, axis) to the focused surface using `wlr_seat_pointer_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_pointer_enter()` on surface focus -- Use `wlr_seat_pointer_notify_motion()` for motion -- Use `wlr_seat_pointer_notify_button()` for button events -- Use `wlr_seat_pointer_notify_axis()` for scroll events -- Use `wlr_seat_pointer_notify_frame()` to group related events - -The wlr_xwm SHALL automatically translate pointer events to X11 for XWayland surfaces. - -#### Scenario: Mouse motion forwarded to Wayland client -- **WHEN** user moves mouse in the viewer window -- **AND** a Wayland surface has pointer focus -- **THEN** the motion event is delivered via wl_pointer.motion protocol - -#### Scenario: Mouse motion forwarded to X11 client -- **WHEN** user moves mouse in the viewer window -- **AND** an XWayland surface has pointer focus -- **THEN** wlr_xwm translates the event to X11 MotionNotify -- **AND** the X11 app receives the event - -### Requirement: Thread-Safe Event Marshaling - -The system SHALL marshal input events from the main thread to the compositor thread safely. - -The implementation SHALL: -- Use `SPSCQueue` for lock-free event passing (per project threading policy) -- Use eventfd for wl_event_loop wakeup notification -- Process events on compositor thread via wl_event_loop integration -- Avoid blocking the main thread during event delivery - -#### Scenario: Event delivered without blocking main thread -- **WHEN** an input event is forwarded -- **THEN** the main thread pushes to SPSCQueue and writes to eventfd -- **AND** the main thread returns immediately -- **AND** the compositor thread drains the queue and dispatches via wlr_seat_* - -### Requirement: Coordinate Handling - -The system SHALL forward pointer coordinates without transformation. - -Note: Coordinate mapping between viewer and target window dimensions is not implemented. Coordinates are passed through 1:1. - -#### Scenario: Raw coordinate passthrough -- **WHEN** pointer motion occurs at position (x, y) in viewer -- **THEN** position (x, y) is forwarded to the target -- **AND** no scaling or transformation is applied - -### Requirement: Virtual Keyboard Device - -The system SHALL create a virtual keyboard device for input delivery. - -The virtual keyboard SHALL: -- Be created via `wlr_keyboard_init()` from the keyboard interface -- Have an xkb keymap configured via `wlr_keyboard_set_keymap()` -- Be attached to the seat via `wlr_seat_set_keyboard()` - -#### Scenario: Virtual keyboard provides keymap to clients -- **WHEN** a Wayland client connects and binds wl_keyboard -- **THEN** the client receives the keyboard's xkb keymap -- **AND** key events use keycodes consistent with the keymap - -### Requirement: No X11/XTest Dependencies - -The input forwarding module SHALL NOT depend on X11 or XTest libraries for input injection. - -All input SHALL be delivered through the unified `wlr_seat_*` APIs, with wlr_xwm handling X11 translation for XWayland surfaces. - -#### Scenario: Input works without X11 client connection -- **WHEN** input events are forwarded -- **THEN** no X11 Display connection is opened by the input forwarder -- **AND** no XTest extension calls are made -- **AND** wlr_xwm handles all X11 protocol translation internally diff --git a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/tasks.md b/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/tasks.md deleted file mode 100644 index 53257116..00000000 --- a/openspec/changes/archive/2026-01-10-add-wayland-input-forwarding/tasks.md +++ /dev/null @@ -1,97 +0,0 @@ -## 1. File Renames - -- [x] 1.1 Rename `xwayland_server.hpp` to `compositor_server.hpp` -- [x] 1.2 Rename `xwayland_server.cpp` to `compositor_server.cpp` -- [x] 1.3 Rename class `XWaylandServer` to `CompositorServer` -- [x] 1.4 Update `#include` paths in `input_forwarder.cpp` -- [x] 1.5 Update CMakeLists.txt source file references - -## 2. Seat and Input Device Setup - -- [x] 2.1 Add `WL_SEAT_CAPABILITY_POINTER` to seat capabilities (alongside keyboard) -- [x] 2.2 Create virtual `wlr_keyboard` via `wlr_keyboard_init()` from keyboard interface -- [x] 2.3 Set xkb keymap on virtual keyboard (`xkb_keymap_new_from_names`) -- [x] 2.4 Attach keyboard to seat via `wlr_seat_set_keyboard()` -- [x] 2.5 Call `wlr_xwayland_set_seat()` to connect XWayland to the seat - -## 3. Surface Tracking - -- [x] 3.1 Add xdg_toplevel tracking via `wlr_xdg_shell.events.new_toplevel` signal -- [x] 3.2 Add XWayland surface tracking via `wlr_xwayland.events.new_surface` signal -- [x] 3.3 Implement unified surface list (`std::vector`) -- [x] 3.4 Add surface destroy listeners for cleanup (Wayland only - see note) -- [x] 3.5 Auto-focus first connected surface (single-app model) - -**Note on 3.4**: Destroy listeners are only wired for native Wayland surfaces (xdg_toplevel). XWayland surface destroy listeners are NOT used because they fire unexpectedly during normal X11 operation, breaking input forwarding. Instead, XWayland focus cleanup uses mutual exclusion: when a new surface gains focus, any stale XWayland pointers are cleared. - -## 4. Thread-Safe Event Marshaling - -- [x] 4.1 Define `InputEvent` struct (type enum, keycode/button, coords, pressed) -- [x] 4.2 Create `SPSCQueue` for lock-free event passing -- [x] 4.3 Create eventfd for wl_event_loop wakeup notification -- [x] 4.4 Register eventfd with `wl_event_loop_add_fd()` -- [x] 4.5 Implement compositor-thread dispatch: drain queue, call wlr_seat_* APIs - -## 5. Unified Input via wlr_seat - -- [x] 5.1 Implement `inject_key()` using `wlr_seat_keyboard_notify_key()` -- [x] 5.2 Implement `inject_pointer_motion()` using `wlr_seat_pointer_notify_motion()` -- [x] 5.3 Implement `inject_pointer_button()` using `wlr_seat_pointer_notify_button()` -- [x] 5.4 Implement `inject_pointer_axis()` using `wlr_seat_pointer_notify_axis()` -- [x] 5.5 Add `wlr_seat_pointer_notify_frame()` after each event batch -- [x] 5.6 Handle keyboard/pointer enter on surface focus change - -## 6. Old Code Cleanup (X11/XTest Removal) - -- [x] 6.1 Remove `#include ` from input_forwarder.cpp -- [x] 6.2 Remove `#include ` from input_forwarder.cpp -- [x] 6.3 Remove `Display* x11_display` member from InputForwarder::Impl -- [x] 6.4 Remove `XOpenDisplay()` call in `InputForwarder::create()` -- [x] 6.5 Remove `XCloseDisplay()` call in `InputForwarder::Impl` destructor -- [x] 6.6 Remove `linux_to_x11_keycode()` function (wlr_xwm handles this) -- [x] 6.7 Remove `XTestFakeKeyEvent()` calls in `forward_key()` -- [x] 6.8 Remove `XTestFakeButtonEvent()` calls in `forward_mouse_button()` and `forward_mouse_wheel()` -- [x] 6.9 Remove `XTestFakeMotionEvent()` calls in `forward_mouse_motion()` -- [x] 6.10 Remove all `XFlush()` calls -- [x] 6.11 Remove X11::X11 from CMakeLists.txt target_link_libraries -- [x] 6.12 Remove X11::Xtst from CMakeLists.txt target_link_libraries -- [x] 6.13 Remove find_package(X11) if local to input module - -## 7. InputForwarder Interface Updates - -- [x] 7.1 Simplify `forward_key()` to call `server.inject_key()` -- [x] 7.2 Simplify `forward_mouse_button()` to call `server.inject_pointer_button()` -- [x] 7.3 Simplify `forward_mouse_motion()` to call `server.inject_pointer_motion()` -- [x] 7.4 Simplify `forward_mouse_wheel()` to call `server.inject_pointer_axis()` -- [x] 7.5 Add `x11_display()` and `wayland_display()` methods to expose display names - -## 8. Documentation - -- [x] 8.1 Update `docs/input_forwarding.md` with unified wlr_seat architecture -- [x] 8.2 Remove XTest references from documentation -- [x] 8.3 Document `WAYLAND_DISPLAY` and `DISPLAY` environment variables -- [x] 8.4 Add architecture diagram showing unified input flow -- [x] 8.5 Document known limitations (coordinate mapping, single-app focus) - -## 9. Test App Updates - -- [x] 9.1 Create `goggles_manual_input_x11` (set `SDL_VIDEODRIVER=x11`) -- [x] 9.2 Create `goggles_manual_input_wayland` (set `SDL_VIDEODRIVER=wayland`) -- [x] 9.3 Update both to log `WAYLAND_DISPLAY` in addition to `DISPLAY` -- [x] 9.4 Remove hardcoded `setenv("SDL_VIDEODRIVER", "x11", 1)` from original -- [x] 9.5 Update `tests/CMakeLists.txt` to build both test binaries -- [x] 9.6 Delete original `goggles_input_test.cpp` after split - -## 10. Build Verification - -- [x] 10.1 Verify build succeeds without X11/XTest dependencies -- [x] 10.2 Verify no X11/XTest symbols in final binary (`nm -u` check) - -## 11. Manual Testing - -- [x] 11.1 Test `goggles_manual_input_x11` receives keyboard events via wlr_seat -> wlr_xwm -- [x] 11.2 Test `goggles_manual_input_x11` receives pointer events via wlr_seat -> wlr_xwm -- [x] 11.3 Test `goggles_manual_input_wayland` receives keyboard events via wlr_seat -- [x] 11.4 Test `goggles_manual_input_wayland` receives pointer events via wlr_seat -- [x] 11.5 Test X11→Wayland focus transition (close X11 app, open Wayland app) -- [x] 11.6 Test Wayland→X11 focus transition (close Wayland app, open X11 app) diff --git a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/proposal.md b/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/proposal.md deleted file mode 100644 index ef2b8d4d..00000000 --- a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/proposal.md +++ /dev/null @@ -1,65 +0,0 @@ -# build-system-overhaul-managed-by-pixi - -## Why - -1. **Strict Environment Control**: We are transitioning the project to be fully managed by Pixi, ensuring that all builds happen within a deterministic, reproducible, and isolated environment. -2. **Cross-Distro Compatibility**: To guarantee that binaries built on modern machines run on older LTS Linux distributions (e.g., RHEL 8, Ubuntu 20.04), we must anchor the build environment to an older Glibc version. -3. **Build System Integrity**: The previous build system had gaps: - * Missing integrity checks (checksums) for downloaded dependencies. - * Implicit reliance on system libraries (like `pthread`), causing failures in strict environments. - * Broken symlinks in the 32-bit sysroot package. - * Ability to accidentally run builds outside the Pixi environment. - -## What - -1. **Pixi Environment Anchor**: - * Pin `sysroot_linux-64` to `2.28.*` (Glibc 2.28). - * Relax all other package versions to `*` to allow Pixi's SAT solver to find the optimal compatible set of dependencies. -2. **Build System Hardening**: - * Enforce `CONDA_PREFIX` check in CMake to prevent non-Pixi builds. - * Upgrade sysroot detection warnings to fatal errors. - * Explicitly handle threading library linkage (`pthread`) which is no longer implicit in strict cross-compilation environments. -3. **Package Recipe Security**: - * Add SHA256 checksum verification for all upstream `.deb` assets in `sysroot-i686`. - * Add Git commit hash verification for source dependencies (Tracy). - * Implement self-repair logic for broken upstream symlinks in the sysroot. - -## How - -1. **`pixi.toml`**: - * Set `sysroot_linux-64 = "2.28.*"`. - * Set build dependencies (CMake, Ninja, Clang, etc.) and libraries (SDL3, Vulkan, etc.) to wildcard `*`. - * Refine `.gitignore` to exclude Pixi artifacts properly. -2. **CMake Configuration**: - * `cmake/Dependencies.cmake`: Add `find_package(Threads REQUIRED)` and `ENV{CONDA_PREFIX}` check. - * `src/util/CMakeLists.txt`: Link `Threads::Threads` to `goggles_util`. - * `cmake/toolchain-i686.cmake`: Change `message(WARNING ...)` to `message(FATAL_ERROR ...)` for missing sysroot. -3. **`packages/sysroot-i686/recipe.yaml`**: - * Implement a `download_extract` function that verifies SHA256 hashes before extraction. - * Add a script block to detect and re-link broken absolute symlinks (like `libstdc++.so`) to relative paths within the sysroot. - * Pin and verify the Tracy git commit hash. - -## Impact - -* **Reproducibility**: `pixi.lock` now fully dictates the build state; `sysroot` anchors it to a stable baseline. -* **Security**: Supply chain attacks via compromised download mirrors are mitigated by checksum verification. -* **Stability**: The 32-bit cross-compilation layer is robust against upstream packaging errors (broken symlinks). -* **Developer Experience**: "It works on my machine" issues are eliminated by enforcing the Pixi environment. - -## Refinement: Environment Isolation (Post-Implementation) - -### Why -During verification, we discovered that `bash -lc` (login shell) commands in `pixi.toml` caused Pixi's environment variables (PATH) to be shadowed by system initialization scripts (e.g., Nix profiles), leading to binary version mismatches and Vulkan Loader errors (`libvulkan.so` version conflict). Additionally, the wrapper script `scripts/pixi-env-clean.sh` was aggressively unsetting variables, preventing mixed-environment testing. - -### What -1. **Strict Pixi Priority**: Ensure Pixi binaries and libraries always take precedence over system/Nix paths. -2. **Robust 32-bit Execution**: Ensure `vkcube32` runs within the Pixi environment without manual `LD_LIBRARY_PATH` hacks that cause glibc conflicts. -3. **Simplified Configuration**: Remove redundant wrapper scripts. - -### How -1. **`pixi.toml`**: - * Replaced all `bash -lc` with `bash -c` to prevent loading user shell profiles. - * Defined global `PKG_CONFIG_PATH` in `[activation.env]` to prioritize Pixi libraries during build. - * Removed `scripts/pixi-env-clean.sh` usage and deleted the file. - * Simplified `start-i686` command to rely on Pixi's RPATH/environment for dependencies, only setting `VK_LAYER_PATH` for the layer manifest. -2. **Dependencies**: Added `pkg-config` to Pixi dependencies to ensure isolated build configuration discovery. \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/ci/spec.md b/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/ci/spec.md deleted file mode 100644 index f1d8ac9a..00000000 --- a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/ci/spec.md +++ /dev/null @@ -1,35 +0,0 @@ -# ci Spec Delta - -## Purpose -Document CI behavior changes tied to Pixi-managed formatting and safe handling of forked pull requests. - -## Requirements - -## MODIFIED Requirements - -### Requirement: Auto-format Code on Push - -The CI system SHALL automatically format code using the Pixi-managed clang-format and gate subsequent jobs on whether formatting changes were pushed. - -#### Scenario: Code with formatting issues is pushed -- **WHEN** code with clang-format violations is pushed to a branch -- **THEN** CI runs clang-format to fix the issues via Pixi -- **AND** CI commits the formatted code with message \"style: auto-format code\" when the branch is non-fork -- **AND** the format check job succeeds - -#### Scenario: Forked PR formatting without push -- **GIVEN** a pull request originates from a fork -- **WHEN** clang-format produces changes -- **THEN** CI SHALL skip auto-commit/push for safety -- **AND** it SHALL expose `formatted=false` so downstream jobs still run - -#### Scenario: Code is already properly formatted -- **WHEN** code that passes clang-format check is pushed -- **THEN** CI detects no changes needed -- **AND** no commit is created -- **AND** the format check job succeeds - -#### Scenario: All C/C++ file types are formatted -- **WHEN** clang-format is run in CI -- **THEN** files with extensions `.c`, `.h`, `.cpp`, `.hpp` are formatted -- **AND** the same clang-format version from Pixi is used diff --git a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/dependency-management/spec.md b/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/dependency-management/spec.md deleted file mode 100644 index 1d3d140c..00000000 --- a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/specs/dependency-management/spec.md +++ /dev/null @@ -1,72 +0,0 @@ -# dependency-management Spec Delta - -## Purpose -Capture build-system hardening driven by Pixi: enforced Pixi environments, Glibc 2.28 baseline, and integrity protections for sysroot assets. - -## Requirements - -## MODIFIED Requirements - -### Requirement: Pixi as Primary Dependency Manager - -The project SHALL use Pixi as the enforced environment for builds and dependency resolution, anchored to a Glibc 2.28 baseline for cross-distro compatibility. - -#### Scenario: Pixi environment enforcement -- **WHEN** CMake config runs for any target -- **THEN** it SHALL require `CONDA_PREFIX` to be set by Pixi -- **AND** it SHALL fail fast with guidance to run `pixi run build [preset]` if invoked outside Pixi - -#### Scenario: Glibc 2.28 compatibility anchor -- **WHEN** dependencies are resolved via Pixi -- **THEN** `sysroot_linux-64` SHALL be pinned to the `2.28.*` series -- **AND** binaries built in the Pixi environment SHALL be compatible with RHEL8/Ubuntu 20.04 class distros - -### Requirement: Pixi-CPM Integration - -System libraries provided by Pixi SHALL be discovered by CMake using `find_package()` without CPM downloads. - -#### Scenario: SDL3 discovery -- **GIVEN** SDL3 is installed via Pixi -- **WHEN** CMake processes `cmake/Dependencies.cmake` -- **THEN** `find_package(SDL3 REQUIRED)` SHALL locate the Pixi-provided SDL3 -- **AND** CPM SHALL NOT be used - -#### Scenario: CLI11 discovery -- **GIVEN** CLI11 is installed via Pixi -- **WHEN** CMake processes `cmake/Dependencies.cmake` -- **THEN** `find_package(CLI11 REQUIRED)` SHALL locate the Pixi-provided CLI11 -- **AND** CPM SHALL NOT be used - -### Requirement: Dependency Version Pinning - -Dependency resolution SHALL be anchored by the sysroot version while allowing Pixi to solve other packages, with exact versions locked in `pixi.lock`. - -#### Scenario: Sysroot version constraint -- **GIVEN** `pixi.toml` -- **WHEN** dependencies are installed -- **THEN** `sysroot_linux-64` SHALL declare version constraint `2.28.*` -- **AND** 32-bit sysroot builds SHALL consume the matching baseline - -#### Scenario: Solver-driven versions with lockfile -- **WHEN** Pixi installs dependencies with wildcard constraints -- **THEN** exact resolved versions SHALL be captured in `pixi.lock` -- **AND** subsequent installs SHALL reproduce those versions from the lockfile - -## ADDED Requirements - -### Requirement: Sysroot Package Integrity - -Sysroot packages SHALL verify upstream artifacts and self-heal known packaging issues before use. - -#### Scenario: SHA256 verification for upstream debs -- **WHEN** the 32-bit sysroot recipe downloads Debian/Ubuntu archives -- **THEN** each archive SHALL be validated against an expected SHA256 -- **AND** the build SHALL fail if any checksum mismatches - -#### Scenario: Symlink self-repair -- **WHEN** GCC development libraries in the sysroot include broken absolute symlinks -- **THEN** the recipe SHALL repoint them to local targets or remove unusable links to avoid linker errors - -#### Scenario: Tracy source integrity -- **WHEN** Tracy sources are fetched for the sysroot build -- **THEN** the recipe SHALL verify the commit hash matches the expected revision before building diff --git a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/tasks.md b/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/tasks.md deleted file mode 100644 index aa755974..00000000 --- a/openspec/changes/archive/2026-01-10-build-system-overhaul-managed-by-pixi/tasks.md +++ /dev/null @@ -1,34 +0,0 @@ -# Tasks - -## 1. Dependency Management Strategy - -- [x] Pin `sysroot_linux-64` to `2.28.*` in `pixi.toml` for Glibc 2.28 compatibility -- [x] Relax all other package constraints to `*` to leverage Pixi's solver -- [x] Update `.gitignore` to better handle Pixi artifacts and Conda packages - -## 2. Build Environment Enforcement - -- [x] Implement `CONDA_PREFIX` check in `cmake/Dependencies.cmake` to ban non-Pixi builds -- [x] Fix implicit threading assumptions by adding `find_package(Threads REQUIRED)` -- [x] Explicitly link `Threads::Threads` to `goggles_util` -- [x] Make missing sysroot a fatal error in 32-bit toolchain configuration - -## 3. Package Integrity & Security (Sysroot-i686) - -- [x] Implement SHA256 checksum verification for all 10+ upstream Debian packages -- [x] Add Git commit hash verification for Tracy source download -- [x] Implement automated fix for broken GCC development symlinks in `usr/lib` -- [x] Increment package build number - -## 4. Verification - -- [x] Verify complete dependency resolution (`pixi install`) with new constraints -- [x] Verify successful compilation and linkage of unit tests (`pixi run test`) - -## 5. Environment Isolation & Robustness (Refinement) - -- [x] Remove conflicting `bash -lc` login shell usage from `pixi.toml` to fix PATH priority -- [x] Add `pkg-config` to dependencies and set `PKG_CONFIG_PATH` for isolated build configuration -- [x] Remove redundant `scripts/pixi-env-clean.sh` wrapper script -- [x] Verify `vkcube` and `vkcube32` binary resolution (Pixi vs System) -- [x] Verify Vulkan Layer loading isolation (Pixi Manifest vs System Manifest) \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/proposal.md b/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/proposal.md deleted file mode 100644 index 08e20f8f..00000000 --- a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/proposal.md +++ /dev/null @@ -1,50 +0,0 @@ -# Change: Enforce Pixi Toolchain Version Consistency - -## Why - -Several dev tools in pixi.toml use `*` (unpinned), risking inconsistent builds across machines and CI. System tools can leak in when versions aren't controlled. Need consistent, reproducible dev environment. - -## What Changes - -- Pin all unpinned dev tools to specific versions in pixi.toml -- Ensure build tools (cmake, ninja, ccache, lld) have consistent versions -- Libraries remain as-is (already pinned or intentionally flexible) - -## Tool Audit - -### Currently Pinned (OK) -| Tool | Version | Purpose | -|------|---------|---------| -| clang-tools | ==21.1.6 | clang-tidy, clang-format | -| sysroot_linux-64 | 2.28.* | glibc compat | -| pkg-config | >=0.29.2,<0.30 | Dependency discovery | -| spdlog | 1.15.* | Logging lib | -| catch2 | 3.8.* | Testing lib | -| toml11 | 4.4.* | TOML parsing lib | -| libvulkan-headers | 1.4.328.* | Vulkan API | -| vulkan-validation-layers | 1.4.328.* | Vulkan debug | -| cli11 | 2.6.* | CLI parsing lib | -| taplo (lint env) | ==0.9.3 | TOML formatter | - -### Unpinned - Needs Pinning -| Tool | Current | Recommend | Reasoning | -|------|---------|-----------|-----------| -| cmake | * | 3.31.* | Latest 3.x LTS; 4.x too new, breaks third-party cmake_minimum_required(3.x) | -| ninja | * | 1.12.* | Proven stable; 1.13 available but no benefit | -| clang_linux-64 | * | 21.* | Must match clang-tools (21.1.6) for consistent diagnostics | -| clangxx_linux-64 | * | 21.* | Must match clang-tools | -| lld | * | 21.* | Must match clang for ABI compatibility | -| ccache | * | 4.* | Stable cache format within major version | -| taplo (default) | * | ==0.9.3 | Match lint env for consistent formatting | -| sdl3 | * | 3.2.* | SDL3 is new (2024), pin minor to avoid API breaks | - -### Intentionally Unpinned (Exception) -| Tool | Reason | -|------|--------| -| c-compiler / cxx-compiler | Meta-packages, version via clang_* | -| xorg-* / wayland / audio libs | System compat, low churn | - -## Impact - -- Affected specs: build-system -- Affected files: pixi.toml diff --git a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/specs/build-system/spec.md b/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/specs/build-system/spec.md deleted file mode 100644 index bf64db29..00000000 --- a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/specs/build-system/spec.md +++ /dev/null @@ -1,17 +0,0 @@ -## ADDED Requirements - -### Requirement: Toolchain Version Pinning - -The build system SHALL pin all development tool versions in pixi.toml to prevent system tool leakage and ensure reproducible builds. - -#### Scenario: Clang toolchain version consistency -- **WHEN** building with pixi -- **THEN** clang, clang++, lld, and clang-tools SHALL use the same major version (21.x) - -#### Scenario: Build tool version pinning -- **WHEN** pixi.toml specifies cmake, ninja, ccache -- **THEN** each tool SHALL have a pinned version constraint (not `*`) - -#### Scenario: Format tool version consistency -- **WHEN** running format tasks in default or lint environment -- **THEN** taplo version SHALL be identical across environments diff --git a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/tasks.md b/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/tasks.md deleted file mode 100644 index 1364c8c9..00000000 --- a/openspec/changes/archive/2026-01-10-enforce-pixi-toolchain-versions/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -## 1. Pin Build Tools - -- [x] 1.1 Pin cmake = "3.31.*" -- [x] 1.2 Pin ninja = "1.12.*" -- [x] 1.3 Pin clang_linux-64 = "21.*" -- [x] 1.4 Pin clangxx_linux-64 = "21.*" -- [x] 1.5 Pin lld = "21.*" -- [x] 1.6 Pin ccache = "4.*" - -## 2. Pin Dev Tools - -- [x] 2.1 Pin taplo = "==0.9.3" in default env (match lint) - -## 3. Pin Libraries - -- [x] 3.1 Pin sdl3 = "3.2.*" - -## 4. Verification - -- [x] 4.1 Run `pixi install` to verify resolution -- [x] 4.2 Run `pixi run build debug` to verify build -- [x] 4.3 Run `pixi run test debug` to verify tests (100% passed) -- [x] 4.4 Verify clang-tidy uses pinned version in quality build diff --git a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/proposal.md b/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/proposal.md deleted file mode 100644 index 9b2cf461..00000000 --- a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Change: Fix child process cleanup on parent crash - -## Why - -When Goggles crashes (e.g., due to Vulkan errors or device mismatch in multi-GPU systems), the spawned child process continues running headlessly. Users are unaware the child is still running, and subsequent launches may fail due to resource conflicts (e.g., capture socket already bound). - -The previous approach using `PR_SET_PDEATHSIG` directly from a multi-threaded process is unreliable because the signal is tied to the **thread** that called `fork()`, not the entire process. - -## What Changes - -Introduce a `goggles-reaper` watchdog process: - -``` -Goggles (main process, multi-threaded) - ↓ fork() + exec("goggles-reaper") [SAFE: immediate exec] -goggles-reaper (watchdog, single-threaded) - ↓ PR_SET_CHILD_SUBREAPER + PR_SET_PDEATHSIG - ↓ fork() + exec(target_app) [SAFE: no threads] -Target Application -``` - -Benefits: -- Single-threaded reaper ensures `PR_SET_PDEATHSIG` works reliably -- `PR_SET_CHILD_SUBREAPER` catches orphaned grandchildren -- Process group kill ensures all descendants are terminated - -## Impact - -- Affected specs: `app-window` -- Affected code: - - `src/app/main.cpp` - spawn reaper instead of target app directly - - `src/app/reaper_main.cpp` - new watchdog process - - `src/app/CMakeLists.txt` - build reaper executable \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/specs/app-window/spec.md b/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/specs/app-window/spec.md deleted file mode 100644 index 932fcce7..00000000 --- a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/specs/app-window/spec.md +++ /dev/null @@ -1,19 +0,0 @@ -## ADDED Requirements - -### Requirement: Child Process Death Signal - -The application SHALL configure spawned child processes to receive SIGTERM when the parent process terminates unexpectedly. - -#### Scenario: Parent crash terminates child - -- **GIVEN** a child process was spawned via `-- ` mode -- **WHEN** the parent goggles process is killed (SIGKILL, crash, or abnormal termination) -- **THEN** the child process SHALL receive SIGTERM automatically -- **AND** the child process SHALL terminate - -#### Scenario: Parent PID 1 reparenting race - -- **GIVEN** a child process is being spawned -- **WHEN** the parent dies between `fork()` and `prctl()` setup -- **THEN** the child SHALL detect reparenting to PID 1 -- **AND** SHALL exit immediately with failure code \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/tasks.md b/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/tasks.md deleted file mode 100644 index c7f4d6b3..00000000 --- a/openspec/changes/archive/2026-01-10-fix-child-process-cleanup/tasks.md +++ /dev/null @@ -1,22 +0,0 @@ -## 1. goggles-reaper Implementation - -- [x] 1.1 Create `src/app/reaper_main.cpp` -- [x] 1.2 Set `PR_SET_CHILD_SUBREAPER` to adopt orphaned descendants -- [x] 1.3 Set `PR_SET_PDEATHSIG(SIGTERM)` to detect parent death -- [x] 1.4 Set up signal handlers for SIGTERM/SIGINT/SIGHUP to trigger child cleanup -- [x] 1.5 Spawn target app via `fork()` + `execvp()` -- [x] 1.6 Implement `kill_process_tree()` - recursively kill children via `/proc` scanning -- [x] 1.7 On signal: kill all children, wait for them, then exit -- [x] 1.8 Update `src/app/CMakeLists.txt` to build `goggles-reaper` - -## 2. main.cpp Changes - -- [x] 2.1 Use `posix_spawn()` to exec `goggles-reaper` (safe: immediate exec) -- [x] 2.2 Pass env vars and target command as arguments to reaper -- [x] 2.3 Remove direct `PR_SET_PDEATHSIG` usage from main.cpp - -## 3. Testing - -- [x] 3.1 Update `tests/app/test_child_death_signal.cpp` for reaper architecture -- [x] 3.2 Manual test: kill goggles, verify target app terminates -- [x] 3.3 Manual test: target app exits, verify goggles exits cleanly \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/proposal.md b/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/proposal.md deleted file mode 100644 index 2b69c9af..00000000 --- a/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/proposal.md +++ /dev/null @@ -1,60 +0,0 @@ -# Proposal: Fix ImGui Pipeline Format Mismatch - -## Summary -Fix Vulkan validation errors when swapchain format changes by deferring rebuild to next frame start. - -## Problem -1. ImGui pipeline initialized before capture format is known -2. Format change mid-frame causes validation errors -3. Resize and format rebuild share similar swapchain recreation logic - -## Solution: Deferred Format Rebuild - -### Flow -``` -Frame N: - poll_frame() - if (needs_format_rebuild) { - m_pending_format = frame.format - return // skip this frame - } - render_frame() - -Frame N+1: - if (m_pending_format != 0 || m_window_resized) { - wait_all_frames() - if (m_pending_format) { - rebuild_for_format() - ui_controller->rebuild_for_format() - } else { - handle_resize() - } - } - poll_frame() - render_frame() -``` - -### Key Points -- Format detection sets flag, returns early (no inline rebuild) -- Rebuild happens at frame start, before any rendering -- Resize and format rebuild unified in one block -- Format rebuild also clears resize flag (both recreate swapchain) - -## API - -### Application (private) -```cpp -uint32_t m_pending_format = 0; // 0 = no pending rebuild -``` - -### VulkanBackend -```cpp -bool needs_format_rebuild(vk::Format source_format) const; -Result rebuild_for_format(vk::Format source_format); -void wait_all_frames(); -``` - -### UiController -```cpp -void rebuild_for_format(vk::Format swapchain_format); -``` diff --git a/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/tasks.md b/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/tasks.md deleted file mode 100644 index 3d48b301..00000000 --- a/openspec/changes/archive/2026-01-10-fix-imgui-format-mismatch/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -# Tasks: Fix ImGui Pipeline Format Mismatch - -## Implementation - -- [x] 1. Add format rebuild APIs to VulkanBackend - - `needs_format_rebuild()` - check if format change requires rebuild - - `rebuild_for_format()` - recreate swapchain with new format - - `wait_all_frames()` - wait for in-flight frames before rebuild - -- [x] 2. Add `rebuild_for_format()` to ImGuiLayer and UiController - - Shutdown and reinitialize ImGui backends with new format - -- [x] 3. Implement deferred format rebuild in Application - - Add `m_pending_format` member - - Detect format mismatch, set flag, return early - - Handle rebuild at next frame start - - Unify resize and format rebuild in one block - -## Verification - -- [x] 4. Build and test - - No validation errors on format change - - ImGui renders correctly after format change - - Input works after format change \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/proposal.md b/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/proposal.md deleted file mode 100644 index e1f3746e..00000000 --- a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Fix WSI Proxy DMA-BUF Metadata (stride/offset/modifier) - -## Why -WSI proxy virtual swapchain frames need to carry correct DMA-BUF layout metadata (DRM format modifier + offset) so the viewer can import and sample them reliably. - -## What Changes -- Update WSI proxy virtual swapchain image allocation to prefer `VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT` when supported, and record the selected DRM modifier per image. -- Extend WSI proxy present-to-viewer IPC to send `stride`, `offset`, and `modifier` for each virtual swapchain frame. -- Extend viewer-side `CaptureFrame` to store the DMA-BUF `offset` and plumb it into Vulkan import. - -## Impact -- Affected specs: `vk-layer-capture`, `render-pipeline` -- Affected code: - - `src/capture/vk_layer/wsi_virtual.*`, `src/capture/vk_layer/vk_hooks.cpp` - - `src/capture/capture_receiver.*` - - `src/render/backend/vulkan_backend.cpp` diff --git a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/render-pipeline/spec.md deleted file mode 100644 index e1d90a5a..00000000 --- a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/render-pipeline/spec.md +++ /dev/null @@ -1,13 +0,0 @@ -## ADDED Requirements - -### Requirement: DMA-BUF Import Uses Exported Plane Layout - -The render backend SHALL import DMA-BUF textures using the plane layout metadata provided by the capture layer. - -#### Scenario: Import explicit modifier + offset -- **GIVEN** the capture layer provides a DMA-BUF FD with `stride`, `offset`, and `modifier` -- **WHEN** the viewer imports the DMA-BUF via `VkImageDrmFormatModifierExplicitCreateInfoEXT` -- **THEN** the render backend SHALL set `VkSubresourceLayout.rowPitch` to the provided `stride` -- **AND** it SHALL set `VkSubresourceLayout.offset` to the provided `offset` -- **AND** it SHALL set `drmFormatModifier` to the provided `modifier` - diff --git a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/vk-layer-capture/spec.md deleted file mode 100644 index a17fe858..00000000 --- a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Virtual Swapchain Creation - -The layer SHALL create DMA-BUF exportable images for virtual swapchains. - -#### Scenario: Virtual swapchain creation -- **GIVEN** a virtual surface exists -- **WHEN** the application calls `vkCreateSwapchainKHR` with that surface -- **THEN** the layer SHALL create VkImages with `VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT` -- **AND** it SHOULD prefer `VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT` when supported for the requested `VkFormat` -- **AND** it SHALL export DMA-BUF file descriptors for each image -- **AND** it SHALL retrieve and store per-image `stride` and `offset` via `vkGetImageSubresourceLayout` -- **AND** if DRM modifier tiling is used, it SHALL retrieve and store the selected DRM format modifier via `vkGetImageDrmFormatModifierPropertiesEXT` -- **AND** it SHALL return a synthetic VkSwapchainKHR handle - -### Requirement: Virtual Swapchain Presentation - -The layer SHALL send DMA-BUF frames to the Goggles application on present. - -#### Scenario: Virtual present -- **GIVEN** a virtual swapchain exists -- **WHEN** the application calls `vkQueuePresentKHR` -- **THEN** the layer SHALL send the presented image's DMA-BUF fd to Goggles -- **AND** it SHALL include `stride`, `offset`, and `modifier` metadata matching the exported image layout -- **AND** it SHALL NOT present to any physical display -- **AND** it SHALL return `VK_SUCCESS` - diff --git a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/tasks.md b/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/tasks.md deleted file mode 100644 index 26d66560..00000000 --- a/openspec/changes/archive/2026-01-10-fix-wsi-proxy-dmabuf-metadata/tasks.md +++ /dev/null @@ -1,10 +0,0 @@ -## 1. Implementation -- [x] 1.1 Prefer DRM modifier tiling for WSI proxy virtual swapchain images and record `modifier`, `stride`, `offset`. -- [x] 1.2 Send `CaptureTextureData` with correct `stride`, `offset`, and `modifier` in WSI proxy present path. -- [x] 1.3 Extend `CaptureFrame` to include DMA-BUF `offset` and plumb into Vulkan import. - -## 2. Validation -- [x] 2.1 Reproduce and verify modifier propagation with `pixi run start -p release -- /home/kingstom/workspaces/vksdk/1.4.328.1/x86_64/bin/vkcube` (no `vulkan-tools` dependency) and confirm `Capture texture: ... modifier=0x...` matches `DMA-BUF imported: ... modifier=0x...` (see `wsi_proxy_dmabuf_modifier_verification_report.md`). - -## 3. OpenSpec Hygiene -- [x] 3.1 Add spec deltas under `openspec/changes/fix-wsi-proxy-dmabuf-metadata/specs/`. diff --git a/openspec/changes/archive/2026-01-10-implement-shader-cache/proposal.md b/openspec/changes/archive/2026-01-10-implement-shader-cache/proposal.md deleted file mode 100644 index efcf0100..00000000 --- a/openspec/changes/archive/2026-01-10-implement-shader-cache/proposal.md +++ /dev/null @@ -1,37 +0,0 @@ -# Change: Implement Shader Cache - -## Why -Compiling complex RetroArch shaders (like `crt-royale`) at runtime is slow, often taking seconds per pass. Previously, the system lacked persistent caching, leading to redundant re-compilations during swapchain recreation or application restarts. This implementation aims to reduce startup latency from seconds to milliseconds. - -## What Changes -- **Persistent Disk Caching**: Implemented a caching mechanism using `.spv.cache` files stored in `$XDG_CACHE_HOME/goggles/shaders` (falling back to `~/.cache` or `/tmp`). -- **Binary Serialization (`util/serializer.hpp`)**: - - Created high-performance `BinaryWriter` and `BinaryReader` utilities. - - **Result-Based API**: All write operations return `Result` to explicitly handle and report size overflows (e.g., if a string exceeds `uint32_t` limits). - - Optimized for "POD-like" types by using `std::is_standard_layout_v` checks, allowing for direct memory copying of Vulkan bitmasks and enums. - - **Safe Deserialization**: `read_vec` automatically clears the output vector on partial failure to prevent inconsistent states. -- **Full Metadata Persistence**: - - Not just SPIR-V bytecode is cached, but also the complete `ReflectionData`. - - Stores UBO layouts, texture bindings, push constant blocks, and vertex input locations. - - Bypasses Slang compilation entirely on cache hits. -- **Atomic Cache Updates**: - - Writes to temporary files (`.tmp`) followed by atomic renames (`std::filesystem::rename`) to prevent cache corruption during partial writes, disk-full events, or crashes. - - **Integrity Validation**: Added strict alignment checks for SPIR-V bytecode during load (verifying element counts match expected word sizes). -- **Collision-Resistant Hashing**: - - Uses stable source hashing (`std::hash`) with a named constant `SHADER_STAGE_DELIMITER` ("---FRAGMENT---") between stages to prevent theoretical hash collisions where a suffix of one stage matches the prefix of another. -- **Refined Observability**: - - Adheres to the **Minimal Output** policy. - - Detailed per-parameter and per-binding logs are moved to `TRACE` level. - - Automatic recovery diagnostics (e.g., version mismatch) are at `DEBUG` level. -- **Testability**: - - `ShaderRuntime::get_cache_dir()` is public to allow unit tests to verify and clean up cache state. - - Comprehensive unit tests added for both the serialization engine and the persistent cache logic, using RAII for cleanup. - -## Impact -- **Performance**: Instantaneous shader loading after the first run (~45% reduction in load time for complex presets). -- **Affected Specs**: `render-pipeline` -- **Affected Code**: - - `src/util/serializer.hpp` (New utility) - - `src/render/shader/shader_runtime.cpp` (Cache logic integration) - - `src/render/shader/shader_runtime.hpp` - - `src/render/chain/filter_pass.cpp` (Logging cleanup) \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-implement-shader-cache/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-01-10-implement-shader-cache/specs/render-pipeline/spec.md deleted file mode 100644 index 54e331db..00000000 --- a/openspec/changes/archive/2026-01-10-implement-shader-cache/specs/render-pipeline/spec.md +++ /dev/null @@ -1,42 +0,0 @@ -## ADDED Requirements - -### Requirement: Shader Caching -The system SHALL cache compiled RetroArch shaders to disk to minimize startup latency and eliminate redundant GPU work. - -#### Scenario: Persistent Cache Lookup -- **GIVEN** a shader has been compiled once -- **WHEN** the same shader is requested again (even after app restart) -- **THEN** it SHALL be loaded from disk cache and Slang compilation SHALL be bypassed. - -#### Scenario: Serialization of Reflection -- **GIVEN** a RetroArch shader requires complex bindings (UBOs, Textures, Push Constants) -- **WHEN** cached to disk -- **THEN** the cache MUST include full `ReflectionData` and it MUST be restored correctly on cache hit, including all binding offsets and stage flags. - -#### Scenario: Automatic Invalidation -- **GIVEN** a cached shader exists -- **WHEN** the source code of that shader is modified -- **THEN** the system SHALL detect the hash mismatch and it SHALL recompile and update the cache. - -#### Scenario: Type-Safe Serialization -- **GIVEN** data being serialized to disk -- **WHEN** using `write_pod` or `read_pod` -- **THEN** the system MUST enforce `std::is_standard_layout_v` to ensure memory safety for Vulkan-specific types like bitmasks and handles. - -#### Scenario: Atomic Cache Updates -- **GIVEN** the system is writing a new cache file -- **WHEN** a crash or disk-full event occurs during the write -- **THEN** the existing valid cache file MUST NOT be corrupted -- **AND** the system SHALL use a temporary file and atomic rename to ensure cache integrity. - -#### Scenario: Integrity Validation -- **GIVEN** a potentially corrupted cache file on disk -- **WHEN** the system attempts to load it -- **THEN** it SHALL validate SPIR-V alignment and header magic/version -- **AND** it SHALL discard the corrupted file and recompile the shader if validation fails. - -#### Scenario: Minimal Log Output -- **GIVEN** the system is running at default log levels -- **WHEN** a cache hit occurs -- **THEN** it SHALL NOT output detailed per-parameter or diagnostic logs -- **AND** detailed information SHALL only be available at `TRACE` or `DEBUG` levels. diff --git a/openspec/changes/archive/2026-01-10-implement-shader-cache/tasks.md b/openspec/changes/archive/2026-01-10-implement-shader-cache/tasks.md deleted file mode 100644 index 529cc825..00000000 --- a/openspec/changes/archive/2026-01-10-implement-shader-cache/tasks.md +++ /dev/null @@ -1,10 +0,0 @@ -## 1. Implementation -- [x] 1.1 Implement `BinaryWriter` and `BinaryReader` with `Result` return types for safe overflow handling. -- [x] 1.2 Implement `RetroArchCacheHeader` and `CacheHeader` with `static_assert` for standard layout enforcement. -- [x] 1.3 Add cache lookup/save logic to `compile_retroarch_shader` using `SHADER_STAGE_DELIMITER` for collision resistance. -- [x] 1.4 Implement atomic cache updates: write to `.tmp` files followed by `std::filesystem::rename`. -- [x] 1.5 Add integrity checks: validate SPIR-V alignment and clear vectors on failed `read_vec`. -- [x] 1.6 Verify cache hit reduces reload time from seconds to milliseconds (validated with `crt-royale`). -- [x] 1.7 Ensure linting compliance: fixed braces, removed narration comments, and adjusted sign conversions. -- [x] 1.8 Refactor logging verbosity: move detailed diagnostics to `TRACE` and automatic recovery to `DEBUG`. -- [x] 1.9 Add comprehensive unit tests: covered basic types, strings, vectors, nested data, and error paths with RAII cleanup. \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md b/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md deleted file mode 100644 index 8a1b51ec..00000000 --- a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md +++ /dev/null @@ -1,25 +0,0 @@ -# Change: Use VK_KHR_present_wait for Frame Pacing - -## Why - -Goggles currently defaults to mailbox present mode and allows uncapped frame submission. On high-end GPUs this drives extremely high FPS, wasting power and producing inconsistent frame pacing. Vulkan's VK_KHR_present_wait lets us explicitly pace presentation and reduce unnecessary GPU/CPU work. - -## What Changes - -- Prefer FIFO present mode with VK_KHR_present_wait when supported. -- Use VK_KHR_present_wait to pace presentation to `render.target_fps` (0 = uncapped). -- Fallback behavior when VK_KHR_present_wait is unavailable: - - Prefer MAILBOX present mode. - - Apply CPU-side frame cap using `render.target_fps` (0 = uncapped). - - If MAILBOX is unavailable, use FIFO without present wait. -- Add CLI override for `render.target_fps` (mirrors config; 0 = uncapped). - -## Impact - -- Affected specs: `render-pipeline`, `app-window`. -- Affected code: - - `src/render/backend/vulkan_backend.cpp` - present mode selection and pacing integration. - - `src/render/backend/vulkan_backend.hpp` - present wait capability state. - - `src/util/config.hpp` + `src/util/config.cpp` - allow `target_fps = 0` for uncapped. - - `src/app/cli.hpp` - CLI override for target fps. - - `src/app/main.cpp` - apply CLI override and log. diff --git a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md b/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md deleted file mode 100644 index bdb6607d..00000000 --- a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md +++ /dev/null @@ -1,17 +0,0 @@ -## ADDED Requirements - -### Requirement: Target FPS CLI Override - -The application SHALL allow overriding the render target FPS from the command line. - -#### Scenario: Override target fps via CLI -- **GIVEN** the application is started with `--target-fps 120` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `120` -- **AND** the override SHALL take precedence over the config file - -#### Scenario: Disable frame cap via CLI -- **GIVEN** the application is started with `--target-fps 0` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `0` -- **AND** presentation pacing SHALL be uncapped diff --git a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md deleted file mode 100644 index bd331549..00000000 --- a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md +++ /dev/null @@ -1,31 +0,0 @@ -## ADDED Requirements - -### Requirement: Present Wait Frame Pacing - -The render backend SHALL use VK_KHR_present_wait when supported to pace presentation and avoid uncapped mailbox behavior on high-end GPUs. - -#### Scenario: Present wait enabled -- **GIVEN** the physical device supports `VK_KHR_present_wait` -- **WHEN** the swapchain is created -- **THEN** the device SHALL enable the extension -- **AND** the present mode SHALL be `FIFO` -- **AND** the backend SHALL use present wait to pace to `render.target_fps` - -#### Scenario: Uncapped target fps -- **GIVEN** `render.target_fps` is set to `0` -- **WHEN** present wait is available -- **THEN** the backend SHALL skip waiting for a target interval -- **AND** presentation SHALL proceed as fast as FIFO allows - -#### Scenario: Present wait unsupported -- **GIVEN** `VK_KHR_present_wait` is not supported -- **WHEN** the swapchain is created -- **THEN** the backend SHALL prefer `MAILBOX` present mode -- **AND** it SHALL apply CPU-side frame capping when `render.target_fps` is non-zero -- **AND** it SHALL fall back to `FIFO` if `MAILBOX` is unavailable - -#### Scenario: Target fps changes via config -- **GIVEN** a configuration file sets `render.target_fps` to a non-zero value -- **WHEN** the application starts -- **THEN** the backend SHALL pace presentation to that value - diff --git a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md b/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md deleted file mode 100644 index ba2ba505..00000000 --- a/openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md +++ /dev/null @@ -1,15 +0,0 @@ -# Tasks - -## Implementation - -1. [x] Add VK_KHR_present_wait capability detection and tracking in the render backend -2. [x] Update swapchain present mode selection to prefer FIFO + present wait, fallback to MAILBOX -3. [x] Implement present-wait pacing using `render.target_fps` (0 = uncapped) -4. [x] Add CLI option to override target FPS and pass into config -5. [x] Allow `target_fps = 0` in config parsing and document meaning - -## Validation - -6. [x] `pixi run build -p debug` -7. [x] `pixi run test -p test` -8. [x] `pixi run dev -p quality` diff --git a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/design.md b/openspec/changes/archive/2026-02-07-add-compositor-popup-support/design.md deleted file mode 100644 index a4fc6ed9..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/design.md +++ /dev/null @@ -1,46 +0,0 @@ -## Context -The compositor currently tracks only xdg_toplevel surfaces and ignores xdg_popup and XWayland -override-redirect surfaces. Menus and dropdowns are created as separate transient surfaces, so -they never receive configure events, input focus, or presentation. - -## Goals / Non-Goals -- Goals: - - Support xdg_popup lifecycle (configure, map, destroy) and render popups above their parent. - - Preserve unified input path while honoring popup grabs. - - Present XWayland override-redirect menus/tooltips as transient popups. -- Non-Goals: - - Full desktop-style window management (multi-app, tiling, workspace logic). - - General-purpose damage tracking or scene graph beyond what popups require. - -## Decisions -- Decision: Track popups in a dedicated list keyed by parent surface and maintain explicit - stacking order (creation order). This keeps the existing single-app focus model while enabling - popup layering. -- Decision: Composite parent + popup surfaces into the presented frame using wlroots surface - traversal utilities rather than introducing a full scene graph. -- Decision: Use wlroots hit-testing (`wlr_xdg_surface_surface_at`) to resolve pointer targets and - popup-local coordinates, falling back to the topmost popup during active grabs. -- Decision: Use `wlr_xdg_popup_get_position` to compute popup offsets so geometry-relative positions - match input coordinates. -- Decision: Clamp cursor movement to the union of root + popup bounds (Wayland and XWayland) instead - of the root surface alone to prevent cursor drift near popups. -- Decision: Treat XWayland override-redirect windows as popup surfaces and render them above the - currently focused XWayland surface when no explicit parent is available. - -## Risks / Trade-offs -- Rendering multiple surfaces per frame adds CPU/GPU work; constrain to the focused surface tree - to avoid unnecessary composition. -- Pointer hit-testing and bounds aggregation add per-event work; keep the logic linear and avoid - extra allocations in hot paths. -- Popup grab handling must not regress the existing input forwarding model; limit changes to - routing decisions and avoid extra threads or blocking calls. - -## Migration Plan -- Implement new popup tracking alongside existing toplevel tracking. -- Integrate composition path for popups in presentation output. -- Update pointer routing to use hit-testing and popup-local offsets. -- Extend cursor bounds to include mapped popups. -- Validate with a native Wayland app (xdg_popup) and an XWayland app (override-redirect menu). - -## Open Questions -- Should popup surfaces be exposed in the surface selector UI or hidden behind their parent? diff --git a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/proposal.md b/openspec/changes/archive/2026-02-07-add-compositor-popup-support/proposal.md deleted file mode 100644 index a0d55df2..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Add compositor popup support - -## Why -Menu dropdowns and other transient popups are rendered as separate surfaces that the compositor -currently ignores, so they never appear in the presented frame. - -## What Changes -- Track and configure Wayland `xdg_popup` surfaces. -- Present popup surfaces composited above their parent surface. -- Resolve pointer targets via wlroots hit-testing with popup-local coordinates. -- Expand cursor bounds to include mapped popups to avoid clamped pointer drift. -- Treat XWayland override-redirect windows as popups for rendering and input routing. - -## Impact -- Affected specs: input-forwarding -- Affected code: src/compositor/compositor_server.cpp, compositor presentation path diff --git a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-add-compositor-popup-support/specs/input-forwarding/spec.md deleted file mode 100644 index c1a2d55b..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/specs/input-forwarding/spec.md +++ /dev/null @@ -1,38 +0,0 @@ -## ADDED Requirements -### Requirement: Popup Surface Support - -The system SHALL support Wayland `xdg_popup` surfaces associated with a mapped `xdg_toplevel`. - -The compositor server SHALL: -- Listen for `new_popup` signals from `xdg_shell` -- Track popup surfaces separately from toplevel surfaces with parent linkage and stacking order -- Send initial configure for popups and respect ack_configure before focus changes -- Render popups above their parent surface in the presented frame -- Route pointer and keyboard events to the topmost mapped popup while a popup grab is active - -#### Scenario: Wayland menu popup renders -- **GIVEN** a Wayland toplevel is mapped -- **WHEN** the client creates and maps an `xdg_popup` (menu/dropdown) -- **THEN** the popup is configured and rendered above the parent surface -- **AND** input is delivered to the popup until it is dismissed - -#### Scenario: Popup dismissed with parent -- **GIVEN** a parent toplevel with a mapped popup -- **WHEN** the parent surface is destroyed -- **THEN** all associated popups are removed from tracking - -### Requirement: XWayland Override-Redirect Popups - -The system SHALL present XWayland override-redirect surfaces (menus/tooltips) as popups. - -The compositor server SHALL: -- Track override-redirect XWayland surfaces as transient popups -- Accept map requests for override-redirect surfaces -- Render override-redirect surfaces above the focused XWayland surface -- Route pointer and keyboard events to the topmost mapped override-redirect surface while visible - -#### Scenario: X11 menu popup renders -- **GIVEN** an XWayland surface is focused -- **WHEN** the app creates an override-redirect menu window -- **THEN** the menu is mapped and rendered above the parent surface -- **AND** pointer clicks are delivered to the menu window diff --git a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/tasks.md b/openspec/changes/archive/2026-02-07-add-compositor-popup-support/tasks.md deleted file mode 100644 index acf16483..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-popup-support/tasks.md +++ /dev/null @@ -1,12 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add xdg_popup hooks and listeners (new_popup, map, commit, destroy, ack_configure). -- [x] 1.2 Track popup stacking per parent surface and maintain topmost ordering. -- [x] 1.3 Composite popup surfaces above the focused surface in the presented frame. -- [x] 1.4 Resolve pointer targets via hit-testing and popup-local coordinates. -- [x] 1.5 Extend cursor bounds to cover mapped popups (Wayland + XWayland). -- [x] 1.6 Track XWayland override-redirect windows as popup surfaces and present them. -- [x] 1.7 Add diagnostics/logging for popup lifecycle at debug level. - -## 2. Verification -- [x] 2.1 Manual test: Wayland menu popup (native app) renders and receives input. -- [x] 2.2 Manual test: XWayland menu popup (X11 app) renders and receives input. diff --git a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/design.md b/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/design.md deleted file mode 100644 index 4a52dcf7..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/design.md +++ /dev/null @@ -1,58 +0,0 @@ -## Context - -- The compositor currently accepts SDL pointer motion and updates a cursor in surface coordinates. - Viewer scaling and window resizing make absolute pointer mapping unreliable, causing the host - cursor and surface cursor to diverge. -- Relative pointer and pointer constraints are already supported for game-style input. We need to - make the compositor cursor fully independent of host cursor positioning. - -## Goals / Non-Goals - -- Goals: - - Provide a software cursor rendered into compositor-presented frames. - - Drive cursor movement using raw relative deltas only (no absolute mapping). - - Preserve raw relative pointer deltas for `zwp_relative_pointer_v1` clients. - - Respect pointer lock/confine constraints and cursor hints. - - Use the Xcursor assets shipped in `assets/cursor`. -- Non-Goals: - - Implement themed XCursor assets or per-client cursor images. - - Change capture-layer behavior or Vulkan-layer cursor rendering. - -## Decisions - -- Decision: Track a compositor cursor in surface-local coordinates. - - Maintain `cursor_x/y` and `cursor_visible` in `CompositorServer::Impl`. - - Initialize cursor on focus change (center of surface) and clamp to surface bounds. - -- Decision: Ignore absolute coordinates and use raw relative deltas for cursor updates. - - SDL motion deltas update the compositor cursor directly. - - Raw deltas are forwarded to `zwp_relative_pointer_v1` without scaling. - -- Decision: Handle pointer constraints in cursor state updates. - - For locked constraints, keep cursor stationary (or update to cursor hint if provided) and hide - the software cursor. - - For confined constraints, clamp cursor position using `wlr_region_confine` on the constraint - region in surface coordinates. - -- Decision: Render the cursor using the Breeze Light Xcursor assets. - - Load the cursor theme via `wlr_xcursor_theme_load`. - - Build a `wlr_texture` from the cursor image and draw it after the surface texture. - - Apply hotspot offsets when positioning the cursor. - -- Decision: Mirror cursor visibility to the viewer window. - - Hide the host cursor and enable relative mouse mode when UI is hidden. - - Show the host cursor and stop forwarding pointer events when UI is visible. - -## Risks / Trade-offs - -- Software cursor may overlap with apps that draw their own cursor without pointer lock. Mitigation: - hide the cursor during pointer lock and consider a future toggle if needed. - -## Migration Plan - -- No data migration. Changes are runtime-only. - -## Open Questions - -- Should cursor size/color be configurable in the runtime user config at `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`? -- Should the software cursor be suppressed when Vulkan-layer frames are active? diff --git a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/proposal.md b/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/proposal.md deleted file mode 100644 index 9fd019c2..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -# Change: Add Compositor Software Cursor - -## Why - -Viewer/window scaling makes absolute cursor mapping unreliable. The host pointer position can -never precisely match surface-local coordinates, and clamping the host pointer prevents the -internal cursor from reaching parts of the surface. We need a compositor-local cursor rendered into -presented frames and driven by relative motion only, with the host cursor hidden in internal mode -and input forwarding suspended while the UI overlay is active. - -## What Changes - -- Track a compositor-local cursor position in surface coordinates and render it into presented - frames from `CompositorServer`. -- Use raw relative motion deltas to update the compositor cursor; ignore absolute window - coordinates entirely. -- Respect pointer constraints by freezing or clamping the cursor and honoring cursor hints when - provided by clients, mirroring pointer lock to the host window. -- Render the cursor using the XCursor assets in `assets/cursor` (Breeze Light, 64px), honoring - hotspot data. -- Hide the host window cursor and disable pointer forwarding when the global UI is visible; show - the host cursor in UI mode and resume forwarding when the UI is hidden. -- Remove dead compositor code paths (unused absolute coordinates and related state). - -## Impact - -- Affected specs: `input-forwarding` -- Affected code: - - `src/compositor/compositor_server.hpp` - - `src/compositor/compositor_server.cpp` - - `src/app/application.cpp` - - `src/app/application.hpp` diff --git a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/specs/input-forwarding/spec.md deleted file mode 100644 index cf4c094c..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/specs/input-forwarding/spec.md +++ /dev/null @@ -1,111 +0,0 @@ -## ADDED Requirements - -### Requirement: Compositor Software Cursor - -The system SHALL render a software cursor inside compositor-presented frames for the focused -surface. - -The compositor server SHALL: -- Track cursor position in surface-local coordinates. -- Render the cursor overlay into the compositor-presented frame buffer. -- Hide the software cursor when pointer lock is active or when input forwarding is suspended. -- Load cursor imagery from the shipped Xcursor assets in `assets/cursor`. - -#### Scenario: Cursor visible for compositor surface -- **WHEN** the compositor is presenting a surface frame -- **AND** the focused surface does not hold a pointer lock -- **THEN** a software cursor is rendered into the presented frame -- **AND** the cursor position matches the compositor cursor coordinates used for pointer events - -#### Scenario: Cursor hidden during pointer lock -- **WHEN** a focused surface activates `zwp_pointer_constraints_v1.lock_pointer` -- **THEN** the software cursor is hidden -- **AND** only relative pointer events continue to be delivered - -#### Scenario: Cursor hidden while UI overlay is visible -- **WHEN** the viewer UI overlay is visible -- **THEN** pointer events are not forwarded to the focused surface -- **AND** the compositor software cursor is hidden - -## MODIFIED Requirements - -### Requirement: Coordinate Handling - -The system SHALL derive compositor cursor motion from raw relative deltas and SHALL NOT rely on -viewer absolute coordinates. - -The system SHALL: -- Maintain a compositor-local cursor position in surface coordinates for the focused surface. -- Apply raw relative motion deltas to the compositor cursor position. -- Clamp cursor motion to the surface bounds and any active pointer confinement region. -- Use the compositor cursor position for `wl_pointer.enter` and `wl_pointer.motion` events. - -#### Scenario: Relative-only cursor updates -- **WHEN** the user moves the mouse with the UI overlay hidden -- **THEN** the compositor cursor updates using raw relative motion deltas -- **AND** absolute viewer coordinates are ignored - -### Requirement: Unified Pointer Input - -The system SHALL forward pointer events (motion, button, axis) to the focused surface using -`wlr_seat_pointer_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_pointer_enter()` on surface focus with the compositor cursor position. -- Use `wlr_seat_pointer_notify_motion()` with compositor cursor coordinates for absolute motion. -- Use `wlr_seat_pointer_notify_button()` for all button events (extended set). -- Use `wlr_seat_pointer_notify_axis()` for scroll events. -- Use `wlr_seat_pointer_notify_frame()` to group related events. -- Send relative motion via `wlr_relative_pointer_manager_v1_send_relative_motion()` using raw - deltas from the viewer (no scale adjustment). -- Respect active pointer constraints when processing motion. - -The wlr_xwm SHALL automatically translate pointer events to X11 for XWayland surfaces. - -#### Scenario: Absolute motion uses software cursor -- **WHEN** the user moves the mouse with no active pointer lock -- **THEN** `wl_pointer.motion` is sent using the compositor cursor position -- **AND** the rendered software cursor matches the motion on-screen - -#### Scenario: Relative motion remains raw under scaling -- **WHEN** a client is bound to `zwp_relative_pointer_v1` -- **THEN** the client receives raw, unscaled deltas -- **AND** relative motion is delivered regardless of viewer scaling - -### Requirement: Pointer Constraints - -The system SHALL support pointer lock and confine via the `zwp_pointer_constraints_v1` Wayland -protocol extension. - -The implementation SHALL: -- Create a `wlr_pointer_constraints_v1` manager on the Wayland display. -- Listen for `new_constraint` signals from clients. -- Listen for `set_region` signals to refresh confinement bounds. -- Activate constraints on the focused surface automatically. -- Deactivate constraints when focus changes to a different surface. -- Support both `locked_pointer` (cursor disappears) and `confined_pointer` (cursor stays in region). -- Send `activated`/`deactivated` events to inform clients of constraint state. -- Apply confinement using the constraint region in surface coordinates. -- Apply cursor hints when provided for locked constraints. - -#### Scenario: Pointer lock activated by game -- **WHEN** a game requests pointer lock via `zwp_pointer_constraints_v1.lock_pointer` -- **AND** the game's surface has focus -- **THEN** the constraint is activated -- **AND** relative motion events continue to be sent -- **AND** absolute cursor position is not updated - -#### Scenario: Pointer lock uses cursor hint -- **WHEN** a locked pointer provides a cursor hint -- **THEN** the compositor cursor position is updated to the hinted location -- **AND** absolute pointer motion remains suppressed while locked - -#### Scenario: Pointer confine region updates -- **WHEN** a client updates the pointer confine region -- **THEN** the compositor clamps cursor motion to the new region - -#### Scenario: Pointer confine restricts cursor -- **WHEN** a client requests pointer confine to a region -- **AND** user moves cursor toward the region boundary -- **THEN** cursor motion is clamped to stay within the region -- **AND** motion events reflect the clamped position diff --git a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/tasks.md b/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/tasks.md deleted file mode 100644 index ae225941..00000000 --- a/openspec/changes/archive/2026-02-07-add-compositor-software-cursor/tasks.md +++ /dev/null @@ -1,11 +0,0 @@ -## 1. Implementation -- [x] 1.1 Remove absolute pointer coordinates from compositor input events; keep relative-only motion. -- [x] 1.2 Load Xcursor assets from `assets/cursor` and build a reusable cursor texture + hotspot. -- [x] 1.3 Update input forwarding to drive the compositor cursor with raw relative deltas only. -- [x] 1.4 Honor pointer constraints: freeze cursor on lock, apply cursor hint, react to set_region, - and confine via `wlr_region_confine` when active. -- [x] 1.5 Render the cursor texture in `render_surface_to_frame` using the compositor cursor - position and hotspot. -- [x] 1.6 Update viewer cursor visibility/relative mode/grab based on UI visibility (no forwarding - when UI is shown). -- [x] 1.7 Restructure cursor assets into a standard Xcursor theme layout. diff --git a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/design.md b/openspec/changes/archive/2026-02-07-add-cursor-forwarding/design.md deleted file mode 100644 index 5e278ce5..00000000 --- a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/design.md +++ /dev/null @@ -1,61 +0,0 @@ -## Context - -Games and mouse-captured applications require more sophisticated input handling than simple absolute coordinate forwarding. The Wayland ecosystem provides two key protocol extensions for this: -- `zwp_relative_pointer_v1` - raw, unaccelerated mouse deltas for mouselook/camera control -- `zwp_pointer_constraints_v1` - cursor lock and confine for keeping cursor within window - -Both protocols are implemented via wlroots and widely supported by game clients (including Wine/Proton). - -## Goals / Non-Goals - -**Goals:** -- Enable FPS games and mouselook applications to work correctly -- Support cursor lock/confine for games that capture the mouse -- Forward all mouse buttons (not just left/middle/right) -- Follow gamescope's proven implementation patterns - -**Non-Goals:** -- Touch input support (separate proposal) -- High-resolution scroll (v120 protocol, separate proposal) -- Tablet/stylus input (future work) -- Coordinate scaling/mapping between windows (existing limitation documented) - -## Decisions - -### Decision: Send both relative and absolute motion - -Following gamescope's pattern, every pointer motion event sends: -1. Relative motion via `wlr_relative_pointer_manager_v1_send_relative_motion()` -2. Absolute motion via `wlr_seat_pointer_notify_motion()` - -**Rationale:** Wayland clients may bind to either or both protocols. Native Wayland games often use relative pointer, while X11 games via XWayland need absolute motion for wlr_xwm translation. - -**Alternatives considered:** -- Only send relative when client binds relative_pointer - more complex tracking, breaks some games -- Only send absolute - current behavior, breaks FPS games - -### Decision: Automatic constraint activation on focused surface - -When a client requests a pointer constraint, we automatically activate it if the requesting surface has focus. - -**Rationale:** Simplifies implementation and matches user expectation - when you click in a game, it should capture the cursor. - -### Decision: Use InputEvent struct for relative deltas - -Extend existing `InputEvent` with `dx`/`dy` fields rather than creating a separate relative motion event type. - -**Rationale:** Keeps the event struct compact and allows a single motion event to carry both absolute and relative data, matching how SDL delivers them. - -## Risks / Trade-offs - -- **Constraint region math**: Confine constraints require region intersection. Initial implementation may only support full-surface confine, with region support added later. -- **Focus race conditions**: Constraint activation/deactivation during rapid focus changes could cause brief input glitches. Mitigate by clearing constraint state atomically with focus changes. - -## Migration Plan - -No migration needed - this is additive functionality. Existing applications continue to work unchanged. - -## Open Questions - -1. Should we expose constraint state to the InputForwarder API for UI feedback (e.g., showing lock icon)? -2. Do we need to handle persistent constraints that survive focus loss (oneshot vs persistent)? diff --git a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/proposal.md b/openspec/changes/archive/2026-02-07-add-cursor-forwarding/proposal.md deleted file mode 100644 index d1e47e17..00000000 --- a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/proposal.md +++ /dev/null @@ -1,53 +0,0 @@ -# Change: Add Cursor Forwarding - -## Why - -Games require relative pointer motion (mouselook), pointer constraints (cursor lock/confine), and extended button support to function correctly. The current implementation only forwards absolute coordinates and a subset of mouse buttons, making FPS games and other mouse-captured applications unusable. - -## What Changes - -- Add `zwp_relative_pointer_v1` protocol support for raw mouse deltas -- Add `zwp_pointer_constraints_v1` protocol support for lock/confine -- Extend mouse button mapping to support all Linux input buttons -- Forward SDL's relative motion (`xrel`, `yrel`) alongside absolute position - -## Impact - -- Affected specs: `input-forwarding` -- Affected code: - - `src/input/compositor_server.hpp` - new wlroots protocol managers, extended InputEvent struct - - `src/input/compositor_server.cpp` - relative pointer, pointer constraints, constraint handling - - `src/input/input_forwarder.hpp` - expose `is_pointer_locked()` for viewer mirror - - `src/input/input_forwarder.cpp` - use SDL xrel/yrel, extend button mapping - - `src/app/application.cpp` - pointer lock mirroring to viewer window - -## Design Rationale - -Following gamescope's pattern: motion events send BOTH relative (via `wlr_relative_pointer_manager_v1`) AND absolute (via `wlr_seat_pointer_notify_motion`). This matches Wayland protocol semantics where clients may listen to either or both. - -Pointer constraints are handled by: -1. Creating `wlr_pointer_constraints_v1` on the display -2. Listening for `new_constraint` signals -3. Activating constraints on the focused surface -4. Applying lock/confine logic during motion processing - -## Testing - -Manual test binaries (`goggles_manual_input_wayland`, `goggles_manual_input_x11`) support: - -- **1**: Toggle pointer lock - tests `zwp_pointer_constraints_v1` LOCKED mode -- **2**: Toggle mouse grab - tests `zwp_pointer_constraints_v1` CONFINED mode -- **3**: Query current state - -Expected behavior: -- Lock ON: cursor hidden, xrel/yrel arrive, x/y frozen -- Grab ON: cursor confined to window, x/y update normally -- Extended buttons (6-8+): logged with Linux button codes - -## Viewer Pointer Lock Mirror - -When target app requests pointer lock, goggles mirrors the lock to the viewer window: -- Cursor hidden and confined to viewer -- **F3** toggles override to temporarily release lock (for ImGui access) -- **F1** (ImGui toggle) auto-releases lock when showing UI -- Lock automatically restores when override is cleared and target still has lock diff --git a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-add-cursor-forwarding/specs/input-forwarding/spec.md deleted file mode 100644 index cf264e2e..00000000 --- a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/specs/input-forwarding/spec.md +++ /dev/null @@ -1,107 +0,0 @@ -## ADDED Requirements - -### Requirement: Relative Pointer Motion - -The system SHALL support relative pointer motion via the `zwp_relative_pointer_v1` Wayland protocol extension. - -The implementation SHALL: -- Create a `wlr_relative_pointer_manager_v1` on the Wayland display -- Send relative motion events via `wlr_relative_pointer_manager_v1_send_relative_motion()` for all pointer motion -- Forward SDL's `xrel/yrel` motion deltas to the compositor -- Send both relative and absolute motion for each pointer event (gamescope pattern) - -#### Scenario: Relative motion forwarded to FPS game -- **WHEN** user moves mouse with relative delta (dx=10, dy=-5) -- **THEN** both `wl_pointer.motion` and `zwp_relative_pointer_v1.relative_motion` events are sent -- **AND** the client receives raw, unaccelerated deltas for mouselook - -#### Scenario: Relative motion works without absolute position -- **WHEN** a game uses only relative pointer protocol (mouselook mode) -- **THEN** mouse movement translates to view rotation -- **AND** no cursor position is displayed or tracked - -### Requirement: Pointer Constraints - -The system SHALL support pointer lock and confine via the `zwp_pointer_constraints_v1` Wayland protocol extension. - -The implementation SHALL: -- Create a `wlr_pointer_constraints_v1` manager on the Wayland display -- Listen for `new_constraint` signals from clients -- Activate constraints on the focused surface automatically -- Deactivate constraints when focus changes to a different surface -- Support both `locked_pointer` (cursor disappears) and `confined_pointer` (cursor stays in region) -- Send `activated`/`deactivated` events to inform clients of constraint state - -#### Scenario: Pointer lock activated by game -- **WHEN** a game requests pointer lock via `zwp_pointer_constraints_v1.lock_pointer` -- **AND** the game's surface has focus -- **THEN** the constraint is activated -- **AND** relative motion events continue to be sent -- **AND** absolute cursor position is not updated (or updated to hint position) - -#### Scenario: Pointer lock released on focus loss -- **WHEN** focus changes from a surface with active pointer lock -- **THEN** the constraint is deactivated -- **AND** the client receives `zwp_locked_pointer_v1.unlocked` event - -#### Scenario: Pointer confine restricts cursor -- **WHEN** a client requests pointer confine to a region -- **AND** user moves cursor toward region boundary -- **THEN** cursor position is clamped to stay within the confine region -- **AND** motion events reflect the clamped position - -### Requirement: Extended Button Support - -The system SHALL forward all mouse button events, not limited to left/middle/right. - -The implementation SHALL: -- Map SDL button codes to Linux `input-event-codes.h` button constants -- Support side buttons: `BTN_SIDE` (X1), `BTN_EXTRA` (X2) -- Support forward/back buttons: `BTN_FORWARD`, `BTN_BACK` -- Support task button: `BTN_TASK` -- Pass through unmapped buttons using `BTN_MISC` + offset as fallback - -#### Scenario: Side button forwarded correctly -- **WHEN** user presses mouse side button (X1) -- **THEN** `BTN_SIDE` (0x113) is forwarded to the focused surface -- **AND** the client receives the button event - -#### Scenario: Unmapped button passed through -- **WHEN** user presses an uncommon button not in standard mapping -- **THEN** the button is forwarded as `BTN_MISC` + button offset -- **AND** logging indicates the fallback mapping - -## MODIFIED Requirements - -### Requirement: Unified Pointer Input - -The system SHALL forward pointer events (motion, button, axis) to the focused surface using `wlr_seat_pointer_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_pointer_enter()` on surface focus -- Use `wlr_seat_pointer_notify_motion()` for absolute motion -- Use `wlr_seat_pointer_notify_button()` for all button events (extended set) -- Use `wlr_seat_pointer_notify_axis()` for scroll events -- Use `wlr_seat_pointer_notify_frame()` to group related events -- **Send relative motion via `wlr_relative_pointer_manager_v1_send_relative_motion()` alongside absolute motion** -- **Respect active pointer constraints when processing motion** - -The wlr_xwm SHALL automatically translate pointer events to X11 for XWayland surfaces. - -#### Scenario: Mouse motion forwarded to Wayland client -- **WHEN** user moves mouse in the viewer window -- **AND** a Wayland surface has pointer focus -- **THEN** the motion event is delivered via wl_pointer.motion protocol -- **AND** relative motion is delivered via zwp_relative_pointer_v1 if client bound it - -#### Scenario: Mouse motion forwarded to X11 client -- **WHEN** user moves mouse in the viewer window -- **AND** an XWayland surface has pointer focus -- **THEN** wlr_xwm translates the event to X11 MotionNotify -- **AND** the X11 app receives the event - -#### Scenario: Motion respects pointer lock -- **WHEN** user moves mouse with active pointer lock -- **THEN** relative motion is sent to the client -- **AND** absolute cursor position is not changed -- **AND** no wl_pointer.motion is sent (only relative) diff --git a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/tasks.md b/openspec/changes/archive/2026-02-07-add-cursor-forwarding/tasks.md deleted file mode 100644 index e1b74db3..00000000 --- a/openspec/changes/archive/2026-02-07-add-cursor-forwarding/tasks.md +++ /dev/null @@ -1,50 +0,0 @@ -## 1. CompositorServer Protocol Setup - -- [x] 1.1 Add `#include ` and `#include ` -- [x] 1.2 Add `wlr_relative_pointer_manager_v1*` member to Impl struct -- [x] 1.3 Add `wlr_pointer_constraints_v1*` member to Impl struct -- [x] 1.4 Create relative pointer manager in `setup_input_devices()` via `wlr_relative_pointer_manager_v1_create()` -- [x] 1.5 Create pointer constraints in `setup_input_devices()` via `wlr_pointer_constraints_v1_create()` - -## 2. Pointer Constraints Implementation - -- [x] 2.1 Add constraint state tracking: `wlr_pointer_constraint_v1* active_constraint`, `wl_listener new_constraint_listener` -- [x] 2.2 Implement `handle_new_constraint()` callback to activate constraints on focused surface -- [x] 2.3 Implement `handle_constraint_destroy()` to clean up when constraint is released -- [x] 2.4 Add constraint activation when surface gains focus -- [x] 2.5 Add constraint deactivation when focus changes - -## 3. Relative Pointer Motion - -- [x] 3.1 Add `dx`, `dy` fields to `InputEvent` struct for relative deltas -- [x] 3.2 Update `inject_pointer_motion()` to accept dx/dy parameters -- [x] 3.3 Update `process_input_events()` to call `wlr_relative_pointer_manager_v1_send_relative_motion()` -- [x] 3.4 Modify absolute motion handling to also send relative motion (gamescope pattern) - -## 4. Extended Button Support - -- [x] 4.1 Extend `sdl_to_linux_button()` to map buttons 6+ to BTN_FORWARD, BTN_BACK, BTN_TASK -- [x] 4.2 Handle unmapped buttons by passing through raw SDL button + BTN_MISC offset -- [x] 4.3 Add trace logging for button mapping - -## 5. InputForwarder API Extension - -- [x] 5.1 Update `forward_mouse_motion()` to also forward xrel/yrel as relative motion -- [x] 5.2 Update docstrings in header file - -## 6. Testing and Validation - -- [x] 6.1 Add key 1 toggle for pointer lock in manual tests -- [x] 6.2 Add key 2 toggle for mouse grab in manual tests -- [x] 6.3 Add key 3 state query in manual tests -- [x] 6.4 Test pointer lock: verify relative motion works, absolute frozen -- [x] 6.5 Test mouse grab: verify cursor confined to window - -## 7. Viewer Pointer Lock Mirror - -- [x] 7.1 Add `is_pointer_locked()` to CompositorServer -- [x] 7.2 Expose `is_pointer_locked()` through InputForwarder -- [x] 7.3 Add F3 toggle for pointer lock override in Application -- [x] 7.4 Poll and mirror pointer lock state each frame -- [x] 7.5 Auto-release lock when ImGui is shown (F1) -- [x] 7.6 Test pointer lock mirroring with captured app diff --git a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/design.md b/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/design.md deleted file mode 100644 index fc152ad1..00000000 --- a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/design.md +++ /dev/null @@ -1,75 +0,0 @@ -## Context - -In profile builds, Goggles runtime behavior spans two instrumented processes: - -1. app-side process with `goggles_vklayer` -2. viewer/compositor process (`goggles`) - -Today, collection is manual and split. Tracy's CLI capture utility captures one client per invocation, -and Tracy tooling does not provide a native "merge two .tracy files into one timeline" command. - -## Goals / Non-Goals - -- Goals: - - One command UX for profile sessions (`pixi run profile ... -- ...`). - - Capture both relevant Tracy clients in one session. - - Emit a single merged timeline artifact for timeline-centric analysis. - - Preserve original raw captures for deep per-process debugging. -- Non-Goals: - - Perfect lossless merge of every Tracy data class (callstacks, lock graph internals, crash payloads, - symbol transfer metadata). - - Replacing `tracy-profiler` interactive workflows. - -## Decisions - -### Decision 1: New Pixi `profile` task with `start`-compatible CLI - -Create `scripts/task/profile.sh` and wire `pixi run profile` to: -- parse `-p/--preset` (default `profile`) -- accept Goggles args before `--` -- accept app command/args after `--` -- run build/install prerequisites equivalent to the normal start flow - -### Decision 2: Capture both Tracy clients automatically - -Profile task launches two capture workers concurrently and assigns each output file to its client role: -- `viewer.tracy` -- `layer.tracy` - -Implementation may use deterministic profiling endpoints and/or Tracy broadcast discovery, but must be -stable for repeated runs without manual port probing. - -### Decision 3: Merge via normalized timeline pipeline - -Because native `.tracy + .tracy -> .tracy` merge is unavailable, merge is defined as: - -1. Extract timeline events from each raw trace. -2. Normalize into one multi-process timeline representation. -3. Build a single merged trace artifact from normalized events. - -The merged artifact is timeline-focused and intended for frame/zone correlation across viewer and layer. -Raw traces remain the source of truth for full-fidelity single-process analysis. - -### Decision 4: Cross-process alignment markers - -Add frame-sequence markers in both processes so merge can align by shared frame identity when available. -Fallback behavior is required when shared markers are missing: merge still completes using relative time -origin with a warning in session metadata. - -## Risks and Mitigations - -- Risk: merge loses some data classes vs native trace - - Mitigation: always keep and expose raw per-process traces. -- Risk: wrong client association during capture - - Mitigation: explicit role mapping with validation metadata (client name/port/PID) in session manifest. -- Risk: tool availability drift - - Mitigation: resolve/build required Tracy tooling from Pixi-managed workflow and fail fast with clear errors. - -## Output Layout - -Each session produces: -- `session.json` (metadata, command line, timestamps, client mapping, warnings) -- `viewer.tracy` -- `layer.tracy` -- `merged.tracy` -- optional intermediate normalized file(s) for troubleshooting diff --git a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/proposal.md b/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/proposal.md deleted file mode 100644 index 73f88ea0..00000000 --- a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/proposal.md +++ /dev/null @@ -1,31 +0,0 @@ -# Change: Add Dual-Process Profiling Workflow - -## Why - -Profiling a normal Goggles launch currently requires manual Tracy orchestration: two independent client -processes (viewer/compositor and app-side Vulkan layer), two `tracy-capture` runs, and no first-class -way to produce one merged timeline artifact. - -This slows down iterative performance work and makes profile collection error-prone. - -## What Changes - -- Add a user-facing `pixi run profile` task that follows the existing `pixi run start` argument pattern. -- Add deterministic dual-client capture orchestration for Goggles profile runs (viewer/compositor + app/layer). -- Produce two raw `.tracy` captures per session and a merged single-timeline trace artifact. -- Add cross-process frame-alignment markers to improve merge accuracy. -- Add structured output layout and diagnostics for failed/incomplete profile sessions. - -## Impact - -- Affected specs: - - `profiling` -- Affected code: - - `pixi.toml` - - `scripts/task/help.sh` - - `scripts/task/profile.sh` (new) - - `scripts/profiling/` (new merge/capture helpers) - - `src/app/application.cpp` - - `src/capture/vk_layer/vk_capture.cpp` - - build wiring for profiling-time Tracy endpoints/tooling availability - - `README.md` diff --git a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/specs/profiling/spec.md b/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/specs/profiling/spec.md deleted file mode 100644 index 3c9fd73c..00000000 --- a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/specs/profiling/spec.md +++ /dev/null @@ -1,58 +0,0 @@ -## ADDED Requirements - -### Requirement: Unified Profile Session Command - -The system SHALL provide a profile-session command that orchestrates build, launch, capture, and -artifact generation for dual-process Goggles profiling. - -#### Scenario: Start-pattern CLI for profile sessions - -- **WHEN** a user runs `pixi run profile [goggles_args...] -- [app_args...]` -- **THEN** the command SHALL parse arguments using the same split semantics as `pixi run start` -- **AND** it SHALL launch a profiling session without requiring manual Tracy command orchestration - -### Requirement: Dual Raw Trace Capture - -A profile session SHALL capture both instrumented process roles and persist separate raw traces. - -#### Scenario: Capture viewer and layer traces in one run - -- **WHEN** a profile session completes successfully -- **THEN** it SHALL produce a raw trace for the viewer/compositor process -- **AND** it SHALL produce a raw trace for the app-side layer process - -#### Scenario: Client-role mapping is explicit - -- **WHEN** raw traces are written -- **THEN** the session metadata SHALL record which client endpoint/process produced each trace - -### Requirement: Merged Single-Timeline Artifact - -A profile session SHALL emit one merged timeline artifact derived from both raw traces. - -#### Scenario: Merge success path - -- **WHEN** both raw traces are present and readable -- **THEN** the system SHALL generate one merged trace file containing both process timelines -- **AND** the merged trace SHALL be suitable for cross-process timeline inspection - -#### Scenario: Alignment-marker-aware merge - -- **WHEN** shared frame alignment markers are available in both raw traces -- **THEN** merge SHALL align timelines using those markers before writing the merged trace - -#### Scenario: Alignment fallback - -- **WHEN** shared alignment markers are missing or insufficient -- **THEN** merge SHALL fall back to relative-time alignment -- **AND** the session metadata SHALL include a warning describing reduced alignment confidence - -### Requirement: Session Artifact Manifest - -The system SHALL persist machine-readable metadata for every profile session. - -#### Scenario: Manifest contains reproducibility metadata - -- **WHEN** a session finishes -- **THEN** it SHALL write a manifest describing command line, timestamps, client mapping, and artifact paths -- **AND** it SHALL include warnings/errors encountered during capture or merge stages diff --git a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/tasks.md b/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/tasks.md deleted file mode 100644 index 872ede8c..00000000 --- a/openspec/changes/archive/2026-02-07-add-dual-process-profiling-workflow/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. Implementation - -- [x] 1.1 Add `pixi run profile` task and help text, matching the existing `start` argument split pattern. -- [x] 1.2 Implement `scripts/task/profile.sh` orchestration: - - build/manifest prerequisites - - target launch - - dual Tracy capture lifecycle - - deterministic session output directory -- [x] 1.3 Add/resolve Tracy tooling required by the profile workflow in a Pixi-compatible way. -- [x] 1.4 Add cross-process frame alignment markers in profiled code paths: - - viewer-side source frame marker - - layer-side captured frame marker -- [x] 1.5 Implement merge pipeline that consumes two raw traces and emits one merged timeline trace artifact. -- [x] 1.6 Write session metadata manifest (`session.json`) with client mapping, warnings, and artifact paths. -- [x] 1.7 Update docs (`README.md`) with profile usage examples and artifact explanation. - -## 2. Verification - -- [x] 2.1 `pixi run help` lists the new `profile` task. -- [x] 2.2 `pixi run profile --help` documents options and argument split behavior. -- [x] 2.3 Manual run: `pixi run profile -p profile -- ` produces `viewer.tracy` and `layer.tracy` in one session directory. -- [x] 2.4 Manual run produces `merged.tracy` with both process timelines visible. -- [x] 2.5 Validate fallback path by forcing missing alignment markers and confirming merge warning + successful artifact generation. -- [x] 2.6 Validate profile task returns non-zero on capture/merge hard failures with actionable diagnostics. diff --git a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/proposal.md b/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/proposal.md deleted file mode 100644 index e0825277..00000000 --- a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/proposal.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change: Add Dynamic Resolution Request - -## Why - -When the Goggles window aspect ratio doesn't match the source, users can only choose between stretching, cropping, or letterboxing. Dynamic resolution requests allow Goggles to notify the source application to adjust its rendering resolution, achieving lossless aspect ratio synchronization. - -## What Changes - -- Add `dynamic` scale mode that triggers resolution requests on window resize -- **BREAKING**: Extend `CaptureControl` protocol with resolution request fields (changes struct layout from `capturing + reserved[]` to `flags + requested_width + requested_height`, breaking binary compatibility with existing viewers/layers) -- Add `request_resolution()` method to `CaptureReceiver` -- Handle resolution requests in vk_layer (WSI Proxy mode only) - -## Impact - -- Affected specs: `vk-layer-capture`, `render-pipeline` -- Affected code: - - `src/util/config.hpp/cpp` - add dynamic scale mode - - `src/capture/capture_protocol.hpp` - protocol extension - - `src/capture/capture_receiver.cpp/hpp` - send requests - - `src/capture/vk_layer/ipc_socket.cpp` - receive requests - - `src/capture/vk_layer/wsi_virtual.cpp/hpp` - resolution update - - `src/app/application.cpp` - resize trigger \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/render-pipeline/spec.md deleted file mode 100644 index 610cbcd8..00000000 --- a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/render-pipeline/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -## ADDED Requirements - -### Requirement: Dynamic Scale Mode - -The viewer SHALL support a dynamic scale mode that requests source resolution changes to match the viewer window. - -#### Scenario: Dynamic mode activation - -- **GIVEN** `scale_mode = "dynamic"` in configuration -- **WHEN** the viewer window is resized -- **THEN** the viewer SHALL send a resolution request to the source -- **AND** render using fit mode until source resolution changes - -#### Scenario: Dynamic mode with WSI proxy source - -- **GIVEN** `scale_mode = "dynamic"` is configured -- **AND** source is running in WSI proxy mode -- **WHEN** the viewer window is resized -- **THEN** the source SHALL recreate its swapchain with the new resolution -- **AND** subsequent frames SHALL match the viewer window resolution - -#### Scenario: Dynamic mode with non-proxy source - -- **GIVEN** `scale_mode = "dynamic"` is configured -- **AND** source is NOT running in WSI proxy mode -- **WHEN** the viewer window is resized -- **THEN** the resolution request SHALL be ignored by the source -- **AND** the viewer SHALL fall back to fit mode behavior \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/vk-layer-capture/spec.md deleted file mode 100644 index d78f5079..00000000 --- a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,78 +0,0 @@ -## ADDED Requirements - -### Requirement: Dynamic Resolution Request Protocol - -The capture protocol SHALL support resolution request messages from the viewer to the layer. - -#### Scenario: Protocol backward compatibility - -- **GIVEN** an older layer that does not support resolution requests -- **WHEN** the viewer sends a `CaptureControl` with `resolution_request = 1` -- **THEN** the older layer SHALL ignore the unknown fields -- **AND** continue normal capture operation - -#### Scenario: Resolution request message format - -- **GIVEN** the viewer wants to request a new resolution -- **WHEN** the viewer sends a `CaptureControl` message -- **THEN** the message SHALL contain: - - `resolution_request` flag set to 1 - - `requested_width` with desired width in pixels - - `requested_height` with desired height in pixels - -### Requirement: WSI Proxy Dynamic Resolution - -The layer SHALL support changing virtual surface resolution at runtime when in WSI proxy mode. - -#### Scenario: Resolution change request handling - -- **GIVEN** WSI proxy mode is enabled -- **AND** a virtual surface exists -- **WHEN** the layer receives a `CaptureControl` with `resolution_request = 1` -- **THEN** the layer SHALL update the virtual surface's configured resolution -- **AND** subsequent `vkGetPhysicalDeviceSurfaceCapabilitiesKHR` calls SHALL return the new resolution - -#### Scenario: Swapchain invalidation on resolution change - -- **GIVEN** WSI proxy mode is enabled -- **AND** a virtual swapchain exists -- **WHEN** the virtual surface resolution changes -- **THEN** the next `vkAcquireNextImageKHR` SHALL return `VK_ERROR_OUT_OF_DATE_KHR` -- **AND** the application SHALL recreate the swapchain - -#### Scenario: Resolution change ignored in non-proxy mode - -- **GIVEN** WSI proxy mode is disabled (normal capture mode) -- **WHEN** the layer receives a `CaptureControl` with `resolution_request = 1` -- **THEN** the layer SHALL ignore the resolution request -- **AND** continue capturing at the application's native resolution - -## MODIFIED Requirements - -### Requirement: Virtual Surface Resolution Configuration - -The layer SHALL allow configuring the virtual surface resolution via environment variables and runtime requests. - -#### Scenario: Default resolution - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_WIDTH` and `GOGGLES_HEIGHT` are not set -- **AND** no runtime resolution request has been received -- **WHEN** a virtual surface is created -- **THEN** the surface SHALL have resolution 1920x1080 - -#### Scenario: Custom resolution via environment - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_WIDTH` is set to a positive integer -- **AND** `GOGGLES_HEIGHT` is set to a positive integer -- **WHEN** a virtual surface is created -- **THEN** the surface SHALL have the specified resolution - -#### Scenario: Runtime resolution override - -- **GIVEN** WSI proxy mode is enabled -- **AND** a virtual surface exists -- **WHEN** a valid resolution request is received from the viewer -- **THEN** the surface resolution SHALL be updated to the requested values -- **AND** this SHALL override any environment variable settings \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/tasks.md b/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/tasks.md deleted file mode 100644 index fa6d73f4..00000000 --- a/openspec/changes/archive/2026-02-07-add-dynamic-resolution-request/tasks.md +++ /dev/null @@ -1,21 +0,0 @@ -## 1. Protocol Extension - -- [x] 1.1 Extend `CaptureControl` struct with `resolution_request` flag and `requested_width/height` fields -- [x] 1.2 Maintain struct size (use reserved fields) for backward compatibility - -## 2. Viewer Side (CaptureReceiver) - -- [x] 2.1 Add `request_resolution(uint32_t width, uint32_t height)` method -- [x] 2.2 Send `CaptureControl` message with resolution request - -## 3. Layer Side (vk_layer) - -- [x] 3.1 Parse resolution request fields in `IpcSocket::poll_control()` -- [x] 3.2 Add `set_resolution()` method to `WsiVirtualSurface` -- [x] 3.3 Update virtual surface capabilities to reflect new resolution -- [x] 3.4 Trigger swapchain out-of-date to force application swapchain recreation - -## 4. Application Integration - -- [x] 4.1 Call `request_resolution()` on window resize -- [x] 4.2 Optional: Add aspect ratio lock mode (explicitly deferred to a follow-up proposal) diff --git a/openspec/changes/archive/2026-02-07-add-extended-shader-support/design.md b/openspec/changes/archive/2026-02-07-add-extended-shader-support/design.md deleted file mode 100644 index a5253d8c..00000000 --- a/openspec/changes/archive/2026-02-07-add-extended-shader-support/design.md +++ /dev/null @@ -1,88 +0,0 @@ -## Context - -Mega-Bezel is the most complex RetroArch shader preset family with 660 presets. Supporting it validates our shader pipeline for virtually all RetroArch shaders. - -### Mega-Bezel Analysis (MBZ__3__STD__GDV) - -| Aspect | Value | -|--------|-------| -| Passes | 30 | -| LUT Textures | 23 (SamplerLUT1-4, IntroImage, TubeDiffuseImage, etc.) | -| Aliases | 20+ (DerezedPass, InfoCachePass, CRTPass, etc.) | -| Scale Types | source, viewport, absolute (up to 800x600) | -| Framebuffer Formats | float_framebuffer, srgb_framebuffer | - -### Key Technical Gaps - -1. **#reference directive** - Mega-Bezel uses modular preset structure: - ``` - MBZ__3__STD.slangp → #reference "Base_CRT_Presets/MBZ__3__STD__GDV.slangp" - ``` - -2. **OriginalHistory** - hsm-afterglow0.slang samples previous frame for phosphor persistence - -3. **frame_count_mod** - NTSC passes use `frame_count_mod = 2` for alternating scanlines - -## Decisions - -### Frame History Ring Buffer - -Store N previous Original textures in a circular buffer. - -```cpp -struct FrameHistory { - static constexpr uint32_t MAX_HISTORY = 7; // OriginalHistory0-6 - std::array textures; - uint32_t write_index = 0; - - void push(const Texture& original); - Texture* get(uint32_t age); // 0 = previous frame -}; -``` - -Auto-detect required depth by scanning shader samplers for `OriginalHistory[N]` pattern. - -### Reference Directive Parsing - -Parse `#reference "path"` before INI parsing: - -```cpp -Result PresetParser::load(const fs::path& path, int depth = 0) { - if (depth > 8) return Error{"Reference depth exceeded"}; - - auto content = read_file(path); - if (content.starts_with("#reference")) { - auto ref_path = parse_reference(content); - return load(path.parent_path() / ref_path, depth + 1); - } - return parse_ini(content, path); -} -``` - -### frame_count_mod Per-Pass - -Store in PassConfig and apply in SemanticBinder: - -```cpp -struct PassConfig { - // ... existing fields - uint32_t frame_count_mod = 0; // 0 = no modulo -}; - -// In SemanticBinder::populate_push_constants() -uint32_t frame_count = (pass.frame_count_mod > 0) - ? (absolute_frame % pass.frame_count_mod) - : absolute_frame; -``` - -## Risks / Trade-offs - -| Risk | Mitigation | -|------|------------| -| Frame history memory (7 textures) | Lazy allocation based on detected depth | -| Circular reference in presets | Depth limit of 8, path cycle detection | -| Performance with 30+ passes | Already handled by existing FilterChain | - -## Open Questions - -- Should we support `feedback_pass` for pass-level feedback (not just frame history)? \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-extended-shader-support/proposal.md b/openspec/changes/archive/2026-02-07-add-extended-shader-support/proposal.md deleted file mode 100644 index 8ec66e5c..00000000 --- a/openspec/changes/archive/2026-02-07-add-extended-shader-support/proposal.md +++ /dev/null @@ -1,47 +0,0 @@ -# Change: Add Extended RetroArch Shader Support - -## Why - -Current shader support covers basic CRT features (LUT textures, aliases, parameters). To support Mega-Bezel, NTSC compositing, and motion effects, we need additional RetroArch semantics. - -## What's Added - -### New Semantics -- **OriginalHistory[0-6]**: Previous frame textures for afterglow/motion effects -- **PassOutput#/PassFeedback#**: Inter-pass texture references by index -- **frame_count_mod**: Per-pass periodic frame counting (for NTSC alternating) -- **Rotation**: Display rotation push constant (0-3 for 0/90/180/270°) - -### Parser Extensions -- **#reference directive**: Preset inclusion for modular preset structure -- Recursive reference loading with depth limit (max 8) -- Path cycle detection to prevent infinite loops - -### Frame History -- Ring buffer for previous frames (MAX_HISTORY = 7) -- Lazy allocation based on shader requirements -- Auto-detect required depth from sampler names - -## Compatibility Results - -**Total: 1702/1906 presets compile (89%)** - -| Status | Categories | -|--------|------------| -| ✅ 100% | anti-aliasing, blurs, border, cel, deblur, deinterlacing, denoisers, dithering, downsample, edge-smoothing, film, gpu, handheld, hdr, interpolation, misc, motionblur, ntsc, pal, reshade, scanlines, sharpen, vhs, warp | -| ⚠️ Partial | bezel (757/958), crt (115/117), pixel-art-scaling (22/23) | - -### Known Limitations -- Mega Bezel GLASS/ADV presets: Missing `shaders` count due to complex #reference chains -- Some bezel presets require parameters not yet exposed via GUI - -## Impact - -- Affected code: - - `src/render/chain/filter_chain.*` - frame history, feedback textures - - `src/render/chain/filter_pass.*` - new semantic bindings - - `src/render/chain/semantic_binder.*` - OriginalHistory, PassOutput, PassFeedback - - `src/render/chain/preset_parser.*` - frame_count_mod, #reference - - `src/render/chain/frame_history.*` - new ring buffer implementation - - `src/render/chain/framebuffer.*` - waitIdle on shutdown -- Breaking changes: none diff --git a/openspec/changes/archive/2026-02-07-add-extended-shader-support/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-extended-shader-support/specs/render-pipeline/spec.md deleted file mode 100644 index eb6f20a6..00000000 --- a/openspec/changes/archive/2026-02-07-add-extended-shader-support/specs/render-pipeline/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -## ADDED Requirements - -### Requirement: Preset Reference Directive - -The preset parser SHALL support `#reference` directive for including other presets. - -#### Scenario: Mega-Bezel modular preset -- **GIVEN** a preset contains `#reference "Base_CRT_Presets/MBZ__3__STD__GDV.slangp"` -- **WHEN** the preset is parsed -- **THEN** the referenced preset SHALL be loaded and merged -- **AND** paths SHALL be resolved relative to the referencing file - -#### Scenario: Nested references -- **GIVEN** preset A references preset B which references preset C -- **WHEN** parsing completes -- **THEN** all references SHALL be resolved recursively -- **AND** final config SHALL contain merged settings from all presets - -#### Scenario: Reference depth limit -- **GIVEN** a reference chain exceeds 8 levels -- **WHEN** parsing is attempted -- **THEN** an error SHALL be returned with message indicating depth exceeded - -#### Scenario: Circular reference detection -- **GIVEN** preset A references preset B which references preset A -- **WHEN** parsing is attempted -- **THEN** an error SHALL be returned indicating circular reference - -### Requirement: Frame History Access - -The filter chain SHALL maintain a ring buffer of previous frame textures and expose them as OriginalHistory[0-6] samplers. - -#### Scenario: Afterglow accesses previous frame -- **GIVEN** a shader samples `OriginalHistory0` -- **WHEN** the pass is recorded -- **THEN** `OriginalHistory0` SHALL be bound to the previous frame's Original texture -- **AND** `OriginalHistory0Size` SHALL be populated as vec4 [width, height, 1/width, 1/height] - -#### Scenario: Motion interpolation accesses multiple frames -- **GIVEN** a shader samples `OriginalHistory1` and `OriginalHistory2` -- **WHEN** the pass is recorded -- **THEN** `OriginalHistory1` SHALL be bound to frame N-2 -- **AND** `OriginalHistory2` SHALL be bound to frame N-3 - -#### Scenario: History depth auto-detection -- **GIVEN** shaders reference `OriginalHistory3` as highest index -- **WHEN** filter chain initializes -- **THEN** a ring buffer of exactly 4 frames SHALL be allocated -- **AND** unused history slots SHALL NOT be allocated - -#### Scenario: First frames without history -- **GIVEN** filter chain has processed fewer frames than history depth -- **WHEN** OriginalHistory[N] is requested for unavailable frame -- **THEN** a black texture SHALL be bound as fallback - -### Requirement: Frame Count Modulo - -The filter chain SHALL apply per-pass frame_count_mod to the FrameCount semantic. - -#### Scenario: NTSC alternating lines -- **GIVEN** pass 27 sets `frame_count_mod27 = 2` -- **AND** current absolute frame is 157 -- **WHEN** FrameCount semantic is populated for pass 27 -- **THEN** FrameCount SHALL be 157 % 2 = 1 - -#### Scenario: Different modulo per pass -- **GIVEN** pass 5 sets `frame_count_mod5 = 4` -- **AND** pass 10 sets `frame_count_mod10 = 100` -- **WHEN** passes are recorded -- **THEN** each pass SHALL receive its own modulo-applied FrameCount - -#### Scenario: No modulo uses absolute count -- **GIVEN** no frame_count_mod is set for a pass -- **WHEN** FrameCount semantic is populated -- **THEN** FrameCount SHALL be the absolute frame count - -#### Scenario: Modulo value of zero -- **GIVEN** `frame_count_mod5 = 0` is set -- **WHEN** FrameCount semantic is populated for pass 5 -- **THEN** FrameCount SHALL be the absolute frame count (0 means disabled) - -### Requirement: Rotation Semantic - -The semantic binder SHALL provide Rotation push constant for display orientation. - -#### Scenario: No rotation (landscape) -- **GIVEN** display rotation is 0 degrees -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 0 - -#### Scenario: Portrait rotation (90 degrees) -- **GIVEN** display rotation is 90 degrees clockwise -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 1 - -#### Scenario: Inverted rotation (180 degrees) -- **GIVEN** display rotation is 180 degrees -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 2 - -#### Scenario: Portrait rotation (270 degrees) -- **GIVEN** display rotation is 270 degrees clockwise -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 3 \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-extended-shader-support/tasks.md b/openspec/changes/archive/2026-02-07-add-extended-shader-support/tasks.md deleted file mode 100644 index f6b9a9f5..00000000 --- a/openspec/changes/archive/2026-02-07-add-extended-shader-support/tasks.md +++ /dev/null @@ -1,61 +0,0 @@ -## 1. Preset Parser Extensions - -- [x] 1.1 Add `#reference` directive detection before INI parsing -- [x] 1.2 Implement recursive reference loading with depth limit (max 8) -- [x] 1.3 Add path cycle detection to prevent infinite loops -- [x] 1.4 Parse `frame_count_modN` per-pass into PassConfig -- [x] 1.5 Resolve relative paths for referenced presets - -## 2. Frame History Ring Buffer - -- [x] 2.1 Add FrameHistory struct with circular buffer (MAX_HISTORY = 7) -- [x] 2.2 Integrate FrameHistory into FilterChain -- [x] 2.3 Push Original texture to history each frame after processing -- [x] 2.4 Auto-detect required history depth from shader sampler names -- [x] 2.5 Lazy allocate history textures based on detected depth - -## 3. Semantic Binding Extensions - -- [x] 3.1 Bind OriginalHistory[0-6] textures by sampler name pattern -- [x] 3.2 Add OriginalHistorySize[0-6] via alias_size binding -- [x] 3.3 Apply frame_count_mod to FrameCount in FilterPass -- [x] 3.4 Add Rotation push constant (0-3 for 0/90/180/270 degrees) - -## 4. Unit Tests - -- [x] 4.1 Test #reference parsing with nested references -- [x] 4.2 Test #reference depth limit enforcement -- [x] 4.3 Test frame_count_mod parsing -- [x] 4.4 Test OriginalHistory sampler name pattern matching - -## 5. Feedback Texture Support - -- [x] 5.1 Detect *Feedback texture patterns from shader bindings -- [x] 5.2 Create feedback framebuffers for passes with aliases referenced as feedback -- [x] 5.3 Bind AliasFeedback textures during pass rendering -- [x] 5.4 Copy current framebuffer to feedback at end of frame -- [x] 5.5 Add AliasFeedbackSize semantics - -## 6. Integration Tests - -- [x] 6.1 Load and parse MBZ__5__POTATO preset (14 passes) -- [x] 6.2 Load and parse MBZ__3__STD preset (30 passes) -- [x] 6.3 Run ntsc-adaptive with frame_count_mod = 2 -- [x] 6.4 Visual verification of hsm-afterglow phosphor effect (tracked in shader compatibility - matrix; no functional blockers found in automated coverage) - -## 7. Spec Compliance (SHADER_SPEC.md) - -- [x] 7.1 Add PassOutput# texture bindings by pass number -- [x] 7.2 Add PassOutput#Size UBO members -- [x] 7.3 Add PassFeedback# bindings by pass number -- [x] 7.4 Add PassFeedback#Size UBO members -- [x] 7.5 Bind OriginalHistory0 = Original (spec requirement) -- [x] 7.6 Fix binding order (clear before set) - -## 8. Pending Issues (Mega Bezel) - -- [x] 8.1 Visual verification of Mega Bezel screen content (deferred to ongoing compatibility - sweep documented in `docs/shader_compatibility.md`) -- [x] 8.2 Verify InfoCachePass receives correct DerezedPassSize (deferred to ongoing compatibility - sweep documented in `docs/shader_compatibility.md`) diff --git a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/design.md b/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/design.md deleted file mode 100644 index 1e6c813d..00000000 --- a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/design.md +++ /dev/null @@ -1,46 +0,0 @@ -## Context -Goggles currently loads a shader preset once at startup from the runtime user config at `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` (bootstrapped from `config/goggles.template.toml` on first run when needed) or via the CLI and cannot change it until restart. There is no user interface beyond CLI flags, and passthrough requires editing the config to clear the preset path. We want to add a Dear ImGui-powered dockable UI so users can browse presets, trigger live reloads, and toggle passthrough without impacting the capture layer. - -## Goals / Non-Goals -- Goals: - - Integrate the Dear ImGui docking branch into the SDL3/Vulkan app UI so we can render control panels. - - Provide an ImGui "Shader Controls" dock that enumerates presets, shows the active selection, and tells the render pipeline to reload or bypass the filter chain. - - Allow passthrough toggling at runtime with zero shader passes and safe fallback to the previous preset. -- Non-Goals: - - No functionality inside the Vulkan capture layer; it must remain dependency-free. - - No persistence UX beyond reusing the existing config defaults (persisting selections is optional future work). - - No generalized file picker; we only need preset discovery from known directories for now. - -## Decisions -1. **Dear ImGui dependency scope** – Use the latest docking tag (`v1.91+ docking`) vendored or fetched via CPM, but link it solely with the Goggles app target. The capture layer build remains unchanged and never links ImGui symbols. -2. **UI integration** – Hook ImGui into the SDL3 event pump and Vulkan swapchain path used by the app window. ImGui draw data is rendered after the filter chain output pass each frame so it overlays results without altering the captured frame texture. -3. **Preset discovery** – Walk the `shader` section of the runtime user config at `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` (bootstrapped from `config/goggles.template.toml` on first run when needed) plus the `shaders/` directory tree at startup to create a preset catalog (relative + absolute paths). Expose this catalog to the UI and refresh it on demand via a "Rescan" action. -4. **Reload protocol** – Introduce a `PresetSelection` channel between the UI thread (main thread) and the render pipeline that carries `{path, passthrough_flag}`. When a new selection arrives, the filter chain waits until the current frame completes, destroys existing passes, and builds the new chain using the requested path (if passthrough is false). Failures leave the previous chain intact and surface an ImGui error toast. -5. **Passthrough handling** – Store the last successful preset metadata so we can restore it when passthrough is toggled off. When passthrough is on (or preset path empty), the filter chain routes DMA-BUFs directly to `OutputPass` without instantiating intermediate passes. - -## Risks / Trade-offs -- **UI perf impact** – ImGui adds CPU/GPU overhead. Mitigate by drawing once per frame with docking disabled when hidden. -- **Reload latency** – Rebuilding filter chains can stutter. Solution: perform rebuild between frames and double-buffer chain state so the previous chain finishes before swapping pointers. -- **Preset parsing errors** – Invalid presets triggered via UI could leave the system without a working chain. We keep the previous preset alive until the new one succeeds and surface errors to the UI/log. - -## Async Shader Reload (Implemented) -9. **Async compilation** – `reload_shader_preset()` spawns a background task via `JobSystem::submit()` to create a new ShaderRuntime and FilterChain, compile shaders, and load the preset. The main thread continues rendering with the old chain. -10. **Pending chain swap** – When the async task completes, it sets `m_pending_chain_ready` (atomic). The render loop calls `check_pending_chain_swap()` each frame to detect completion and swap in the new chain. -11. **Deferred destruction** – Old chains are queued for deferred destruction (`m_deferred_destroys` fixed-size array) to ensure in-flight frames complete before resources are freed. Cleanup happens in `cleanup_deferred_destroys()` called each frame. -12. **UI notification** – `consume_chain_swapped()` (atomic exchange) signals when a swap occurred so the main loop can update UI parameters with the new chain's parameter list. -13. **Shutdown safety** – `shutdown()` waits for pending async tasks with a 3-second timeout before proceeding with resource cleanup. - -## Migration Plan -1. Vendor/fetch ImGui docking sources and wire them into the app build. -2. Add ImGui initialization, frame begin/end hooks, and docking layout persisted under `~/.config/goggles/` (optional) or session memory. -3. Implement the preset catalog + UI controls and connect them to a `PresetSelection` dispatcher. -4. Update the render pipeline to support runtime reload/passthrough toggles and add error reporting hooks consumed by the UI. - -## Shader Parameter Editing -6. **Parameter access** – Expose `FilterChain::get_all_parameters()` returning a flat list of `{pass_index, ShaderParameter, current_value}` tuples. The UI iterates this list and renders sliders/inputs per parameter. -7. **Parameter modification** – `FilterChain::set_parameter(pass_index, name, value)` delegates to `FilterPass::set_parameter_override()` and triggers `update_ubo_parameters()`. Changes apply on the next frame without reload. -8. **Parameter reset** – `FilterChain::clear_parameter_overrides()` clears all pass overrides and restores preset defaults. - -## Open Questions -- Should preset selections persist back to `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`, or remain session-only? -- Do we constrain the preset catalog to curated directories, or expose a file picker for arbitrary paths? diff --git a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/proposal.md b/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/proposal.md deleted file mode 100644 index 5a6a65ad..00000000 --- a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/proposal.md +++ /dev/null @@ -1,14 +0,0 @@ -# Change: Runtime ImGui Shader Controls - -## Why -Users currently must restart Goggles or pass CLI overrides to experiment with shader presets, and there is no UI to toggle passthrough processing. Integrating an ImGui-based control surface lets us reload presets instantly, inspect active settings, and switch between passthrough and filter chains without touching CLI flags. - -## What Changes -- Integrate the latest Dear ImGui docking branch into the Goggles application UI layer (SDL3 window) without adding dependencies to the capture layer. -- Add a dockable "Shader Controls" panel that surfaces the active preset path, enumerates available presets, and emits reload requests for the render pipeline. -- Add a passthrough toggle in the UI that bypasses the filter chain and falls back to direct DMA-BUF blitting when enabled. -- Extend the render pipeline to rebuild the filter chain at runtime when a new preset or passthrough state is selected, handling errors gracefully and falling back to the previous preset when reload fails. - -## Impact -- Affected specs: `app-window`, `render-pipeline` -- Affected code: `src/app` (SDL3 + ImGui integration), `src/render/chain`, `src/render/shader`, `config/` load + runtime wiring. diff --git a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/app-window/spec.md deleted file mode 100644 index 2eef846c..00000000 --- a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/app-window/spec.md +++ /dev/null @@ -1,53 +0,0 @@ -## ADDED Requirements -### Requirement: Docked ImGui Control Surface -The Goggles application SHALL embed the latest Dear ImGui docking branch UI inside the SDL3 window to expose runtime controls without impacting the Vulkan capture layer. - -#### Scenario: ImGui docking build -- **GIVEN** the Goggles app target is built -- **WHEN** dependencies are resolved -- **THEN** the Dear ImGui docking branch (>= 1.91) SHALL be linked into the app executable -- **AND** an ImGui dockspace SHALL be rendered each frame after the filter chain output -- **AND** SDL3 events SHALL be forwarded into ImGui so widgets respond to input - -#### Scenario: UI scope limited to app -- **GIVEN** the Vulkan capture layer is compiled separately -- **WHEN** ImGui is added to the repository -- **THEN** no ImGui headers or libraries SHALL be included or linked into `src/capture/vk_layer` -- **AND** only the Goggles app binary SHALL depend on ImGui symbols - -### Requirement: Shader Control Panel UI -The Goggles application SHALL expose a dockable "Shader Controls" panel that shows the active shader preset, enumerates available presets, and emits runtime preset or passthrough selection events. - -#### Scenario: Preset selection list -- **GIVEN** the runtime user config at `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` (bootstrapped from `config/goggles.template.toml` on first run when needed) contains a `[shader] preset` value and the `shaders/` tree contains `.slangp` files -- **WHEN** the Shader Controls panel is opened -- **THEN** it SHALL display the active preset path -- **AND** it SHALL list discoverable preset entries sourced from the config value and presets found under `shaders/` -- **AND** selecting a preset from the list SHALL send a reload request containing the new path to the render pipeline - -#### Scenario: Passthrough toggle from UI -- **GIVEN** the Shader Controls panel is visible -- **WHEN** the user toggles "Passthrough (no filter chain)" -- **THEN** the UI SHALL highlight the passthrough state -- **AND** it SHALL emit a selection event instructing the render pipeline to bypass or restore the filter chain accordingly - -### Requirement: Shader Parameter Controls -The Goggles application SHALL expose runtime shader parameter editing through the ImGui interface, allowing users to adjust filter chain parameters in real-time. - -#### Scenario: Parameter display -- **GIVEN** a shader preset is loaded with parameters (min, max, step, description) -- **WHEN** the Shader Controls panel displays parameters -- **THEN** it SHALL list all available parameters with their current values -- **AND** each parameter SHALL be editable via slider or input field respecting min/max bounds - -#### Scenario: Parameter modification -- **GIVEN** the user adjusts a shader parameter value -- **WHEN** the new value is within valid bounds -- **THEN** the filter chain SHALL update the parameter override -- **AND** the change SHALL take effect on the next rendered frame without requiring preset reload - -#### Scenario: Parameter reset -- **GIVEN** parameters have been modified from their defaults -- **WHEN** the user clicks "Reset to Defaults" -- **THEN** all parameter overrides SHALL be cleared -- **AND** the shader SHALL render using original preset values diff --git a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/render-pipeline/spec.md deleted file mode 100644 index 3966362d..00000000 --- a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/specs/render-pipeline/spec.md +++ /dev/null @@ -1,53 +0,0 @@ -## ADDED Requirements -### Requirement: Runtime Shader Preset Reload -The render pipeline SHALL support rebuilding the RetroArch filter chain at runtime when the application requests a new `.slangp` preset without forcing an application restart. - -#### Scenario: UI-triggered reload success -- **GIVEN** the Shader Controls panel emits a preset selection event containing `` -- **WHEN** the render pipeline handles the event -- **THEN** it SHALL wait for the in-flight frame to complete -- **AND** it SHALL destroy the current filter chain state -- **AND** it SHALL parse and instantiate the new preset from `` -- **AND** the next frame SHALL render using the newly loaded passes - -#### Scenario: Reload failure fallback -- **GIVEN** the render pipeline attempts to load a preset that fails to parse or compile -- **WHEN** the failure occurs -- **THEN** the previously active preset SHALL remain bound and continue rendering -- **AND** the failure SHALL be reported back to the UI/log so the user can pick a different preset - -### Requirement: Passthrough Mode Toggle -The render pipeline SHALL provide a passthrough mode that bypasses all filter passes and blits the captured frame directly when requested, while remembering the last successful preset for restoration. - -#### Scenario: Passthrough enabled at runtime -- **GIVEN** the Shader Controls panel requests passthrough mode -- **WHEN** the render pipeline processes the request -- **THEN** it SHALL stop invoking the filter chain and route the captured texture directly into `OutputPass` -- **AND** no RetroArch preset compilation SHALL occur while passthrough is active - -#### Scenario: Passthrough disabled restores preset -- **GIVEN** passthrough mode is active and the user turns it off -- **WHEN** the render pipeline receives the request -- **THEN** it SHALL reload the last successful preset (or the default from config if none exists) -- **AND** rendering SHALL resume using the restored filter chain without requiring an application restart - -### Requirement: Runtime Parameter Access -The render pipeline SHALL expose shader parameter metadata and runtime override capabilities so the UI layer can display and modify filter chain parameters. - -#### Scenario: Parameter list query -- **GIVEN** a filter chain with one or more passes is loaded -- **WHEN** the UI queries available parameters -- **THEN** the pipeline SHALL return a list of ShaderParameter (name, description, min, max, step, default, current value) -- **AND** parameters from all passes SHALL be accessible - -#### Scenario: Parameter override -- **GIVEN** a valid parameter name and value within bounds -- **WHEN** set_parameter_override(name, value) is called -- **THEN** the filter chain SHALL apply the override to the appropriate pass -- **AND** update_ubo_parameters() SHALL be invoked before the next render - -#### Scenario: Parameter reset -- **GIVEN** one or more parameters have been overridden -- **WHEN** clear_parameter_overrides() is called -- **THEN** all overrides SHALL be removed -- **AND** parameters SHALL revert to preset defaults on the next frame diff --git a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/tasks.md b/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/tasks.md deleted file mode 100644 index 42202f5f..00000000 --- a/openspec/changes/archive/2026-02-07-add-imgui-runtime-preset-control/tasks.md +++ /dev/null @@ -1,18 +0,0 @@ -## 1. Implementation -- [x] 1.1 Vendor or fetch the Dear ImGui docking branch, hook it into the SDL3/Vulkan render loop, and ensure it only links into the Goggles app (not the Vulkan capture layer). -- [x] 1.2 Build an ImGui "Shader Controls" dock/panel that shows the active preset, lists presets discovered from the config/shaders directory, and lets the user request a different preset or passthrough mode. -- [x] 1.3 Extend the render pipeline/filter chain to rebuild passes at runtime when a preset selection changes, with graceful fallback on failure. -- [x] 1.4 Implement a passthrough flag in the pipeline that bypasses the chain and reuses the zero-effect OutputPass when enabled, and restore the previous preset when disabled. -- [x] 1.5 Expose shader parameter access via FilterChain (get_all_parameters, set_parameter, clear_parameter_overrides) and render parameter sliders in the UI. -- [x] 1.6 Add validation/tests (unit or integration hooks) covering the reload/passthrough state machine and update docs/config defaults as needed. - - Covered by existing render/chain unit coverage and runtime build/test validation; docs are in `docs/filter_chain_workflow.md`. - -## 2. Async Shader Reload (Added) -- [x] 2.1 Implement async shader compilation using JobSystem to avoid blocking the render loop during preset changes. -- [x] 2.2 Add deferred destruction mechanism (fixed-size array) for old FilterChain/ShaderRuntime to prevent use-after-free. -- [x] 2.3 Add consume_chain_swapped() notification to update UI parameters after async chain swap completes. -- [x] 2.4 Refactor render methods (record_render_commands, record_clear_commands) to accept optional UI callback, reducing code duplication. - -## 3. Parameter UI Improvements (Added) -- [x] 3.1 Skip dummy/separator parameters (min == max) in UI, display as disabled text. -- [x] 3.2 Add debug logging for parameter update tracing. diff --git a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/design.md b/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/design.md deleted file mode 100644 index 22864294..00000000 --- a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/design.md +++ /dev/null @@ -1,40 +0,0 @@ -## Context - -The input compositor is headless and only forwards input while sending frame-done callbacks. -Non-Vulkan clients never appear in the Goggles viewer because no compositor render/capture path -exists. The viewer already knows how to import DMA-BUF frames from Vulkan capture, so the goal is -to reuse that pipeline for compositor-rendered frames. - -## Goals / Non-Goals - -- Goals: - - Render the selected non-Vulkan surface into a headless output. - - Export frames via DMA-BUF for zero-copy presentation. - - Use the existing surface selector (manual override + focus fallback). - - Keep input-only mode working if presentation cannot be enabled. -- Non-Goals: - - No compositor-wide layout/tiling. - - No changes to the Vulkan layer capture protocol. - - No multi-surface blending or overlays. - -## Decisions - -- Render on the compositor thread using `wlr_render_pass` into a `wlr_swapchain` buffer. -- Export DMA-BUF attributes from the swapchain buffer and duplicate the FD for the viewer thread. -- Present only the selected surface (manual override or focused surface) per commit callback. -- Treat presentation as optional: if swapchain creation fails, continue input forwarding only. - -## Risks / Trade-offs - -- Renderer/export format support varies by backend; only single-plane linear DMA-BUFs are accepted. -- Surface commits can be more frequent than viewer renders; last-frame caching is used to decouple. - -## Migration Plan - -- No config changes required. Presentation is enabled when swapchain creation succeeds. -- Failure to create the swapchain leaves existing input forwarding behavior intact. - -## Open Questions - -- Should the headless output size be configurable rather than fixed (currently 1920x1080)? -- Do we need format negotiation beyond linear formats for broader hardware coverage? diff --git a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/proposal.md b/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/proposal.md deleted file mode 100644 index 7e7b550f..00000000 --- a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/proposal.md +++ /dev/null @@ -1,43 +0,0 @@ -# Change: Add Non-Vulkan Surface Presentation - -## Why - -Non-Vulkan clients (Wayland/XWayland) can connect for input, but their frames never reach the -Goggles viewer. That blocks overlays, launchers, and desktop UIs from being visible, even though -input forwarding works. The viewer needs a compositor-driven presentation path for these clients. - -## What Changes - -- Render the selected non-Vulkan surface into a headless compositor output and export a DMA-BUF. -- Expose the latest compositor-presented frame via `InputForwarder` without altering input routing. -- When no Vulkan capture frame is available, feed compositor DMA-BUF frames into the existing - Vulkan viewer render path. -- Keep the Vulkan layer capture pipeline unchanged. -- Fall back to input-only mode if compositor presentation is unavailable. - -## Impact - -- Affected specs: `compositor-capture` (new) -- Affected code: - - `src/input/compositor_server.hpp` - SurfaceFrame and presentation state - - `src/input/compositor_server.cpp` - swapchain render/export of selected surface - - `src/input/input_forwarder.hpp` - compositor frame access API - - `src/input/input_forwarder.cpp` - forwarder passthrough - - `src/app/application.hpp` - surface frame tracking - - `src/app/application.cpp` - render path selection + DRM/Vulkan format mapping - - `src/util/drm_fourcc.hpp` - local DRM FourCC constants - - `src/util/drm_format.hpp` - DRM to Vulkan format mapping - -## Design Rationale - -- **Reuse existing viewer pipeline:** importing compositor DMA-BUF frames through the Vulkan backend - avoids a second renderer and keeps shader/UI handling centralized. -- **No wlr_scene:** the compositor is headless; a full scene graph adds complexity without benefit. -- **Selection via surface selector:** manual override already exists and maps cleanly to a single - presented surface. - -## Non-Goals - -- Multi-surface composition, stacking, or tiling. -- Replacing or modifying the Vulkan layer capture path. -- New IPC protocols for compositor frames. diff --git a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/specs/compositor-capture/spec.md b/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/specs/compositor-capture/spec.md deleted file mode 100644 index 2c7aa8c3..00000000 --- a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/specs/compositor-capture/spec.md +++ /dev/null @@ -1,31 +0,0 @@ -## ADDED Requirements -### Requirement: Non-Vulkan Surface Presentation -The system SHALL render a selected non-Vulkan client surface (Wayland or XWayland) into the -viewer using the compositor capture path when compositor presentation is available. - -#### Scenario: Present selected surface -- **GIVEN** a non-Vulkan client surface is connected to the compositor -- **AND** the surface is selected via the existing surface selector -- **WHEN** the compositor produces a new frame -- **THEN** the viewer presents the selected surface - -#### Scenario: Presentation unavailable -- **GIVEN** compositor presentation cannot be initialized -- **WHEN** non-Vulkan clients connect for input -- **THEN** input forwarding continues without presenting non-Vulkan frames - -### Requirement: DMA-BUF Export for Compositor Frames -The compositor capture path SHALL export frames using DMA-BUF for zero-copy presentation. - -#### Scenario: Export compositor frame via DMA-BUF -- **WHEN** the compositor renders a frame for the selected surface -- **THEN** it exports a DMA-BUF with width, height, format, stride, and modifier metadata -- **AND** the viewer imports and presents the frame without CPU readback - -### Requirement: Vulkan Layer Path Unchanged -The compositor capture path SHALL NOT alter the Vulkan layer capture behavior. - -#### Scenario: Vulkan capture unaffected -- **GIVEN** a Vulkan application is captured via the layer -- **WHEN** compositor capture is enabled -- **THEN** the Vulkan capture path continues to function as before diff --git a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/tasks.md b/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/tasks.md deleted file mode 100644 index ee746132..00000000 --- a/openspec/changes/archive/2026-02-07-add-non-vulkan-surface-present/tasks.md +++ /dev/null @@ -1,18 +0,0 @@ -## 1. Compositor Presentation Path - -- [x] 1.1 Create a headless output swapchain for compositor presentation. -- [x] 1.2 Render the selected surface into the swapchain and export DMA-BUF attributes. -- [x] 1.3 Cache the latest compositor frame for cross-thread retrieval. -- [x] 1.4 Reset cached compositor frames when the selection changes. - -## 2. Viewer Integration - -- [x] 2.1 Expose the compositor-presented frame via `InputForwarder`. -- [x] 2.2 Convert DRM formats to Vulkan formats in the viewer render path. -- [x] 2.3 Reuse the existing Vulkan backend UI/render path for compositor frames. - -## 3. Manual Verification - -- [x] 3.1 Build: `pixi run build -p debug` -- [x] 3.2 Launch Goggles with input forwarding enabled. -- [x] 3.5 Use the surface selector to pick the client and confirm it renders + input works. diff --git a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/proposal.md b/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/proposal.md deleted file mode 100644 index 3fb766a8..00000000 --- a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/proposal.md +++ /dev/null @@ -1,56 +0,0 @@ -# add-pass-parameter-interface - -## Summary - -Extend the `Pass` base class with a uniform interface for exposing tunable shader parameters. This enables internal passes (DownsamplePass, future post-chain passes) to expose runtime-adjustable uniforms through the same mechanism used by FilterPass. - -To validate the interface, DownsamplePass gains a runtime-selectable filter type parameter. - -## Problem - -Currently, shader parameters are only accessible through FilterPass, which handles RetroArch preset parameters. Internal passes like DownsamplePass have no mechanism to expose tunable uniforms (e.g., filter type selection) to the UI layer. - -The existing `ShaderParameter` type in `retroarch_preprocessor.hpp` already defines the parameter metadata structure. This proposal reuses that type to provide a consistent interface across all pass types. - -## Solution - -1. Add two virtual methods to the `Pass` base class: - - `get_shader_parameters()` - returns parameter metadata for UI rendering - - `set_shader_parameter(name, value)` - updates a parameter value - -2. Implement `filter_type` parameter in DownsamplePass with two options: - - **0 = Area** (current behavior) - weighted average of covered source pixels - - **1 = Gaussian** - Gaussian-weighted bilinear sampling (4 bilinear taps = 16 texels) - -Default implementations return empty/no-op, allowing passes to opt-in. - -## Scope - -- **In scope:** Pass interface extension, UI helper, DownsamplePass filter_type parameter -- **Out of scope:** Additional filter types beyond area and gaussian - -## Key Design Decisions - -1. **Reuse ShaderParameter** - no new types; matches RetroArch semantics -2. **Default empty implementation** - passes opt-in to parameters -3. **Float-based values** - filter type uses 0.0/1.0 internally, UI shows labels -4. **Separate from pipeline config** - resolution stays as explicit typed API -5. **Gaussian filter name** - clean user-facing name; bilinear optimization is impl detail - -## Filter Type Details - -| Value | Name | Description | -|-------|------|-------------| -| 0 | Area | Box filter with coverage weighting (current) | -| 1 | Gaussian | 4 bilinear taps approximating Gaussian kernel (16 texels effective) | - -The Gaussian filter uses strategically placed bilinear samples to approximate a Gaussian kernel. Each bilinear tap averages 4 texels via hardware filtering, so 4 taps effectively sample 16 texels with minimal ALU cost. - -## Dependencies - -- None (builds on existing infrastructure) - -## Risks - -- **Low:** Minimal interface change with backward-compatible default implementations -- **Low:** Shader change is additive (branch on filter_type uniform) diff --git a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/specs/render-pipeline/spec.md deleted file mode 100644 index b3fe590c..00000000 --- a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/specs/render-pipeline/spec.md +++ /dev/null @@ -1,131 +0,0 @@ -# Spec Delta: render-pipeline - -## MODIFIED Requirements - -### Requirement: Pass Infrastructure - -The render chain subsystem SHALL provide a Pass abstraction compatible with RetroArch shader system. - -#### Scenario: Pass interface - -- **GIVEN** a Pass implementation -- **WHEN** initialized with device, target format, num_sync_indices, and shader runtime -- **THEN** the pass SHALL create its pipeline with `VkPipelineRenderingCreateInfo` -- **AND** allocate `num_sync_indices` descriptor sets from its pool - -#### Scenario: PassContext provides rendering target - -- **GIVEN** a PassContext for recording -- **WHEN** passed to `Pass::record()` -- **THEN** it SHALL contain `target_image_view` (swapchain or intermediate image view) -- **AND** it SHALL contain `target_format` (for barrier transitions) -- **AND** it SHALL contain `source_texture` (previous pass output) -- **AND** it SHALL contain `original_texture` (normalized input) -- **AND** it SHALL contain `frame_index` for descriptor set selection -- **AND** it SHALL contain `output_extent` for viewport/scissor setup - -#### Scenario: Per-frame descriptor isolation - -- **GIVEN** num_sync_indices = 2 -- **WHEN** frame N is recording while frame N-1 is still on GPU -- **THEN** the pass SHALL update `descriptor_sets[N % 2]` -- **AND** NOT touch `descriptor_sets[(N-1) % 2]` -- **AND** no validation error SHALL occur - -## ADDED Requirements - -### Requirement: Pass Shader Parameter Interface - -The `Pass` base class SHALL provide virtual methods for exposing tunable shader uniforms, allowing internal passes to opt-in to runtime parameter adjustment. - -#### Scenario: Default implementation returns no parameters - -- **GIVEN** a Pass subclass that does not override parameter methods -- **WHEN** `get_shader_parameters()` is called -- **THEN** an empty vector SHALL be returned - -#### Scenario: Pass exposes shader parameters - -- **GIVEN** a Pass subclass that overrides `get_shader_parameters()` -- **WHEN** the method is called -- **THEN** a vector of `ShaderParameter` metadata SHALL be returned -- **AND** each entry SHALL include name, description, default, min, max, and step - -#### Scenario: Parameter value update - -- **GIVEN** a Pass with exposed parameters -- **WHEN** `set_shader_parameter(name, value)` is called -- **THEN** the internal parameter value SHALL be updated -- **AND** the change SHALL take effect on the next frame - -#### Scenario: FilterPass implements parameter interface - -- **GIVEN** a FilterPass with shader parameters from preprocessing -- **WHEN** `get_shader_parameters()` is called -- **THEN** it SHALL return the parameters extracted from the shader -- **AND** `set_shader_parameter()` SHALL update the parameter override map - -### Requirement: Downsample Filter Type Selection - -The DownsamplePass SHALL support runtime selection of downsampling filter algorithm via the shader parameter interface. - -#### Scenario: Area filter (default) - -- **GIVEN** DownsamplePass with filter_type = 0 -- **WHEN** downsampling is performed -- **THEN** weighted box filter SHALL be used -- **AND** each source pixel SHALL be weighted by coverage overlap - -#### Scenario: Gaussian filter - -- **GIVEN** DownsamplePass with filter_type = 1 -- **WHEN** downsampling is performed -- **THEN** Gaussian-weighted bilinear sampling SHALL be used -- **AND** 4 bilinear taps SHALL approximate a Gaussian kernel -- **AND** effective sampling SHALL cover 16 source texels - -#### Scenario: Filter type exposed as parameter - -- **GIVEN** a DownsamplePass instance -- **WHEN** `get_shader_parameters()` is called -- **THEN** a parameter named "filter_type" SHALL be returned -- **AND** min SHALL be 0, max SHALL be 1, step SHALL be 1 - -#### Scenario: Filter type runtime change - -- **GIVEN** DownsamplePass is actively rendering -- **WHEN** `set_shader_parameter("filter_type", 1.0)` is called -- **THEN** the next frame SHALL use Gaussian filter -- **AND** no pipeline rebuild SHALL occur - -### Requirement: Unified Pass Parameter UI - -The ImGui layer SHALL provide a reusable helper for rendering pass parameter controls. - -#### Scenario: Parameter sliders rendered for pass with parameters - -- **GIVEN** a Pass with non-empty `get_shader_parameters()` result -- **WHEN** the parameter UI helper is invoked -- **THEN** a slider SHALL be rendered for each parameter -- **AND** slider range SHALL use min/max from parameter metadata -- **AND** slider step SHALL use step from parameter metadata - -#### Scenario: Enum-style parameter rendered as combo box - -- **GIVEN** a parameter with step = 1 and integer min/max range -- **WHEN** the parameter UI helper is invoked -- **THEN** a combo box MAY be rendered instead of a slider -- **AND** values SHALL map to descriptive labels - -#### Scenario: No UI rendered for pass without parameters - -- **GIVEN** a Pass with empty `get_shader_parameters()` result -- **WHEN** the parameter UI helper is invoked -- **THEN** no UI elements SHALL be rendered - -#### Scenario: Parameter changes propagate to pass - -- **GIVEN** a parameter control is displayed -- **WHEN** the user adjusts the value -- **THEN** `set_shader_parameter(name, value)` SHALL be called on the pass -- **AND** the change SHALL be reflected in the next rendered frame diff --git a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/tasks.md b/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/tasks.md deleted file mode 100644 index ed13d403..00000000 --- a/openspec/changes/archive/2026-02-07-add-pass-parameter-interface/tasks.md +++ /dev/null @@ -1,45 +0,0 @@ -# Tasks - -## 1. Add virtual methods to Pass interface -- [x] Add `get_shader_parameters()` virtual method with empty default -- [x] Add `set_shader_parameter(name, value)` virtual method with empty default -- [x] Forward-declare or include ShaderParameter type in pass.hpp - -**Validation:** Build succeeds, existing passes compile without modification - -## 2. Implement interface in FilterPass -- [x] Override `get_shader_parameters()` to return `m_parameters` -- [x] Override `set_shader_parameter()` to update `m_parameter_overrides` -- [x] Mark UBO dirty when parameter changes (trigger `update_ubo_parameters()`) - -**Validation:** Existing shader parameter controls continue to work - -## 3. Add Gaussian filter to downsample shader -- [x] Add `filter_type` push constant to downsample.frag.slang -- [x] Implement Gaussian bilinear sampling path (4 taps, 16 texels effective) -- [x] Branch on filter_type: 0=area (current), 1=gaussian - -**Validation:** Shader compiles, visual comparison shows smoother output for gaussian - -## 4. Implement parameter interface in DownsamplePass -- [x] Add `m_filter_type` member variable (default 0.0) -- [x] Override `get_shader_parameters()` to return filter_type metadata -- [x] Override `set_shader_parameter()` to update m_filter_type -- [x] Pass filter_type to shader via push constants - -**Validation:** Filter type changes at runtime without rebuild - -## 5. Add UI helper for pass parameter rendering -- [x] Create `draw_pass_parameters(Pass*)` helper in imgui_layer.cpp -- [x] Render combo box for enum-style parameters (filter_type) -- [x] Render sliders for continuous parameters -- [x] Call `set_shader_parameter()` on value change - -**Validation:** Pre-chain section shows filter type dropdown - -## 6. Wire up parameter rendering in shader stage sections -- [x] Add parameter rendering call to Pre-Chain section -- [x] Add parameter rendering call to Post-Chain section -- [x] Skip rendering if `get_shader_parameters()` returns empty - -**Validation:** Manual test: filter type dropdown appears, switching works diff --git a/openspec/changes/archive/2026-02-07-add-postchain-stage/proposal.md b/openspec/changes/archive/2026-02-07-add-postchain-stage/proposal.md deleted file mode 100644 index 1195d59b..00000000 --- a/openspec/changes/archive/2026-02-07-add-postchain-stage/proposal.md +++ /dev/null @@ -1,43 +0,0 @@ -# Change: Introduce post-chain stage with generic pass infrastructure - -## Why - -The current `OutputPass` is a single, hardcoded pass that renders the final output to the swapchain using blit shaders. This design limits extensibility for post-RetroArch effects like scanline overlays, color grading, or format conversion that should run after the RetroArch shader chain but before final presentation. - -A post-chain stage, symmetric to the recently-implemented pre-chain, enables future post-processing while maintaining the existing blit-to-swapchain behavior as one pass in a vector. - -## What Changes - -### Post-Chain Infrastructure (generic, extensible) - -- Introduce post-chain as a **vector of passes** (`m_postchain_passes`) in `FilterChain`, analogous to `m_prechain_passes` -- Add corresponding **vector of framebuffers** (`m_postchain_framebuffers`) for intermediate results -- Post-chain executes after RetroArch passes; its final output goes to the swapchain -- The existing `OutputPass` becomes the **last** pass in the post-chain vector (always present) -- Post-chain passes can be added/configured independently (future: scanline overlay, HDR tonemapping, etc.) - -### OutputPass Refactoring - -- `OutputPass` remains unchanged internally (still uses blit.vert/frag.slang) -- `OutputPass` is added to `m_postchain_passes` during `FilterChain::create()` -- Remove `m_output_pass` single pointer, use `m_postchain_passes.back()` for final output -- `record_postchain()` iterates all post-chain passes, with the last one rendering to swapchain - -### Recording Flow - -Current: -``` -pre-chain passes -> RetroArch passes -> m_output_pass->record() -``` - -After: -``` -pre-chain passes -> RetroArch passes -> post-chain passes (last = output) -``` - -## Impact - -- Affected specs: render-pipeline -- Affected code: - - `src/render/chain/filter_chain.hpp` - add post-chain vectors, remove `m_output_pass` member - - `src/render/chain/filter_chain.cpp` - implement `record_postchain()`, refactor initialization diff --git a/openspec/changes/archive/2026-02-07-add-postchain-stage/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-postchain-stage/specs/render-pipeline/spec.md deleted file mode 100644 index ae893342..00000000 --- a/openspec/changes/archive/2026-02-07-add-postchain-stage/specs/render-pipeline/spec.md +++ /dev/null @@ -1,62 +0,0 @@ -## ADDED Requirements - -### Requirement: Post-Chain Infrastructure - -The filter chain subsystem SHALL provide a generic post-chain stage that executes after RetroArch passes and before final swapchain presentation. - -#### Scenario: Post-chain as vector of passes - -- **GIVEN** `FilterChain` is initialized -- **THEN** it SHALL maintain `m_postchain_passes` as a vector of `Pass` pointers -- **AND** it SHALL maintain `m_postchain_framebuffers` as a vector of `Framebuffer` pointers -- **AND** the vectors SHALL have the same count (minus one for final output) - -#### Scenario: Post-chain execution order - -- **GIVEN** post-chain contains N passes -- **WHEN** `record_postchain()` is called -- **THEN** passes SHALL execute in vector order (0 to N-1) -- **AND** each pass output SHALL become the next pass input -- **AND** the final pass SHALL render to the swapchain - -#### Scenario: OutputPass as final post-chain entry - -- **GIVEN** `FilterChain` is initialized -- **THEN** `OutputPass` SHALL be added as the last entry in `m_postchain_passes` -- **AND** it SHALL always be present (minimum post-chain size is 1) -- **AND** no framebuffer SHALL be allocated for the final pass (renders to swapchain) - -#### Scenario: Post-chain extensibility - -- **GIVEN** a post-processing effect is needed after RetroArch passes -- **WHEN** a new pass is added to `m_postchain_passes` before OutputPass -- **THEN** it SHALL receive the RetroArch chain output as input -- **AND** its output SHALL be passed to subsequent post-chain passes - -## MODIFIED Requirements - -### Requirement: OutputPass Behavior - -The `OutputPass` SHALL serve as the final post-chain pass, rendering to the swapchain. - -#### Scenario: OutputPass in post-chain vector - -- **GIVEN** `FilterChain` is initialized -- **THEN** `OutputPass` SHALL be stored in `m_postchain_passes` vector -- **AND** there SHALL NOT be a separate `m_output_pass` member pointer -- **AND** `m_postchain_passes.back()` SHALL reference the OutputPass - -#### Scenario: Direct DMA-BUF to swapchain (no RetroArch passes) - -- **GIVEN** no RetroArch shader passes are configured -- **WHEN** OutputPass processes a frame -- **THEN** it SHALL sample `ctx.source_texture` (the pre-chain output or DMA-BUF import) -- **AND** begin dynamic rendering with `ctx.target_image_view` -- **AND** use `ctx.frame_index` for descriptor set selection - -#### Scenario: Post-RetroArch to swapchain - -- **GIVEN** RetroArch shader passes are configured -- **WHEN** OutputPass (as final post-chain pass) processes a frame -- **THEN** it SHALL sample the previous post-chain pass output (or RetroArch output if first) -- **AND** render to the swapchain image view diff --git a/openspec/changes/archive/2026-02-07-add-postchain-stage/tasks.md b/openspec/changes/archive/2026-02-07-add-postchain-stage/tasks.md deleted file mode 100644 index bbd5554c..00000000 --- a/openspec/changes/archive/2026-02-07-add-postchain-stage/tasks.md +++ /dev/null @@ -1,25 +0,0 @@ -## 1. Implement Generic Post-Chain Infrastructure - -- [x] 1.1 Add `m_postchain_passes` vector to `FilterChain` (type: `std::vector>`) -- [x] 1.2 Add `m_postchain_framebuffers` vector to `FilterChain` for intermediate results -- [x] 1.3 Rename `PreChainResult` to `ChainResult` (used by both pre-chain and post-chain) -- [x] 1.4 Implement `record_postchain()` to iterate all post-chain passes - -## 2. Refactor OutputPass Integration - -- [x] 2.1 Remove `m_output_pass` single member pointer from `FilterChain` -- [x] 2.2 Add `OutputPass` to `m_postchain_passes` vector during `FilterChain::create()` -- [x] 2.3 Update `record()` to call `record_postchain()` instead of `m_output_pass->record()` -- [x] 2.4 Update `shutdown()` to iterate `m_postchain_passes` vector - -## 3. Update Recording Flow - -- [x] 3.1 Ensure `record_postchain()` chains pass outputs correctly (source_view progression) -- [x] 3.2 Final pass (OutputPass) renders to swapchain via `ctx.target_image_view` -- [x] 3.3 Intermediate passes render to `m_postchain_framebuffers[i]` - -## 4. Verification - -- [x] 4.1 Verify quality build passes -- [x] 4.2 Verify existing behavior unchanged (OutputPass still renders correctly) -- [x] 4.3 Verify no validation errors with RetroArch shader presets diff --git a/openspec/changes/archive/2026-02-07-add-prechain-downsample/proposal.md b/openspec/changes/archive/2026-02-07-add-prechain-downsample/proposal.md deleted file mode 100644 index efb4e10c..00000000 --- a/openspec/changes/archive/2026-02-07-add-prechain-downsample/proposal.md +++ /dev/null @@ -1,38 +0,0 @@ -# Change: Introduce pre-chain stage with area downsampling pass - -## Why - -The `--app-width` / `--app-height` CLI options currently only work with WSI proxy mode. Users need a way to control the source resolution fed into the RetroArch filter chain regardless of capture mode. A pre-chain stage enables resolution control and other preprocessing for shader effects that benefit from modified input (CRT simulation, pixel art upscalers) without requiring WSI proxy overhead. - -## What Changes - -### Pre-Chain Infrastructure (generic, extensible) - -- Introduce pre-chain as a **vector of passes** (`m_prechain_passes`) in `FilterChain`, analogous to `m_passes` for RetroArch -- Add corresponding **vector of framebuffers** (`m_prechain_framebuffers`) for intermediate results -- Pre-chain executes before RetroArch passes; its final output becomes `original_view` for the RetroArch chain -- Pre-chain passes can be added/configured independently (future: sharpening, color correction, etc.) - -### Downsample Pass (first pre-chain pass) - -- Add `downsample.frag.slang` shader in `shaders/internal/` using area filtering -- Create `DownsamplePass` class implementing the pass interface -- Add `DownsamplePass` to pre-chain when `--app-width`/`--app-height` are specified -- Support single-dimension specification with aspect-ratio preservation - -### CLI Semantics - -- Change `--app-width`/`--app-height` semantics: set source resolution for filter chain input (all capture modes) -- Support single-dimension: other dimension calculated from captured frame's aspect ratio -- Store configured resolution in `Config::Render` - -## Impact - -- Affected specs: render-pipeline -- Affected code: - - `shaders/internal/downsample.frag.slang` (new) - - `src/render/chain/downsample_pass.hpp/cpp` (new) - - `src/render/chain/filter_chain.hpp` - add pre-chain vectors - - `src/render/chain/filter_chain.cpp` - implement generic pre-chain recording - - `src/app/cli.cpp` - update option descriptions - - `src/util/config.hpp` - add source resolution fields diff --git a/openspec/changes/archive/2026-02-07-add-prechain-downsample/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-prechain-downsample/specs/render-pipeline/spec.md deleted file mode 100644 index c1016e6c..00000000 --- a/openspec/changes/archive/2026-02-07-add-prechain-downsample/specs/render-pipeline/spec.md +++ /dev/null @@ -1,93 +0,0 @@ -## ADDED Requirements - -### Requirement: Pre-Chain Stage Infrastructure - -The filter chain SHALL support a generic pre-chain stage that processes captured frames before the RetroArch shader passes. The pre-chain is a vector of passes, analogous to the RetroArch pass vector, allowing multiple preprocessing steps. - -#### Scenario: Pre-chain as extensible pass vector - -- **GIVEN** `FilterChain` is initialized -- **WHEN** pre-chain passes are configured -- **THEN** `m_prechain_passes` SHALL be a vector capable of holding multiple passes -- **AND** `m_prechain_framebuffers` SHALL be a vector of corresponding framebuffers -- **AND** passes SHALL execute in vector order - -#### Scenario: Pre-chain disabled by default - -- **GIVEN** no pre-chain passes are configured -- **WHEN** `FilterChain::record()` executes -- **THEN** captured frames SHALL pass directly to RetroArch passes (or OutputPass in passthrough mode) - -#### Scenario: Pre-chain output becomes Original for RetroArch chain - -- **GIVEN** pre-chain contains one or more passes -- **WHEN** `FilterChain::record()` executes -- **THEN** pre-chain passes SHALL execute first in vector order -- **AND** the final pre-chain output SHALL be used as `original_view` for RetroArch passes -- **AND** `OriginalSize` semantic SHALL reflect final pre-chain output dimensions - -#### Scenario: Generic pre-chain recording - -- **GIVEN** pre-chain contains N passes -- **WHEN** `record_prechain()` executes -- **THEN** each pass SHALL receive the previous pass's output as input -- **AND** image barriers SHALL be inserted between passes -- **AND** the loop SHALL NOT be hardcoded to a specific pass type - -### Requirement: Downsample Pass - -The internal pass library SHALL include an area-filter downsampling pass that can be added to the pre-chain. - -#### Scenario: Area filter downsampling - -- **GIVEN** source image at 1920x1080 and target resolution 640x480 -- **WHEN** downsample pass executes -- **THEN** each output pixel SHALL be computed as a weighted average of covered source pixels -- **AND** the result SHALL exhibit minimal aliasing compared to point sampling - -#### Scenario: Downsample added to pre-chain when configured - -- **GIVEN** source resolution is configured via `--app-width` and/or `--app-height` -- **WHEN** `FilterChain` is created -- **THEN** a `DownsamplePass` SHALL be added to `m_prechain_passes` -- **AND** a framebuffer sized to target resolution SHALL be added to `m_prechain_framebuffers` - -#### Scenario: Identity passthrough at same resolution - -- **GIVEN** source and target resolution are identical -- **WHEN** downsample pass executes -- **THEN** output SHALL exactly match input -- **AND** no blurring or aliasing SHALL occur - -### Requirement: Source Resolution CLI Semantics - -The `--app-width` and `--app-height` CLI options SHALL configure the downsample pass in the pre-chain. Either option may be specified alone, with the other dimension calculated to preserve aspect ratio. - -#### Scenario: Both dimensions specified - -- **GIVEN** user specifies `--app-width 640 --app-height 480` -- **WHEN** Goggles starts -- **THEN** `DownsamplePass` SHALL be added to pre-chain with target 640x480 - -#### Scenario: Only width specified preserves aspect ratio - -- **GIVEN** user specifies `--app-width 640` without `--app-height` -- **AND** captured frame is 1920x1080 (16:9 aspect ratio) -- **WHEN** first frame is processed -- **THEN** height SHALL be computed as `round(640 * 1080 / 1920) = 360` -- **AND** downsample pass target SHALL be 640x360 - -#### Scenario: Only height specified preserves aspect ratio - -- **GIVEN** user specifies `--app-height 480` without `--app-width` -- **AND** captured frame is 1920x1080 (16:9 aspect ratio) -- **WHEN** first frame is processed -- **THEN** width SHALL be computed as `round(480 * 1920 / 1080) = 853` -- **AND** downsample pass target SHALL be 853x480 - -#### Scenario: Options still set environment variables - -- **GIVEN** user specifies `--app-width` and/or `--app-height` -- **WHEN** target app is launched -- **THEN** `GOGGLES_WIDTH` and `GOGGLES_HEIGHT` environment variables SHALL be set for specified dimensions -- **AND** WSI proxy (if enabled) SHALL use these values for virtual surface sizing diff --git a/openspec/changes/archive/2026-02-07-add-prechain-downsample/tasks.md b/openspec/changes/archive/2026-02-07-add-prechain-downsample/tasks.md deleted file mode 100644 index 7482b119..00000000 --- a/openspec/changes/archive/2026-02-07-add-prechain-downsample/tasks.md +++ /dev/null @@ -1,41 +0,0 @@ -## 1. Create Area Downsample Shader - -- [x] 1.1 Create `shaders/internal/downsample.frag.slang` with area filter algorithm -- [x] 1.2 Add push constant for source/target dimensions to calculate sample weights -- [x] 1.3 Verify shader compiles with Slang in HLSL mode - -## 2. Create DownsamplePass Class - -- [x] 2.1 Create `downsample_pass.hpp/cpp` with pass interface -- [x] 2.2 Implement pipeline creation, descriptor sets, push constants -- [x] 2.3 Implement `record()` method for command buffer recording - -## 3. Implement Generic Pre-Chain Infrastructure - -- [x] 3.1 Add `m_prechain_passes` vector to `FilterChain` (not single `m_downsample_pass`) -- [x] 3.2 Add `m_prechain_framebuffers` vector to `FilterChain` (not single `m_downsample_framebuffer`) -- [x] 3.3 Implement `add_prechain_pass()` method to append passes to pre-chain -- [x] 3.4 Implement `record_prechain()` to iterate all pre-chain passes (not hardcoded downsample) -- [x] 3.5 Ensure pre-chain output becomes `original_view` for RetroArch chain - -## 4. Integrate Downsample Pass into Pre-Chain - -- [x] 4.1 Add `DownsamplePass` to pre-chain when source resolution configured -- [x] 4.2 Size final pre-chain framebuffer to configured resolution -- [x] 4.3 Implement lazy initialization for single-dimension (aspect-ratio calculation) - -## 5. Update CLI and Config - -- [x] 5.1 Update `--app-width`/`--app-height` help text to describe new semantics -- [x] 5.2 Add `source_width`/`source_height` to `Config::Render` -- [x] 5.3 Remove validation requiring both dimensions (allow single-dimension specification) -- [x] 5.4 Pass configured resolution through to `FilterChain::create()` - -## 6. Integration and Testing - -- [x] 6.1 Test downsampling with high-res capture (e.g., 1920x1080 -> 640x480) -- [x] 6.2 Verify RetroArch shaders receive correct `OriginalSize` after downsampling -- [x] 6.3 Test with `--app-width 320 --app-height 240` to simulate retro resolution -- [x] 6.4 Verify existing behavior unchanged when options not provided -- [x] 6.5 Test single-dimension: `--app-width 640` with 1920x1080 source -> 640x360 -- [x] 6.6 Test single-dimension: `--app-height 480` with 1920x1080 source -> 854x480 diff --git a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/design.md b/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/design.md deleted file mode 100644 index dbeacf82..00000000 --- a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/design.md +++ /dev/null @@ -1,28 +0,0 @@ -## Context -Scale mode is currently cached in application state while rendering uses a copy stored in the Vulkan backend. Dynamic mode resolution requests depend on the application copy, which prevents runtime switching and risks stale requests. - -## Goals / Non-Goals -- Goals: - - Centralize scale mode ownership in the Vulkan backend. - - Expose runtime controls via the existing Pre-Chain UI section. - - Ensure dynamic mode resolution requests always reflect the active backend state. -- Non-Goals: - - Changing RetroArch per-pass scale types. - - Altering shader preset semantics or pass scale behavior. - -## Decisions -- Decision: Store scale mode and integer scale in VulkanBackend as the single source of truth with explicit getters/setters. -- Decision: Route ImGui changes through Application callbacks into VulkanBackend, keeping UI state synchronized with backend accessors. -- Decision: Trigger dynamic resolution requests based on backend scale mode at swapchain resize and on dynamic-mode activation. - -## Risks / Trade-offs -- Risk: UI/backend desynchronization if callbacks are not wired consistently. - - Mitigation: Initialize UI state from backend getters and re-sync after preset reloads. - -## Migration Plan -1. Add backend accessors + update API. -2. Wire ImGui Pre-Chain controls to backend setters. -3. Update Application dynamic-resolution checks to use backend getters. - -## Open Questions -- Should integer scale changes be applied immediately or deferred until next frame boundary? diff --git a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/proposal.md b/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/proposal.md deleted file mode 100644 index 0bd86e0e..00000000 --- a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/proposal.md +++ /dev/null @@ -1,21 +0,0 @@ -# Change: Add Runtime Scale Mode Switching - -## Why - -Scale mode is currently split between application state and the Vulkan backend, which prevents runtime switching and makes dynamic resolution requests depend on stale state. Consolidating scale mode ownership in the backend and exposing runtime controls in the Pre-Chain UI enables live tuning without restarting the viewer and ensures dynamic mode requests always reflect the active scale mode. - -## What Changes - -- Make the Vulkan backend the single source of truth for the active render scale mode (and integer scale) with read/write accessors -- Route application dynamic-resolution requests through the backend’s current scale mode -- Add a scale mode selector to the existing Pre-Chain stage controls in the ImGui shader controls window -- Apply scale mode changes immediately to subsequent frames without restarting the viewer - -## Impact - -- Affected specs: `render-pipeline` -- Affected code: - - `src/app/application.hpp/cpp` (dynamic resolution request logic) - - `src/render/backend/vulkan_backend.hpp/cpp` (scale mode ownership + accessors) - - `src/ui/imgui_layer.hpp/cpp` (Pre-Chain UI controls + callbacks) - - `src/render/chain/filter_chain.hpp/cpp` (runtime scale mode propagation) diff --git a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/specs/render-pipeline/spec.md deleted file mode 100644 index 359f14d2..00000000 --- a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/specs/render-pipeline/spec.md +++ /dev/null @@ -1,48 +0,0 @@ -## ADDED Requirements - -### Requirement: Render Scale Mode Ownership - -The render backend SHALL own the active scale mode and integer scale values and expose them for application and UI synchronization. - -#### Scenario: Query returns current runtime state - -- **GIVEN** the active scale mode has been updated at runtime -- **WHEN** the application queries the render backend for the active scale mode -- **THEN** the backend SHALL return the updated mode -- **AND** the current integer scale SHALL be available alongside it - -### Requirement: Runtime Scale Mode Switching - -The viewer SHALL allow switching the render scale mode at runtime and apply changes to subsequent frames without restart. - -#### Scenario: UI change updates active scale mode - -- **GIVEN** the shader controls window is visible -- **WHEN** the user selects a new scale mode in the Pre-Chain section -- **THEN** the active render scale mode SHALL update without restart -- **AND** subsequent frames SHALL use the new mode - -#### Scenario: Dynamic mode request uses active backend state - -- **GIVEN** the capture receiver is connected -- **WHEN** the active scale mode is `dynamic` and the swapchain extent changes or dynamic mode becomes active -- **THEN** the viewer SHALL request the source resolution to match the swapchain extent -- **AND** no request SHALL be sent when the active scale mode is not `dynamic` - -### Requirement: Pre-Chain Scale Mode Controls - -The Pre-Chain stage UI SHALL expose controls for the viewer scale mode. - -#### Scenario: Scale mode selector is available - -- **GIVEN** the shader controls window is visible -- **WHEN** the Pre-Chain section is expanded -- **THEN** a scale mode selector SHALL be displayed -- **AND** it SHALL include fit, fill, stretch, integer, and dynamic options - -#### Scenario: Integer scale input visibility - -- **GIVEN** the scale mode selector is set to `integer` -- **WHEN** the Pre-Chain section is visible -- **THEN** an integer scale input SHALL be displayed -- **AND** changes SHALL update the active integer scale at runtime diff --git a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/tasks.md b/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/tasks.md deleted file mode 100644 index 49a0eaf9..00000000 --- a/openspec/changes/archive/2026-02-07-add-runtime-scale-mode-switching/tasks.md +++ /dev/null @@ -1,15 +0,0 @@ -## 1. Backend Ownership -- [x] 1.1 Add VulkanBackend accessors for scale_mode/integer_scale and a runtime update API -- [x] 1.2 Ensure render path uses backend state as the single source of truth -- [x] 1.3 Update Application dynamic-resolution logic to query backend scale mode - -## 2. ImGui Controls -- [x] 2.1 Extend ImGuiLayer state and callbacks for scale mode selection in Pre-Chain controls -- [x] 2.2 Add Pre-Chain UI for scale mode selection and integer scale (when integer mode is active) -- [x] 2.3 Sync ImGui state from backend accessors on startup and when changes occur - -## 3. Behavior Validation -- [x] 3.1 Verify dynamic mode sends resolution requests on activation and swapchain resize - - Verified via runtime code path and test build validation (`pixi run build -p test`, `pixi run test -p test`). -- [x] 3.2 Verify scale mode changes apply to subsequent frames without restart - - Verified via runtime code path and test build validation (`pixi run build -p test`, `pixi run test -p test`). diff --git a/openspec/changes/archive/2026-02-07-add-semaphore-export/design.md b/openspec/changes/archive/2026-02-07-add-semaphore-export/design.md deleted file mode 100644 index c18b5225..00000000 --- a/openspec/changes/archive/2026-02-07-add-semaphore-export/design.md +++ /dev/null @@ -1,91 +0,0 @@ -# Design: Cross-Process Timeline Semaphore Export - -## Context - -The capture layer needs efficient GPU synchronization with the Goggles application. Current architecture uses CPU-side semaphore wait in worker thread before IPC, adding latency. - -**Constraints:** -- Layer is pure C API (no vulkan-hpp) -- Must use OPAQUE_FD handle type (same driver requirement) -- Handle reconnection gracefully - -## Goals / Non-Goals - -**Goals:** -- Export two timeline semaphores for bidirectional GPU sync -- Implement back-pressure to throttle Layer when Goggles is slow -- Send semaphore FDs once per connection (not per frame) - -**Non-Goals:** -- Support SYNC_FD handle type (less portable) - -**Deprecation:** -- Async worker thread (replaced by cross-process semaphore sync) -- Sync fence fallback mode - -## Decisions - -### Two Semaphores vs Single Semaphore - -**Decision:** Use two separate timeline semaphores. - -**Rationale:** -- `frame_ready`: Layer signals N, Goggles waits N -- `frame_consumed`: Goggles signals N, Layer waits N-1 -- Clear separation of concerns, easier debugging -- Single semaphore with dual values is more complex - -### Sync Flow - -``` -Frame N: - Layer: wait(frame_consumed, N-1) → copy → signal(frame_ready, N) → send metadata - Goggles: recv metadata → wait(frame_ready, N) → render → signal(frame_consumed, N) -``` - -### Protocol Changes - -New message types in `capture_protocol.hpp`: - -```cpp -enum class CaptureMessageType : uint32_t { - client_hello = 1, - texture_data = 2, - control = 3, - semaphore_init = 4, // NEW: carries two semaphore FDs - frame_metadata = 5, // NEW: per-frame data with timeline value -}; - -struct CaptureSemaphoreInit { - CaptureMessageType type = CaptureMessageType::semaphore_init; - uint32_t version = 1; - uint64_t initial_value = 0; - // Two FDs via SCM_RIGHTS: [frame_ready_fd, frame_consumed_fd] -}; - -struct CaptureFrameMetadata { - CaptureMessageType type = CaptureMessageType::frame_metadata; - uint32_t width, height; - VkFormat format; - uint32_t stride, offset; - uint64_t modifier; - uint64_t frame_number; // Timeline value for this frame -}; -``` - -### Required Extensions - -Layer must enable: -- `VK_KHR_external_semaphore` -- `VK_KHR_external_semaphore_fd` - -## Risks / Trade-offs - -| Risk | Mitigation | -|------|------------| -| OPAQUE_FD requires same driver | Document requirement, fail gracefully | -| Deadlock on disconnect | Use timeout in WaitSemaphoresKHR (100ms) | - -## Open Questions - -- Timeout value for back-pressure wait (currently 100ms) \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-semaphore-export/proposal.md b/openspec/changes/archive/2026-02-07-add-semaphore-export/proposal.md deleted file mode 100644 index cb9b96f4..00000000 --- a/openspec/changes/archive/2026-02-07-add-semaphore-export/proposal.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change: Export Cross-Process Timeline Semaphores - -## Why - -Currently the Layer uses an internal timeline semaphore for GPU sync, with a Worker thread waiting on CPU-side before sending the DMA-BUF FD. This introduces extra CPU-GPU sync overhead and cannot achieve true back-pressure frame throttling. - -By exporting semaphores for Goggles to participate in GPU sync directly: -1. Eliminate CPU wait in Layer Worker thread -2. Enable GPU-to-GPU direct synchronization -3. Implement back-pressure via dual semaphores - Layer waits when Goggles is slow - -## What Changes - -- Create two exportable timeline semaphores: `frame_ready` (Layer→Goggles) and `frame_consumed` (Goggles→Layer) -- Export FDs using `VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT` -- Add new IPC message type to transfer semaphore FDs (once per connection) -- Modify capture_frame() to use dual-semaphore sync logic -- Goggles imports semaphores and uses them in render submission - -## Impact - -- Affected specs: `vk-layer-capture` -- Affected code: - - `src/capture/capture_protocol.hpp` - New message types - - `src/capture/vk_layer/vk_capture.cpp` - Semaphore creation and sync logic - - `src/capture/vk_layer/ipc_socket.cpp` - FD transfer - - `src/capture/capture_receiver.cpp` - Receive semaphores - - `src/render/backend/vulkan_backend.cpp` - Import and use semaphores \ No newline at end of file diff --git a/openspec/changes/archive/2026-02-07-add-semaphore-export/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-02-07-add-semaphore-export/specs/vk-layer-capture/spec.md deleted file mode 100644 index cef2d127..00000000 --- a/openspec/changes/archive/2026-02-07-add-semaphore-export/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,103 +0,0 @@ -## ADDED Requirements - -### Requirement: Cross-Process Semaphore Export - -The layer SHALL create and export timeline semaphores for cross-process GPU synchronization with the Goggles application. - -#### Scenario: Exportable semaphore creation - -- **WHEN** capture is initialized for a swapchain -- **THEN** the layer SHALL create two timeline semaphores with `VK_SEMAPHORE_TYPE_TIMELINE` -- **AND** use `VkExportSemaphoreCreateInfo` with `VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT` -- **AND** name them `frame_ready` (Layer signals) and `frame_consumed` (Goggles signals) - -#### Scenario: Semaphore FD export - -- **WHEN** exportable semaphores are created -- **THEN** the layer SHALL export file descriptors via `vkGetSemaphoreFdKHR` -- **AND** store the FDs for transfer to Goggles - -#### Scenario: Required extensions - -- **WHEN** `vkCreateDevice` is hooked -- **THEN** the layer SHALL add `VK_KHR_external_semaphore` to enabled extensions -- **AND** add `VK_KHR_external_semaphore_fd` to enabled extensions - -### Requirement: Semaphore IPC Transfer - -The layer SHALL transfer semaphore file descriptors to the Goggles application via Unix socket. - -#### Scenario: Semaphore init message - -- **WHEN** the first frame is captured after connection -- **THEN** the layer SHALL send a `semaphore_init` message via IPC -- **AND** attach two FDs via `SCM_RIGHTS`: `[frame_ready_fd, frame_consumed_fd]` -- **AND** include the initial timeline value (0) - -#### Scenario: One-time transfer - -- **GIVEN** semaphore FDs have been sent for this connection -- **WHEN** subsequent frames are captured -- **THEN** the layer SHALL NOT resend semaphore FDs -- **AND** SHALL send only frame metadata with timeline values - -### Requirement: Bidirectional GPU Synchronization - -The layer SHALL use the exported semaphores for bidirectional synchronization with Goggles. - -#### Scenario: Back-pressure wait - -- **GIVEN** frame N is being captured -- **AND** N > 1 -- **WHEN** `vkQueuePresentKHR` is called -- **THEN** the layer SHALL wait on `frame_consumed` semaphore for value N-1 -- **AND** use a timeout of 100ms to detect disconnection -- **AND** skip the frame if timeout occurs - -#### Scenario: Frame ready signal - -- **WHEN** the copy command is submitted -- **THEN** the layer SHALL signal `frame_ready` semaphore with value N -- **AND** increment the frame counter - -#### Scenario: Frame metadata transfer - -- **WHEN** the copy command is submitted -- **THEN** the layer SHALL send `frame_metadata` message via IPC -- **AND** include width, height, format, stride, offset, modifier -- **AND** include the frame number (timeline value N) - -### Requirement: Semaphore Reconnection Handling - -The layer SHALL handle client reconnection by resetting semaphore state. - -#### Scenario: Disconnect detection - -- **WHEN** `vkWaitSemaphoresKHR` times out -- **OR** IPC send fails -- **THEN** the layer SHALL mark semaphores as not sent -- **AND** reset frame counter to 0 - -#### Scenario: Re-export on reconnection - -- **WHEN** a new client connects after disconnect -- **THEN** the layer SHALL call `vkGetSemaphoreFdKHR` again for new FDs -- **AND** send `semaphore_init` message to the new client -- **AND** resume sync from frame 1 - -## MODIFIED Requirements - -### Requirement: Instance and Device Hooking - -The layer SHALL intercept `vkCreateDevice` to establish dispatch chains and add required extensions. - -#### Scenario: Device creation with extensions - -- **WHEN** the target application calls `vkCreateDevice` -- **THEN** the layer SHALL add required device extensions: - - `VK_KHR_EXTERNAL_MEMORY_FD` - - `VK_EXT_EXTERNAL_MEMORY_DMA_BUF` - - `VK_KHR_external_semaphore` - - `VK_KHR_external_semaphore_fd` -- **AND** enumerate and store all queues -- **AND** identify the graphics queue for capture operations diff --git a/openspec/changes/archive/2026-02-07-add-semaphore-export/tasks.md b/openspec/changes/archive/2026-02-07-add-semaphore-export/tasks.md deleted file mode 100644 index 58bc6984..00000000 --- a/openspec/changes/archive/2026-02-07-add-semaphore-export/tasks.md +++ /dev/null @@ -1,54 +0,0 @@ -# Tasks: Export Cross-Process Timeline Semaphores - -## 1. Protocol Layer -- [x] 1.1 Add `semaphore_init` message type to `capture_protocol.hpp` -- [x] 1.2 Add `frame_metadata` message type with timeline value field -- [x] 1.3 Add static_assert for struct sizes - -## 2. Layer - Vulkan Setup -- [x] 2.1 Add `GetSemaphoreFdKHR` to `VkDeviceFuncs` in `vk_dispatch.hpp` -- [x] 2.2 Add `VK_KHR_external_semaphore` and `VK_KHR_external_semaphore_fd` to required extensions -- [x] 2.3 Add `frame_ready_sem` and `frame_consumed_sem` fields to `SwapData` -- [x] 2.4 Modify `init_sync_primitives()` to create exportable timeline semaphores -- [x] 2.5 Export semaphore FDs via `vkGetSemaphoreFdKHR` - -## 3. Layer - IPC -- [x] 3.1 Add `send_semaphores()` to `ipc_socket.cpp` (SCM_RIGHTS with two FDs) -- [x] 3.2 Add `send_frame_metadata()` for per-frame data without FD -- [x] 3.3 Send semaphores on first frame after connection - -## 4. Layer - Sync Logic -- [x] 4.1 Modify `capture_frame()` to wait on `frame_consumed` before copy (back-pressure) -- [x] 4.2 Signal `frame_ready` after copy submission -- [x] 4.3 Handle timeout in wait (detect disconnect) -- [x] 4.4 Reset semaphore state on reconnection - -## 5. Goggles - Receiver -- [x] 5.1 Handle `semaphore_init` message in `capture_receiver.cpp` -- [x] 5.2 Extract two FDs from SCM_RIGHTS ancillary data -- [x] 5.3 Store FDs and expose via getters -- [x] 5.4 Handle `frame_metadata` message type - -## 6. Goggles - Backend -- [x] 6.1 Add `import_sync_semaphores()` to `vulkan_backend.cpp` -- [x] 6.2 Create timeline semaphores and import FDs via `vkImportSemaphoreFdKHR` -- [x] 6.3 Modify render submission to wait on `frame_ready` -- [x] 6.4 Signal `frame_consumed` after render - -## 7. Integration -- [x] 7.1 Call `import_sync_semaphores()` when semaphores received -- [x] 7.2 Handle fallback when import fails -- [x] 7.3 Test reconnection scenario - -## 8. Testing -- [x] 8.1 Test basic sync flow with vkcube -- [x] 8.2 Test back-pressure (slow Goggles) - - Back-pressure behavior validated through queue/timeout handling paths and reconnection coverage; dedicated soak profile can be tracked separately. -- [x] 8.3 Test reconnection - -## 9. Robustness (Added) -- [x] 9.1 Use raw C API for `vkGetMemoryFdPropertiesKHR` to handle stale DMA-BUF fds -- [x] 9.2 Use raw C API for `vkImportSemaphoreFdKHR` to handle stale semaphore fds -- [x] 9.3 Use raw C API for `vkAcquireNextImageKHR` to handle device errors gracefully -- [x] 9.4 Reset handles to null after destroy in `cleanup_imported_image()` -- [x] 9.5 Track `m_last_signaled_frame` to prevent signal value collision diff --git a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/proposal.md b/openspec/changes/archive/2026-02-07-add-shader-stage-controls/proposal.md deleted file mode 100644 index 8edb7a2d..00000000 --- a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/proposal.md +++ /dev/null @@ -1,21 +0,0 @@ -# Change: Add Shader Stage Controls to ImGui Window - -## Why - -The shader controls window currently only exposes RetroArch effect stage settings. The pre-chain stage (downsample pass) has hardcoded resolution that can only be set via CLI flags at startup. Runtime control over pre-chain resolution would enable interactive tuning without restarting the application. - -## What Changes - -- Restructure shader controls window into three collapsible sections: Pre-Chain, Effect, Post-Chain -- Add UI controls for pre-chain downsample resolution (width/height inputs) -- Add callback infrastructure to propagate pre-chain settings to FilterChain at runtime -- FilterChain exposes method to update pre-chain resolution dynamically -- Post-chain section shows placeholder (no controls for now) - -## Impact - -- Affected specs: `render-pipeline` (pre-chain runtime configuration) -- Affected code: - - `src/ui/imgui_layer.hpp/cpp` - UI state and drawing - - `src/render/chain/filter_chain.hpp/cpp` - runtime pre-chain update - - `src/app/application.cpp` - callback wiring diff --git a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-shader-stage-controls/specs/render-pipeline/spec.md deleted file mode 100644 index c972c0ab..00000000 --- a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/specs/render-pipeline/spec.md +++ /dev/null @@ -1,69 +0,0 @@ -## ADDED Requirements - -### Requirement: Shader Stage UI Organization - -The shader controls window SHALL organize controls into three collapsible sections corresponding to pipeline stages: Pre-Chain, Effect, and Post-Chain. - -#### Scenario: Pre-chain section displays downsample controls - -- **GIVEN** the shader controls window is visible -- **WHEN** the Pre-Chain section is expanded -- **THEN** resolution width and height input fields SHALL be displayed -- **AND** an "Apply" button SHALL be displayed to confirm changes - -#### Scenario: Effect section displays RetroArch controls - -- **GIVEN** the shader controls window is visible -- **WHEN** the Effect section is expanded -- **THEN** the shader enable checkbox SHALL be displayed -- **AND** the current preset label SHALL be displayed -- **AND** the available presets tree SHALL be displayed -- **AND** shader parameters SHALL be displayed if a preset is loaded - -#### Scenario: Post-chain section displays placeholder - -- **GIVEN** the shader controls window is visible -- **WHEN** the Post-Chain section is expanded -- **THEN** a label indicating "Output Blit" SHALL be displayed -- **AND** no controls SHALL be displayed - -### Requirement: Pre-Chain Pipeline Configuration - -The filter chain SHALL support runtime updates to pre-chain pipeline configuration (resolution) without requiring application restart. Pipeline configuration is distinct from shader parameters - it affects resource allocation and triggers pass rebuilds. - -#### Scenario: Resolution update triggers pass rebuild - -- **GIVEN** a pre-chain downsample pass exists -- **WHEN** `FilterChain::set_prechain_resolution(width, height)` is called with new values -- **THEN** existing pre-chain passes and framebuffers SHALL be cleared -- **AND** new passes SHALL be created on the next frame with the updated resolution - -#### Scenario: Resolution query returns current state - -- **GIVEN** a pre-chain resolution is configured -- **WHEN** `FilterChain::get_prechain_resolution()` is called -- **THEN** the current target width and height SHALL be returned - -#### Scenario: Zero resolution disables pre-chain - -- **GIVEN** pre-chain passes exist -- **WHEN** `set_prechain_resolution(0, 0)` is called -- **THEN** pre-chain processing SHALL be disabled -- **AND** captured frames SHALL pass directly to effect stage - -### Requirement: Pre-Chain UI State Synchronization - -The UI layer SHALL maintain synchronized state with the filter chain pre-chain configuration. - -#### Scenario: UI initialized from backend state - -- **GIVEN** the application starts with `--app-width 640 --app-height 480` -- **WHEN** the ImGui layer is initialized -- **THEN** the pre-chain resolution inputs SHALL display 640 and 480 - -#### Scenario: UI callback propagates changes - -- **GIVEN** the pre-chain section is visible -- **WHEN** the user changes resolution and clicks Apply -- **THEN** the pre-chain change callback SHALL be invoked -- **AND** the new resolution SHALL be passed to the filter chain diff --git a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/tasks.md b/openspec/changes/archive/2026-02-07-add-shader-stage-controls/tasks.md deleted file mode 100644 index c0968b55..00000000 --- a/openspec/changes/archive/2026-02-07-add-shader-stage-controls/tasks.md +++ /dev/null @@ -1,40 +0,0 @@ -## 1. UI State and Types - -- [x] 1.1 Add `PreChainState` struct to `imgui_layer.hpp` with `target_width`, `target_height`, and `dirty` flag -- [x] 1.2 Add `PreChainState prechain` member to `ShaderControlState` -- [x] 1.3 Add `PreChainChangeCallback` type alias for `std::function` -- [x] 1.4 Add `set_prechain_change_callback()` method to `ImGuiLayer` -- [x] 1.5 Add `set_prechain_state()` method to initialize UI from current backend state - -## 2. UI Drawing Refactor - -- [x] 2.1 Extract effect stage widgets from `draw_shader_controls()` into `draw_effect_stage_controls()` -- [x] 2.2 Create `draw_prechain_stage_controls()` with resolution width/height inputs -- [x] 2.3 Create `draw_postchain_stage_controls()` as placeholder section (displays "Output Blit") -- [x] 2.4 Restructure `draw_shader_controls()` to use three `CollapsingHeader` sections -- [x] 2.5 Add "Apply" button in pre-chain section that invokes callback when resolution changes - -## 3. FilterChain Runtime Update - -- [x] 3.1 Add `set_prechain_resolution(uint32_t width, uint32_t height)` method to `FilterChain` -- [x] 3.2 Method clears existing pre-chain passes/framebuffers to force rebuild on next frame -- [x] 3.3 Update `m_source_resolution` member when method is called -- [x] 3.4 Add `get_prechain_resolution()` getter for UI initialization - -## 4. VulkanBackend Exposure - -- [x] 4.1 Add `set_prechain_resolution()` forwarding method to `VulkanBackend` -- [x] 4.2 Add `get_prechain_resolution()` forwarding method to `VulkanBackend` - -## 5. Application Wiring - -- [x] 5.1 In `Application::create()`, set `ImGuiLayer` pre-chain callback to forward to `VulkanBackend` -- [x] 5.2 Initialize `ImGuiLayer` pre-chain state from `VulkanBackend::get_prechain_resolution()` -- [x] 5.3 Update pre-chain UI state after filter chain operations that may change resolution - -## 6. Validation - -- [x] 6.1 Build with `pixi run dev -p quality` and verify no warnings -- [x] 6.2 Test UI displays three sections correctly -- [x] 6.3 Test pre-chain resolution change triggers downsample pass rebuild -- [x] 6.4 Verify effect stage controls work as before (no regression) diff --git a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/design.md b/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/design.md deleted file mode 100644 index 4b798a74..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/design.md +++ /dev/null @@ -1,34 +0,0 @@ -## Context -The ImGui Surface List already exposes per-surface metadata for input routing. Users need a per-surface way to bypass shader processing while keeping the rest of the pipeline unchanged. The change crosses UI state, surface metadata, and render pipeline routing. - -## Goals / Non-Goals -- Goals: - - Provide a per-surface checkbox in the Surface List to enable or bypass the filter chain. - - Provide a global Window Management checkbox to bypass the filter chain for all surfaces in the - session. - - Bypass mode uses a real maximize-style resize so the client re-renders at the new size (no stretch-blit). - - Popups/subsurfaces inherit the parent xdg_toplevel setting. -- Non-Goals: - - Persisting per-surface toggles across app restarts. - - Adding new global shader toggles (reuse existing passthrough behavior if present). - -## Decisions -- Decision: Store the toggle in app-side surface state keyed by surface id; default to enabled when a new surface appears. -- Decision: Default the per-surface toggle to enabled for Vulkan capture path surfaces and disabled for non-Vulkan capture path surfaces. -- Decision: Add a Window Management global toggle; effective filter-chain usage is - `window_mgmt_filter_chain_enabled && surface_filter_chain_enabled` (global passthrough still - overrides per-surface settings). -- Decision: The Window Management toggles control filter-chain enablement, while the effect stage - is additionally gated by the Effect Stage "Enable Shader" control. -- Decision: Render pipeline accepts a per-surface render mode and switches to a compositor-style resize path when bypassed, so the client renders at the window size instead of stretching the captured image. -- Decision: Surface metadata includes a stable reference to the parent xdg_toplevel id so popups inherit the parent setting. - -## Risks / Trade-offs -- UI clarity: the Surface List grows more complex. Mitigation: keep checkbox label minimal and add a tooltip. -- Render routing complexity: per-surface path selection could introduce branching in the render loop. Mitigation: resolve the mode once per surface/frame and keep the hot path simple. - -## Migration Plan -- No migration; defaults preserve current behavior (filter chain enabled for all surfaces). - -## Open Questions -- Should the Surface List show an explicit tooltip describing maximize-style resize behavior when unchecked? diff --git a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/proposal.md b/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/proposal.md deleted file mode 100644 index 31eed627..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Add per-surface filter chain toggle in Surface List - -## Why -Some surfaces (overlays, launchers, popups) need a quick way to bypass shader processing while keeping other surfaces fully processed. A per-surface toggle lets users disable the filter chain on a specific surface while a global Window Management toggle provides a fast, session-wide override. - -## What Changes -- Add a checkbox per surface entry in `Application -> Window Management -> Surface List` to enable or bypass the filter chain for that surface, with an icon label and tooltip. -- Add a global checkbox in `Application -> Window Management` to enable or bypass the filter chain for all surfaces during the session. -- The Effect Stage "Enable Shader" toggle continues to gate the effect stage only; Window Management toggles control the filter chain routing together. -- When the filter chain is bypassed (global or per-surface), the surface is resized using a real maximize-style resize event so the client re-renders at the new size, matching common compositor behavior (no stretch-blit). -- Toggle applies to the entire xdg_toplevel and all popups/subsurfaces belonging to it. -- New surfaces default to filter chain enabled for Vulkan capture path surfaces, and disabled for non-Vulkan capture path surfaces; state is cleared when a surface is removed. - -## Impact -- Affected specs: app-window, render-pipeline -- Affected code: `src/ui/imgui_layer.hpp`, `src/ui/imgui_layer.cpp`, `src/app/application.cpp`, `src/render/chain/*`, `src/render/backend/*`, `src/input/*` diff --git a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/app-window/spec.md deleted file mode 100644 index 6dc24942..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/app-window/spec.md +++ /dev/null @@ -1,52 +0,0 @@ -## ADDED Requirements - -### Requirement: Surface List Filter Chain Toggle -The Goggles application SHALL provide a per-surface checkbox in -`Application -> Window Management -> Surface List` that controls whether the filter chain is -applied to that surface. - -Each surface entry SHALL display a checkbox with a short icon label (e.g., `FX`) and a tooltip -describing the filter-chain toggle. The checkbox SHALL be checked when filter chain processing is -enabled for that surface. Unchecking the box SHALL request a bypass mode that uses a real -maximize-style resize so the client re-renders at the window size (no stretch-blit). - -Newly discovered surfaces SHALL default to filter chain enabled when the surface uses the Vulkan capture path, and disabled when the surface uses a non-Vulkan capture path. - -#### Scenario: Surface list shows per-surface toggle -- **GIVEN** the Surface List window is visible -- **WHEN** surfaces are listed -- **THEN** each surface entry SHALL display a filter-chain checkbox reflecting the current per-surface state - -#### Scenario: User disables filter chain for a surface -- **GIVEN** the Surface List window is visible -- **WHEN** the user unchecks a surface's filter-chain checkbox -- **THEN** the UI SHALL emit an update request for that surface's render mode -- **AND** the UI SHALL indicate the surface is in bypass mode - -#### Scenario: Toggle shows tooltip -- **GIVEN** the Surface List window is visible -- **WHEN** the user hovers the filter-chain checkbox for a surface -- **THEN** a tooltip SHALL describe that the toggle enables or bypasses the filter chain for that surface - -#### Scenario: Vulkan surface defaults to enabled -- **WHEN** a new surface using the Vulkan capture path is added to the Surface List -- **THEN** its filter-chain checkbox SHALL be checked by default - -#### Scenario: Non-Vulkan surface defaults to disabled -- **WHEN** a new surface using a non-Vulkan capture path is added to the Surface List -- **THEN** its filter-chain checkbox SHALL be unchecked by default - -### Requirement: Window Management Filter Chain Global Toggle -The Goggles application SHALL provide a global filter-chain checkbox in `Application -> Window Management` that enables or bypasses filter chain processing for all surfaces during the current session. -The effect stage remains additionally gated by `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader`. - -#### Scenario: Global toggle disables filter chain -- **GIVEN** the Window Management panel is visible -- **WHEN** the user unchecks the global filter-chain checkbox -- **THEN** the UI SHALL emit a session-wide request to bypass filter chain processing -- **AND** per-surface checkboxes SHALL remain visible but treated as disabled overrides while global bypass is active - -#### Scenario: Global toggle restores per-surface behavior -- **GIVEN** the global filter-chain checkbox is unchecked -- **WHEN** the user re-checks the global filter-chain checkbox -- **THEN** filter chain processing SHALL resume using each surface's per-surface setting diff --git a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/render-pipeline/spec.md deleted file mode 100644 index 94d8a0f5..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/specs/render-pipeline/spec.md +++ /dev/null @@ -1,39 +0,0 @@ -## ADDED Requirements - -### Requirement: Per-Surface Filter Chain Routing -The render pipeline SHALL honor a per-surface filter-chain enable flag and a session-wide global -enable flag when deciding whether to execute the filter chain for a frame. - -The effect stage SHALL also respect `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader`. - -When the global flag is disabled, the pipeline SHALL bypass the filter chain for all surfaces and -render captured surfaces using a compositor-style maximize resize so the client re-renders at the -window size (no stretch-blit). - -When the global flag is enabled but the per-surface flag is disabled, the pipeline SHALL bypass the -filter chain for that surface and render it using a compositor-style maximize resize so the client -re-renders at the window size (no stretch-blit). - -The per-surface mode SHALL apply to the entire xdg_toplevel surface, including all popups and subsurfaces belonging to that toplevel. - -#### Scenario: Default uses filter chain -- **GIVEN** a surface has no explicit override -- **WHEN** a frame is rendered for that surface -- **THEN** the filter chain SHALL be executed for that frame - -#### Scenario: Bypass filter chain for a surface -- **GIVEN** a surface has filter-chain disabled -- **WHEN** a frame is rendered for that surface -- **THEN** the filter chain SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit - -#### Scenario: Global bypass overrides per-surface -- **GIVEN** the global filter-chain flag is disabled -- **WHEN** a frame is rendered for any surface -- **THEN** the filter chain SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit - -#### Scenario: Popup inherits parent mode -- **GIVEN** an xdg_toplevel surface has filter-chain disabled -- **WHEN** a popup or subsurface belonging to that toplevel is rendered -- **THEN** the popup SHALL be rendered with filter-chain bypass and maximize-style resize without stretch-blit diff --git a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/tasks.md b/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/tasks.md deleted file mode 100644 index e4460a51..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-filter-chain-toggle/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add per-surface filter-chain enable state keyed by surface id, with defaults based on capture path (Vulkan on, non-Vulkan off), and clear it on surface removal. - - Implemented in src/app/application.cpp; verified by `pixi run build -p debug`. -- [x] 1.2 Expose the per-surface state and capture-path metadata through the surface list model used by the ImGui Surface List window. - - Implemented in src/compositor/compositor_server.hpp, src/compositor/compositor_server.cpp, src/ui/imgui_layer.cpp; verified by `pixi run build -p debug`. -- [x] 1.3 Add a global filter-chain checkbox to `Application -> Window Management` and emit a session-wide override event. - - Implemented in src/ui/imgui_layer.cpp, src/ui/imgui_layer.hpp, src/app/application.cpp; verified by `pixi run build -p debug`. -- [x] 1.4 Add a checkbox per surface entry in `Application -> Window Management -> Surface List` with icon label + tooltip that toggles the per-surface filter-chain state and emits an update event. - - Implemented in src/ui/imgui_layer.cpp, src/ui/imgui_layer.hpp, src/app/application.cpp; verified by `pixi run build -p debug`. -- [x] 1.5 Route the global + per-surface toggle into the render pipeline and bypass the filter chain - when disabled, using compositor-style maximize resizing (client - re-renders) rather than stretch-blit output. - - Implemented in src/app/application.cpp, src/render/chain/filter_chain.cpp, and - src/render/backend/vulkan_backend.hpp; verified by `pixi run build -p debug` (effect-stage - toggle no longer drives resize bypass). -- [x] 1.6 Ensure popup/subsurface rendering inherits the parent xdg_toplevel filter-chain state. - - Implemented by applying resize to toplevel roots via src/compositor/compositor_server.cpp; verified by `pixi run build -p debug`. - -## 2. Verification -- [x] 2.1 Manual: open two surfaces, disable filter chain for one, confirm it resizes/maximizes and re-renders while the other remains filtered. -- [x] 2.2 Manual: toggle the global Window Management checkbox off and confirm all surfaces bypass the filter chain, then re-enable it and confirm per-surface settings apply again. -- [x] 2.3 Manual: trigger an xdg_toplevel popup on a surface with filter chain disabled and confirm the popup is also unfiltered. -- [x] 2.4 Automated: add/update a unit test covering surface-id override resolution, capture-path defaults, and cleanup on surface removal. - - Explicitly deferred to Application harness follow-up; current behavior is covered by manual verification steps above. diff --git a/openspec/changes/archive/2026-02-07-add-surface-selector/proposal.md b/openspec/changes/archive/2026-02-07-add-surface-selector/proposal.md deleted file mode 100644 index 68adeef6..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-selector/proposal.md +++ /dev/null @@ -1,51 +0,0 @@ -# Change: Add Surface Selector for Multi-Surface Input Routing - -## Why - -When running inside Steam, multiple surfaces may connect to the compositor (game window, Steam overlay, notifications, MangoHud). The current single-surface focus model cannot distinguish between them, causing input to go to the wrong surface or overlay failures. Auto-detection via X11 atoms is complex and error-prone. A hybrid approach with manual override via ImGui provides visibility and control. - -## What Changes - -- Add `SurfaceInfo` struct to expose surface metadata (id, title, class, dimensions, type) -- Track all connected surfaces with unique IDs in CompositorServer -- Add `get_surfaces()` API to enumerate connected surfaces -- Add `set_input_target(id)` and `clear_input_override()` for manual selection -- Add new ImGui window (F4) displaying surface list with click-to-select -- Show current input target with "(auto)" or "(manual)" indicator - -## Impact - -- Affected specs: `input-forwarding` -- Affected code: - - `src/input/compositor_server.hpp` - SurfaceInfo struct, surface enumeration API - - `src/input/compositor_server.cpp` - surface tracking, manual override logic - - `src/input/input_forwarder.hpp` - expose surface list and selection - - `src/input/input_forwarder.cpp` - forward to CompositorServer - - `src/ui/imgui_layer.hpp` - SurfaceSelectorState, draw method, F4 toggle - - `src/ui/imgui_layer.cpp` - surface selector window implementation - - `src/app/application.cpp` - poll surfaces, update UI state, F4 handler - -## Design Rationale - -**Why NOT wlr_scene:** Gamescope does not use wlr_scene. The scene-graph API is designed for compositors that render surfaces to a display. Goggles is headless (no rendering output), so wlr_scene provides no benefit and adds unnecessary complexity. - -**Why NOT full auto-detection:** Detecting overlay windows via X11 atoms (`STEAM_OVERLAY`, `GAMESCOPE_EXTERNAL_OVERLAY`) requires: -- Polling X11 properties on XWayland surfaces -- Handling property change events for dynamic atom updates -- Race conditions between window creation and atom assignment -- Edge cases (Wine helper windows, third-party overlays without atoms) - -**Why hybrid approach:** -- Simple default: first surface receives input (predictable) -- frame_done already sent to all surfaces on commit (no change needed) -- Manual override via UI for edge cases -- Visible surface list aids debugging -- Can add auto-detection later if needed - -**X11 metadata retrieval:** Surface title (`WM_NAME`) and class (`WM_CLASS`) are queried from XWayland surfaces during association. This uses the existing X11 connection from wlr_xwayland, adding no new dependencies. - -## Non-Goals - -- Automatic overlay detection via X11 atoms (deferred, may add later) -- Multiple simultaneous input targets -- Surface z-ordering or stacking (not needed for headless compositor) diff --git a/openspec/changes/archive/2026-02-07-add-surface-selector/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-add-surface-selector/specs/input-forwarding/spec.md deleted file mode 100644 index 66790854..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-selector/specs/input-forwarding/spec.md +++ /dev/null @@ -1,136 +0,0 @@ -## ADDED Requirements - -### Requirement: Surface Enumeration - -The system SHALL provide an API to enumerate all connected surfaces with metadata. - -The `SurfaceInfo` struct SHALL contain: -- `id`: Unique surface identifier (assigned on creation) -- `title`: Window title (from `WM_NAME` for XWayland, empty for XDG) -- `class_name`: Window class (from `WM_CLASS` for XWayland, empty for XDG) -- `width`, `height`: Surface dimensions in pixels -- `is_xwayland`: True for X11 surfaces, false for native Wayland -- `is_input_target`: True if this surface currently receives input - -The `CompositorServer::get_surfaces()` method SHALL return a snapshot of all tracked surfaces. - -#### Scenario: Surface list includes XWayland client -- **WHEN** an X11 app connects via XWayland -- **AND** `get_surfaces()` is called -- **THEN** the returned list includes the surface with `is_xwayland = true` -- **AND** `title` contains the X11 `WM_NAME` property value -- **AND** `class_name` contains the X11 `WM_CLASS` property value - -#### Scenario: Surface list includes Wayland client -- **WHEN** a native Wayland client creates an xdg_toplevel -- **AND** `get_surfaces()` is called -- **THEN** the returned list includes the surface with `is_xwayland = false` -- **AND** `title` and `class_name` are empty strings - -#### Scenario: Surface removed from list on disconnect -- **WHEN** a client disconnects -- **AND** `get_surfaces()` is called -- **THEN** the returned list does not include the disconnected surface - -### Requirement: Manual Input Target Selection - -The system SHALL allow manual selection of which surface receives input. - -The `CompositorServer` SHALL provide: -- `set_input_target(uint32_t surface_id)`: Route input to specified surface -- `clear_input_override()`: Revert to automatic selection (first surface) - -When a manual target is set, input SHALL be routed to that surface regardless of connection order. - -When the manual target surface disconnects, the system SHALL clear the override and revert to automatic selection. - -#### Scenario: Manual selection routes input -- **WHEN** two surfaces are connected -- **AND** `set_input_target(surface_2_id)` is called -- **THEN** input events are delivered to surface 2 -- **AND** surface 1 does not receive input - -#### Scenario: Clear override reverts to auto -- **WHEN** a manual target is active -- **AND** `clear_input_override()` is called -- **THEN** input is routed to the first connected surface (auto behavior) - -#### Scenario: Manual target disconnect clears override -- **WHEN** `set_input_target(surface_id)` is active -- **AND** the target surface disconnects -- **THEN** the override is cleared automatically -- **AND** input is routed to the first remaining surface - -### Requirement: Surface Selector UI - -The system SHALL provide an ImGui window for viewing and selecting input targets. - -The surface selector window SHALL: -- Toggle visibility with F4 key -- Display a list of all connected surfaces -- Show surface ID, title (or fallback), and dimensions for each entry -- Highlight the current input target -- Indicate whether selection is "(auto)" or "(manual)" -- Provide a "Reset to Auto" button to clear manual override -- Allow clicking a surface to select it as input target - -#### Scenario: F4 toggles surface selector -- **WHEN** user presses F4 -- **THEN** the surface selector window visibility toggles -- **AND** when visible, the current surface list is displayed - -#### Scenario: Click surface to select -- **WHEN** the surface selector is visible -- **AND** user clicks a surface entry -- **THEN** that surface becomes the input target -- **AND** the indicator changes to "(manual)" - -#### Scenario: Reset to Auto button -- **WHEN** a manual target is active -- **AND** user clicks "Reset to Auto" -- **THEN** the override is cleared -- **AND** the indicator changes to "(auto)" - -## MODIFIED Requirements - -### Requirement: Surface Tracking - -The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients. - -The compositor server SHALL: -- Listen for `new_toplevel` signals from xdg_shell -- Listen for `new_surface` signals from XWayland -- Maintain a unified list of active surfaces (Wayland surfaces only) -- Register destroy listeners for Wayland surfaces only -- Assign a unique ID to each surface on creation -- Query X11 window properties (`WM_NAME`, `WM_CLASS`) for XWayland surfaces -- Use manual target if set, otherwise auto-focus the first connected surface -- Use mutual exclusion to manage focus between Wayland and XWayland surfaces - -**Important**: XWayland surfaces SHALL NOT use destroy listeners. XWayland destroy signals fire at unpredictable times during normal X11 operation, causing input forwarding failures. Instead, stale XWayland pointers are cleared during focus transitions. - -#### Scenario: Wayland client connects and receives focus -- **WHEN** a Wayland client creates an xdg_toplevel -- **THEN** the surface is tracked by the compositor with a unique ID -- **AND** if no manual target is set and no surface was previously focused, the new surface receives keyboard and pointer focus -- **AND** if an XWayland surface had focus, the Wayland surface steals focus (XWayland pointer may be stale) - -#### Scenario: XWayland client connects and receives focus -- **WHEN** an X11 app creates a window via XWayland -- **THEN** the XWayland surface is tracked by the compositor with a unique ID (not in m_surfaces list) -- **AND** X11 properties (`WM_NAME`, `WM_CLASS`) are queried and stored -- **AND** if no manual target is set and no surface was previously focused, the surface receives keyboard and pointer focus -- **AND** if a Wayland surface already has focus, the XWayland surface does NOT steal focus - -#### Scenario: Wayland client disconnects -- **WHEN** a tracked Wayland surface is destroyed -- **THEN** the surface is removed from tracking via destroy listener -- **AND** if it was the manual target, the override is cleared -- **AND** if it was focused, focus is cleared - -#### Scenario: XWayland client disconnects -- **WHEN** an X11 app exits -- **THEN** no destroy listener fires (by design) -- **AND** m_focused_xsurface becomes a dangling pointer -- **AND** if it was the manual target, the override is cleared on next focus attempt -- **AND** when a new surface gains focus, stale XWayland pointers are cleared safely diff --git a/openspec/changes/archive/2026-02-07-add-surface-selector/tasks.md b/openspec/changes/archive/2026-02-07-add-surface-selector/tasks.md deleted file mode 100644 index 995a230a..00000000 --- a/openspec/changes/archive/2026-02-07-add-surface-selector/tasks.md +++ /dev/null @@ -1,73 +0,0 @@ -## 1. Surface Metadata and Tracking - -- [x] 1.1 Add `SurfaceInfo` struct to `compositor_server.hpp` with: `id`, `title`, `class_name`, `width`, `height`, `is_xwayland`, `is_input_target` -- [x] 1.2 Add `uint32_t` surface ID counter to `CompositorServer::Impl` -- [x] 1.3 Add `id` field to `XWaylandSurfaceHooks` and `XdgToplevelHooks` -- [x] 1.4 Assign unique ID when surface is created -- [x] 1.5 Add `std::vector` cache updated on surface add/remove/focus change - -## 2. X11 Metadata Retrieval - -- [x] 2.1 Add helper function `get_x11_window_title(wlr_xwayland_surface*)` using `WM_NAME` atom -- [x] 2.2 Add helper function `get_x11_window_class(wlr_xwayland_surface*)` using `WM_CLASS` atom -- [x] 2.3 Query title/class in `handle_xwayland_surface_associate()` when X11 window is bound -- [x] 2.4 Store title/class in `XWaylandSurfaceHooks` - -## 3. Surface Enumeration API - -- [x] 3.1 Add `[[nodiscard]] auto get_surfaces() const -> std::vector` to `CompositorServer` -- [x] 3.2 Implement by iterating tracked surfaces and building `SurfaceInfo` list -- [x] 3.3 Mark current input target in returned list - -## 4. Manual Input Target Selection - -- [x] 4.1 Add `std::optional m_manual_input_target` to `Impl` -- [x] 4.2 Add `void set_input_target(uint32_t surface_id)` to `CompositorServer` -- [x] 4.3 Add `void clear_input_override()` to `CompositorServer` -- [x] 4.4 Modify focus logic: if manual target set, use it; otherwise use first surface -- [x] 4.5 Validate target ID exists before applying - -## 5. InputForwarder Integration - -- [x] 5.1 Add `get_surfaces()` forwarding method to `InputForwarder` -- [x] 5.2 Add `set_input_target(uint32_t)` forwarding method -- [x] 5.3 Add `clear_input_override()` forwarding method - -## 6. ImGui Surface Selector Window - -- [x] 6.1 Add `SurfaceSelectorState` struct to `imgui_layer.hpp` with surface list and selection state -- [x] 6.2 Add `m_surface_selector_visible` bool (default false) -- [x] 6.3 Add `toggle_surface_selector()` and `is_surface_selector_visible()` methods -- [x] 6.4 Add `set_surfaces(std::vector)` to update displayed list -- [x] 6.5 Add callback type for surface selection -- [x] 6.6 Implement `draw_surface_selector()` method - -## 7. Surface Selector UI Implementation - -- [x] 7.1 Draw window with "Surfaces" title -- [x] 7.2 List each surface: radio button, ID, title (or "XDG Surface N"), dimensions -- [x] 7.3 Highlight current input target -- [x] 7.4 Show "(auto)" or "(manual)" indicator for selection mode -- [x] 7.5 Add "Reset to Auto" button that calls `clear_input_override()` -- [x] 7.6 Invoke selection callback when surface clicked - -## 8. Application Integration - -- [x] 8.1 Add F4 key handler to toggle surface selector -- [x] 8.2 Poll `get_surfaces()` each frame when selector visible -- [x] 8.3 Update ImGui layer with surface list -- [x] 8.4 Connect selection callback to `InputForwarder::set_input_target()` -- [x] 8.5 Connect reset callback to `InputForwarder::clear_input_override()` - -## 9. Testing and Validation - -- [x] 9.1 Run `pixi run dev -p quality` and fix any issues -- [x] 9.2 Manual test: launch app, verify surface appears in list -- [x] 9.3 Manual test: click surface to select, verify input routes correctly -- [x] 9.4 Manual test: "Reset to Auto" restores first-surface behavior -- [x] 9.5 Manual test with Steam: verify overlay surfaces appear when invoked -- [x] 9.6 Multi-surface test: Run `pixi run start -- goggles_manual_surface_selector_x11`, verify 3 windows appear in F4 selector -- [x] 9.7 Multi-surface test: Select each surface in F4 UI, verify input routes to correct window -- [x] 9.8 Multi-surface test: Click "Reset to Auto", verify first-surface behavior restored -- [x] 9.9 Multi-surface test: Close one window, verify selector updates correctly -- [x] 9.10 Repeat 9.6-9.9 with `goggles_manual_surface_selector_wayland` diff --git a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/design.md b/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/design.md deleted file mode 100644 index 934b2219..00000000 --- a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/design.md +++ /dev/null @@ -1,33 +0,0 @@ -## Context - -Frame dumping is a debugging feature with non-trivial overhead (extra GPU work + disk I/O). The -capture layer is performance-sensitive and has strict constraints in hot paths like -`vkQueuePresentKHR`. - -## Goals / Non-Goals - -- Goals: - - Enable opt-in dumping selected frames with zero overhead when disabled. - - Support both WSI proxy and non-WSI-proxy paths. - - Avoid file I/O and logging in `vkQueuePresentKHR`. - - Emit image dumps and a sidecar metadata file without adding new dependencies. -- Non-Goals: - - Support formats beyond `ppm` in the initial version. - -## Decisions - -- Decision: Dump selection is enabled only when `GOGGLES_DUMP_FRAME_RANGE` is non-empty. -- Decision: The “frame number” used for matching `GOGGLES_DUMP_FRAME_RANGE` is the frame number the - layer uses for capture metadata (`CaptureFrameMetadata.frame_number`) for that frame. -- Decision: Dump output directory is configurable via `GOGGLES_DUMP_DIR` with default - `/tmp/goggles_dump`. -- Decision: Each dumped frame produces `{processname}_{frameid}.ppm` and `{processname}_{frameid}.ppm.desc`. - - The `.desc` file is plain text `key=value` pairs to avoid format dependencies. -- Decision: Any disk I/O is performed off-thread; the present thread only schedules GPU work and - enqueues work items. - -## Risks / Trade-offs - -- Dumping can impact frametime; it is best-effort and intended for debugging. -- Correctness depends on handling swapchain formats and channel order; `ppm` requires RGB output - and may require swizzling for common BGRA formats. diff --git a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/proposal.md b/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/proposal.md deleted file mode 100644 index 3e378610..00000000 --- a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/proposal.md +++ /dev/null @@ -1,42 +0,0 @@ -# Change: Add vk_layer Present Frame Dumping - -## Why - -Capture correctness issues (format, swizzle, alpha, sync) are hard to diagnose without attaching -the full Goggles viewer pipeline. A targeted “dump selected presented frames to disk” option helps -with regression testing and bug reports, and it must work in both WSI proxy and non-WSI-proxy -paths. - -## What Changes - -- Add `GOGGLES_DUMP_DIR` to select the dump output directory. - - Default: `/tmp/goggles_dump`. -- Add `GOGGLES_DUMP_FRAME_MODE` to select dump output format. - - Supported: `ppm` only (for now). - - Default: `ppm`. -- Add `GOGGLES_DUMP_FRAME_RANGE` to enable dumping and select which frames to dump: - - Unset / empty → dumping disabled. - - Supports single numbers (`3`), lists (`3,5,8`), and inclusive ranges (`8-13`). -- In default mode (launching a target app), add CLI flags to configure the launched process: - - `--dump-dir ` → `GOGGLES_DUMP_DIR` - - `--dump-frame-range ` → `GOGGLES_DUMP_FRAME_RANGE` - - `--dump-frame-mode ` → `GOGGLES_DUMP_FRAME_MODE` -- When dumping a selected frame, write two artifacts with the same base name: - - `{processname}_{frameid}.ppm` (image dump) - - `{processname}_{frameid}.ppm.desc` (image metadata as raw `key=value` pairs) -- Support dumping for: - - Standard swapchain capture (non-WSI-proxy). - - WSI proxy mode (`GOGGLES_WSI_PROXY=1`) virtual swapchains. -- Preserve capture layer constraints (per `docs/project_policies.md`): - - No file I/O and no logging in `vkQueuePresentKHR`. - - Any disk I/O occurs off-thread. - -## Impact - -- Affected specs: `vk-layer-capture`, `app-window` -- Affected code: - - `src/capture/vk_layer/vk_capture.cpp` / `src/capture/vk_layer/vk_capture.hpp` - - `src/capture/vk_layer/vk_hooks.cpp` - - `src/capture/vk_layer/wsi_virtual.cpp` / `src/capture/vk_layer/wsi_virtual.hpp` - - `src/app/cli.hpp` - - `src/app/main.cpp` diff --git a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/app-window/spec.md deleted file mode 100644 index 3695659e..00000000 --- a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/app-window/spec.md +++ /dev/null @@ -1,23 +0,0 @@ -## ADDED Requirements - -### Requirement: Frame Dump CLI Env Overrides (Default Mode) - -When running in default mode (launching a target app), the application SHALL provide CLI flags to -configure the capture layer frame dump feature for the launched target process. - -#### Scenario: Dump CLI flags set target environment variables - -- **GIVEN** the application is launched in default mode with `-- [args...]` -- **WHEN** the user provides `--dump-dir ` -- **THEN** the launched target process SHALL receive `GOGGLES_DUMP_DIR=` -- **WHEN** the user provides `--dump-frame-range ` -- **THEN** the launched target process SHALL receive `GOGGLES_DUMP_FRAME_RANGE=` -- **WHEN** the user provides `--dump-frame-mode ` -- **THEN** the launched target process SHALL receive `GOGGLES_DUMP_FRAME_MODE=` - -#### Scenario: Dump CLI flags rejected in detach mode - -- **GIVEN** the application is launched with `--detach` -- **WHEN** the user provides any of `--dump-dir`, `--dump-frame-range`, or `--dump-frame-mode` -- **THEN** CLI parsing SHALL fail with a parse error - diff --git a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/vk-layer-capture/spec.md deleted file mode 100644 index 3bdd213e..00000000 --- a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,154 +0,0 @@ -## ADDED Requirements - -### Requirement: Present Frame Dump Directory - -The dump output directory SHALL be configurable via `GOGGLES_DUMP_DIR`. - -#### Scenario: Default dump directory - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set to a non-empty value -- **AND** `GOGGLES_DUMP_DIR` is not set or is an empty string -- **WHEN** a selected frame is dumped -- **THEN** the layer SHALL write dumps under `/tmp/goggles_dump` - -#### Scenario: Custom dump directory - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set to a non-empty value -- **AND** `GOGGLES_DUMP_DIR` is set to a non-empty value -- **WHEN** a selected frame is dumped -- **THEN** the layer SHALL write dumps under that directory - -### Requirement: Present Frame Dump Configuration - -The capture layer SHALL support dumping the presented image to disk when -`GOGGLES_DUMP_FRAME_RANGE` is set to a non-empty value. - -#### Scenario: Dumping disabled by default - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is not set or is an empty string -- **WHEN** `vkQueuePresentKHR` is called -- **THEN** the layer SHALL NOT dump any images to disk - -#### Scenario: Dumping enabled when range is set - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set to a non-empty value -- **WHEN** a frame is presented whose frame number matches the range -- **THEN** the layer SHALL dump that presented image to disk - -### Requirement: Present Frame Dump Outputs - -When dumping a selected frame, the layer SHALL write both: -- a `ppm` image file -- a metadata sidecar file with `.ppm.desc` suffix - -Both files SHALL share the same base name `{processname}_{frameid}` and be written to the same -directory. - -For dumping, `{frameid}` SHALL be the frame number used for capture metadata -(`CaptureFrameMetadata.frame_number`) for that frame. - -#### Scenario: Image and metadata outputs are produced - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE=3` -- **AND** the process name is `vkcube` -- **WHEN** frame number 3 is dumped -- **THEN** the layer SHALL write `vkcube_3.ppm` -- **AND** it SHALL write `vkcube_3.ppm.desc` - -### Requirement: Present Frame Dump Range Syntax - -`GOGGLES_DUMP_FRAME_RANGE` SHALL support selecting frames using a union of: -- single frame numbers (e.g. `3`) -- comma-separated lists (e.g. `3,5,8`) -- inclusive ranges (e.g. `8-13`) - -Whitespace around commas and hyphens MAY be ignored. - -For the purpose of `GOGGLES_DUMP_FRAME_RANGE`, the “frame number” SHALL be the value used by the -layer for capture metadata (`CaptureFrameMetadata.frame_number`) for that frame. - -#### Scenario: Single frame selection - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE=3` -- **WHEN** the layer observes frame numbers 1, 2, 3, 4 -- **THEN** it SHALL dump frame 3 -- **AND** it SHALL NOT dump frames 1, 2, or 4 - -#### Scenario: Multi-frame list selection - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE=3,5,8` -- **WHEN** the layer observes frame numbers 1 through 9 -- **THEN** it SHALL dump frames 3, 5, and 8 -- **AND** it SHALL NOT dump other frames - -#### Scenario: Inclusive range selection - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE=8-13` -- **WHEN** the layer observes frame numbers 1 through 14 -- **THEN** it SHALL dump frames 8 through 13 (inclusive) -- **AND** it SHALL NOT dump frames 7 or 14 - -### Requirement: Present Frame Dump Metadata Format - -The `.ppm.desc` file SHALL be plain text with one `key=value` pair per line to avoid requiring -additional serialization dependencies. - -#### Scenario: Metadata includes core fields - -- **GIVEN** a frame is dumped -- **WHEN** the layer writes the `.desc` file -- **THEN** it SHALL include a `frame_number` key -- **AND** it SHALL include a `width` key -- **AND** it SHALL include a `height` key -- **AND** it SHALL include a `format` key -- **AND** it SHALL include a `stride` key -- **AND** it SHALL include an `offset` key -- **AND** it SHALL include a `modifier` key - -### Requirement: Present Frame Dump Mode - -The dump output format SHALL be controlled by `GOGGLES_DUMP_FRAME_MODE`. - -#### Scenario: Default dump mode - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set -- **AND** `GOGGLES_DUMP_FRAME_MODE` is not set or is an empty string -- **WHEN** a selected frame is dumped -- **THEN** the layer SHALL write a `ppm` dump - -#### Scenario: Unsupported dump mode fallback - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set -- **AND** `GOGGLES_DUMP_FRAME_MODE` is set to an unsupported value -- **WHEN** a selected frame is dumped -- **THEN** the layer SHALL fall back to `ppm` - -### Requirement: Present Frame Dump Compatibility - -Present frame dumping SHALL work in both WSI proxy and non-WSI-proxy capture paths. - -#### Scenario: Dumping in WSI proxy mode - -- **GIVEN** WSI proxy mode is enabled (`GOGGLES_WSI_PROXY=1` and `GOGGLES_CAPTURE=1`) -- **AND** `GOGGLES_DUMP_FRAME_RANGE` selects a frame number -- **WHEN** the application presents a virtual swapchain image -- **THEN** the layer SHALL dump the presented image to disk - -#### Scenario: Dumping in non-WSI-proxy mode - -- **GIVEN** WSI proxy mode is disabled -- **AND** `GOGGLES_DUMP_FRAME_RANGE` selects a frame number -- **WHEN** the application presents a real swapchain image -- **THEN** the layer SHALL dump the presented image to disk - -### Requirement: Present Frame Dump Performance Constraints - -Dumping SHALL be implemented such that `vkQueuePresentKHR` does not perform file I/O or logging. - -#### Scenario: No file I/O or logging on present hook - -- **GIVEN** `GOGGLES_DUMP_FRAME_RANGE` is set to a non-empty value -- **WHEN** `vkQueuePresentKHR` is called -- **THEN** the layer SHALL NOT perform file I/O on the present hook thread -- **AND** it SHALL NOT log on the present hook thread -- **AND** any disk writes SHALL be performed asynchronously (off-thread) diff --git a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/tasks.md b/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/tasks.md deleted file mode 100644 index b839a447..00000000 --- a/openspec/changes/archive/2026-02-07-add-vk-layer-frame-dump/tasks.md +++ /dev/null @@ -1,22 +0,0 @@ -## 1. Specification - -- [x] 1.1 Add `GOGGLES_DUMP_DIR` + `GOGGLES_DUMP_FRAME_MODE` + `GOGGLES_DUMP_FRAME_RANGE` requirements to `vk-layer-capture` -- [x] 1.2 Specify `{processname}_{frameid}.ppm` + `{processname}_{frameid}.ppm.desc` output rules and metadata key/value format -- [x] 1.3 Add CLI requirements for default mode flags that map to `GOGGLES_DUMP_*` for the launched app - -## 2. Implementation (vk_layer) - -- [x] 2.0 Determine `processname` for filename prefix (sanitize for filesystem safety) -- [x] 2.1 Parse `GOGGLES_DUMP_FRAME_MODE` (default `ppm`) -- [x] 2.2 Parse `GOGGLES_DUMP_DIR` (default `/tmp/goggles_dump`) -- [x] 2.3 Parse `GOGGLES_DUMP_FRAME_RANGE` (single/list/range); treat empty as disabled -- [x] 2.4 In non-WSI-proxy path, dump the presented swapchain image when its frame number matches -- [x] 2.5 In WSI proxy path, dump the presented virtual swapchain image when its frame number matches -- [x] 2.6 For each dumped frame, write `{processname}_{frameid}.ppm` and `{processname}_{frameid}.ppm.desc` -- [x] 2.7 Ensure `vkQueuePresentKHR` performs no file I/O and no logging (off-thread write path) -- [x] 2.8 Ensure dumping failures are best-effort and never break present/capture behavior - -## 3. Validation - -- [x] 3.1 Run `openspec validate add-vk-layer-frame-dump --strict` (pending: `openspec` CLI not available in this environment) -- [x] 3.2 Run unit tests (`pixi run test -p test`) if impacted tests exist diff --git a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/proposal.md b/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/proposal.md deleted file mode 100644 index c7d4a184..00000000 --- a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/proposal.md +++ /dev/null @@ -1,13 +0,0 @@ -# Change: Add wlroots Log Bridge - -## Why -The compositor server currently relies on stderr output from wlroots/XWayland, which is suppressed at non-debug log levels. This bypasses the project's logging configuration and can hide actionable errors. - -## What Changes -- Route wlroots logs through the project logger via a wlroots log callback. -- Map wlroots log importance to project log levels and respect configured verbosity. -- Keep stderr suppression limited to external helper noise (e.g., XWayland/xkbcomp), while ensuring wlroots logs remain visible via the project logger. - -## Impact -- Affected specs: input-forwarding -- Affected code: src/compositor/compositor_server.cpp, src/util/logging.* diff --git a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/specs/input-forwarding/spec.md deleted file mode 100644 index 68d4b235..00000000 --- a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/specs/input-forwarding/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -## ADDED Requirements -### Requirement: Wlroots Logging Bridge -The compositor server SHALL route wlroots log output through the project logging utilities and respect configured verbosity. - -#### Scenario: Default log level filters wlroots debug noise -- **GIVEN** the application log level is info (default) -- **WHEN** the compositor initializes wlroots logging -- **THEN** wlroots debug logs are suppressed -- **AND** wlroots info/error logs are emitted through the project logger - -#### Scenario: Debug level exposes wlroots diagnostics -- **GIVEN** the application log level is debug or trace -- **WHEN** wlroots emits a debug log message -- **THEN** the message is emitted via the project logger at debug level - -### Requirement: Stderr Suppression Does Not Hide Wlroots Logs -The compositor server SHALL NOT suppress wlroots logs when stderr suppression is active for external helper noise. - -#### Scenario: External stderr suppression enabled -- **GIVEN** stderr suppression is enabled to reduce external helper noise -- **WHEN** wlroots emits an error log -- **THEN** the error is still visible via the project logger diff --git a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/tasks.md b/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/tasks.md deleted file mode 100644 index 1575cbff..00000000 --- a/openspec/changes/archive/2026-02-07-add-wlroots-log-bridge/tasks.md +++ /dev/null @@ -1,14 +0,0 @@ -## 1. Implementation -- [x] 1.1 Initialize wlroots logging with a project log callback during compositor startup (before backend/XWayland creation). - - Implemented in src/compositor/compositor_server.cpp (wlroots log bridge + init hook). - - Verified by `pixi run build -p debug`. -- [x] 1.2 Map wlroots importance levels to GOGGLES_LOG_* levels and respect configured verbosity. - - Implemented in src/compositor/compositor_server.cpp (importance mapping + logger bridge). - - Verified by `pixi run build -p debug`. -- [x] 1.3 Ensure stderr suppression remains scoped to external helper noise without hiding wlroots logs. - - Implemented in src/compositor/compositor_server.cpp (wlroots logs routed through project logger). - - Verified by `pixi run build -p debug`. - -## 2. Verification -- [x] 2.1 Manual test: run with default log level and confirm wlroots debug logs are filtered while info/error logs appear via project logger. -- [x] 2.2 Manual test: run with debug/trace and confirm wlroots debug logs appear via project logger. diff --git a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/proposal.md b/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/proposal.md deleted file mode 100644 index ad5de66e..00000000 --- a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Fix GPU device selection for multi-GPU systems - -## Why - -On multi-GPU systems, goggles may select a GPU that cannot properly present to the display surface, causing "device lost" errors. The current logic selects the first device with required extensions, ignoring whether it's actually suitable for the Wayland/X11 surface. - -## What Changes - -- Add `--gpu` CLI option to explicitly select a GPU by index or name substring -- Improve device selection to prefer GPUs that can actually present to the surface -- Log available GPUs at startup for user visibility - -## Impact - -- Affected specs: `app-window` -- Affected code: `src/render/backend/vulkan_backend.cpp`, `src/app/cli.cpp` diff --git a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/specs/app-window/spec.md deleted file mode 100644 index 4ebc15ae..00000000 --- a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/specs/app-window/spec.md +++ /dev/null @@ -1,37 +0,0 @@ -## ADDED Requirements - -### Requirement: GPU Device Selection - -The application SHALL allow users to select a specific GPU and SHALL improve automatic GPU selection for multi-GPU systems. - -#### Scenario: Explicit GPU selection by index - -- **GIVEN** multiple GPUs are available -- **WHEN** the user runs with `--gpu 0` -- **THEN** the application SHALL use the GPU at index 0 - -#### Scenario: Explicit GPU selection by name - -- **GIVEN** multiple GPUs are available including one with "AMD" in its name -- **WHEN** the user runs with `--gpu AMD` -- **THEN** the application SHALL use the GPU whose name contains "AMD" - -#### Scenario: Invalid GPU selector - -- **GIVEN** no GPU matches the selector -- **WHEN** the user runs with `--gpu nonexistent` -- **THEN** the application SHALL exit with an error message listing available GPUs - -#### Scenario: Ambiguous GPU selector - -- **GIVEN** multiple suitable GPUs match the name selector -- **WHEN** the user runs with a non-unique selector like `--gpu AMD` -- **THEN** the application SHALL exit with an error listing matching GPUs -- **AND** it SHALL instruct the user to choose a numeric index - -#### Scenario: Default GPU selection - -- **GIVEN** multiple GPUs are available and no `--gpu` option is specified -- **WHEN** the application selects a GPU -- **THEN** it SHALL prefer a GPU that supports presenting to the current surface -- **AND** it SHALL log all available GPUs with their indices diff --git a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/tasks.md b/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/tasks.md deleted file mode 100644 index ca599a1d..00000000 --- a/openspec/changes/archive/2026-02-07-fix-gpu-device-selection/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. CLI Option - -- [x] 1.1 Add `--gpu ` option to CLI (accepts index like "0" or name substring like "AMD") -- [x] 1.2 Pass GPU selector through to VulkanBackend - -## 2. Device Selection Logic - -- [x] 2.1 Log all available GPUs with index and name at startup -- [x] 2.2 Improve default selection: prefer discrete GPU that supports the surface -- [x] 2.3 Skip devices where `getSurfaceSupportKHR` returns false for all queue families - -## 3. Child Process GPU Inheritance - -- [x] 3.1 Add `gpu_uuid()` getter to VulkanBackend and Application -- [x] 3.2 Pass `GOGGLES_GPU_UUID` env var to spawned child process -- [x] 3.3 Hook `vkEnumeratePhysicalDevices` in layer to filter devices based on UUID matching -- [x] 3.4 Add null check for `GetPhysicalDeviceProperties2` (Vulkan 1.0 fallback) - -## 4. Testing - -- [x] 4.1 Validate explicit `--gpu` selection parsing and config propagation in automated tests - - No automated backend selector tests were added; backend behavior remains runtime/manual follow-up. -- [x] 4.2 Test default selection picks working GPU -- [x] 4.3 Test child process uses same GPU as goggles diff --git a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/design.md b/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/design.md deleted file mode 100644 index d131171b..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/design.md +++ /dev/null @@ -1,84 +0,0 @@ -# Design: CLI parsing refactor + `CliParseOutcome` - -## Goals - -- Remove the `ErrorCode::ok` sentinel used for “help/version requested”. -- Preserve existing CLI behavior (flags, validation rules, error messages where feasible). -- Improve readability by splitting the CLI logic into focused helpers with clear responsibilities. -- Reduce header bloat by moving CLI11-heavy parsing code out of `src/app/cli.hpp`. -- Keep “no exceptions into the main execution flow” by catching CLI11 parse exceptions and returning - `Result<>` values. - -## Non-Goals - -- Changing CLI11 as the parsing library. -- Broadly migrating the project to a different `expected` implementation. -- Reworking application startup orchestration beyond the CLI parse boundary. - -## Proposed API - -Add a value type representing the CLI parse disposition: - -- `enum class CliAction { run, exit_ok };` -- `struct CliParseOutcome { CliAction action; CliOptions options; };` - -Notes: -- For `action == exit_ok`, `options` MAY be default-initialized and MUST NOT be consumed by callers. -- `parse_cli(argc, argv)` returns `Result`. - - `exit_ok` is returned as a *successful* result. - - Parse failures return `ErrorCode::parse_error` and a human-readable message. - -## Internal Structure (Implementation Outline) - -Split the current monolithic flow into helpers: - -1) `split_argv_on_separator(argc, argv)` - - Finds `--` once and returns: - - `viewer_argc` (argv span presented to CLI11) - - `has_separator` - - `app_args` (vector of strings after `--`) - -2) `register_options(CLI::App&, CliOptions&)` - - Pure wiring: defines CLI options/flags and validation checks. - - No mode-specific logic. - -3) `parse_viewer_args(CLI::App&, viewer_argc, argv) -> Result` - - Catches `CLI::ParseError`. - - Uses `app.exit(e)` to trigger CLI11 printing. - - Returns: - - `exit_ok` when CLI11 indicates success exit (help/version). - - `parse_error` otherwise. - -4) `validate_by_mode(options, has_separator, app_args) -> Result` - - Encodes “detach vs default mode” invariants: - - Detach: rejects default-mode-only flags and rejects app command. - - Default mode: requires `--` and a non-empty app command. - -5) `normalize(options)` - - Applies derived fields consistently: - - `--layer-log-level` implies `--layer-log`. - - Width/height pairing invariant enforced (or represented as a single optional “dimensions” - concept and normalized into the existing fields for downstream compatibility). - -`parse_cli` becomes a small orchestration function calling these helpers in order. - -## Alternatives Considered - -1) Keep `ErrorCode::ok` sentinel in the error channel - - Minimal code churn, but retains an awkward contract (success encoded as “error”). - -2) Introduce a separate `parse_cli_or_exit(...)` function - - Clarifies intent but duplicates parsing logic or forces extra call-site decisions. - -3) Return `std::optional` with out-params for errors (rejected) - - Violates project error handling conventions and loses structured error reporting. - -Chosen: `CliParseOutcome` as a value-based, explicit disposition. - -## Testing Strategy - -- Extend `tests/app/test_cli.cpp` to cover: - - `--help` and `--version` return `action == exit_ok` as success (no `ErrorCode::ok` in error). - - Existing detach/default mode validation remains unchanged. - - `--layer-log-level` implies `--layer-log` normalization. - diff --git a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/proposal.md b/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/proposal.md deleted file mode 100644 index d330327e..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/proposal.md +++ /dev/null @@ -1,46 +0,0 @@ -# Change: Refactor CLI Parsing With Explicit Parse Outcome - -## Why - -The current CLI parsing implementation (`goggles::app::parse_cli`) is a large inline function that -mixes option registration, argument splitting (`--`), exception handling, validation, and -normalization. This makes it difficult to maintain and easy to introduce duplicated or inconsistent -rules. - -Additionally, “help/version requested” is currently represented as an error payload with -`ErrorCode::ok`, which complicates top-level error handling and blurs the meaning of `ErrorCode`. - -## What Changes - -- Introduce `goggles::app::CliParseOutcome` to represent CLI parsing results: - - `action = run` with parsed `CliOptions` - - `action = exit_ok` for `--help` / `--version` (and other “exit successfully” dispositions) -- Update CLI parsing so “exit requested” is expressed via `CliParseOutcome` rather than an - `ErrorCode::ok` sentinel error. -- Refactor `parse_cli` implementation into small, testable helpers (argument splitting, option - registration, parse, validation by mode, normalization) while preserving current CLI behavior. -- Move CLI parsing implementation out of `src/app/cli.hpp` into a `.cpp` translation unit to reduce - header bloat and improve readability (header remains data + declarations). - -## Impact - -- Affected specs: - - `app-window` (CLI behavior and parsing contract) -- Affected code (expected): - - `src/app/cli.hpp` / `src/app/cli.cpp` (new) (public API + implementation split) - - `src/app/main.cpp` (handle `CliParseOutcome` without sentinel errors) - - `tests/app/test_cli.cpp` (update/extend tests for help/version and outcome handling) - -## Non-Goals - -- Changing the set of supported CLI flags or their semantics. -- Changing the project-wide `Result` type implementation (`nonstd::expected` vs `tl::expected`). -- Changing launch-mode behavior beyond improving validation structure and readability. - -## Open Questions - -- Should `parse_cli` change signature directly to `Result`, or should a transitional - wrapper be kept for `Result` call sites? -- Should `CliParseOutcome::exit_ok` carry any optional metadata (e.g., “help” vs “version”), or is a - single “exit successfully” action sufficient? - diff --git a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/specs/app-window/spec.md deleted file mode 100644 index ca6a1831..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/specs/app-window/spec.md +++ /dev/null @@ -1,30 +0,0 @@ -# app-window Specification (Delta) - -## MODIFIED Requirements - -### Requirement: Command Line Interface -The application SHALL support command-line arguments to override default behavior and provide -information without throwing exceptions into the main execution flow. - -#### Scenario: Display help -- **WHEN** the application is run with `--help` -- **THEN** it SHALL print usage information -- **AND** CLI parsing SHALL indicate an “exit successfully” disposition without using an error - sentinel -- **AND** the application SHALL exit with code 0 - -#### Scenario: Display version -- **WHEN** the application is run with `--version` -- **THEN** it SHALL print version information -- **AND** CLI parsing SHALL indicate an “exit successfully” disposition without using an error - sentinel -- **AND** the application SHALL exit with code 0 - -#### Scenario: Exception encapsulation -- **GIVEN** the application uses CLI11 for parsing -- **WHEN** `parse_cli` is called -- **THEN** it MUST catch all library exceptions internally -- **AND** it MUST return a value-based `goggles::Result<>` result -- **AND** “help/version requested” MUST be represented as a successful outcome (not as an error with - `ErrorCode::ok`) - diff --git a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/tasks.md b/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/tasks.md deleted file mode 100644 index 87b2ceb6..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-cli-parse-outcome/tasks.md +++ /dev/null @@ -1,19 +0,0 @@ -## 1. Implementation -- [x] 1.1 Introduce `CliAction` + `CliParseOutcome` in `src/app/cli.hpp` -- [x] 1.2 Refactor `parse_cli` to return `Result` (remove `ErrorCode::ok` sentinel) -- [x] 1.3 Move CLI parsing implementation into `src/app/cli.cpp` and slim `src/app/cli.hpp` -- [x] 1.4 Split parsing into helpers: argv split, option registration, parse, validation, normalize -- [x] 1.5 Update `src/app/main.cpp` to handle `CliParseOutcome` (`exit_ok` -> `EXIT_SUCCESS`) - -## 2. Tests -- [x] 2.1 Update `tests/app/test_cli.cpp` for new `parse_cli` return type -- [x] 2.2 Add tests for `--help` and `--version` returning `exit_ok` as success -- [x] 2.3 Ensure existing validation tests remain passing (detach restrictions, `--` separator rules) - -## 3. Validation -- [x] 3.1 `pixi run format` -- [x] 3.2 `pixi run build -p test` (covered by `pixi run test -p test`) -- [x] 3.3 `pixi run test -p test` - -## 4. OpenSpec Hygiene -- [x] 4.1 Run `openspec validate refactor-cli-parse-outcome --strict` diff --git a/openspec/changes/archive/2026-02-07-refactor-compositor-module/design.md b/openspec/changes/archive/2026-02-07-refactor-compositor-module/design.md deleted file mode 100644 index 4f4cc85e..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-compositor-module/design.md +++ /dev/null @@ -1,32 +0,0 @@ -## Context -The compositor server has expanded beyond input forwarding to include compositor-presented surface frame capture. The existing InputForwarder wrapper now mirrors most of CompositorServer and obscures ownership boundaries. - -## Goals / Non-Goals -- Goals: - - Make compositor responsibilities explicit with a dedicated module. - - Eliminate redundant wrapper layers around the compositor server. - - Keep Wayland/wlroots details encapsulated behind an Impl. -- Non-Goals: - - Changing compositor behavior or input semantics. - - Introducing new capture backends or altering IPC protocols. - -## Decisions -- Decision: Create `src/compositor/` module and move CompositorServer there. - - Why: It reflects the expanded scope (input + surface frames) and avoids miscategorizing compositor logic as input-only. -- Decision: Merge InputForwarder API into CompositorServer (SDL event forwarding methods). - - Why: The wrapper provides no additional abstraction; merging reduces indirection while preserving the Impl boundary. -- Decision: Move generated Wayland protocol headers into `src/compositor/protocol/`. - - Why: Keep compositor module organized and clearly separate generated protocol artifacts. - -## Risks / Trade-offs -- SDL types become part of CompositorServer's public API; this increases module coupling to SDL. - - Mitigation: Keep SDL usage limited to forwarding methods; preserve internal input event types and queue. - -## Migration Plan -- Move compositor files and update includes/targets. -- Delete InputForwarder files and update app call sites to use CompositorServer directly. -- Update CMake target names/links. -- Relocate generated protocol headers and ensure include paths include the protocol subdir. - -## Open Questions -- Should SDL translation remain in compositor module long-term, or be split into a thin SDL helper later? diff --git a/openspec/changes/archive/2026-02-07-refactor-compositor-module/proposal.md b/openspec/changes/archive/2026-02-07-refactor-compositor-module/proposal.md deleted file mode 100644 index 89f9bd47..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-compositor-module/proposal.md +++ /dev/null @@ -1,15 +0,0 @@ -# Change: Refactor compositor module and public API - -## Why -The compositor server now handles both input forwarding and compositor-presented surface frames. The current InputForwarder wrapper adds indirection without clear separation of concerns. Aligning module structure and public API with current responsibilities reduces confusion and simplifies integration. - -## What Changes -- Move compositor implementation into a dedicated `src/compositor/` module. -- Remove `InputForwarder`; expose SDL input forwarding methods directly on `CompositorServer`. -- Update application wiring and build targets to depend on the compositor module. -- Consolidate compositor entry points under a single public header (`compositor_server.hpp`). -- Relocate generated Wayland protocol headers under `src/compositor/protocol/`. - -## Impact -- Affected specs: `input-forwarding` -- Affected code: `src/input/*`, `src/app/*`, CMake targets for input/compositor diff --git a/openspec/changes/archive/2026-02-07-refactor-compositor-module/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-refactor-compositor-module/specs/input-forwarding/spec.md deleted file mode 100644 index db10a6c9..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-compositor-module/specs/input-forwarding/spec.md +++ /dev/null @@ -1,10 +0,0 @@ -## ADDED Requirements -### Requirement: Compositor Public API - -The system SHALL expose input forwarding and compositor-presented surface frames via the `CompositorServer` public API, without requiring a separate forwarding wrapper. - -#### Scenario: Application integrates compositor directly -- **WHEN** the application initializes input forwarding -- **THEN** it creates and owns a `CompositorServer` -- **AND** it forwards SDL input events via `CompositorServer` methods -- **AND** it can query compositor-presented surface frames from the same instance diff --git a/openspec/changes/archive/2026-02-07-refactor-compositor-module/tasks.md b/openspec/changes/archive/2026-02-07-refactor-compositor-module/tasks.md deleted file mode 100644 index 9a47b561..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-compositor-module/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -## 1. Implementation -- [x] 1.1 Move compositor server sources into `src/compositor/` and update headers/includes. -- [x] 1.2 Merge InputForwarder API into `CompositorServer` and remove InputForwarder files. -- [x] 1.3 Update application usage to call `CompositorServer` directly. -- [x] 1.4 Update CMake targets and dependencies for the new compositor module. -- [x] 1.5 Move generated protocol headers into `src/compositor/protocol/` and update include paths. -- [x] 1.6 Adjust any docs or comments that reference input forwarder or old paths. diff --git a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/design.md b/openspec/changes/archive/2026-02-07-refactor-external-image-frame/design.md deleted file mode 100644 index 80df6ee0..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/design.md +++ /dev/null @@ -1,35 +0,0 @@ -## Context -The compositor path emits DMA-BUF-backed surface frames using DRM FourCC format metadata, while -capture frames use `VkFormat`. Both paths carry similar metadata and a frame sequence number, but -are modeled as separate structs. This creates duplication and makes it unclear where conversion -should happen when adding new external handle types (e.g., shm). - -## Goals / Non-Goals -- Goals: - - Define a shared external image metadata type in `util` - - Keep handle metadata separate from sequence information - - Use `vk::Format` consistently for application-facing image metadata - - Allow future handle types without renaming the struct -- Non-Goals: - - Change IPC wire formats - - Add new handle types now (shm support deferred) - -## Decisions -- Decision: Create `util::ExternalImage` for width/height/stride/offset/format/modifier + handle. -- Decision: Add `ExternalImageFrame` wrapper containing `ExternalImage` + `frame_number`. -- Decision: Standardize `ExternalImage::format` on `vk::Format` and convert DRM FourCC at the - compositor ingestion boundary. - -## Risks / Trade-offs -- Conversion at ingestion adds a small amount of per-frame CPU work (format mapping). -- Converting DRM FourCC to `vk::Format` can yield `vk::Format::eUndefined` for unsupported - formats; these frames will be skipped as they are today. - -## Migration Plan -1. Add `util::ExternalImage` and `ExternalImageFrame` types. -2. Update compositor to convert DRM FourCC to `vk::Format` when building the frame. -3. Update capture receiver and render backend to use `ExternalImageFrame`. -4. Remove legacy `SurfaceFrame` and `CaptureFrame` structs. - -## Open Questions -- None. diff --git a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/proposal.md b/openspec/changes/archive/2026-02-07-refactor-external-image-frame/proposal.md deleted file mode 100644 index cbba1384..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/proposal.md +++ /dev/null @@ -1,18 +0,0 @@ -# Change: Unify external image frame metadata - -## Why -The compositor and capture paths each define their own frame metadata structs, which diverge in -format representation and sequencing. This duplication makes it harder to share common image -handling logic and to add new external handle types beyond DMA-BUF. - -## What Changes -- Introduce a shared `util::ExternalImage` for external image metadata + handle ownership -- Introduce a wrapper type (e.g., `ExternalImageFrame`) that pairs `ExternalImage` with - a `frame_number` sequence -- Standardize external image format representation on `vk::Format` and convert DRM FourCC at - ingestion boundaries - -## Impact -- Affected specs: `render-pipeline` -- Affected code: compositor server frame export, capture receiver, app frame selection, render - backend import paths diff --git a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-refactor-external-image-frame/specs/render-pipeline/spec.md deleted file mode 100644 index dcbc99a3..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/specs/render-pipeline/spec.md +++ /dev/null @@ -1,16 +0,0 @@ -## ADDED Requirements -### Requirement: External Image Format Normalization -The application SHALL represent external image metadata using `VkFormat` for all render imports. -Sources that provide DRM FourCC formats (e.g., compositor surface frames) SHALL be converted to -`VkFormat` before reaching the render backend. - -#### Scenario: Compositor surface frame conversion -- **GIVEN** a compositor frame provides DRM FourCC format metadata -- **WHEN** the frame is ingested by the application -- **THEN** the metadata SHALL be converted to the equivalent `VkFormat` -- **AND** frames with unsupported formats SHALL be skipped - -#### Scenario: Capture receiver format passthrough -- **GIVEN** a capture frame provides `VkFormat` metadata over IPC -- **WHEN** the application ingests the frame -- **THEN** the metadata SHALL be forwarded unchanged to the render backend diff --git a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/tasks.md b/openspec/changes/archive/2026-02-07-refactor-external-image-frame/tasks.md deleted file mode 100644 index c8754ab0..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-external-image-frame/tasks.md +++ /dev/null @@ -1,11 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add `util::ExternalImage` and `ExternalImageFrame` types -- [x] 1.2 Convert compositor DRM FourCC to `vk::Format` at frame creation -- [x] 1.3 Update capture receiver to use `ExternalImageFrame` -- [x] 1.4 Update app frame selection logic to use unified types -- [x] 1.5 Update render backend imports to use unified types -- [x] 1.6 Remove `SurfaceFrame` and `CaptureFrame` definitions - -## 2. Validation -- [x] 2.1 Build with a debug preset -- [x] 2.2 Run existing unit tests (if applicable) diff --git a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/proposal.md b/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/proposal.md deleted file mode 100644 index 24d89d83..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/proposal.md +++ /dev/null @@ -1,53 +0,0 @@ -# Change: Consolidate ImGui Toggle Keys to Single Hotkey - -## Why - -The current design uses multiple function keys (F1-F4) for ImGui layer controls: -- F1: Toggle shader controls window -- F2: Toggle debug overlay -- F3: Toggle pointer lock override -- F4: Toggle application management window - -This creates conflicts with games that use F1-F4 for their own functions (save/load, menus, quicksave, etc.). Since goggles forwards input to the target application, every hotkey we consume is a potential conflict. - -## What Changes - -- **BREAKING**: Remove all F1-F4 shortcuts -- Single Ctrl+Alt+Shift+Q toggles all Goggles Overlay visibility (master switch) -- Two windows (dockable, can be tabbed together by user): - - **Shader Controls**: Pre-chain, effect, and post-chain shader settings only - - **Application**: Performance stats, pointer lock, surface selector -- Debug overlay content (FPS graphs, frame times) moved INTO Application as "Performance" section -- "Force Enable Pointer Lock" checkbox with hint showing toggle shortcut when enabled -- Surface selector remains in Application window (under "Input" section) - -## Impact - -- Affected specs: `input-forwarding` (hotkey behavior) -- Affected code: `src/app/application.cpp`, `src/ui/imgui_layer.cpp`, `src/ui/imgui_layer.hpp` -- User workflow: Press Ctrl+Alt+Shift+Q to show/hide Goggles Overlay -- Migration: Users learn new modifier-key workflow instead of F1/F2/F3/F4 - -## UI Structure - -``` -Ctrl+Alt+Shift+Q toggles Goggles Overlay -├── Shader Controls (window, dockable) -│ ├── Pre-Chain Stage -│ ├── Effect Stage -│ └── Post-Chain Stage -└── Application (window, dockable) - ├── Performance (collapsible) - │ ├── Render FPS graph - │ └── Source FPS graph - └── Input (collapsible) - ├── [x] Force Enable Pointer Lock - │ └── (hint: "Press Ctrl+Alt+Shift+Q to toggle overlay") - └── Surface list + Reset to Auto -``` - -## Alternatives Considered - -1. **Single F1 key**: Still conflicts with many games (F1 = help is common) -2. **Mouse chord (middle+right click)**: No keyboard conflict but less discoverable -3. **Edge hotspot (move to corner)**: Works but can interfere with gameplay diff --git a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/specs/input-forwarding/spec.md deleted file mode 100644 index ce42b49e..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/specs/input-forwarding/spec.md +++ /dev/null @@ -1,98 +0,0 @@ -## ADDED Requirements - -### Requirement: Goggles Overlay Toggle - -The viewer application SHALL use Ctrl+Alt+Shift+Q to toggle visibility of the Goggles Overlay. - -When Goggles Overlay is hidden: -- All overlay windows SHALL be invisible -- All keyboard and mouse input SHALL be forwarded to the target application without interception -- The Ctrl+Alt+Shift+Q key combination itself SHALL NOT be forwarded (consumed by toggle) - -When Goggles Overlay is visible: -- Shader Controls and Application windows appear side-by-side (dockable by user) -- Input forwarding to target application SHALL be blocked when ImGui wants keyboard/mouse capture - -#### Scenario: User toggles overlay visibility -- **WHEN** user presses Ctrl+Alt+Shift+Q -- **AND** Goggles Overlay is currently hidden -- **THEN** the tabbed overlay windows become visible - -#### Scenario: User hides overlay -- **WHEN** user presses Ctrl+Alt+Shift+Q -- **AND** Goggles Overlay is currently visible -- **THEN** all overlay windows are hidden - -#### Scenario: All function keys forwarded to application -- **WHEN** user presses F1, F2, F3, or F4 -- **THEN** the key event is forwarded to the target application -- **AND** no viewer UI action occurs - -### Requirement: Dockable Windows - -The Goggles Overlay windows SHALL be dockable via ImGui's docking feature. - -- "Shader Controls" window for shader-related settings -- "Application" window for performance and input controls - -Users MAY dock windows together as tabs if desired. The layout is saved in imgui.ini. - -#### Scenario: User docks overlay windows -- **WHEN** Goggles Overlay is visible -- **AND** user drags "Shader Controls" onto "Application" -- **THEN** both windows become docked in a shared tab stack -- **AND** the docking layout persists in imgui.ini for the next launch - -### Requirement: Application Window - -The Application window SHALL consolidate all application and view controls. - -The window SHALL include: -- A "Performance" collapsible section containing: - - Render FPS histogram showing frame-to-frame timing - - Source FPS histogram showing captured frame cadence -- An "Input" collapsible section containing: - - A checkbox labeled "Force Enable Pointer Lock" to force pointer lock regardless of app requests - - When pointer lock is enabled, a hint SHALL display: "Press Ctrl+Alt+Shift+Q to toggle overlay" - - A list of connected surfaces for input target selection - - A "Reset to Auto" button to clear manual surface selection - -#### Scenario: User views performance metrics -- **WHEN** Application window is visible -- **AND** user expands the "Performance" section -- **THEN** FPS graphs and frame timing statistics are visible - -#### Scenario: User enables forced pointer lock -- **WHEN** Application window is visible -- **AND** user checks "Force Enable Pointer Lock" -- **THEN** the viewer window enters relative mouse mode -- **AND** pointer lock is active regardless of target application requests -- **AND** a hint displays the overlay toggle shortcut - -#### Scenario: User returns to automatic pointer lock -- **WHEN** Application window is visible -- **AND** user unchecks "Force Enable Pointer Lock" -- **THEN** pointer lock follows the target application's requests - -### Requirement: Shader Controls Window - -The Shader Controls window SHALL contain only shader-related settings. - -The window SHALL include: -- Pre-Chain Stage controls (resolution profile, parameters) -- Effect Stage controls (preset selection, parameters) -- Post-Chain Stage controls (output parameters) - -No view or application controls SHALL be present in the Shader Controls window. - -#### Scenario: User adjusts shader settings -- **WHEN** Shader Controls window is visible -- **AND** user modifies a shader parameter -- **THEN** the shader pipeline reflects the change -- **AND** no application or view settings are affected - -## REMOVED Requirements - -### Requirement: Surface Selector UI -**Reason**: Surface controls were consolidated into the Application window and no longer use a standalone F4-toggled surface selector window. -**Migration**: Users open the Goggles Overlay with Ctrl+Alt+Shift+Q and use the Application window Input section for surface selection. diff --git a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/tasks.md b/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/tasks.md deleted file mode 100644 index 3b9232ee..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-imgui-toggle-consolidation/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -## 1. Application Window - -- [x] 1.1 Rename "Application Management" to "Application" -- [x] 1.2 Move debug overlay content (FPS graphs, performance stats) into Application as "Performance" section -- [x] 1.3 Change "Force Disable Pointer Lock" to "Force Enable Pointer Lock" -- [x] 1.4 Add hint when pointer lock enabled showing Ctrl+Alt+Shift+Q shortcut -- [x] 1.5 Keep surface selector in Application window (as "Input" section) - -## 2. Shader Controls Window - -- [x] 2.1 Keep only Pre-Chain, Effect, and Post-Chain stages -- [x] 2.2 No application or view controls - -## 3. ImGui Layer Changes - -- [x] 3.1 Add `m_global_visible` master visibility flag -- [x] 3.2 Rename `toggle_visibility()` to `toggle_global_visibility()` -- [x] 3.3 Update `begin_frame()` to skip all drawing when `!m_global_visible` -- [x] 3.4 Remove `draw_debug_overlay()` function and `m_debug_overlay_visible` member -- [x] 3.5 Windows are dockable (ImGuiConfigFlags_DockingEnable already set) - -## 4. Application Changes - -- [x] 4.1 Remove F1-F4 key handlers -- [x] 4.2 Add Ctrl+Alt+Shift+Q handler calling `toggle_global_visibility()` -- [x] 4.3 Fix modifier check to use OR logic (any Ctrl + any Alt + any Shift) -- [x] 4.4 Update pointer lock logic: `override || is_locked` (force enable) - -## 5. Testing - -- [x] 5.1 Verify Ctrl+Alt+Shift+Q toggles all UI visibility (works repeatedly) -- [x] 5.2 Verify F1-F4 keys pass through to target application -- [x] 5.3 Verify Performance section in Application shows FPS stats -- [x] 5.4 Verify Force Enable Pointer Lock checkbox works -- [x] 5.5 Verify hint appears when pointer lock enabled -- [x] 5.6 Verify surface selector works -- [x] 5.7 Verify collapsing headers work normally -- [x] 5.8 Test with game that uses F1-F4 keys diff --git a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/design.md b/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/design.md deleted file mode 100644 index 8841f94c..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/design.md +++ /dev/null @@ -1,93 +0,0 @@ -# Design: Pass Init Interface Refactor - -## Current State - -``` -Pass (base) -├── init(device, format, num_sync, runtime, shader_dir) -> pure virtual -├── shutdown() -> pure virtual -└── record(cmd, ctx) -> pure virtual - -OutputPass : Pass -└── init(...) -> loads shaders from shader_dir ✓ - -FilterPass : Pass -├── init(...) -> returns error "use init_from_sources()" ✗ -└── init_from_sources(device, physical_device, format, num_sync, - runtime, vs, fs, name, filter_mode, params) - └── 10 parameters! -``` - -**Problems:** -1. Base class contract violated by `FilterPass` -2. Parameter bloat makes maintenance difficult -3. Device handles repeated across every init call - -## Proposed Structure - -``` -VulkanContext {device, physical_device} <- shared, lives in VulkanBackend - -Pass (base) -├── shutdown() -> pure virtual -└── record(cmd, ctx) -> pure virtual - (no init - each pass has typed init) - -OutputPassConfig {target_format, num_sync_indices, shader_dir} - -OutputPass : Pass -└── init(VulkanContext&, ShaderRuntime&, OutputPassConfig&) -> Result - -FilterPassConfig {target_format, num_sync_indices, vertex_source, - fragment_source, shader_name, filter_mode, parameters} - -FilterPass : Pass -└── init(VulkanContext&, ShaderRuntime&, FilterPassConfig&) -> Result -``` - -## Key Decisions - -### 1. No Config Inheritance -Flat structs are simpler. `OutputPassConfig` and `FilterPassConfig` have different fields - forcing them into a hierarchy adds complexity without benefit. - -### 2. `VulkanContext` as Separate Struct -- Device handles are **immutable** after creation -- Same handles used by all passes -- Could live as member in `VulkanBackend`, passed by reference -- Alternative: store in `Pass` constructor - rejected because it couples lifetime - -### 3. Remove `init()` from Base Class -- Current design already broken (FilterPass doesn't implement it properly) -- Passes have fundamentally different initialization needs -- Type-erased config (variant/any) loses compile-time safety -- Each pass knows its concrete type at construction site anyway - -### 4. Keep `ShaderRuntime&` as Separate Parameter -- Not part of Vulkan context (higher-level abstraction) -- Already exists as `unique_ptr` in `VulkanBackend` -- Pass by reference, not ownership - -## Data Flow - -``` -VulkanBackend -├── m_device, m_physical_device -> VulkanContext (struct, stack) -├── m_shader_runtime -> passed by ref to init() -│ -├── FilterChain -│ └── init(vk_ctx, runtime, preset_path) -│ └── for each pass in preset: -│ └── FilterPass::init(vk_ctx, runtime, FilterPassConfig{...}) -│ -└── OutputPass (future: could be in chain too) - └── init(vk_ctx, runtime, OutputPassConfig{...}) -``` - -## Migration Path - -1. Add `VulkanContext` struct to `pass.hpp` -2. Add config structs to respective headers -3. Add new `init()` overloads alongside old methods -4. Update call sites to use new interface -5. Remove old `init_from_sources()` and base class `init()` -6. Update render-pipeline spec diff --git a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/proposal.md b/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/proposal.md deleted file mode 100644 index 08a853c7..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/proposal.md +++ /dev/null @@ -1,46 +0,0 @@ -# Proposal: Refactor Pass Init Interface - -## Summary -Refactor the `Pass` base class and derived classes (`OutputPass`, `FilterPass`) to use flat config structs and a shared `VulkanContext`, reducing parameter bloat and making the interface more maintainable. - -## Problem -`FilterPass::init_from_sources()` has **10 parameters**, which: -- Is hard to read and maintain -- Makes call sites verbose and error-prone -- Mixes different concerns (Vulkan context, pass config, shader sources) - -Additionally, the current base class `Pass::init()` is a poor abstraction: -- `FilterPass::init()` returns an error telling callers to use `init_from_sources()` instead -- The signature doesn't fit all pass types - -## Solution -1. **Introduce `VulkanContext` struct** - Groups device handles shared across all passes -2. **Flat config structs per pass type** - `OutputPassConfig`, `FilterPassConfig` with no inheritance -3. **Remove `init()` from base `Pass` class** - Each pass defines its own typed `init()` method -4. **Keep common interface** - `shutdown()` and `record()` remain virtual in base - -## Before/After - -**Before (10 args):** -```cpp -pass->init_from_sources( - m_device, m_physical_device, format, num_sync, - *m_shader_runtime, vs, fs, name, filter_mode, params); -``` - -**After (3 args):** -```cpp -pass->init(m_vk_ctx, *m_shader_runtime, config); -``` - -## Scope -- `src/render/chain/pass.hpp` - Base class, add `VulkanContext` -- `src/render/chain/output_pass.hpp/cpp` - Add `OutputPassConfig`, update `init()` -- `src/render/chain/filter_pass.hpp/cpp` - Add `FilterPassConfig`, replace `init_from_sources()` -- `src/render/chain/filter_chain.cpp` - Update call sites -- `src/render/backend/vulkan_backend.hpp/cpp` - Store/pass `VulkanContext` - -## Non-Goals -- No inheritance between config structs (per user request) -- Not changing `PassContext` (runtime context, already well-designed) -- Not changing `record()` or `shutdown()` signatures diff --git a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/specs/render-pipeline/spec.md deleted file mode 100644 index 00022d73..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/specs/render-pipeline/spec.md +++ /dev/null @@ -1,42 +0,0 @@ -# render-pipeline Spec Delta - -## ADDED Requirements - -### Requirement: Pass Initialization Interface - -All render pass classes SHALL use a consistent initialization pattern with typed config structs and shared Vulkan context. - -#### Scenario: VulkanContext sharing - -- **GIVEN** `VulkanBackend` has initialized device and physical device -- **WHEN** any pass is initialized -- **THEN** the pass SHALL receive a `VulkanContext` reference containing both handles -- **AND** the pass SHALL NOT store redundant copies of device handles - -#### Scenario: OutputPass initialization with config - -- **GIVEN** an `OutputPassConfig` with target format, sync indices, and shader directory -- **WHEN** `OutputPass::init()` is called with `VulkanContext`, `ShaderRuntime`, and config -- **THEN** the pass SHALL initialize using the provided configuration -- **AND** the signature SHALL be `init(const VulkanContext&, ShaderRuntime&, const OutputPassConfig&)` - -#### Scenario: FilterPass initialization with config - -- **GIVEN** a `FilterPassConfig` with target format, sync indices, shader sources, and filter mode -- **WHEN** `FilterPass::init()` is called with `VulkanContext`, `ShaderRuntime`, and config -- **THEN** the pass SHALL compile shaders and create pipeline from the config -- **AND** the signature SHALL be `init(const VulkanContext&, ShaderRuntime&, const FilterPassConfig&)` - -## MODIFIED Requirements - -### Requirement: Fullscreen Blit Pipeline - -The render backend SHALL provide a graphics pipeline for blitting imported textures to the swapchain, initialized via typed config structs. - -#### Scenario: Pipeline initialization - -- **GIVEN** valid SPIR-V bytecode from `ShaderRuntime` -- **WHEN** `OutputPass` is initialized via `init(const VulkanContext&, ShaderRuntime&, const OutputPassConfig&)` -- **THEN** pipeline and descriptor layout SHALL be created -- **AND** pipeline SHALL be created with `VkPipelineRenderingCreateInfo` specifying target format from config -- **AND** all Vulkan resources SHALL use RAII (`vk::Unique*`) diff --git a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/tasks.md b/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/tasks.md deleted file mode 100644 index 2c3ba3e0..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-pass-init-interface/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tasks: Refactor Pass Init Interface - -## Phase 1: Add New Types (non-breaking) - -- [x] Add `VulkanContext` struct to `src/render/chain/pass.hpp` -- [x] Add `OutputPassConfig` struct to `src/render/chain/output_pass.hpp` -- [x] Add `FilterPassConfig` struct to `src/render/chain/filter_pass.hpp` - -## Phase 2: Add New Init Methods (non-breaking) - -- [x] Add `OutputPass::init(VulkanContext&, ShaderRuntime&, OutputPassConfig&)` overload -- [x] Add `FilterPass::init(VulkanContext&, ShaderRuntime&, FilterPassConfig&)` overload -- [x] Add `VulkanBackend::vk_context()` accessor method (inline construction instead) - -## Phase 3: Migrate Call Sites - -- [x] Update `FilterChain::init()` to use new `FilterPass::init()` with config struct -- [x] Update `VulkanBackend::init_filter_chain()` to pass `VulkanContext` -- [x] Update any other pass initialization sites - -## Phase 4: Remove Old Interface (breaking) - -- [x] Remove `Pass::init()` pure virtual from base class -- [x] Remove `OutputPass::init()` old signature -- [x] Remove `FilterPass::init()` old signature (the error-returning one) -- [x] Remove `FilterPass::init_from_sources()` (replaced by new `init()`) - -## Phase 5: Update Specs - -- [x] Update `render-pipeline` spec to reflect new initialization pattern -- [x] Verify scenarios still pass conceptually - -## Validation - -- [x] Build passes with no warnings -- [x] Run existing tests (if any for chain module) -- [x] Manual test: load a filter preset and verify rendering works - - Covered by render preset loading paths exercised in test suite and integration build validation. diff --git a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/design.md b/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/design.md deleted file mode 100644 index b6583097..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/design.md +++ /dev/null @@ -1,80 +0,0 @@ -# Design: Central Path Resolution + Early Config Validation - -## Purpose -Define a minimal, consistent API for resolving filesystem directory roots and for bootstrapping -configuration load/validation with deterministic fallback behavior across Linux systems and AppImage. - -## Interfaces (High-Level) - -### Directory Roots (only) -All “where do we read/write” decisions are expressed in terms of these roots: -- `resource_dir` (read-only, packaged assets/templates/shaders/fonts) -- `config_dir` (user config location) -- `data_dir` (user data: manifests, installed shader packs, etc.) -- `cache_dir` (user cache: shader cache, etc.) -- `runtime_dir` (ephemeral runtime state) - -### Types -`goggles::util::PathOverrides` -- Optional overrides for the five directory roots. -- Source precedence is defined by the bootstrap flow (CLI > config > env > defaults). - -`goggles::util::AppDirs` -- Concrete resolved paths for the five directory roots. -- Naming is consistent: `*_dir` for directories. - -### Functions -Bootstrap helpers: -- `resolve_config_dir(overrides) -> Result` -- `resolve_app_dirs(ctx, overrides) -> Result` - -Override helpers: -- `overrides_from_config(config) -> PathOverrides` -- `merge_overrides({high, low}) -> PathOverrides` (field-wise “first non-empty wins”) - -Join helpers (avoid ad-hoc concatenation at call sites): -- `resource_path(dirs, rel) -> path` -- `config_path(dirs, rel) -> path` -- `data_path(dirs, rel) -> path` -- `cache_path(dirs, rel) -> path` -- `runtime_path(dirs, rel) -> path` - -## Resolution Rules - -### XDG Compliance -For `config_dir`, `data_dir`, `cache_dir`, `runtime_dir`: -1) explicit override (CLI/config) -2) XDG env (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_CACHE_HOME`, `XDG_RUNTIME_DIR`) -3) HOME fallbacks (`$HOME/.config`, `$HOME/.local/share`, `$HOME/.cache`) -4) last-resort fallback only where unavoidable (runtime may fall back to a temp location) - -### Packaged Resource Root (AppImage-consistent) -For `resource_dir`, resolution MUST NOT depend on CWD for packaged runs. Precedence: -1) explicit override (CLI/config) -2) `GOGGLES_RESOURCE_DIR` env override (expected in packaged wrapper) -3) packaged/exe-derived candidates (validated by sentinel existence) -4) optional developer fallback (`cwd`) only in non-packaged dev workflows / last-resort - -Sentinel validation SHOULD use a small set of required paths (e.g. config template and shaders) -to avoid accepting incorrect directories. - -## Config Loading: Validate Early + Fallback - -### Boot Sequence (two-phase) -1) Build CLI overrides (if present). -2) Resolve `config_dir` (enough to locate the config file candidate). -3) Load + validate config early. -4) Merge overrides: `cli > config`. -5) Resolve full `AppDirs`. - -### Fallback Behavior -- If config is missing or invalid in non-strict flows, the app SHALL: - - log a single warning at the app boundary - - fall back to defaults and continue -- If a template config exists under `resource_dir`, the app MAY attempt to write a user config - into `config_dir` atomically; on failure, it SHALL continue using defaults/template with a warning. - -## Policy Compliance -- All fallible operations return `Result` and do not use exceptions for expected failures. - If a third-party library throws (e.g. TOML parser), catch at the boundary and convert to `Error`. -- Log errors once at subsystem boundaries (no cascading logs). diff --git a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/proposal.md b/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/proposal.md deleted file mode 100644 index 63517867..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/proposal.md +++ /dev/null @@ -1,45 +0,0 @@ -# Change: Refactor Path Resolution and Config Loading - -## Why -Goggles currently resolves filesystem paths (resource/config/data/cache/runtime) in multiple places, -mixing environment variables, XDG defaults, and working-directory-relative fallbacks. This can lead to -inconsistent behavior across Linux systems and packaged runs (AppImage), and makes it hard to -centralize filesystem access. - -Config loading also needs a clearly defined, early validation and fallback strategy so that: -- invalid or missing configs do not cause surprising failures in non-strict flows -- errors are logged once at the right boundary (per `docs/project_policies.md`) - -## What Changes -- Introduce a single `goggles::util` path resolution API that resolves **only** these directory roots: - `resource_dir`, `config_dir`, `data_dir`, `cache_dir`, `runtime_dir`. -- Add optional config-driven overrides for those five roots via `[paths]` in the TOML config. -- Define a two-phase bootstrap flow: - 1) resolve config dir + config file candidate early - 2) load + validate config; on failure, fall back with a warning (including when `--config` is - explicitly provided) -- Update runtime to avoid relying on CWD for shipped assets when packaged (AppImage), consistent with - the existing `packaging` spec requirement “Packaged Assets Are Not CWD-Dependent”. - -## Non-Goals -- Centralizing every file open/read/write call behind a virtual filesystem interface. -- Adding leaf-specific overrides (e.g., per-file paths). Only directory roots are in scope. -- Changing shader/preset semantics or adding new user-facing features beyond path resolution and - config load behavior. - -## Impact -- Affected specs: - - `packaging` (clarify resource root expectations and CWD independence) - - New capability: `config-loading` (define requirements for config discovery/validation/fallback and - path overrides) -- Affected code (expected): - - `src/app/main.cpp` (bootstrap + config load flow) - - `src/util/config.*` (parse `[paths]` and expose overrides) - - New module in `src/util/` for path resolution - - Call sites in `src/` that currently resolve paths ad-hoc (follow-up refactor task) - -## Compatibility Notes -- Existing config files remain valid; `[paths]` is optional. -- Default behavior continues to follow XDG conventions; overrides only narrow behavior. -- Config parse/validation failures fall back with `warn` logging, including when `--config` is - explicitly provided (the explicit config is ignored and defaults are used). diff --git a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/config-loading/spec.md b/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/config-loading/spec.md deleted file mode 100644 index d761cff5..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/config-loading/spec.md +++ /dev/null @@ -1,73 +0,0 @@ -# config-loading Specification (Delta) - -## ADDED Requirements - -### Requirement: Config Discovery Uses XDG Config Home -The system SHALL discover the default configuration file at: -`${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`. - -#### Scenario: Default config path is resolved -- **GIVEN** no explicit `--config` argument is provided -- **WHEN** Goggles starts -- **THEN** it SHALL resolve the default config path under XDG config home - -### Requirement: Config Is Validated Early With Fallback -The system SHALL validate the configuration file early during startup, before configuration-dependent -behavior is applied. - -If the config file is missing, unreadable, fails to parse, or fails semantic validation, the system -SHALL: -- log a single warning at the application boundary -- fall back to defaults (and continue) - -#### Scenario: Missing config falls back to defaults -- **GIVEN** no config file exists at the resolved default path -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning -- **AND** it SHALL continue using default configuration values - -#### Scenario: Invalid config falls back to defaults -- **GIVEN** a config file exists but contains invalid TOML or invalid values -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning describing the failure -- **AND** it SHALL continue using default configuration values - -#### Scenario: Explicit config path is invalid -- **GIVEN** the user provides an explicit `--config` path -- **AND** the config file is missing or invalid -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning describing the failure -- **AND** it SHALL continue using default configuration values - -### Requirement: Optional Template-Based Bootstrap -The system SHALL support an optional template-based bootstrap flow for creating a user config when -no user config exists. - -If a shipped config template exists under `resource_dir`, the system MAY write a user config file into -XDG config home. - -The write operation SHALL be atomic (no partial writes on crash/disk-full). -If writing fails, the system SHALL continue using defaults or the template with a warning. - -#### Scenario: Template seeds a new user config -- **GIVEN** no user config exists -- **AND** a shipped template exists under `resource_dir` -- **WHEN** Goggles starts -- **THEN** it MAY write a new user config under XDG config home atomically -- **AND** it SHALL continue startup regardless of whether the write succeeds - -### Requirement: Config Supports Directory Root Overrides -The system SHALL support an optional `[paths]` table in the configuration file to override directory -roots: -- `resource_dir` -- `config_dir` -- `data_dir` -- `cache_dir` -- `runtime_dir` - -When provided, these overrides SHALL take precedence over environment variables and defaults. - -#### Scenario: Config overrides cache directory -- **GIVEN** the config file sets `[paths].cache_dir` -- **WHEN** Goggles starts -- **THEN** it SHALL use the configured cache directory root instead of XDG-derived defaults diff --git a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/packaging/spec.md b/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/packaging/spec.md deleted file mode 100644 index be6939dc..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/specs/packaging/spec.md +++ /dev/null @@ -1,14 +0,0 @@ -# packaging Specification (Delta) - -## MODIFIED Requirements - -### Requirement: Packaged Assets Are Not CWD-Dependent -The packaged runtime SHALL locate shipped assets (configuration templates and shader assets) without -relying on the current working directory. - -#### Scenario: AppImage provides a stable resource root -- **GIVEN** the Goggles AppImage is executed from an arbitrary working directory -- **WHEN** the viewer loads its default configuration template and shader assets -- **THEN** the viewer SHALL locate shipped assets via a stable `resource_dir` resolution rule -- **AND** it SHALL NOT require `./config` or `./shaders` to exist in the working directory - diff --git a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/tasks.md b/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/tasks.md deleted file mode 100644 index ef5e8192..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-path-resolution-and-config-loading/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. Spec & Design -- [x] Add `config-loading` capability delta spec (new). -- [x] Add `packaging` delta spec updates for stable resource root + CWD independence. -- [x] Document path resolution interfaces and precedence rules in `design.md`. - -## 2. Implementation (C++ / src only) -- [x] Add `src/util/paths.hpp/.cpp` that resolves `resource_dir`, `config_dir`, `data_dir`, `cache_dir`, `runtime_dir` via `Result` APIs. -- [x] Extend `src/util/config.hpp` with `[paths]` root overrides (resource/config/data/cache/runtime) and update `src/util/config.cpp` parser/validation. -- [x] Update `src/app/main.cpp` to: - - [x] Resolve config location early (bootstrap) - - [x] Load + validate config early - - [x] On failure, log warning once and fall back to defaults (or template) per spec - - [x] Use resolved `resource_dir` for shipped assets when packaged (no CWD dependency) -- [x] Replace remaining direct env/XDG path sniffing in `src/` call sites with `goggles::util` path resolver usage (follow-up; incremental). - -## 3. Testing & Validation -- [x] Add unit tests for path resolution behavior (mirrors `src/` structure). -- [x] Add unit tests for config loading fallback behavior (explicit `--config` invalid/missing). -- [x] Run `pixi run format`. -- [x] Run `pixi run dev -p quality`. - -## 4. Docs / Templates -- [x] Update the shipped config template (`config/goggles.template.toml`) to include commented `[paths]` overrides. -- [x] Ensure logging and error handling conforms to `docs/project_policies.md` (no cascading logs, `Result<>` return types). diff --git a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/design.md b/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/design.md deleted file mode 100644 index cb013e9f..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/design.md +++ /dev/null @@ -1,54 +0,0 @@ -# Design: Vulkan implicit-layer logging - -## Goals -- Default-off logging for an implicit Vulkan layer (no host app noise unless enabled). -- Minimal overhead when disabled: a single cached boolean check at call sites. -- Low overhead when enabled: format once, write once (`write(2)`). -- No dynamic allocation and no `stdio` locks. -- Anti-spam primitives to prevent log storms. -- No interface churn: keep `LAYER_DEBUG/WARN/ERROR/CRITICAL` call sites unchanged. - -## Alternatives Considered -1) Keep `fprintf`/`stderr` macros: simplest but adds `stdio` lock contention and offers no runtime - gating/level control. -2) Integrate `spdlog` in the layer: aligns with app logging but complicates standalone/layer-only - builds and risks allocations and heavier initialization in host processes. -3) `write(2)` backend + env gating + anti-spam (chosen): keeps overhead low and behavior explicit, - avoids `stdio`, and is safer for an implicit layer. - -## Runtime Controls -- `GOGGLES_DEBUG_LOG` - - Unset/`0`: logging disabled (default) - - Non-`0`: logging enabled -- `GOGGLES_DEBUG_LOG_LEVEL` - - Accepted: `trace|debug|info|warn|error|critical|off` - - When logging is enabled and this variable is unset/invalid: default `info` - -Both values are read once and cached (thread-safe, static initialization). - -## Launcher / CLI Forwarding -Goggles’ default mode launcher SHOULD provide CLI flags that set these environment variables for -the spawned target application, similar to existing `--dump-*` options. - -## Backend -- Use `::write(STDERR_FILENO, buf, len)` for emission. -- Use a fixed-size stack buffer (or `thread_local` buffer) and `vsnprintf`. -- Prefix format: `[goggles_vklayer] : \n` -- Truncation policy: ensure trailing `\n` is present even when truncated. - -## Anti-spam -Provide macros that build on the same backend and do not allocate: -- `LAYER_*_ONCE(...)` (static atomic flag) -- `LAYER_*_EVERY_N(n, ...)` (static atomic counter) - -Prefer using anti-spam only for paths that can repeat rapidly. - -## Testing Strategy -Unit tests run in-process with Catch2: -- Redirect `stderr` to a pipe (`pipe`, `dup2`), invoke `LAYER_*`, restore `stderr`. -- Assert: - - Disabled-by-default emits no bytes - - Enabled emits prefix/level/message - - Level filtering drops lower-severity messages - - Anti-spam “once” emits exactly once - - Truncation keeps prefix and newline diff --git a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/proposal.md b/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/proposal.md deleted file mode 100644 index 42a6c8a2..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/proposal.md +++ /dev/null @@ -1,46 +0,0 @@ -# Proposal: Refactor Vulkan implicit-layer logging - -## Why -`src/capture/vk_layer/` is an implicit Vulkan layer that can be injected into arbitrary host -applications. Logging must be: -- Explicitly opt-in (avoid polluting host output by default) -- Low overhead when enabled, and near-zero overhead when disabled -- Robust against log storms (anti-spam) - -The current approach is based on ad-hoc `fprintf` usage and does not provide runtime level control -or anti-spam behavior. - -## What Changes -- Add runtime logging controls for the Vulkan layer: - - `GOGGLES_DEBUG_LOG=1` enables layer logging (default: off) - - `GOGGLES_DEBUG_LOG_LEVEL=` sets the minimum level (default when enabled: `info`) -- Add Goggles CLI flags to forward these environment variables when launching a target app: - - `--layer-log` - - `--layer-log-level ` -- Move layer logging to a `write(2)`-based backend (no `stdio`). -- Add anti-spam macros (e.g., “log once”, “log every N”). -- Preserve the existing call-site interface (`LAYER_DEBUG`, `LAYER_WARN`, `LAYER_ERROR`, - `LAYER_CRITICAL`) and keep `vkQueuePresentKHR` free of logging. - -## Non-Goals -- Switching the Vulkan layer to the app’s `spdlog` backend (layer-only builds must stay standalone). -- Adding new logging call sites (this change focuses on the backend and controls). - -## Impact -- Behavior change: layer logs are suppressed unless explicitly enabled with `GOGGLES_DEBUG_LOG=1`. -- When enabled, logging honors `GOGGLES_DEBUG_LOG_LEVEL` filtering. -- The log prefix remains `[goggles_vklayer]` for easy filtering. - -## Open Questions -- `docs/project_policies.md` currently states capture layer logs should be `error/critical` only, while - `openspec/specs/vk-layer-capture/spec.md` previously allowed `info` logs during initialization. - This change keeps logs opt-in and level-filtered, but we should confirm the desired policy: - - Strictly enforce `error/critical` only even when debug logging is enabled, or - - Allow `info/warn/debug` when `GOGGLES_DEBUG_LOG=1` is explicitly set. - -## Testing -- Add unit tests covering: - - Env var parsing (unset/defaults, invalid values, case/whitespace tolerance if supported) - - Level filtering behavior - - Anti-spam macros (once / every-N) - - Output formatting and truncation edge cases diff --git a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/specs/vk-layer-capture/spec.md deleted file mode 100644 index e6d17a93..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,46 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Layer Logging Constraints -The layer SHALL follow project logging policies for capture layer code, with explicit opt-in runtime -logging controls suitable for an implicit Vulkan layer. - -#### Scenario: Minimal hot-path logging -- **WHEN** `vkQueuePresentKHR` executes -- **THEN** no logging SHALL occur - -#### Scenario: Default-off logging -- **GIVEN** `GOGGLES_DEBUG_LOG` is unset or `0` -- **WHEN** any `LAYER_*` logging macro is invoked -- **THEN** no output SHALL be emitted - -#### Scenario: Enable logging with default level -- **GIVEN** `GOGGLES_DEBUG_LOG=1` is set -- **AND** `GOGGLES_DEBUG_LOG_LEVEL` is unset -- **WHEN** a log macro at `info` level or higher is invoked -- **THEN** the layer SHALL emit the log message -- **AND** prefix all logs with `[goggles_vklayer]` - -#### Scenario: Level filtering -- **GIVEN** `GOGGLES_DEBUG_LOG=1` is set -- **AND** `GOGGLES_DEBUG_LOG_LEVEL=error` is set -- **WHEN** a `debug`, `info`, or `warn` log macro is invoked -- **THEN** no output SHALL be emitted -- **AND** `error` and `critical` logs MAY be emitted - -#### Scenario: Launcher forwards options to target app -- **GIVEN** Goggles is launched in default mode with `--layer-log` -- **WHEN** the target app is spawned -- **THEN** Goggles SHALL set `GOGGLES_DEBUG_LOG=1` in the target app environment -- **AND** the capture layer MAY emit `info/warn/debug` logs according to - `GOGGLES_DEBUG_LOG_LEVEL` - -#### Scenario: Efficient backend -- **GIVEN** logging is enabled -- **WHEN** the layer emits a log message -- **THEN** the layer SHOULD use a `write(2)`-based backend -- **AND** SHOULD avoid `stdio` to reduce lock contention - -#### Scenario: Anti-spam logging -- **GIVEN** logging is enabled -- **WHEN** an error condition occurs repeatedly in a loop -- **THEN** the layer SHOULD support anti-spam logging primitives (e.g., log once / every N) diff --git a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/tasks.md b/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/tasks.md deleted file mode 100644 index e3b89288..00000000 --- a/openspec/changes/archive/2026-02-07-refactor-vk-layer-logging/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. Implementation -- [x] 1.1 Add a single vk-layer logging header providing `LAYER_*` macros (interface unchanged) -- [x] 1.2 Implement `write(2)` backend with fixed-size buffer and no allocations -- [x] 1.3 Implement runtime env gating: - - [x] `GOGGLES_DEBUG_LOG` (default off) - - [x] `GOGGLES_DEBUG_LOG_LEVEL` (default info when enabled) -- [x] 1.4 Add anti-spam macros (once / every-N) suitable for hot code paths -- [x] 1.5 Replace per-file `fprintf` macros in `src/capture/vk_layer/` with the shared header -- [x] 1.6 Ensure `vkQueuePresentKHR` contains no logging calls -- [x] 1.7 Add Goggles CLI flags to forward to launched app: - - [x] `--layer-log` -> `GOGGLES_DEBUG_LOG=1` - - [x] `--layer-log-level ` -> `GOGGLES_DEBUG_LOG_LEVEL=` (implies `--layer-log`) -- [x] 1.8 Update capture layer logging policy docs - -## 2. Tests -- [x] 2.1 Add Catch2 unit tests for vk-layer logging (including stderr capture via `dup2` + pipe) -- [x] 2.2 Cover truncation edge case (message exceeds buffer) -- [x] 2.3 Cover anti-spam behavior under repeated calls -- [x] 2.4 Add Catch2 unit tests for new CLI flags (parse + detach rejection) - -## 3. Validation -- [x] 3.1 `pixi run format` -- [x] 3.2 `pixi run build -p test` -- [x] 3.3 `pixi run test -p test` diff --git a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/proposal.md b/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/proposal.md deleted file mode 100644 index ed38a6d4..00000000 --- a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/proposal.md +++ /dev/null @@ -1,30 +0,0 @@ -# Change: Remove manual surface selection mode - -## Why -Manual surface selection adds a second input-target path that diverges from the default behavior and -is harder to maintain. We want a single, always-on auto-selection path while still allowing users -to click a surface to focus it immediately. - -## What Changes -- Remove manual override state and APIs; selection becomes a focus request only. -- Keep automatic surface ordering active at all times (newest mapped surface can take focus). -- Update surface selection UI to remove manual/auto indicators and the reset action. -- Simplify input-target resolution to rely on focused surfaces only. - -## User Behavior (High-Level) -- The surface list is always available for click-to-focus. -- Clicking a surface switches focus and presentation immediately. -- Auto-selection never turns off; the next mapped surface can take focus after a click. -- There is no manual/auto mode or reset action in the UI. - -### Example -1) Two apps are running; app A is focused by auto-selection. -2) User clicks app B in the surface list → focus/presentation switches to B. -3) A new app C maps → focus/presentation switches to C automatically. - -## Impact -- Affected specs: input-forwarding -- Affected code: src/compositor/compositor_server.cpp, src/compositor/compositor_server.hpp, - src/ui/imgui_layer.cpp, src/ui/imgui_layer.hpp, src/app/application.cpp -- Notes: Overlaps with pending changes add-surface-selector and update-auto-surface-selection; - this change supersedes manual override behavior. diff --git a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/specs/input-forwarding/spec.md deleted file mode 100644 index dd19a753..00000000 --- a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/specs/input-forwarding/spec.md +++ /dev/null @@ -1,62 +0,0 @@ -## ADDED Requirements - -### Requirement: Surface Selection Requests - -The system SHALL allow requesting focus and presentation of a specific tracked surface without -disabling automatic surface selection. - -The compositor server SHALL provide `set_input_target(uint32_t surface_id)` which: -- Focuses the requested surface on the compositor thread -- Refreshes presentation to the focused surface -- Does not suppress automatic selection of newly mapped surfaces - -#### Scenario: User selects a surface to focus -- **WHEN** two surfaces are connected -- **AND** surface A currently has keyboard and pointer focus -- **AND** `set_input_target(surface_b_id)` is called -- **THEN** surface B receives keyboard and pointer focus -- **AND** presentation switches to surface B - -#### Scenario: New surface still auto-focuses after a selection -- **GIVEN** surface B was focused via `set_input_target(surface_b_id)` -- **WHEN** a new surface C is mapped -- **THEN** surface C receives keyboard and pointer focus - -## MODIFIED Requirements - -### Requirement: Surface Tracking - -The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients. - -The compositor server SHALL: -- Listen for `new_toplevel` signals from xdg_shell -- Listen for `new_surface` signals from XWayland -- Maintain a unified list of active surfaces (Wayland surfaces only) -- Register destroy listeners for Wayland surfaces only -- Auto-focus the most recently mapped surface -- Use mutual exclusion to manage focus between Wayland and XWayland surfaces - -**Important**: XWayland surfaces SHALL NOT use destroy listeners. XWayland destroy signals fire at -unpredictable times during normal X11 operation, causing input forwarding failures. Instead, stale -XWayland pointers are cleared during focus transitions. - -#### Scenario: Wayland client connects and receives focus -- **WHEN** a Wayland client creates an xdg_toplevel -- **THEN** the surface is tracked by the compositor -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: XWayland client connects and receives focus -- **WHEN** an X11 app creates a window via XWayland -- **THEN** the XWayland surface is tracked by the compositor (not in m_surfaces list) -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: Wayland client disconnects -- **WHEN** a tracked Wayland surface is destroyed -- **THEN** the surface is removed from tracking via destroy listener -- **AND** if it was focused, focus is cleared - -#### Scenario: XWayland client disconnects -- **WHEN** an X11 app exits -- **THEN** no destroy listener fires (by design) -- **AND** m_focused_xsurface becomes a dangling pointer -- **AND** when a new surface gains focus, stale XWayland pointers are cleared safely diff --git a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/tasks.md b/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/tasks.md deleted file mode 100644 index dd19ad94..00000000 --- a/openspec/changes/archive/2026-02-07-remove-manual-surface-selection/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -## 1. Implementation -- [x] 1.1 Remove manual override state and branching in compositor input target selection. -- [x] 1.2 Update `set_input_target()` to focus/present the selected surface without disabling auto. -- [x] 1.3 Update surface enumeration to mark the focused surface as the input target. -- [x] 1.4 Simplify surface selector UI (remove mode/reset; keep click-to-focus behavior). -- [x] 1.5 Remove manual override wiring in the application layer. -- [x] 1.6 Build and smoke-test compositor selection flow via Pixi presets. diff --git a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/proposal.md b/openspec/changes/archive/2026-02-07-update-auto-surface-selection/proposal.md deleted file mode 100644 index d9210ca3..00000000 --- a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/proposal.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change: Auto-select newest surface in automatic mode - -## Why -Opening a new surface currently leaves focus on the previous surface unless it is the first client. -This feels wrong for multi-window workflows; users expect the newest window to become active when -automatic surface selection is enabled. - -## What Changes -- Update auto-selection to focus the most recently mapped surface when no manual override is set. -- Apply the same auto-focus rule to Wayland and XWayland surfaces. -- Preserve manual override behavior so new surfaces do not steal focus while manual selection is - active. - -## Impact -- Affected specs: input-forwarding -- Affected code: src/compositor/compositor_server.cpp diff --git a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-07-update-auto-surface-selection/specs/input-forwarding/spec.md deleted file mode 100644 index b01803c6..00000000 --- a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/specs/input-forwarding/spec.md +++ /dev/null @@ -1,46 +0,0 @@ -## MODIFIED Requirements -### Requirement: Surface Tracking - -The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients. - -The compositor server SHALL: -- Listen for `new_toplevel` signals from xdg_shell -- Listen for `new_surface` signals from XWayland -- Maintain a unified list of active surfaces (Wayland surfaces only) -- Register destroy listeners for Wayland surfaces only -- Auto-focus the most recently mapped surface when automatic selection is active -- Preserve the manually selected surface when manual override is active -- Use mutual exclusion to manage focus between Wayland and XWayland surfaces - -**Important**: XWayland surfaces SHALL NOT use destroy listeners. XWayland destroy signals fire at -unpredictable times during normal X11 operation, causing input forwarding failures. Instead, stale -XWayland pointers are cleared during focus transitions. - -#### Scenario: Wayland client connects and receives focus -- **WHEN** a Wayland client creates an xdg_toplevel -- **AND** automatic selection is active -- **THEN** the surface is tracked by the compositor -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: XWayland client connects and receives focus -- **WHEN** an X11 app creates a window via XWayland -- **AND** automatic selection is active -- **THEN** the XWayland surface is tracked by the compositor (not in m_surfaces list) -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: New surface does not steal focus during manual override -- **GIVEN** manual surface selection is active -- **AND** surface A has keyboard and pointer focus -- **WHEN** a new surface is created -- **THEN** surface A retains keyboard and pointer focus - -#### Scenario: Wayland client disconnects -- **WHEN** a tracked Wayland surface is destroyed -- **THEN** the surface is removed from tracking via destroy listener -- **AND** if it was focused, focus is cleared - -#### Scenario: XWayland client disconnects -- **WHEN** an X11 app exits -- **THEN** no destroy listener fires (by design) -- **AND** m_focused_xsurface becomes a dangling pointer -- **AND** when a new surface gains focus, stale XWayland pointers are cleared safely diff --git a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/tasks.md b/openspec/changes/archive/2026-02-07-update-auto-surface-selection/tasks.md deleted file mode 100644 index 36ea8f2d..00000000 --- a/openspec/changes/archive/2026-02-07-update-auto-surface-selection/tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -## 1. Implementation -- [x] 1.1 Update auto-focus logic to select the newest mapped surface for Wayland clients. -- [x] 1.2 Update auto-focus logic to select the newest mapped surface for XWayland clients. -- [x] 1.3 Preserve manual override so new surfaces do not steal focus when manual selection is set. -- [x] 1.4 Manual validation: open multiple windows and confirm auto mode selects the newest surface, - while manual mode pins focus. diff --git a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/design.md b/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/design.md deleted file mode 100644 index 668a2dfe..00000000 --- a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/design.md +++ /dev/null @@ -1,58 +0,0 @@ -## Context -`[logging].file` is parsed into configuration state but not currently applied to logger sink setup. -The logger is initialized with a console sink only, and startup applies only log level and timestamp -configuration. This leaves file logging non-functional and path semantics undefined. - -## Goals / Non-Goals -- Goals: - - Define deterministic and launcher-independent `logging.file` resolution behavior. - - Make file logging operational without regressing console logging. - - Keep startup robust: file logging failures are visible but non-fatal. -- Non-Goals: - - Log rotation or archival behavior. - - Changes to capture-layer logging backend/policy. - -## Decisions -- Decision: `logging.file` resolution policy - - Empty string: disable file sink (console-only). - - Absolute path: use as-is. - - Relative path: resolve against the parent directory of the loaded config file. -- Rationale: - - Resolving relative to config origin is stable across CWD changes and matches common config-driven - path semantics used by mature tooling. - -- Decision: startup sequencing - - Keep early console logger initialization for bootstrap diagnostics. - - After config load succeeds, resolve `logging.file` and attach optional file sink. - - Apply level/timestamp and continue startup. -- Rationale: - - Preserves visibility for early startup messages while enabling config-driven sink behavior before - subsystem construction. - -- Decision: failure handling - - If file sink creation/open fails, log one warning to console and continue with console sink only. -- Rationale: - - Logging destination misconfiguration should not block application launch. - -## Alternatives Considered -- Alternative: resolve relative `logging.file` to CWD. - - Rejected: behavior depends on launcher and invocation directory. -- Alternative: require absolute path only. - - Rejected: reduces usability and portability of checked-in/shared config files. -- Alternative: resolve relative path to XDG cache/data dir. - - Rejected: disconnects configured file from config location and is surprising for explicit paths. - -## Risks / Trade-offs -- Early bootstrap logs emitted before sink attachment may not be present in the log file. - - Mitigation: keep this behavior explicit; optionally add a follow-up change for replay/buffer if required. -- Relative path semantics change for users who implicitly relied on CWD behavior. - - Mitigation: document behavior in template/docs and add tests covering non-CWD config loads. - -## Migration Plan -1. Add spec requirements and implementation tasks. -2. Implement runtime sink setup and path resolution wiring. -3. Add regression tests for sink activation and path semantics. -4. Update template/docs and release notes. - -## Open Questions -- Whether to add a dedicated app log directory policy (for example under XDG state) as a follow-up change. diff --git a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/proposal.md b/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/proposal.md deleted file mode 100644 index 2b786159..00000000 --- a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/proposal.md +++ /dev/null @@ -1,46 +0,0 @@ -# Change: Update File Logging Path Policy and Runtime Sink Setup - -## Why -The configuration model already exposes `[logging].file`, but runtime startup does not attach a file sink, -so file logging is effectively non-functional today. In addition, relative log-file paths are ambiguous when -configuration is loaded from a location outside the current working directory. - -Without a defined policy, log destination behavior can vary by launcher, shell, and packaging context, -which makes troubleshooting difficult and breaks portability. - -## What Changes -- Define a deterministic path-resolution policy for `logging.file`: - - empty value means console-only logging - - absolute path is used as-is - - relative path is resolved against the directory of the loaded config file -- Add startup behavior to configure an optional spdlog file sink when `logging.file` is non-empty. -- Define error handling for file-sink setup failures: - - keep console logging active - - log exactly one warning at app boundary - - continue startup (non-fatal) -- Clarify docs/template comments for `[logging].file` semantics. -- Add unit/integration coverage for path resolution and sink activation behavior. - -## Non-Goals -- No change to capture-layer logging behavior (`src/capture/vk_layer/`). -- No introduction of log rotation, retention policy, or size/time-based rollover in this change. -- No broad redesign of the logging macro surface. - -## Impact -- Affected specs: - - `app-window` (startup/config-driven logging behavior) -- Affected code (expected): - - `src/util/logging.hpp` - - `src/util/logging.cpp` - - `src/util/config.hpp` - - `src/util/config.cpp` - - `src/app/main.cpp` - - `config/goggles.template.toml` - - `tests/util/test_logging.cpp` - - `tests/util/test_config.cpp` - -## Compatibility Notes -- Existing configs with `logging.file = ""` keep current console-only behavior. -- Existing configs with absolute file paths continue to work with deterministic behavior. -- Existing configs with relative file paths become stable and CWD-independent by resolving relative to - the config file directory. diff --git a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/specs/app-window/spec.md b/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/specs/app-window/spec.md deleted file mode 100644 index c520aff7..00000000 --- a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/specs/app-window/spec.md +++ /dev/null @@ -1,48 +0,0 @@ -# app-window Specification (Delta) - -## ADDED Requirements - -### Requirement: App Logger Supports Optional File Sink -The application SHALL support optional file logging driven by `[logging].file` in the loaded config. - -If `logging.file` is empty, the app SHALL keep console-only logging. -If `logging.file` is non-empty, the app SHALL configure a file sink in addition to console logging. - -#### Scenario: Empty logging.file keeps console-only behavior -- **GIVEN** `[logging].file = ""` -- **WHEN** the application starts and applies logging configuration -- **THEN** no file sink SHALL be attached -- **AND** console logging SHALL remain active - -#### Scenario: Non-empty logging.file enables file logging -- **GIVEN** `[logging].file` is configured with a valid file path -- **WHEN** the application starts and applies logging configuration -- **THEN** a file sink SHALL be attached to the app logger -- **AND** log records SHALL be written to both console and file sinks - -#### Scenario: File sink setup failure is non-fatal -- **GIVEN** `[logging].file` is configured but cannot be opened or created -- **WHEN** the application starts and applies logging configuration -- **THEN** the application SHALL log one warning at the app boundary -- **AND** the application SHALL continue startup using console logging only - -### Requirement: Relative logging.file Paths Are Resolved Against Config Origin -The application SHALL resolve relative `[logging].file` paths against the directory containing the -config file that provided the `logging.file` value. - -#### Scenario: Relative path from default config location -- **GIVEN** config is loaded from `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` -- **AND** `[logging].file = "logs/goggles.log"` -- **WHEN** logging is configured -- **THEN** the effective log path SHALL be `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/logs/goggles.log` - -#### Scenario: Relative path from explicit --config location -- **GIVEN** config is loaded from an explicit `--config /custom/path/goggles.toml` -- **AND** `[logging].file = "logs/goggles.log"` -- **WHEN** logging is configured -- **THEN** the effective log path SHALL be `/custom/path/logs/goggles.log` - -#### Scenario: Relative path is independent of current working directory -- **GIVEN** the same config file path and `logging.file` value -- **WHEN** the application is started from different working directories -- **THEN** the effective log file path SHALL remain identical diff --git a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/tasks.md b/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/tasks.md deleted file mode 100644 index 281db7ee..00000000 --- a/openspec/changes/archive/2026-02-07-update-file-logging-path-policy/tasks.md +++ /dev/null @@ -1,46 +0,0 @@ -## 1. Spec & Design -- [x] 1.1 Add `app-window` delta requirements for file sink setup and relative path resolution rules. - - Implemented in `openspec/changes/update-file-logging-path-policy/specs/app-window/spec.md`. - - Verified by `openspec validate update-file-logging-path-policy --strict`. -- [x] 1.2 Document runtime sequencing and fallback policy in `design.md`. - - Implemented in `openspec/changes/update-file-logging-path-policy/design.md`. - - Verified by design review against implemented startup flow in `src/app/main.cpp`. - -## 2. Implementation -- [x] 2.1 Extend logging initialization APIs to support optional file sink configuration using resolved path input. - - Implemented `set_log_file_path()` in `src/util/logging.hpp` and `src/util/logging.cpp`. - - Verified by `tests/util/test_logging.cpp` file sink success/failure coverage. -- [x] 2.2 Track config source path in startup flow so `logging.file` relative paths can be resolved against the config file directory. - - Implemented `LoadedConfig` source-path tracking in `src/app/main.cpp`. - - Verified by startup path resolution flow and `resolve_logging_file_path()` tests. -- [x] 2.3 Update startup ordering to apply log level, timestamp, and file sink after config load and before major subsystem initialization. - - Implemented log config application sequence in `src/app/main.cpp` before `Application::create`. - - Verified by `pixi run test -p test` and runtime log output in test harness. -- [x] 2.4 Implement non-fatal fallback when file sink setup fails: keep console sink, emit one warning, continue startup. - - Implemented warning-on-failure path in `src/app/main.cpp` and non-throwing `set_log_file_path()` result API. - - Verified by `tests/util/test_logging.cpp` invalid destination test. -- [x] 2.5 Update config/template comments to define `logging.file` semantics (empty/absolute/relative). - - Implemented in `config/goggles.template.toml` and `docs/project_policies.md`. - - Verified by doc/template content review. - -## 3. Verification -- [x] 3.1 Add tests for `logging.file` path resolution using config files loaded from non-CWD locations. - - Added `resolve_logging_file_path` tests in `tests/util/test_config.cpp`. - - Verified by `pixi run test -p test`. -- [x] 3.2 Add tests confirming file sink is enabled when valid path is provided and logs are written. - - Added file sink write/read assertion test in `tests/util/test_logging.cpp`. - - Verified by `pixi run test -p test`. -- [x] 3.3 Add tests confirming invalid/unwritable file paths do not abort startup and preserve console logging. - - Added failing-path test in `tests/util/test_logging.cpp` checking error result and continued logger usability. - - Verified by `pixi run test -p test`. -- [x] 3.4 Run `pixi run test -p test`. - - Executed successfully (all tests passed). - - Quality follow-up also passed via `pixi run build -p quality`. - -## 4. Documentation -- [x] 4.1 Update user-facing configuration guidance for `[logging].file` path policy. - - Updated guidance in `config/goggles.template.toml` and `docs/project_policies.md`. - - Verified by content review and formatter pass (`pixi run format`). -- [x] 4.2 Ensure docs explicitly state that this change applies to app logging, not vk-layer logging. - - Added explicit scope note in `docs/project_policies.md` section B.5. - - Verified by `openspec validate update-file-logging-path-policy --strict`. diff --git a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/proposal.md b/openspec/changes/archive/2026-02-07-update-surface-frame-retention/proposal.md deleted file mode 100644 index 8340baa7..00000000 --- a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/proposal.md +++ /dev/null @@ -1,15 +0,0 @@ -# Change: Preserve last presented surface frame on target switch - -## Why -Switching compositor targets clears the presented frame and shows black until a new commit arrives. -UI toolkits like Qt often redraw only on demand, so the viewer goes black even though the surface -already has a valid last frame. - -## What Changes -- Retain the last presented frame per surface and reuse it on target changes. -- Refresh presentation on manual and auto target switches without waiting for a new commit. -- Clear presentation only when the new target has no retained frame. - -## Impact -- Affected specs: surface-frame-presentation (new) -- Affected code: src/compositor/compositor_server.cpp diff --git a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/specs/surface-frame-presentation/spec.md b/openspec/changes/archive/2026-02-07-update-surface-frame-retention/specs/surface-frame-presentation/spec.md deleted file mode 100644 index fa10f172..00000000 --- a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/specs/surface-frame-presentation/spec.md +++ /dev/null @@ -1,29 +0,0 @@ -## ADDED Requirements -### Requirement: Preserve last surface frame on target change -The compositor presentation path SHALL retain the most recently presented frame for each surface -and use it when that surface becomes the active presentation target without waiting for a new -commit. - -#### Scenario: Manual target switch -- **GIVEN** surface A and surface B have each presented at least one frame -- **AND** surface A is currently the presentation target -- **WHEN** the user sets surface B as the manual input target -- **THEN** the compositor SHALL present surface B's most recent retained frame without waiting for - a new commit - -#### Scenario: Auto focus switch -- **GIVEN** surface A and surface B have each presented at least one frame -- **AND** surface A is currently the presentation target -- **WHEN** focus changes to surface B via auto selection -- **THEN** the compositor SHALL present surface B's most recent retained frame without waiting for - a new commit - -### Requirement: Clear presentation when no retained frame exists -The compositor presentation path SHALL clear the presented frame when the new target surface has -no retained frame to display. - -#### Scenario: Switch to a surface without a retained frame -- **GIVEN** surface A is currently presenting a frame -- **AND** surface B exists but has not yet presented a frame -- **WHEN** surface B becomes the presentation target -- **THEN** the compositor SHALL clear the presented frame diff --git a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/tasks.md b/openspec/changes/archive/2026-02-07-update-surface-frame-retention/tasks.md deleted file mode 100644 index a9e3ba0b..00000000 --- a/openspec/changes/archive/2026-02-07-update-surface-frame-retention/tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -## 1. Implementation -- [x] 1.1 Use the surface's current texture as the retained frame for refresh. -- [x] 1.2 Refresh the presented frame on manual target changes using retained frames. -- [x] 1.3 Refresh the presented frame on auto focus changes using retained frames. -- [x] 1.4 Clear the presented frame only when the new target has no retained frame. -- [x] 1.5 Manual validation: switch between two Qt windows without redraw and verify no black frame. diff --git a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/proposal.md b/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/proposal.md deleted file mode 100644 index b9bbf4bb..00000000 --- a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/proposal.md +++ /dev/null @@ -1,27 +0,0 @@ -# Change: Update WSI Proxy Pacing to Use Viewer Back-Pressure - -## Why - -WSI proxy mode (`GOGGLES_WSI_PROXY=1`) currently paces the target application by sleeping in -`WsiVirtualizer::acquire_next_image()` based on a local timestamp and `GOGGLES_FPS_LIMIT`. This is -CPU scheduling-dependent and does not naturally react to viewer load. - -The default (non-WSI-proxy) path already uses cross-process timeline semaphores (`frame_ready` and -`frame_consumed`) to provide deterministic back-pressure from the viewer. Reusing the same -mechanism for WSI proxy mode should improve frame pacing stability and reduce jitter. - -## What Changes - -- Add WSI proxy acquire pacing based on viewer back-pressure using existing cross-process timeline - semaphores (`frame_consumed` wait). -- Keep `GOGGLES_FPS_LIMIT` as an upper bound; it no longer needs to be the sole pacing mechanism. -- If sync semaphores cannot be established or the viewer becomes unresponsive, fall back to the - existing `sleep_until` limiter. - -## Impact - -- Affected specs: `vk-layer-capture` -- Affected code: - - `src/capture/vk_layer/wsi_virtual.cpp` (acquire pacing) - - `src/capture/vk_layer/vk_capture.cpp` (shared sync primitives reuse/refactor) - - `docs/dmabuf_sharing.md` (WSI proxy pacing description) diff --git a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/specs/vk-layer-capture/spec.md b/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/specs/vk-layer-capture/spec.md deleted file mode 100644 index c73fbe9b..00000000 --- a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/specs/vk-layer-capture/spec.md +++ /dev/null @@ -1,49 +0,0 @@ -## ADDED Requirements - -### Requirement: Virtual Swapchain Sync Pacing - -When WSI proxy mode is enabled, the layer SHALL pace `vkAcquireNextImageKHR` using viewer-provided -back-pressure based on cross-process synchronization primitives. - -#### Scenario: Back-pressure pacing - -- **GIVEN** WSI proxy mode is enabled -- **AND** the viewer has provided valid synchronization primitives to the layer -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL block acquisition until the viewer indicates it has consumed the - previous frame - -#### Scenario: Pacing fallback - -- **GIVEN** WSI proxy mode is enabled -- **AND** synchronization primitives are unavailable or the viewer is unresponsive -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL fall back to its local CPU-based limiter - -## MODIFIED Requirements - -### Requirement: Virtual Swapchain Frame Rate Limiting - -The layer SHALL provide frame rate limiting for virtual swapchains to prevent runaway frame rates. -When viewer back-pressure pacing is available, the frame rate limiter SHALL act as an upper bound. - -#### Scenario: Default frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT` environment variable is not set -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL limit acquisition rate to 60 FPS - -#### Scenario: Custom frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT` is set to a positive integer -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL limit acquisition rate to the specified FPS - -#### Scenario: Disable frame rate limit - -- **GIVEN** WSI proxy mode is enabled -- **AND** `GOGGLES_FPS_LIMIT=0` is set -- **WHEN** the application calls `vkAcquireNextImageKHR` -- **THEN** the layer SHALL NOT limit acquisition rate diff --git a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/tasks.md b/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/tasks.md deleted file mode 100644 index 10c82440..00000000 --- a/openspec/changes/archive/2026-02-07-update-wsi-proxy-sync-pacing/tasks.md +++ /dev/null @@ -1,24 +0,0 @@ -## 1. Investigation - -- [x] 1.1 Identify which viewer sync primitives can be reused in WSI proxy mode -- [x] 1.2 Define WSI proxy pacing fallback behavior when viewer is unresponsive - -## 2. Specification - -- [x] 2.1 Update `vk-layer-capture` delta spec with WSI proxy sync pacing requirements - -## 3. Implementation - -- [x] 3.1 Add or reuse a device-level timeline semaphore pair for WSI proxy pacing -- [x] 3.2 Block `WsiVirtualizer::acquire_next_image()` on viewer back-pressure (frame_consumed) -- [x] 3.3 Keep `GOGGLES_FPS_LIMIT` as a secondary cap -- [x] 3.4 Preserve existing `sleep_until` path as fallback - -## 4. Documentation - -- [x] 4.1 Update `docs/dmabuf_sharing.md` to describe WSI proxy sync pacing and fallback - -## 5. Validation - -- [x] 5.1 Run `openspec validate update-wsi-proxy-sync-pacing --strict` -- [x] 5.2 Run unit tests (`pixi run test -p test`) if impacted tests exist diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/.openspec.yaml b/openspec/changes/archive/2026-02-26-add-layer-shell-support/.openspec.yaml deleted file mode 100644 index e331c975..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-25 diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/design.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/design.md deleted file mode 100644 index 0e2819a5..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/design.md +++ /dev/null @@ -1,130 +0,0 @@ -## Context - -`CompositorServer` is a single-file headless wlroots compositor (~3 000 lines in -`compositor_server.cpp`) that implements Wayland protocol support for game input forwarding and -surface capture. Existing protocol support follows a uniform pattern: a `setup_*()` function -creates the wlroots global, registers `wl_listener` callbacks on an `Impl::Listeners` struct, and -per-surface state is heap-allocated as a `*Hooks` struct that carries its own `wl_listener` -members for lifecycle signals. - -The compositor uses a single headless output with no panel/desktop shell, so there is no existing -exclusive-zone arrangement system or output-layout-aware geometry resolver. - -Frame export is driven by `render_surface_to_frame()`, which opens a wlr render pass, composites -the primary capture surface tree via `render_root_surface_tree()`, optionally adds XWayland -override-redirect popups, then overlays the software cursor. The resulting buffer is exported as a -DMA-BUF for the viewer. - -## Goals / Non-Goals - -**Goals:** -- Expose `wlr-layer-shell-unstable-v1` v4 on the compositor Wayland display -- Track mapped layer surfaces per-surface lifecycle (configure, map, unmap, destroy) -- Render layer surfaces in protocol layer order around the primary capture surface in - `render_surface_to_frame()` -- Forward keyboard focus to layer surfaces that request `exclusive` interactivity without - changing the capture target -- Route `xdg_popup` children of layer surfaces through the existing popup infrastructure - -**Non-Goals:** -- Full exclusive-zone arrangement (panel reservations, usable-area shrinking) — not needed with a - single headless output and no desktop shell -- Exposing layer surfaces in `get_surfaces()` — they are render-only overlays, not selectable - capture targets -- Supporting layer surface resize requests (`set_size`, `set_anchor` after map) — game launcher - overlays are configured once at map time -- Input hit-testing for pointer delivery to layer surfaces — pointer events always go to the - primary capture surface - -## Decisions - -### D1: Layer surfaces are render-only overlays, excluded from `get_surfaces()` - -**Decision**: `layer_hooks` is never iterated in `get_surfaces()` or `focus_surface_by_id()`. - -**Rationale**: Layer surfaces are transient UI decorations (overlays, panels). Including them in -the surface selector would confuse users who would then try to "capture" an overlay. The capture -path remains anchored to XDG toplevel / XWayland surfaces. - -**Alternative considered**: Add a `is_layer_surface` flag to `SurfaceInfo` and let the UI filter. -Rejected — adds UI complexity for no user benefit; omission is simpler and correct. - ---- - -### D2: Simplified geometry — full output for fully-anchored, edge size for partially-anchored - -**Decision**: On first commit, compute configure dimensions as follows: -- If all four anchor edges are set (`ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | BOTTOM | LEFT | RIGHT`): - send output width × height. -- If only horizontal edges (top or bottom) with left+right: send output width × `desired_height` - (or a sensible default). -- If only vertical edges (left or right) with top+bottom: send `desired_width` × output height. -- Otherwise: send `desired_width` × `desired_height` (clamped to non-zero output dimensions). - -**Rationale**: Game launcher overlays use full-screen or full-width configurations. A full -exclusive-zone layout engine is ~500 lines and not needed for a single headless output. - -**Alternative considered**: Implement the full layer arrangement algorithm from sway. Rejected as -out of scope; can be added later if needed. - ---- - -### D3: Keyboard focus is separate from the capture target - -**Decision**: When a layer surface with `exclusive` keyboard interactivity maps, call -`wlr_seat_keyboard_enter()` on its `wlr_surface`. When it unmaps or is destroyed, restore -keyboard focus to `focused_surface` (the current capture target). The `focused_surface` / -`focused_xsurface` pointers that drive frame capture are NOT changed. - -**Rationale**: The capture pipeline is driven by `focused_surface` / `focused_xsurface`. If -an overlay steals keyboard focus for text input (e.g., a search field in Steam Big Picture), the -game's frame should still be what is captured and displayed. Conflating keyboard focus with the -capture target would break the capture path. - -**Alternative considered**: Change `focused_surface` to the layer surface for the duration of -keyboard focus. Rejected — `render_surface_to_frame()` and `get_input_target()` both read -`focused_surface` to determine what to render as the primary frame. - ---- - -### D4: `LayerSurfaceHooks` follows the existing `*Hooks` pattern exactly - -**Decision**: `LayerSurfaceHooks` is heap-allocated in `handle_new_layer_surface()` and -self-destructs in its own `layer_destroy` handler (same pattern as `XdgToplevelHooks`). - -**Rationale**: Consistency with the existing codebase. The hooks are added to -`layer_hooks : std::vector` guarded by `hooks_mutex`, identical to -`xdg_hooks` and `xwayland_hooks`. - ---- - -### D5: Layer surface popups delegate to existing `handle_new_xdg_popup()` - -**Decision**: Connect `layer_surface->events.new_popup` and forward the `wlr_xdg_popup*` to the -existing `handle_new_xdg_popup()`. No separate popup tracking for layer surfaces. - -**Rationale**: `XdgPopupHooks` already handles popup lifecycle, configure, render, and destroy. -The parent surface context for positioning differs, but because goggles composites everything into -a flat render pass, offset computation is the key concern — and the existing popup offset logic -is based on the popup's own `positioner`, which is self-contained. - -## Risks / Trade-offs - -**[Risk] Keyboard focus restoration on crash/unmap may miss edge cases** → -Mitigation: restore `focused_surface` keyboard focus in both `surface_unmap` and `layer_destroy` -handlers. Add a null check before `wlr_seat_keyboard_enter()` in all restoration paths. - -**[Risk] Popup geometry offset wrong for layer surface parents** → -Mitigation: layer surface popups use `wlr_xdg_popup_get_position()` from the positioner, which -is compositor-position-agnostic. Since the headless output has no offset, popup positions are -in output coordinates, which match the render pass coordinates. Verify with Steam overlay in -manual testing. - -**[Risk] wlroots 0.19 `initial_commit` field may not be set on first commit** → -Mitigation: read the `initial_commit` field from `wlr_layer_surface_v1` at commit time (same -field used by sway and river for wlroots 0.19). If already configured (`hooks->configured`), -skip the configure call to avoid protocol errors. - -## Open Questions - -None — all decisions are resolved above. diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/proposal.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/proposal.md deleted file mode 100644 index 038545eb..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/proposal.md +++ /dev/null @@ -1,42 +0,0 @@ -## Why - -The compositor does not implement `wlr-layer-shell-unstable-v1`, so game launcher overlays (Steam -Big Picture, Epic Games overlay) and desktop panels that depend on this protocol cannot connect or -render. This blocks Goggles from being usable as an overlay tool for overlay-heavy workflows. - -## What Changes - -- Implement `wlr_layer_shell_v1` protocol support in `src/compositor/compositor_server.cpp` -- Register and create the `wlr_layer_shell_v1` global on the compositor Wayland display -- Track layer surfaces with per-surface lifecycle hooks (commit, map, unmap, destroy) -- Configure layer surfaces on first commit with geometry derived from anchor/margin/size state -- Render layer surfaces in protocol-defined layer order around the primary capture surface -- Route keyboard focus to layer surfaces that request `exclusive` interactivity, without changing the capture target -- Handle `xdg_popup` children spawned from layer surfaces via the existing popup infrastructure - -## Capabilities - -### New Capabilities - -- `layer-shell-overlay`: `wlr-layer-shell-unstable-v1` protocol support — creates the global, - tracks `LayerSurfaceHooks`, configures geometry, and exposes layer surfaces as render-only - overlays distinct from the primary capture target. - -### Modified Capabilities - -- `compositor-capture`: Render ordering now includes layer surface passes (background, bottom - before the primary surface; top, overlay after it) in the compositor-presented DMA-BUF frame. -- `input-forwarding`: Keyboard focus may transfer to a mapped layer surface that requests - `exclusive` interactivity without changing the surface used for frame capture or surface - enumeration. - -## Impact - -- **`src/compositor/compositor_server.cpp`** — all implementation; adds ~200–300 lines -- **`src/compositor/compositor_server.hpp`** — no public API changes; layer surfaces are render-only overlays, not enumerated in `get_surfaces()` -- **No new CMake targets** — `wlr_layer_shell_v1.h` is part of the already-linked `PkgConfig::wlroots` -- **Policy-sensitive impacts:** - - Ownership: `LayerSurfaceHooks` are heap-allocated and destroyed in their `layer_destroy` signal handler (RAII-equivalent; no raw `new`/`delete` invariant preserved via explicit delete in handler) - - Threading: all layer shell event handlers run on the compositor thread; `hooks_mutex` guards `layer_hooks` as with existing hook vectors - - Error handling: `setup_layer_shell()` returns `Result` via `tl::expected`; setup failure propagates to `start()` via `GOGGLES_TRY` - - Logging: layer shell lifecycle events use `GOGGLES_LOG_DEBUG`; no `error`/`critical` unless setup fails diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/compositor-capture/spec.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/compositor-capture/spec.md deleted file mode 100644 index e9f4aa0d..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/compositor-capture/spec.md +++ /dev/null @@ -1,52 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Non-Vulkan Surface Presentation - -The system SHALL render a selected non-Vulkan client surface (Wayland or XWayland) into the -viewer using the compositor capture path when compositor presentation is available. - -The render pass for compositor-presented frames SHALL composite surfaces in the following order: -1. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `background` layer -2. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `bottom` layer -3. The primary capture surface tree (xdg_toplevel or XWayland surface) -4. XWayland override-redirect popup surfaces belonging to the primary surface -5. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `top` layer -6. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `overlay` layer -7. The compositor software cursor - -#### Scenario: Present selected surface -- **GIVEN** a non-Vulkan client surface is connected to the compositor -- **AND** the surface is selected via the existing surface selector -- **WHEN** the compositor produces a new frame -- **THEN** the viewer presents the selected surface - -#### Scenario: Overlay layer surface composited above game -- **GIVEN** a game surface is the primary capture target -- **AND** a layer surface with layer `overlay` is mapped -- **WHEN** the compositor produces a frame -- **THEN** the overlay layer surface appears above the game surface in the presented frame - -#### Scenario: Background layer surface composited below game -- **GIVEN** a layer surface with layer `background` is mapped -- **WHEN** the compositor produces a frame -- **THEN** the background layer surface appears below the game surface in the presented frame - -#### Scenario: No layer surfaces does not affect existing behavior -- **GIVEN** no layer surfaces are connected -- **WHEN** the compositor produces a frame -- **THEN** the presented frame contains only the primary surface tree and cursor (existing behavior) - -#### Scenario: Presentation unavailable -- **GIVEN** compositor presentation cannot be initialized -- **WHEN** non-Vulkan clients connect for input -- **THEN** input forwarding continues without presenting non-Vulkan frames - -#### Scenario: Export compositor frame via DMA-BUF -- **WHEN** the compositor renders a frame for the selected surface -- **THEN** it exports a DMA-BUF with width, height, format, stride, and modifier metadata -- **AND** the viewer imports and presents the frame without CPU readback - -#### Scenario: Vulkan capture unaffected -- **GIVEN** a Vulkan application is captured via the layer -- **WHEN** compositor capture is enabled -- **THEN** the Vulkan capture path continues to function as before diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/input-forwarding/spec.md deleted file mode 100644 index 7050d9bb..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/input-forwarding/spec.md +++ /dev/null @@ -1,61 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Input Forwarding Infrastructure - -The system SHALL provide a compositor server that supports both XWayland (for X11 apps) and native -Wayland clients using a unified `wlr_seat` input path, and SHALL additionally support -`wlr-layer-shell-unstable-v1` overlay surfaces. - -The compositor server SHALL: -- Create a headless wlroots backend -- Bind a Wayland socket for client connections -- Start XWayland server for X11 application support -- Create a wlr_seat with keyboard and pointer capabilities -- Connect XWayland to the seat via `wlr_xwayland_set_seat()` -- Create a `wlr_layer_shell_v1` global (version 4) for overlay surface support -- Run the compositor event loop on a dedicated thread - -#### Scenario: Compositor initializes with unified input -- **WHEN** the input forwarding system starts -- **THEN** a Wayland socket is created (wayland-N) -- **AND** an XWayland server is started (DISPLAY :N) -- **AND** XWayland is connected to the seat for automatic input translation -- **AND** a seat with keyboard and pointer capabilities is available -- **AND** a `zwlr_layer_shell_v1` global is advertised on the display - -## ADDED Requirements - -### Requirement: Layer Surface Keyboard Interactivity - -The system SHALL temporarily transfer seat keyboard focus to a mapped layer surface that requests -`exclusive` keyboard interactivity, without changing the surface used for frame capture or surface -enumeration. - -The compositor server SHALL: -- In the layer surface `map` handler: if `keyboard_interactive` is `exclusive`, call - `wlr_seat_keyboard_enter()` for the layer surface's `wlr_surface` -- In the layer surface `unmap` and `destroy` handlers: if `focused_surface` is non-null, restore - keyboard focus via `wlr_seat_keyboard_enter()` for `focused_surface` -- NOT modify `focused_surface` or `focused_xsurface` in any layer surface handler -- NOT include layer surfaces in `get_surfaces()` or `focus_surface_by_id()` enumeration - -#### Scenario: Exclusive layer surface takes keyboard focus -- **GIVEN** a game surface (`focused_surface`) has keyboard focus -- **WHEN** a layer surface with `exclusive` keyboard interactivity maps -- **THEN** `wlr_seat_keyboard_enter()` is called for the layer surface's `wlr_surface` -- **AND** `focused_surface` remains pointing to the game surface - -#### Scenario: Exclusive layer surface unmaps, focus restored -- **GIVEN** a layer surface with `exclusive` interactivity currently has keyboard focus -- **WHEN** the layer surface unmaps or is destroyed -- **THEN** `wlr_seat_keyboard_enter()` is called to restore focus to `focused_surface` - -#### Scenario: None-interactivity layer surface does not take keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `none` keyboard interactivity maps -- **THEN** keyboard focus remains with the game surface unchanged - -#### Scenario: Layer surface not enumerated as input target -- **WHEN** `get_surfaces()` is called -- **THEN** the returned list does not include any layer surfaces -- **AND** layer surfaces cannot be selected via `set_input_target()` diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/layer-shell-overlay/spec.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/layer-shell-overlay/spec.md deleted file mode 100644 index a043b3f1..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/specs/layer-shell-overlay/spec.md +++ /dev/null @@ -1,170 +0,0 @@ -## ADDED Requirements - -### Requirement: Layer Shell Global - -The compositor server SHALL create and advertise a `wlr_layer_shell_v1` global (version 4) on its -Wayland display during startup. - -The compositor server SHALL: -- Call `wlr_layer_shell_v1_create(display, 4)` in `setup_layer_shell()` -- Return a `Result` error if creation fails, propagated via `GOGGLES_TRY` in `start()` -- Register a `new_surface` signal listener to receive incoming layer surface connections -- Detach the `new_surface` listener and null the pointer during `stop()` - -#### Scenario: Layer shell global created on startup -- **WHEN** `CompositorServer::start()` succeeds -- **THEN** a `zwlr_layer_shell_v1` global is available on the Wayland display -- **AND** Wayland clients can bind to it - -#### Scenario: Layer shell creation failure propagates -- **WHEN** `wlr_layer_shell_v1_create()` returns null -- **THEN** `start()` returns an error -- **AND** the compositor does not reach the running state - ---- - -### Requirement: Layer Surface Lifecycle Tracking - -The compositor server SHALL track each incoming `wlr_layer_surface_v1` with a `LayerSurfaceHooks` -struct allocated on the heap, following the existing `XdgToplevelHooks` pattern. - -`LayerSurfaceHooks` SHALL contain: -- `impl`: pointer back to `Impl` -- `layer_surface`: the `wlr_layer_surface_v1*` -- `surface`: the associated `wlr_surface*` -- `id`: unique surface identifier assigned on creation -- `layer`: the `zwlr_layer_shell_v1_layer` enum value from `pending.layer` at creation time -- `configured`: bool, set to true after the first `wlr_layer_surface_v1_configure()` call -- `mapped`: bool, toggled by map/unmap signal handlers -- Listeners: `surface_commit`, `surface_map`, `surface_unmap`, `surface_destroy`, `layer_destroy`, - `new_popup` - -The `layer_hooks` vector SHALL be guarded by `hooks_mutex` (same mutex as `xdg_hooks`). - -#### Scenario: New layer surface registered -- **WHEN** a Wayland client creates a layer surface -- **THEN** a `LayerSurfaceHooks` is allocated and pushed to `layer_hooks` -- **AND** all six signal listeners are registered - -#### Scenario: Layer surface destroyed -- **WHEN** the `layer_destroy` signal fires -- **THEN** all listeners on the hooks struct are detached -- **AND** the hooks struct is removed from `layer_hooks` and deleted - ---- - -### Requirement: Layer Surface Initial Configuration - -The compositor server SHALL send a configure event to each layer surface on its first commit, -using the headless output dimensions and the surface's requested anchor, size, and margin state. - -The compositor server SHALL: -- Check `layer_surface->initial_commit` in the `surface_commit` handler -- Compute `width` and `height` using the rules in design.md (D2) -- Call `wlr_layer_surface_v1_configure(layer_surface, width, height)` exactly once -- Set `hooks->configured = true` after the call -- Skip the configure call on subsequent commits - -#### Scenario: Fully-anchored layer surface configured at output size -- **GIVEN** a layer surface with all four anchor edges set -- **WHEN** the surface commits for the first time (`initial_commit == true`) -- **THEN** `wlr_layer_surface_v1_configure()` is called with the headless output width and height - -#### Scenario: Partially-anchored layer surface configured with requested size -- **GIVEN** a layer surface with only top+left+right anchors and `desired_height = 40` -- **WHEN** the surface commits for the first time -- **THEN** `wlr_layer_surface_v1_configure()` is called with output width and height 40 - -#### Scenario: Subsequent commits do not re-configure -- **GIVEN** a layer surface that has already been configured -- **WHEN** it commits again -- **THEN** `wlr_layer_surface_v1_configure()` is NOT called again - ---- - -### Requirement: Layer Surface Render Integration - -The compositor server SHALL render mapped layer surfaces in the `render_surface_to_frame()` render -pass, ordered by protocol layer before and after the primary capture surface tree. - -Render order within the pass SHALL be: -1. `ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND` -2. `ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM` -3. Primary capture surface tree (existing) -4. XWayland override-redirect popups (existing) -5. `ZWLR_LAYER_SHELL_V1_LAYER_TOP` -6. `ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY` -7. Software cursor (existing) - -`render_layer_surfaces(pass, layer)` SHALL: -- Iterate `layer_hooks` under `hooks_mutex` -- Skip hooks where `mapped == false` or `layer_surface == nullptr` -- Use `wlr_layer_surface_v1_for_each_surface()` with `render_surface_iterator` to render the - surface tree -- Compute position from `current.anchor` and `current.margin` relative to the headless output - -#### Scenario: Overlay layer surface renders above game -- **GIVEN** a game surface is the primary capture target -- **AND** a layer surface with layer `overlay` is mapped -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the layer surface is rendered after the game surface and before the cursor - -#### Scenario: Background layer surface renders below game -- **GIVEN** a layer surface with layer `background` is mapped -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the background layer surface is rendered before the game surface - -#### Scenario: Unmapped layer surface not rendered -- **GIVEN** a layer surface exists but `mapped == false` -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the layer surface is not included in the render pass - ---- - -### Requirement: Layer Surface Popup Children - -The compositor server SHALL handle `xdg_popup` surfaces spawned by a mapped layer surface by -routing them through the existing `handle_new_xdg_popup()` infrastructure. - -The compositor server SHALL: -- Connect `layer_surface->events.new_popup` in `handle_new_layer_surface()` -- Forward the `wlr_xdg_popup*` to `handle_new_xdg_popup()` in the signal handler - -#### Scenario: Layer surface spawns a popup -- **GIVEN** a mapped layer surface (e.g., a Steam overlay panel) -- **WHEN** the client creates an `xdg_popup` child surface -- **THEN** the popup is tracked via `XdgPopupHooks` -- **AND** the popup is rendered via the existing popup render path - ---- - -### Requirement: Layer Surface Keyboard Interactivity - -The compositor server SHALL forward keyboard focus to a mapped layer surface that requests -`exclusive` keyboard interactivity, and restore focus to the primary capture surface on unmap or -destroy. - -The compositor server SHALL: -- In the `surface_map` handler: if `layer_surface->current.keyboard_interactive == - ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE`, call - `wlr_seat_keyboard_enter(seat, surface, ...)` for the layer surface -- In the `surface_unmap` and `layer_destroy` handlers: restore keyboard focus to - `focused_surface` (the primary capture target) if it is non-null, via `wlr_seat_keyboard_enter()` -- NOT change `focused_surface` or `focused_xsurface` in any layer surface handler - -#### Scenario: Exclusive layer surface takes keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `exclusive` keyboard interactivity maps -- **THEN** the seat's keyboard focus moves to the layer surface -- **AND** `focused_surface` is unchanged - -#### Scenario: Exclusive layer surface unmaps, focus restored -- **GIVEN** a layer surface with `exclusive` interactivity has keyboard focus -- **WHEN** the layer surface unmaps -- **THEN** keyboard focus is restored to `focused_surface` -- **AND** `focused_surface` is unchanged - -#### Scenario: None-interactivity layer surface does not take keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `none` keyboard interactivity maps -- **THEN** keyboard focus remains with the game surface diff --git a/openspec/changes/archive/2026-02-26-add-layer-shell-support/tasks.md b/openspec/changes/archive/2026-02-26-add-layer-shell-support/tasks.md deleted file mode 100644 index 27c479f4..00000000 --- a/openspec/changes/archive/2026-02-26-add-layer-shell-support/tasks.md +++ /dev/null @@ -1,46 +0,0 @@ -## 1. Scaffold — Include and Struct Definitions - -- [x] 1.1 Add `#include ` inside the `extern "C"` block in `compositor_server.cpp` -- [x] 1.2 Add `LayerSurfaceHooks` struct to `CompositorServer::Impl` with all fields: `impl`, `layer_surface`, `surface`, `id`, `layer`, `configured`, `mapped`, and six `wl_listener` members (`surface_commit`, `surface_map`, `surface_unmap`, `surface_destroy`, `layer_destroy`, `new_popup`) -- [x] 1.3 Add `wlr_layer_shell_v1* layer_shell = nullptr` and `std::vector layer_hooks` to `CompositorServer::Impl` -- [x] 1.4 Add `wl_listener new_layer_surface{}` to `Impl::Listeners` - -## 2. Setup and Teardown - -- [x] 2.1 Implement `CompositorServer::Impl::setup_layer_shell() -> Result`: call `wlr_layer_shell_v1_create(display, 4)`, error on null, init and register `listeners.new_layer_surface` via `wl_list_init` + lambda + `wl_signal_add` -- [x] 2.2 Call `GOGGLES_TRY(impl.setup_layer_shell())` in `CompositorServer::start()` after `setup_xdg_shell()` -- [x] 2.3 In `CompositorServer::stop()`: call `detach_listener(impl.listeners.new_layer_surface)` and set `impl.layer_shell = nullptr` - -## 3. New Layer Surface Handler - -- [x] 3.1 Implement `Impl::handle_new_layer_surface(wlr_layer_surface_v1*)`: assign `layer_surface->output = output` if null, allocate `LayerSurfaceHooks`, assign `id` and `layer` from `pending.layer`, set `surface = layer_surface->surface` -- [x] 3.2 In `handle_new_layer_surface`: init and register all six listeners using the `offsetof(Impl::Listeners, ...)` lambda pattern; push hooks to `layer_hooks` under `hooks_mutex` - -## 4. Commit Handler — Initial Configure - -- [x] 4.1 Implement `Impl::handle_layer_surface_commit(LayerSurfaceHooks*)`: if `!hooks->configured && layer_surface->initial_commit`, compute `width`/`height` per design D2 geometry rules (fully-anchored → output size; partial → mix of output dimension and `desired_width`/`desired_height`), call `wlr_layer_surface_v1_configure()`, set `hooks->configured = true` -- [x] 4.2 On subsequent commits (already configured): call `wlr_surface_send_frame_done()` on `hooks->surface` to unblock the client - -## 5. Map / Unmap Handlers - -- [x] 5.1 Implement `Impl::handle_layer_surface_map(LayerSurfaceHooks*)`: set `mapped = true`; if `layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE`, call `wlr_seat_keyboard_enter()` for `hooks->surface`; call `request_present_reset()` -- [x] 5.2 Implement `Impl::handle_layer_surface_unmap(LayerSurfaceHooks*)`: set `mapped = false`; if keyboard was given to this surface (`seat->keyboard_state.focused_surface == hooks->surface`), restore keyboard focus to `focused_surface` via `wlr_seat_keyboard_enter()`; call `request_present_reset()` - -## 6. Destroy Handler - -- [x] 6.1 Implement `Impl::handle_layer_surface_destroy(LayerSurfaceHooks*)`: detach all six listeners via `detach_listener()`; if `seat->keyboard_state.focused_surface == hooks->surface` and `focused_surface` is non-null, restore keyboard focus; remove hooks from `layer_hooks` under `hooks_mutex`; `delete hooks` - -## 7. Layer Surface Popup Forwarding - -- [x] 7.1 In the `new_popup` listener registered on `layer_surface->events.new_popup`, forward the `wlr_xdg_popup*` to `handle_new_xdg_popup()` - -## 8. Render Integration - -- [x] 8.1 Implement `Impl::render_layer_surfaces(wlr_render_pass*, zwlr_layer_shell_v1_layer)`: iterate `layer_hooks` under `hooks_mutex`, skip unmapped or null entries, compute position from `current.anchor` and `current.margin` relative to the output, call `wlr_layer_surface_v1_for_each_surface()` with `render_surface_iterator` -- [x] 8.2 In `render_surface_to_frame()`, insert layer render calls in the correct order: `render_layer_surfaces(pass, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND)` and `BOTTOM` before `render_root_surface_tree()`; `TOP` and `OVERLAY` after XWayland popup rendering and before `render_cursor_overlay()` - -## 9. Verification - -- [x] 9.1 Build with quality preset and confirm zero clang-tidy warnings: `pixi run build -p quality` -- [x] 9.2 Run full test suite: `pixi run test -p test` (pre-existing X11 test failure confirmed on unmodified `main` — not caused by this change) -- [x] 9.3 Manual smoke test: launch `pixi run dev` and connect a client that uses `zwlr_layer_shell_v1` (e.g., `wlr-layer-shell-example` or `waybar` against the goggles socket); confirm layer surface appears in the compositor-presented frame diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/.openspec.yaml b/openspec/changes/archive/2026-02-27-add-headless-mode/.openspec.yaml deleted file mode 100644 index 85ae75c1..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-26 diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/design.md b/openspec/changes/archive/2026-02-27-add-headless-mode/design.md deleted file mode 100644 index 61765f72..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/design.md +++ /dev/null @@ -1,81 +0,0 @@ -## Context - -The Goggles viewer currently creates an SDL3 window on every startup path. `VulkanBackend::create()` accepts `SDL_Window*` and builds a `vk::SurfaceKHR` from it, then constructs a swapchain tied to that surface. `ImGuiLayer` also requires the window. There is no path through `Application::create()` that skips these two subsystems. - -The `CompositorServer` is already surfaceless — it uses `wlr_headless_backend` and is unaffected by this change. The coupling to SDL lives entirely in `VulkanBackend` (surface + swapchain) and `Application` (SDL init, ImGui init). - -## Goals / Non-Goals - -**Goals:** -- `goggles --headless --frames N --output path.png -- ` runs and exits cleanly -- No SDL window, no ImGui, no swapchain created in headless mode -- Filter chain renders to an offscreen `vk::Image`; final frame is read back and written as PNG -- Existing windowed mode is fully unaffected -- All fallible operations return `tl::expected` - -**Non-Goals:** -- Streaming output (video, network sink) — offscreen image is write-once at end of N frames -- Selecting output format other than PNG -- Per-frame PNG export (only the last frame is written) -- Modifying the compositor, filter chain, or shader system internals - -## Decisions - -### Decision 1: Separate `VulkanBackend::create_headless()` factory - -**Chosen:** Add a static `create_headless(RenderSettings) -> ResultPtr` factory alongside the existing `create(SDL_Window*, RenderSettings)`. - -**Rationale:** Passing `nullptr` for the window adds implicit, fragile nullable logic throughout. A distinct factory makes the two modes structurally different — the headless instance never holds a `vk::SurfaceKHR` or swapchain members, avoiding dead state. The existing factory signature and call sites are completely unchanged. - -**Alternative considered:** `std::optional` parameter — rejected because it leaks the optionality into every call site and all internal methods that gate on the surface. - -### Decision 2: Single offscreen image, not double-buffered - -**Chosen:** Allocate one `vk::Image` as the render target, with `MAX_FRAMES_IN_FLIGHT = 1` for the headless path (or simply a single command buffer + fence). - -**Rationale:** The swapchain double-buffering exists to overlap CPU record and GPU present. Without `vkQueuePresentKHR`, there is nothing to overlap with. A single image + fence-wait-before-record is simpler and correct. - -### Decision 3: `vk::Format::eB8G8R8A8Unorm` for the offscreen image - -**Chosen:** Fixed format `eB8G8R8A8Unorm` for the offscreen render target and staging buffer. - -**Rationale:** `stb_image_write_png` writes RGBA8 directly; `eB8G8R8A8Unorm` maps to BGRA8 on the GPU, requiring a channel swap during readback (or using `eR8G8B8A8Unorm` to avoid it). Using `eR8G8B8A8Unorm` avoids any channel conversion code and maps directly to stb's expected layout. - -**Correction:** Use `vk::Format::eR8G8B8A8Unorm` as the offscreen image format to avoid channel-swap logic during PNG readback. - -### Decision 4: `readback_to_png` as a method on `VulkanBackend` - -**Chosen:** `auto readback_to_png(std::filesystem::path output) -> tl::expected` on `VulkanBackend`. - -**Rationale:** The staging buffer allocation and `vkCmdCopyImageToBuffer` require access to the device, queue, command pool, and offscreen image — all owned by `VulkanBackend`. Keeping the readback inside the backend avoids exposing Vulkan internals to `Application`. - -### Decision 5: Signal handling via `signalfd` + existing run loop - -**Chosen:** In headless mode, open a `signalfd` for `SIGTERM` and `SIGINT` and poll it with zero timeout each tick alongside the compositor frame poll. - -**Rationale:** `signalfd` integrates cleanly with the existing poll-based loop without shared state or async-signal-safety concerns. No `std::atomic` flag or global variable needed. The fd is wrapped in `UniqueFd`. - -**Alternative considered:** `std::atomic g_shutdown` set in a `sigaction` handler — simpler but relies on async-signal-safe guarantee of the write; `signalfd` is cleaner in a C++ context. - -### Decision 6: Frame count semantics - -**Chosen:** `--frames N` counts **compositor frames delivered** (non-zero `get_presented_frame()` returns), not render ticks. The last delivered frame is exported. - -**Rationale:** The compositor delivers frames only when the target app commits a surface. Counting render ticks could export a black frame if the app hasn't committed yet. Counting delivered frames guarantees the PNG reflects actual app output. - -**Warm-up:** The first compositor frame is accepted immediately; no discard/warm-up count is applied. If the app needs warm-up frames before its output is stable, callers can set `--frames` to a higher value. - -## Risks / Trade-offs - -| Risk | Mitigation | -|------|------------| -| Target app takes too long to produce its first frame; `--frames N` hangs | Add `--timeout ` in a follow-up; for now document that callers should set a process-level timeout | -| Offscreen image layout transition missed → readback returns garbage | Explicit barrier sequence in `readback_to_png`: `eColorAttachmentOptimal → eTransferSrcOptimal` before copy, `eTransferSrcOptimal → eColorAttachmentOptimal` after | -| `stb_image_write_png` write failure not propagated | `stb_image_write_png` returns 0 on failure; check return value and propagate as `tl::expected` error | -| Headless Vulkan device selection skips surface-support check | Physical device selection in headless path checks only DMA-BUF + external memory extensions; no present-queue requirement | -| `signalfd` blocked by `SIG_DFL` reset after `fork()` in child spawn | `signalfd` is opened after `spawn_target_app()`; child inherits nothing from the fd since it is not `O_CLOEXEC`-exempt by default | - -## Open Questions - -- Should `--frames 0` mean "run until child exits" (no PNG output) as a future capability? Currently `--frames` is required alongside `--output`; both should be required together or both absent. -- Should the offscreen image resolution be configurable separately from `--app-width`/`--app-height`? Currently the output resolution equals the compositor output resolution. diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/proposal.md b/openspec/changes/archive/2026-02-27-add-headless-mode/proposal.md deleted file mode 100644 index 36a74e84..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -## Why - -Goggles requires an SDL window for all operation modes, making automated visual testing and CI rendering impossible without a display. A `--headless` mode removes this constraint, enabling the compositor and filter chain to run without any window, display server output, or ImGui layer — producing a PNG snapshot after a configurable number of frames. - -## What Changes - -- Add `--headless`, `--frames `, and `--output ` CLI flags to `CliOptions` -- Add `VulkanBackend` surfaceless factory path: Vulkan instance + device + queue created without `vk::SurfaceKHR`, swapchain, or present resources; an offscreen `vk::Image` (`eB8G8R8A8Unorm`, usage `eColorAttachment | eTransferSrc`) serves as the render target -- Add PNG readback: staging buffer + `vkCmdCopyImageToBuffer` + `stb_image_write_png` after N frames -- `Application::create()` skips `init_sdl()`, `init_imgui_layer()`, and `init_shader_system()` in headless mode; `CompositorServer` starts unchanged -- Headless render loop: polls `get_presented_frame()`, imports DMA-BUF, runs filter chain into offscreen image, no `vkQueuePresentKHR`; exits when N frames rendered or SIGTERM/SIGINT received -- Signal handler (or self-pipe) sets `m_running = false` for clean shutdown (no window close event available) - -## Capabilities - -### New Capabilities - -- `headless-mode`: Surfaceless, windowless operation of the Goggles pipeline — compositor + filter chain render to an offscreen `vk::Image` and export PNG output after N frames; no SDL, no ImGui, no swapchain - -### Modified Capabilities - -- `app-window`: Application initialization gains a conditional path that skips SDL and ImGui entirely; the SDL window is no longer mandatory for all startup paths -- `render-pipeline`: `VulkanBackend` gains a surfaceless factory that omits `vk::SurfaceKHR`, swapchain creation, and present-queue requirements; `FilterChain::record()` target becomes an offscreen image view instead of a swapchain image view - -## Impact - -- **`src/app/cli.hpp` / `cli.cpp`**: new fields `headless`, `frames`, `output_path` in `CliOptions` -- **`src/app/application.hpp` / `application.cpp`**: conditional SDL/ImGui skip, new headless run loop, frame counter, PNG export call -- **`src/app/main.cpp`**: signal handler for SIGTERM/SIGINT in headless path -- **`src/render/backend/vulkan_backend.hpp` / `.cpp`**: new `create_headless()` factory or `nullptr`-window branch; offscreen image allocation and `readback_to_png()` method -- **`stb_image_write.h`**: already vendored in `packages/stb`; no new dependencies -- **Policy**: `readback_to_png` is fallible → returns `tl::expected`; offscreen image and staging buffer use RAII wrappers; no `vk::Unique*` or `vk::raii::*` diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/app-window/spec.md b/openspec/changes/archive/2026-02-27-add-headless-mode/specs/app-window/spec.md deleted file mode 100644 index 2edcee86..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/app-window/spec.md +++ /dev/null @@ -1,36 +0,0 @@ -## MODIFIED Requirements - -### Requirement: SDL3 Window Creation -The application SHALL create an SDL3 window with Vulkan support enabled on startup **unless `--headless` is active**, in which case SDL SHALL NOT be initialized and no window SHALL be created. - -#### Scenario: Window creation success -- **GIVEN** SDL3 is properly initialized -- **WHEN** the application starts without `--headless` -- **THEN** a window titled "Goggles" SHALL be created -- **AND** the window SHALL have the Vulkan flag set - -#### Scenario: SDL3 initialization failure -- **GIVEN** SDL3 cannot be initialized -- **WHEN** the application starts without `--headless` -- **THEN** an error SHALL be logged -- **AND** the application SHALL exit with a non-zero code - -#### Scenario: Headless mode skips SDL entirely -- **GIVEN** the application is launched with `--headless` -- **WHEN** initialization runs -- **THEN** `SDL_Init` SHALL NOT be called -- **AND** no SDL window SHALL be created - -## ADDED Requirements - -### Requirement: Headless Mode CLI Flags - -The application SHALL accept `--headless`, `--frames `, and `--output ` as top-level CLI flags. `--frames` and `--output` are required when `--headless` is present; providing either without the other SHALL produce an error. - -#### Scenario: Valid headless invocation -- **WHEN** run with `--headless --frames 10 --output /tmp/frame.png -- ./app` -- **THEN** `CliOptions.headless` SHALL be `true`, `frames` SHALL be `10`, `output_path` SHALL be `/tmp/frame.png` - -#### Scenario: --frames without --output -- **WHEN** run with `--headless --frames 10 -- ./app` and `--output` is absent -- **THEN** the application SHALL print a descriptive error and exit with a non-zero code diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/headless-mode/spec.md b/openspec/changes/archive/2026-02-27-add-headless-mode/specs/headless-mode/spec.md deleted file mode 100644 index cefeb0d3..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/headless-mode/spec.md +++ /dev/null @@ -1,100 +0,0 @@ -## ADDED Requirements - -### Requirement: Headless CLI Flags -The application SHALL accept `--headless`, `--frames `, and `--output ` as CLI flags. When `--headless` is present, `--frames` and `--output` MUST both be provided; missing either SHALL produce a descriptive error and exit with a non-zero code. - -#### Scenario: All three flags provided -- **WHEN** the application is run with `--headless --frames 10 --output /tmp/frame.png -- ./app` -- **THEN** `CliOptions.headless` SHALL be `true`, `frames` SHALL be `10`, and `output_path` SHALL be `/tmp/frame.png` - -#### Scenario: Missing --output with --headless -- **WHEN** the application is run with `--headless --frames 10 -- ./app` without `--output` -- **THEN** the application SHALL print a descriptive error message -- **AND** SHALL exit with a non-zero code - -#### Scenario: Missing --frames with --headless -- **WHEN** the application is run with `--headless --output /tmp/frame.png -- ./app` without `--frames` -- **THEN** the application SHALL print a descriptive error message -- **AND** SHALL exit with a non-zero code - -### Requirement: Headless Initialization Skips SDL and ImGui -When `--headless` is set, the application SHALL NOT initialize SDL, create a window, or initialize the ImGui layer. The `CompositorServer` SHALL be initialized and operational. - -#### Scenario: No SDL window in headless mode -- **GIVEN** the application is launched with `--headless` -- **WHEN** initialization completes -- **THEN** no SDL window SHALL exist -- **AND** no ImGui context SHALL be created -- **AND** `CompositorServer` SHALL report a valid Wayland display name - -#### Scenario: Child app receives Wayland display -- **GIVEN** the application is launched with `--headless -- ./test_app` -- **WHEN** the child process is spawned -- **THEN** `WAYLAND_DISPLAY` SHALL be set in the child's environment to the compositor's socket - -### Requirement: Surfaceless VulkanBackend for Headless Mode -When initialized for headless mode, `VulkanBackend` SHALL NOT create a `vk::SurfaceKHR`, swapchain, or present-related semaphores. It SHALL allocate an offscreen `vk::Image` with format `eR8G8B8A8Unorm` and usage flags `eColorAttachment | eTransferSrc` as the sole render target. - -#### Scenario: Headless factory creates no surface -- **GIVEN** `VulkanBackend::create_headless()` is called -- **WHEN** initialization completes -- **THEN** no `vk::SurfaceKHR` SHALL exist -- **AND** no swapchain SHALL exist -- **AND** the offscreen image SHALL be allocated with format `eR8G8B8A8Unorm` - -#### Scenario: Physical device selection without present queue -- **GIVEN** headless mode is active -- **WHEN** a physical device is selected -- **THEN** the selection SHALL require DMA-BUF and external memory extensions -- **AND** SHALL NOT require surface present support - -### Requirement: Headless Render Loop -In headless mode, the application SHALL run a loop that consumes compositor frames via `get_presented_frame()`, imports each DMA-BUF into `VulkanBackend`, records and submits render commands into the offscreen image, and waits on a fence before the next frame. The loop SHALL exit when the configured number of frames have been rendered. - -#### Scenario: N frames rendered then exit -- **GIVEN** the application is launched with `--headless --frames 5 --output /tmp/out.png -- ./app` -- **WHEN** 5 compositor frames have been delivered and rendered -- **THEN** the application SHALL call `readback_to_png` and write the PNG -- **AND** SHALL exit with code 0 - -#### Scenario: No vkQueuePresentKHR called -- **GIVEN** headless mode is active -- **WHEN** a frame is rendered -- **THEN** `vkQueuePresentKHR` SHALL NOT be called -- **AND** the render fence SHALL be waited on synchronously before the next frame - -### Requirement: PNG Readback and Export -After rendering the final frame, `VulkanBackend` SHALL read back the offscreen image to CPU memory using a staging buffer and write a PNG file to the configured output path using `stb_image_write_png`. The operation SHALL return `tl::expected`; write failure SHALL propagate as an error. - -#### Scenario: Successful PNG write -- **GIVEN** headless rendering of N frames is complete -- **WHEN** `readback_to_png(output_path)` is called -- **THEN** a valid PNG file SHALL exist at `output_path` -- **AND** the image dimensions SHALL match the configured compositor output resolution - -#### Scenario: PNG write failure propagated -- **GIVEN** `output_path` is in a non-writable directory -- **WHEN** `readback_to_png(output_path)` is called -- **THEN** the function SHALL return an `Error` describing the failure -- **AND** the application SHALL exit with a non-zero code - -#### Scenario: Image layout transition before readback -- **WHEN** `readback_to_png` is called after rendering -- **THEN** the offscreen image SHALL be transitioned from `eColorAttachmentOptimal` to `eTransferSrcOptimal` before `vkCmdCopyImageToBuffer` -- **AND** the staging buffer memory SHALL be invalidated before CPU read if the memory type is not host-coherent - -### Requirement: Signal-Based Shutdown in Headless Mode -In headless mode, the application SHALL handle `SIGTERM` and `SIGINT` via `signalfd`. On signal receipt the run loop SHALL exit cleanly, child processes SHALL be terminated with the same SIGTERM→SIGKILL escalation used in windowed mode, and all Vulkan resources SHALL be released before process exit. - -#### Scenario: SIGTERM triggers clean shutdown -- **GIVEN** the application is running in headless mode -- **WHEN** `SIGTERM` is delivered to the goggles process -- **THEN** the run loop SHALL exit at the next tick -- **AND** the child process SHALL receive SIGTERM -- **AND** the application SHALL exit with a non-zero code indicating signal termination - -#### Scenario: Child exit triggers headless shutdown -- **GIVEN** the application is running in headless mode with a child app -- **WHEN** the child process exits before N frames are rendered -- **THEN** the run loop SHALL exit -- **AND** the application SHALL exit with a non-zero code diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-27-add-headless-mode/specs/render-pipeline/spec.md deleted file mode 100644 index 9c7fffe3..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/specs/render-pipeline/spec.md +++ /dev/null @@ -1,55 +0,0 @@ -## ADDED Requirements - -### Requirement: Surfaceless VulkanBackend Factory -`VulkanBackend` SHALL provide a `create_headless(RenderSettings) -> ResultPtr` static factory that creates a Vulkan instance, selects a physical device, and creates a logical device and queue without requiring a `vk::SurfaceKHR`. This factory SHALL NOT create a swapchain, present semaphores, or frame-pacing resources. - -#### Scenario: Headless factory succeeds without display -- **GIVEN** a GPU supporting DMA-BUF import and external memory extensions is available -- **WHEN** `VulkanBackend::create_headless(settings)` is called -- **THEN** it SHALL return a valid `VulkanBackend` instance -- **AND** the instance SHALL hold no `vk::SurfaceKHR` or swapchain - -#### Scenario: Device selection without present support -- **GIVEN** headless mode is active -- **WHEN** a physical device is selected -- **THEN** the device SHALL be required to support `VK_EXT_external_memory_dma_buf`, `VK_EXT_image_drm_format_modifier`, and `VK_KHR_external_semaphore_fd` -- **AND** surface present support SHALL NOT be a selection criterion - -### Requirement: Offscreen Render Target Allocation -When operating in headless mode, `VulkanBackend` SHALL allocate a single `vk::Image` with format `eR8G8B8A8Unorm`, tiling `eOptimal`, and usage `eColorAttachment | eTransferSrc` as the render target. This image SHALL be used as the target passed to `FilterChain::record()` in place of a swapchain image view. - -#### Scenario: Offscreen image created at initialization -- **GIVEN** `VulkanBackend::create_headless()` completes -- **WHEN** the first render call is made -- **THEN** the offscreen image SHALL exist in device-local memory -- **AND** its format SHALL be `eR8G8B8A8Unorm` -- **AND** its dimensions SHALL match the configured compositor output resolution - -#### Scenario: Filter chain writes to offscreen image -- **GIVEN** headless mode is active and a compositor frame has been imported -- **WHEN** `render()` is called -- **THEN** `FilterChain::record()` SHALL receive the offscreen image view as its render target -- **AND** no swapchain image view SHALL be passed - -### Requirement: Headless Frame Submission Without Present -In headless mode, frame submission SHALL queue render commands and wait on a fence for GPU completion. `vkQueuePresentKHR` SHALL NOT be called. Frame pacing via `throttle_present` or `vkWaitForPresentKHR` SHALL NOT be applied. - -#### Scenario: Fence-based synchronization replaces present -- **GIVEN** headless mode is active -- **WHEN** a frame's render commands are submitted -- **THEN** `vkWaitForFences` SHALL be called to synchronize before the next frame -- **AND** `vkQueuePresentKHR` SHALL NOT be called - -### Requirement: Offscreen Image Readback to PNG -`VulkanBackend` SHALL expose `readback_to_png(std::filesystem::path) -> tl::expected` that copies the offscreen image to a host-visible staging buffer and writes a PNG via `stb_image_write_png`. The image SHALL be transitioned to `eTransferSrcOptimal` before copy and back to `eColorAttachmentOptimal` after. - -#### Scenario: Successful readback and PNG write -- **GIVEN** headless mode has completed N render frames -- **WHEN** `readback_to_png("/tmp/out.png")` is called -- **THEN** a valid PNG file SHALL be written to `/tmp/out.png` -- **AND** the image dimensions SHALL match the offscreen image extent - -#### Scenario: Staging buffer invalidated before CPU read -- **GIVEN** the staging buffer memory type is not `eHostCoherent` -- **WHEN** the GPU-to-buffer copy completes -- **THEN** `vkInvalidateMappedMemoryRanges` SHALL be called before the CPU reads the buffer diff --git a/openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md b/openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md deleted file mode 100644 index f91994bf..00000000 --- a/openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md +++ /dev/null @@ -1,46 +0,0 @@ -## 1. CLI Flags - -- [x] 1.1 Add `headless` (bool), `frames` (uint32_t), and `output_path` (std::filesystem::path) fields to `CliOptions` in `src/app/cli.hpp` -- [x] 1.2 Register `--headless`, `--frames`, and `--output` with CLI11 in `src/app/cli.cpp`; validate that `--frames` and `--output` are both present when `--headless` is set, returning an error otherwise -- [x] 1.3 Verify: `goggles --headless --frames 5 --output /tmp/x.png --help` prints new flags; `goggles --headless --frames 5 -- app` exits with non-zero and descriptive message about missing `--output` - -## 2. VulkanBackend Surfaceless Factory - -- [x] 2.1 Add `static auto create_headless(RenderSettings) -> tl::expected, Error>` declaration to `src/render/backend/vulkan_backend.hpp`; mark existing `create(SDL_Window*, RenderSettings)` unchanged -- [x] 2.2 Implement `create_headless()` in `vulkan_backend.cpp`: call `create_instance()` without surface extensions, skip `create_surface()`, call `select_physical_device()` with a headless-aware predicate (no present-queue requirement), then `create_device()`, `create_command_resources()`, `create_sync_objects()`, `init_filter_chain()` -- [x] 2.3 Add `m_headless` bool and offscreen image members (`m_offscreen_image`, `m_offscreen_memory`, `m_offscreen_view`, `m_offscreen_extent`) to `VulkanBackend`; allocate offscreen `vk::Image` (`eR8G8B8A8Unorm`, `eColorAttachment | eTransferSrc`, device-local) in `create_headless()` -- [x] 2.4 Guard `create_swapchain()`, present semaphore creation, and `throttle_present()` behind `!m_headless` checks so they are never called in headless path - -## 3. Headless Render Submission - -- [x] 3.1 In `VulkanBackend::render()`, add headless branch: skip `acquire_next_image()`; pass `m_offscreen_view` and `m_offscreen_extent` to `FilterChain::record()` instead of swapchain image view; submit commands with a fence; call `vkWaitForFences` before returning — no `vkQueuePresentKHR` -- [x] 3.2 Verify the fence is properly reset with `vkResetFences` before each submission in the headless path - -## 4. PNG Readback - -- [x] 4.1 Add `auto readback_to_png(std::filesystem::path output) -> tl::expected` declaration to `vulkan_backend.hpp` -- [x] 4.2 Implement `readback_to_png()`: allocate host-visible staging buffer, record commands to transition offscreen image `eColorAttachmentOptimal → eTransferSrcOptimal`, `vkCmdCopyImageToBuffer`, transition back; submit + fence wait; if memory is not host-coherent call `vkInvalidateMappedMemoryRanges`; map buffer, call `stbi_write_png`; check return value and propagate failure as `tl::expected` error; unmap + destroy staging buffer via RAII -- [x] 4.3 Verify: running the full pipeline with a known test client produces a non-empty PNG at the specified output path - -## 5. Application Headless Path - -- [x] 5.1 In `Application::create()` (`src/app/application.cpp`), add early branch: when `cli_opts.headless == true`, skip `init_sdl()`, `init_imgui_layer()`, and `init_shader_system()`; call `VulkanBackend::create_headless()` instead of `create(window, settings)` -- [x] 5.2 Add `run_headless(uint32_t frames, std::filesystem::path output) -> tl::expected` to `Application`: loop calling `get_presented_frame()`, skipping ticks with no new frame; on new frame call `render_frame()` (headless render path); count delivered frames; after N frames call `m_vulkan_backend->readback_to_png(output)` and return -- [x] 5.3 In `src/app/main.cpp`, after spawning the child app, check `cli_opts.headless`: if true call `app->run_headless(frames, output_path)`, log result, and exit; otherwise run the existing windowed event loop unchanged - -## 6. Signal Handling - -- [x] 6.1 In the headless path in `main.cpp`, open a `signalfd` for `SIGTERM` and `SIGINT` (block both signals first via `sigprocmask`); wrap the fd in `goggles::util::UniqueFd` -- [x] 6.2 In `run_headless()`, poll the signalfd with zero timeout each tick; on signal receipt, log the signal, terminate the child, and return an `Error` causing main to exit with non-zero code -- [x] 6.3 Verify: `kill -TERM ` while goggles runs in headless mode causes clean shutdown with child process terminated - -## 7. Tests - -- [x] 7.1 Add CLI parsing tests in `tests/app/test_cli.cpp`: `--headless --frames 10 --output /tmp/x.png` parses correctly; `--headless --frames 10` (missing `--output`) returns error -- [x] 7.2 Add a CTest integration test (disabled in CI via `DEFINED ENV{CI}` guard) that runs `goggles --headless --frames 3 --output /tmp/goggles_headless_test.png -- ` and verifies exit code 0 and file exists - -## 8. Verification - -- [x] 8.1 `pixi run build -p test` completes without errors or clang-tidy warnings -- [x] 8.2 `pixi run test -p test` passes all existing unit tests with no regressions -- [x] 8.3 Manual smoke test: `pixi run build -p debug && build/debug/bin/goggles --headless --frames 5 --output /tmp/smoke.png -- vkcube` exits 0 and produces a valid PNG diff --git a/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/proposal.md b/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/proposal.md deleted file mode 100644 index 132de135..00000000 --- a/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/proposal.md +++ /dev/null @@ -1,52 +0,0 @@ -# Change: Drop WSI Proxy and Simplify Capture Path - -## Why - -The WSI proxy mode (`GOGGLES_WSI_PROXY=1`) intercepts platform surface creation and replaces -real swapchains with virtual ones. This breaks compatibility with Vulkan overlay layers (e.g. -Steam overlay, MangoHud) that rely on a real swapchain being visible to downstream layers, and -adds significant complexity: two parallel code paths through every hook function, ~1,252 lines of -virtual surface/swapchain bookkeeping, and duplicated DMA-BUF export infrastructure. - -The normal capture path (add `TRANSFER_SRC_BIT`, `CmdCopyImage` to export image, timeline -semaphore sync) covers all Linux gaming use cases reliably without these drawbacks. - -Additionally, the compositor `present_swapchain` is hardcoded to `XRGB8888 + DRM_FORMAT_MOD_LINEAR`, -which discards sRGB format information from game output. CRT shaders that expect sRGB-tagged input -receive incorrect gamma through the compositor capture path. - -## What Changes - -- **REMOVED:** WSI proxy mode — `WsiVirtualizer`, virtual surface/swapchain tracking, all - `if (virt.is_enabled())` branches in hook functions (~1,252 LOC) -- **REMOVED:** `GOGGLES_WSI_PROXY` environment variable and its handling in `CaptureManager` -- **REMOVED:** `enqueue_virtual_frame()`, `VirtualFrameInfo`, `try_dump_present_image()`, - virtual frame counter from `CaptureManager` -- **REMOVED:** Resolution relay from `CaptureReceiver` control messages to `WsiVirtualizer` -- **REMOVED:** `SurfaceCapturePath::vulkan` enum value — never set anywhere in the codebase -- **SIMPLIFIED:** `vk_hooks.cpp` collapses to a single real-swapchain capture path with no - virtual dispatch branches -- **FIXED:** Compositor `present_swapchain` format negotiated from the DRM allocator's supported - format/modifier set instead of being hardcoded to `XRGB8888 + LINEAR` -- **DOCUMENTED:** `downsample_pass` pre-chain stage is the canonical resolution control for CRT - shader workflows; `request_surface_resize()` remains best-effort UI convenience only - -## Impact - -- Affected specs: `vk-layer-capture` -- Affected code: - - `src/capture/vk_layer/wsi_virtual.hpp` — deleted - - `src/capture/vk_layer/wsi_virtual.cpp` — deleted - - `src/capture/vk_layer/vk_hooks.cpp` — remove all virtual branches, simplify all hooks - - `src/capture/vk_layer/vk_capture.hpp` — remove `VirtualFrameInfo`, virtual frame counter, - `enqueue_virtual_frame`, `try_dump_present_image` - - `src/capture/vk_layer/vk_capture.cpp` — remove virtual frame path in `CaptureManager` - - `src/compositor/compositor_server.hpp` — remove `SurfaceCapturePath::vulkan` - - `src/compositor/compositor_server.cpp` — fix `setup_output()` format negotiation - - `src/app/application.cpp` — remove `SurfaceCapturePath::vulkan` reference in - `sync_surface_filters()` - - `config/goggles.template.toml` and runtime config bootstrap/loading flow — remove any WSI - proxy config keys if present for `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` -- No new dependencies -- **BREAKING:** `GOGGLES_WSI_PROXY=1` launch scripts stop working silently (env var ignored or - logged as unknown) diff --git a/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/tasks.md b/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/tasks.md deleted file mode 100644 index b87e1c3b..00000000 --- a/openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/tasks.md +++ /dev/null @@ -1,60 +0,0 @@ -## 1. Delete WSI Proxy Files - -- [x] 1.1 Delete `src/capture/vk_layer/wsi_virtual.hpp` -- [x] 1.2 Delete `src/capture/vk_layer/wsi_virtual.cpp` -- [x] 1.3 Remove `wsi_virtual` from `src/capture/vk_layer/CMakeLists.txt` source list - -## 2. Simplify `vk_hooks.cpp` - -- [x] 2.1 Remove `#include "wsi_virtual.hpp"` and the `virt` accessor call -- [x] 2.2 `CreateXlibSurfaceKHR` — remove virtual branch, keep only real surface passthrough -- [x] 2.3 `CreateXcbSurfaceKHR` — remove virtual branch -- [x] 2.4 `CreateWaylandSurfaceKHR` — remove virtual branch -- [x] 2.5 `DestroySurfaceKHR` — remove virtual surface check, keep only real destroy -- [x] 2.6 `GetPhysicalDeviceSurfaceCapabilitiesKHR` — remove virtual branch -- [x] 2.7 `GetPhysicalDeviceSurfaceFormatsKHR` — remove virtual branch -- [x] 2.8 `GetPhysicalDeviceSurfacePresentModesKHR` — remove virtual branch -- [x] 2.9 `GetPhysicalDeviceSurfaceSupportKHR` — remove virtual branch -- [x] 2.10 `GetPhysicalDeviceSurfaceCapabilities2KHR` — remove virtual branch -- [x] 2.11 `GetPhysicalDeviceSurfaceFormats2KHR` — remove virtual branch -- [x] 2.12 `CreateSwapchainKHR` — remove virtual swapchain branch; keep only real path -- [x] 2.13 `DestroySwapchainKHR` — remove virtual swapchain check -- [x] 2.14 `GetSwapchainImagesKHR` — remove virtual image list branch -- [x] 2.15 `AcquireNextImageKHR` — remove virtual acquire path (FPS limiter, semaphore wait) -- [x] 2.16 `AcquireNextImage2KHR` — remove virtual acquire path -- [x] 2.17 `QueuePresentKHR` — remove virtual-only branch; keep only real present + `on_present()` -- [x] 2.18 `WaitForPresentKHR` — remove virtual early-return branch - -## 3. Remove Virtual Frame Infrastructure from `CaptureManager` - -- [x] 3.1 Remove `VirtualFrameInfo` struct from `vk_capture.hpp` -- [x] 3.2 Remove `virtual_frame_counter_` member and `get_virtual_frame_counter()` accessor -- [x] 3.3 Remove `enqueue_virtual_frame()` declaration and implementation -- [x] 3.4 Remove `try_dump_present_image()` declaration and implementation -- [x] 3.5 Remove resolution relay to `WsiVirtualizer` in `on_present()` / control message handling -- [x] 3.6 Verify async worker thread `worker_func()` handles only timeline-semaphore frames (virtual path removed) - -## 4. Fix `SurfaceCapturePath` Enum - -- [x] 4.1 Remove `SurfaceCapturePath::vulkan` from enum in `compositor_server.hpp` -- [x] 4.2 Update `sync_surface_filters()` in `application.cpp`: remove `SurfaceCapturePath::vulkan` branch from `default_filter_enabled` expression - -## 5. Fix Compositor `present_swapchain` Format Negotiation - -- [x] 5.1 In `CompositorServer::Impl::setup_output()`, replace hardcoded `DRM_FORMAT_XRGB8888 + DRM_FORMAT_MOD_LINEAR` with a query to `wlr_output_get_primary_formats()` against allocator buffer caps -- [x] 5.2 Select preferred format/modifier from the reported set (prefer XRGB8888 or ARGB8888 with tiled modifier if available, fall back to LINEAR) -- [x] 5.3 Keep `present_modifiers` storage as a `std::vector` to hold the negotiated modifier list -- [x] 5.4 Recreate `present_swapchain` on output reconfigure using the negotiated format - -## 6. Config and Environment Cleanup - -- [x] 6.1 Remove `GOGGLES_WSI_PROXY` environment variable check from `CaptureManager` / `should_use_wsi_proxy()` -- [x] 6.2 Remove `GOGGLES_WIDTH` / `GOGGLES_HEIGHT` / `GOGGLES_FPS_LIMIT` env var handling that was exclusive to WSI proxy -- [x] 6.3 Log a warning if `GOGGLES_WSI_PROXY=1` is detected at startup, stating the mode has been removed - -## 7. Build and Quality Gates - -- [x] 7.1 `pixi run build -p debug` — confirm zero compile errors -- [x] 7.2 `pixi run build -p quality` — confirm zero clang-tidy warnings -- [x] 7.3 `pixi run test -p test` — all existing tests pass -- [x] 7.4 Confirm no remaining references to `WsiVirtualizer`, `wsi_virtual`, `enqueue_virtual_frame`, `GOGGLES_WSI_PROXY` in `src/` diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/.openspec.yaml b/openspec/changes/archive/2026-02-27-test-framework-phase1/.openspec.yaml deleted file mode 100644 index d1c6cc6f..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-27 diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/design.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/design.md deleted file mode 100644 index 5b25e54a..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/design.md +++ /dev/null @@ -1,73 +0,0 @@ -## Context - -Goggles has a fully-implemented headless mode (PR #96): `goggles --headless --frames N --output -- ` runs the compositor and filter chain without a display and writes the final rendered frame as PNG via `VulkanBackend::readback_to_png()`. This is the execution primitive for all visual tests. - -Phase 1 builds the three layers that visual tests require: -1. **Test clients** — deterministic Wayland apps that produce known pixel patterns as compositor input -2. **Image comparison** — a C++ library that can assert rendered PNG output against golden references or mathematical expectations -3. **CMake/CTest wiring** — unconditional build of test clients and image comparison library alongside the main project, with CTest label taxonomy (`unit`/`integration`/`visual`) for filtering - -No new runtime code touches the production pipeline. All new code lives under `tests/`. - -## Goals / Non-Goals - -**Goals:** -- Provide `solid_color_client`, `gradient_client`, `quadrant_client`, `multi_surface_client` as deterministic source apps for headless tests -- Provide `CompareResult compare_images(actual, reference, tolerance)` usable in Catch2 tests and as a standalone CLI -- Wire up CTest label taxonomy (`unit`/`integration`/`visual`) so all test infrastructure builds unconditionally — no separate option or preset needed since wayland-client, stb_image, and Catch2 are already project dependencies -- Add a CTest smoke test that proves `Application::create_headless()` round-trips through the full pipeline - -**Non-Goals:** -- Golden image files (created in Phase 2 once the framework exists) -- Phase 2–6 test content (aspect ratio tests, shader tests, compositor tests, CI integration) -- RenderDoc / GPU capture integration (Phase 3) -- SwiftShader integration (Phase 6) - -## Decisions - -### Decision 1: wl_shm for test clients (not Vulkan DMA-BUF) - -**Chosen**: `wl_shm` shared memory buffers, CPU-rendered. - -**Rationale**: Test clients are sources, not renderers. Their role is to deliver known pixel patterns into the compositor. `wl_shm` is synchronous, requires no GPU, and produces bit-exact pixel values regardless of hardware. Vulkan DMA-BUF from a test client would introduce GPU driver variance in the source content, defeating the purpose of deterministic golden-image testing. The goggles pipeline (DMA-BUF import → filter chain → offscreen image) is what's under test, not the client's rendering path. - -**Alternative considered**: Vulkan + DMA-BUF clients. Rejected because: more code, GPU-dependent, not available on CI without a real device. - -### Decision 2: stb_image for PNG I/O in the comparison library (no new dependency) - -**Chosen**: Use `stb_image` for reading PNGs in `image_compare.cpp`. - -**Rationale**: `stb_image` is already vendored in `packages/stb` and used via `stb_image_write_impl.cpp`. Adding a `stb_image_impl.cpp` TU in the test library avoids any new dependency. The comparison library only reads PNGs produced by `readback_to_png()` (always RGBA8 or RGB8 via stb_image_write), so a full-featured PNG library is not needed. - -**Alternative considered**: libpng. Rejected: new dependency, no benefit for the narrow use case. - -### Decision 3: CTest `add_test()` for headless smoke (not Catch2) - -**Chosen**: Plain CTest `add_test()` wrapping the goggles binary directly. - -**Rationale**: The smoke test is an end-to-end binary invocation (`goggles --headless --frames 5 --output -- solid_color_client`). Wrapping this in a Catch2 fixture would require a test binary that calls `exec()` or `system()`, which adds complexity and a second process boundary. CTest `add_test()` is the standard CMake mechanism for exactly this pattern and gives clean pass/fail semantics with stdout/stderr capture. - -**Alternative considered**: Catch2 with `GENERATE`/`SECTION` fixture. Rejected: unnecessary layer, harder to diagnose failures. - -### Decision 4: Separate `tests/clients/` and `tests/visual/` subdirectories - -**Chosen**: Two distinct directories under `tests/`. - -**Rationale**: Clients and the comparison library have different consumers. Clients are binaries (used by CTest add_test and eventually pytest fixtures). The comparison library is a static library linked into Catch2 test TUs. Keeping them separate makes the CMake target graph clear and avoids conflating test infrastructure with test content. - -### Decision 5: `CompareResult` as a plain struct (not `tl::expected`) - -**Chosen**: `compare_images()` returns `CompareResult` directly; only PNG I/O operations return `Result`. - -**Rationale**: A comparison itself is not fallible — given two loaded images, it always produces a result. The `tl::expected` contract applies to operations that can fail at runtime (I/O, GPU calls). The fallible boundary is `load_png() -> Result`, which precedes comparison. This keeps call sites in test code clean (`auto result = compare_images(a, b, tol);`) without awkward error-handling boilerplate in assertions. - -## Risks / Trade-offs - -- **wl_shm client frame count timing**: The headless loop polls for frames at 1 ms intervals. If `solid_color_client` commits its first buffer before the compositor's Wayland socket is ready, the frame will be missed. **Mitigation**: clients retry `wl_display_dispatch()` until the first successful surface commit is acknowledged; the headless loop's 1 ms backoff handles transient latency. -- **stb_image alpha premultiplication**: stb_image can optionally premultiply alpha on load. **Mitigation**: always load with `stbi_set_unpremultiply_on_load(0)` and compare raw RGBA channels. -- **CTest working directory for smoke test**: The smoke test needs the goggles binary and `solid_color_client` binary both on `PATH` or via absolute paths. **Mitigation**: use CMake generator expressions (`$`) in `add_test()` to inject absolute paths. -- **CMake option footprint**: All visual test targets now build unconditionally. This is correct because all dependencies (wayland-client, wayland-protocols, stb_image, Catch2) are already hard requirements of the main goggles build. CTest labels (`unit`/`integration`/`visual`) provide the filtering mechanism for selective test execution. - -## Open Questions - -None blocking Phase 1. Phase 2 open question: should golden images be stored via Git LFS or as untracked binary blobs committed directly? (LFS preferred but requires repo-level setup.) diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/proposal.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/proposal.md deleted file mode 100644 index b7148fe8..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/proposal.md +++ /dev/null @@ -1,31 +0,0 @@ -## Why - -Goggles has 27 unit tests and no automated visual validation. 30+ manual test scenarios exist (aspect ratio, shaders, surface composition, filter chain) that are exercised by hand on every change. The headless mode (PR #96) provides the execution primitive needed to drive a real GPU pipeline in CI without a display — Phase 1 builds the infrastructure layer that all visual tests will depend on. - -## What Changes - -- **New**: `tests/clients/` — four deterministic Wayland/`wl_shm` test client apps rendering known solid-color patterns (`solid_color_client`, `gradient_client`, `quadrant_client`, `multi_surface_client`) -- **New**: `tests/visual/image_compare.hpp/.cpp` — image comparison library with fuzzy per-channel tolerance, `CompareResult` struct, and diff-image generation -- **New**: `tests/visual/image_compare_cli.cpp` — standalone CLI comparison tool (`goggles_image_compare`) -- **New**: `headless_smoke` — CTest integration test running the full headless pipeline against `solid_color_client` -- **Modified**: `tests/CMakeLists.txt` — unconditional inclusion of `tests/clients/` and `tests/visual/`, headless smoke tests - -## Capabilities - -### New Capabilities - -- `test-client-apps`: Deterministic Wayland test clients rendering known pixel patterns via `wl_shm`; used as target apps for headless visual tests -- `visual-regression`: Image comparison library and CLI tool providing fuzzy per-channel pixel comparison, configurable tolerance thresholds, and diff-image visualization - -### Modified Capabilities - -- `build-system`: Test clients and image comparison library build unconditionally alongside the main project — no separate option or preset needed since all dependencies (wayland-client, stb_image, Catch2) are already project requirements - -## Impact - -- **New code**: `tests/clients/` (~4 C++ files), `tests/visual/` (~3 C++ files + header) -- **CMake**: root `CMakeLists.txt`, `CMakePresets.json`, `tests/CMakeLists.txt`, new `tests/clients/CMakeLists.txt`, `tests/visual/CMakeLists.txt` -- **CTest labels**: adds `visual` and `integration` (smoke) test labels alongside existing `unit` -- **Dependencies**: `stb_image_write` and `stb_image` already vendored in `packages/stb` — no new external dependencies for Phase 1 -- **Policy-sensitive**: image comparison library uses `tl::expected` for fallible I/O (PNG read/write); test client apps use RAII for `wl_display`/`wl_surface` handles; no threading in pipeline code -- **No breaking changes** to any existing code or APIs diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/build-system/spec.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/build-system/spec.md deleted file mode 100644 index 34dd6805..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/build-system/spec.md +++ /dev/null @@ -1,30 +0,0 @@ -## ADDED Requirements - -### Requirement: Visual test targets build unconditionally -The build system SHALL build all visual test clients and the image comparison library as part of the default build, since all dependencies (wayland-client, wayland-protocols, stb_image, Catch2) are already project requirements. - -#### Scenario: Default build includes visual targets -- **GIVEN** a clean CMake configuration using any preset -- **WHEN** the build completes -- **THEN** all test client binaries (`solid_color_client`, `gradient_client`, `quadrant_client`, `multi_surface_client`) SHALL be built -- **AND** `goggles_image_compare` CLI binary SHALL be built -- **AND** the `image_compare` static library SHALL be built -- **AND** `test_image_compare` Catch2 test binary SHALL be built - -### Requirement: CTest label taxonomy -The build system SHALL register test targets under a consistent label taxonomy using `set_tests_properties(... LABELS ...)`. - -#### Scenario: Unit label unchanged -- **WHEN** `ctest -L unit` is run with any preset -- **THEN** existing Catch2 unit tests and `image_compare_unit_tests` SHALL run -- **AND** no integration tests SHALL be included - -#### Scenario: Integration label includes headless smoke -- **WHEN** `ctest -L integration` is run with any preset -- **THEN** the headless pipeline smoke test (`headless_smoke`, `headless_smoke_png_check`) SHALL be included -- **AND** existing integration tests (e.g., `auto_input_forwarding`, when available) SHALL also be included - -#### Scenario: Visual label for visual regression tests -- **WHEN** `ctest -L visual` is run with any preset -- **THEN** only visual regression test targets (Phase 2+) SHALL run -- **AND** unit and integration tests SHALL NOT be included unless also labeled `visual` diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/test-client-apps/spec.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/test-client-apps/spec.md deleted file mode 100644 index 5446971f..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/test-client-apps/spec.md +++ /dev/null @@ -1,83 +0,0 @@ -## ADDED Requirements - -### Requirement: Solid color test client -The system SHALL provide a `solid_color_client` binary that connects to a Wayland compositor and renders a single solid color surface via `wl_shm`. - -#### Scenario: Default color rendering -- **GIVEN** `solid_color_client` is launched with `WAYLAND_DISPLAY` set to the compositor socket -- **WHEN** the client connects and commits its first buffer -- **THEN** the surface SHALL be filled with RGBA(255, 0, 0, 255) by default - -#### Scenario: Color override via environment variable -- **GIVEN** `TEST_COLOR=0,255,0,255` is set in the environment -- **WHEN** `solid_color_client` renders its surface -- **THEN** every pixel SHALL be RGBA(0, 255, 0, 255) - -#### Scenario: Clean exit after frame count -- **GIVEN** `solid_color_client` has committed its buffers -- **WHEN** it has rendered at least 30 stable frames (default) -- **THEN** the process SHALL exit with code 0 - -### Requirement: Quadrant test client -The system SHALL provide a `quadrant_client` binary that renders four colored quadrants at deterministic pixel positions. - -#### Scenario: Quadrant color layout -- **GIVEN** `quadrant_client` is launched and connected to the compositor -- **WHEN** it commits its first buffer -- **THEN** the top-left quadrant SHALL be red (255, 0, 0, 255) -- **AND** the top-right quadrant SHALL be green (0, 255, 0, 255) -- **AND** the bottom-left quadrant SHALL be blue (0, 0, 255, 255) -- **AND** the bottom-right quadrant SHALL be white (255, 255, 255, 255) - -#### Scenario: Quadrant boundary precision -- **GIVEN** a surface of width W and height H -- **WHEN** quadrant colors are rendered -- **THEN** the pixel at (W/2 - 1, H/2 - 1) SHALL be red -- **AND** the pixel at (W/2 + 1, H/2 - 1) SHALL be green -- **AND** the pixel at (W/2 - 1, H/2 + 1) SHALL be blue -- **AND** the pixel at (W/2 + 1, H/2 + 1) SHALL be white - -#### Scenario: Clean exit after frame count -- **GIVEN** `quadrant_client` has committed its buffers -- **WHEN** it has rendered at least 30 stable frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Gradient test client -The system SHALL provide a `gradient_client` binary that renders a horizontal linear gradient from black (left) to white (right) via `wl_shm`. - -#### Scenario: Gradient pixel values -- **GIVEN** `gradient_client` is connected and has committed its buffer -- **WHEN** the output PNG is read -- **THEN** the pixel at x=0 SHALL have R=G=B=0 (black) -- **AND** the pixel at x=W-1 SHALL have R=G=B=255 (white) -- **AND** intermediate pixels SHALL increase monotonically left to right - -#### Scenario: Clean exit after frame count -- **GIVEN** `gradient_client` has rendered at least 30 stable frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Multi-surface test client -The system SHALL provide a `multi_surface_client` binary that creates a main surface and one `wl_subsurface` with distinct solid colors. - -#### Scenario: Two-surface layout -- **GIVEN** `multi_surface_client` is connected and both surfaces are committed -- **WHEN** the compositor presents the scene -- **THEN** the main surface SHALL render its background color (blue: 0, 0, 255, 255) -- **AND** the subsurface SHALL render its foreground color (red: 255, 0, 0, 255) -- **AND** the subsurface SHALL be composited on top of the main surface at a known parent-relative offset set via `wl_subsurface.set_position()` - -#### Scenario: Clean exit after frame count -- **GIVEN** both surfaces have been committed for at least 30 frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Wayland protocol compliance -All test clients SHALL use `wl_shm` shared memory buffers and comply with the `xdg-wm-base` shell protocol for toplevel surface creation. - -#### Scenario: No Vulkan dependency -- **WHEN** any test client binary is executed in an environment without a GPU or Vulkan loader -- **THEN** it SHALL connect, render, and exit successfully using only `wl_shm` - -#### Scenario: WAYLAND_DISPLAY is required -- **GIVEN** `WAYLAND_DISPLAY` is not set or the socket does not exist -- **WHEN** any test client is launched -- **THEN** the process SHALL exit with a non-zero code and print an error to stderr diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/visual-regression/spec.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/visual-regression/spec.md deleted file mode 100644 index 9923c876..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/specs/visual-regression/spec.md +++ /dev/null @@ -1,80 +0,0 @@ -## ADDED Requirements - -### Requirement: Image comparison library -The system SHALL provide a C++ image comparison library at `tests/visual/image_compare.hpp` that compares two PNG images with configurable per-channel tolerance. - -#### Scenario: Identical images pass -- **GIVEN** two PNG files with identical pixel data -- **WHEN** `compare_images(actual_path, reference_path, tolerance)` is called with any tolerance ≥ 0 -- **THEN** `CompareResult.passed` SHALL be `true` -- **AND** `CompareResult.max_channel_diff` SHALL be `0.0` -- **AND** `CompareResult.failing_pixels` SHALL be `0` - -#### Scenario: Differing images fail at zero tolerance -- **GIVEN** two PNG files where at least one pixel differs by 1 channel value -- **WHEN** `compare_images()` is called with `tolerance = 0.0` -- **THEN** `CompareResult.passed` SHALL be `false` -- **AND** `CompareResult.failing_pixels` SHALL be ≥ 1 - -#### Scenario: Tolerance allows small differences -- **GIVEN** two PNG files where all pixels differ by at most 2/255 per channel -- **WHEN** `compare_images()` is called with `tolerance = 2.0/255.0` -- **THEN** `CompareResult.passed` SHALL be `true` - -#### Scenario: CompareResult fields are populated -- **GIVEN** a comparison that produces failures -- **WHEN** `compare_images()` returns -- **THEN** `CompareResult.max_channel_diff` SHALL be the maximum per-channel delta across all pixels (normalized 0.0–1.0) -- **AND** `CompareResult.mean_diff` SHALL be the mean per-pixel average-channel delta -- **AND** `CompareResult.failing_percentage` SHALL equal `failing_pixels / (width * height)` × 100 - -#### Scenario: Diff image is generated on failure -- **GIVEN** a comparison that fails AND `diff_output_path` is provided -- **WHEN** `compare_images()` returns -- **THEN** a PNG SHALL be written at `diff_output_path` -- **AND** pixels that exceeded tolerance SHALL be highlighted in red (255, 0, 0, 255) in the diff image -- **AND** passing pixels SHALL be shown at reduced intensity (≤ 25% of original value) - -#### Scenario: Size mismatch is a failure -- **GIVEN** two PNG files with different dimensions -- **WHEN** `compare_images()` is called -- **THEN** `CompareResult.passed` SHALL be `false` -- **AND** an error message describing the dimension mismatch SHALL be set - -### Requirement: Image comparison CLI tool -The system SHALL provide a `goggles_image_compare` CLI binary that wraps the comparison library for use in shell scripts and pixi tasks. - -#### Scenario: Pass exit code -- **WHEN** `goggles_image_compare actual.png reference.png --tolerance 0.01` is run and images match within tolerance -- **THEN** the process SHALL exit with code 0 - -#### Scenario: Fail exit code -- **WHEN** `goggles_image_compare actual.png reference.png --tolerance 0.0` is run and images differ -- **THEN** the process SHALL exit with code 1 -- **AND** a summary SHALL be printed to stdout including `failing_pixels` and `max_channel_diff` - -#### Scenario: Diff image output -- **WHEN** `--diff diff.png` is passed and the comparison fails -- **THEN** a diff PNG SHALL be written to `diff.png` - -#### Scenario: Missing file error -- **WHEN** either `actual.png` or `reference.png` does not exist -- **THEN** the process SHALL exit with code 2 and print an error to stderr - -### Requirement: Headless pipeline smoke test -The system SHALL provide a CTest integration test that exercises the full `Application::create_headless()` → filter chain → `readback_to_png()` pipeline. - -#### Scenario: Smoke test produces a valid PNG -- **GIVEN** the `visual-test` preset is built (includes `goggles` binary and `solid_color_client`) -- **WHEN** CTest runs the headless smoke test -- **THEN** `goggles --headless --frames 5 --output /smoke.png -- solid_color_client` SHALL exit with code 0 -- **AND** `/smoke.png` SHALL be a valid PNG file with width > 0 and height > 0 - -#### Scenario: Smoke test is labeled integration -- **GIVEN** the `visual-test` CTest configuration -- **WHEN** `ctest -L integration` is run -- **THEN** the headless smoke test SHALL be included in the run - -#### Scenario: Smoke test is excluded from unit tier -- **WHEN** `ctest -L unit` is run -- **THEN** the headless smoke test SHALL NOT be included diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase1/tasks.md b/openspec/changes/archive/2026-02-27-test-framework-phase1/tasks.md deleted file mode 100644 index 7ab10702..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase1/tasks.md +++ /dev/null @@ -1,41 +0,0 @@ -## 1. CMake and CTest Infrastructure - -- [x] 1.1 Add `tests/clients/` and `tests/visual/` subdirectories unconditionally to `tests/CMakeLists.txt` (no gating option needed — all deps are already project requirements) -- [x] 1.2 Add CTest label taxonomy: `unit` for Catch2 tests, `integration` for headless smoke, `visual` for future regression tests -- [x] 1.3 Verify `cmake --preset debug && cmake --build --preset debug` builds all test infrastructure (85/85 targets) -- [x] 1.4 Verify `pixi run test -p test` passes all tests including new smoke and image_compare tests (10/10) - -## 2. Test Client Apps - -- [x] 2.1 Create `tests/clients/CMakeLists.txt` with helper function `add_test_client(name sources...)` that creates an executable with wayland-client and xdg-shell protocol linkage -- [x] 2.2 Implement `tests/clients/wl_helpers.hpp` — thin RAII wrappers for `wl_display*`, `wl_registry*`, `wl_compositor*`, `wl_shm*`, `xdg_wm_base*` using custom deleters (no raw `wl_*_destroy` calls in client code) -- [x] 2.3 Implement `tests/clients/solid_color_client.cpp` — connects via `WAYLAND_DISPLAY`, creates `xdg_toplevel`, allocates a `wl_shm` buffer filled with `TEST_COLOR` (env var, default 255,0,0,255), commits 30 frames, exits 0 -- [x] 2.4 Implement `tests/clients/quadrant_client.cpp` — same structure as solid_color but fills four quadrants: red/green/blue/white at W/2 × H/2 boundaries -- [x] 2.5 Implement `tests/clients/gradient_client.cpp` — fills buffer with horizontal gradient R=G=B=x/(W-1)*255 per column -- [x] 2.6 Implement `tests/clients/multi_surface_client.cpp` — creates main surface (blue background) + subsurface (red foreground) at a fixed offset; commits 30 frames each; exits 0 -- [x] 2.7 Build and verify all four clients compile under `cmake --build --preset test` - -## 3. Image Comparison Library - -- [x] 3.1 Create `tests/visual/CMakeLists.txt` with `image_compare` static library target and `goggles_image_compare` CLI executable target; add `stb_image_impl.cpp` TU to the library (using `STB_IMAGE_IMPLEMENTATION`) so it does not conflict with `stb_image_write_impl.cpp` -- [x] 3.2 Define `CompareResult` struct and `compare_images()` function signature in `tests/visual/image_compare.hpp` -- [x] 3.3 Implement `tests/visual/image_compare.cpp` — per-channel comparison (R/G/B/A independently), diff image generation (failing pixels → red overlay at full intensity, passing pixels → 25% intensity), size-mismatch early return with `error_message` -- [x] 3.4 Implement `tests/visual/image_compare_cli.cpp` — CLI: `goggles_image_compare [--tolerance T] [--diff out.png]`; exit 0 = pass, 1 = fail, 2 = file error; print summary line to stdout on failure -- [x] 3.5 Add Catch2 unit tests for `image_compare` in `tests/visual/test_image_compare.cpp` covering: identical images pass, 1-value diff fails at tolerance=0, tolerance allows small diffs, size mismatch fails, diff image is written on failure; register under CTest label `unit` -- [x] 3.6 Build and run unit tests: `ctest --preset test -L unit -R image_compare --output-on-failure` - -## 4. Headless Pipeline Smoke Test - -- [x] 4.1 Add `add_test(NAME headless_smoke COMMAND ...)` to `tests/CMakeLists.txt` using `$` and `$` as absolute paths; pass `--frames 5 --output ${CMAKE_CURRENT_BINARY_DIR}/smoke_out.png` -- [x] 4.2 Set `LABELS integration` on the smoke test via `set_tests_properties(headless_smoke PROPERTIES LABELS "integration")` -- [x] 4.3 Add a second `add_test(NAME headless_smoke_png_check ...)` that verifies the PNG was written; set `DEPENDS headless_smoke` via `set_tests_properties` -- [x] 4.4 Run the smoke test: `ctest --preset test -L integration --output-on-failure`; confirm exit 0 - -## 5. Verification - -- [x] 5.1 Confirm `ctest --preset test` passes all tests with no regressions (10/10) -- [x] 5.2 Confirm `ctest --preset test -L unit` runs unit tests including new `image_compare` tests -- [x] 5.3 Confirm `ctest --preset test -L integration` passes the headless smoke test -- [x] 5.4 Confirm `ctest --preset test -L visual` runs zero tests (Phase 2 content not yet added — zero is correct) -- [x] 5.5 Run `pixi run build -p quality` (clang-tidy WarningsAsErrors) against all new source files; fix any clang-tidy violations -- [x] 5.6 Run `pixi run format`; confirm no formatting changes needed diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase2/proposal.md b/openspec/changes/archive/2026-02-27-test-framework-phase2/proposal.md deleted file mode 100644 index a56380b0..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase2/proposal.md +++ /dev/null @@ -1,185 +0,0 @@ -# Change: Test Framework Phase 2 — Core Visual Regression Tests - -## Problem - -Goggles has no automated assertions on render output. Every regression in aspect ratio math, scale mode geometry, shader bypass, or filter-chain application is invisible until a developer manually inspects a screen. The headless mode and test client infrastructure from Phase 1 provide the execution primitive to capture render output deterministically — but no test yet consumes that output to assert correctness. The result is a gap between "the pipeline runs" (smoke test) and "the pipeline renders what we expect" (visual regression). - -## Why - -Phase 1 delivered the foundational infrastructure: four deterministic test client apps, the `image_compare` library, and a headless pipeline smoke test. That work established that the full render pipeline can be exercised end-to-end without a display. However, no test currently validates *what* the pipeline renders. - -Phase 2 closes that gap by introducing the first batch of automated visual tests. Aspect ratio modes are the most load-bearing render-pipeline behaviour: eight distinct scale modes apply different geometric transformations that are mathematically verifiable without golden images. Shader bypass and zfast-crt application are the entry points for visual validation of the filter chain. Golden image management defines the update-and-review workflow that all future visual tests will follow. - -This phase completes the "render-what-we-expect" assertion tier and lays the golden image workflow that Phases 3–6 will build on. - -## Scope - -- **Aspect ratio tests**: `tests/visual/test_aspect_ratio.cpp` — 8 parametrised CTest cases covering `fit`, `fill`, `stretch`, `integer` (1×, 2×, auto), and `dynamic` scale modes using `quadrant_client` as source; assertions are mathematical pixel-region checks, not golden-image comparisons -- **Shader tests**: `tests/visual/test_shader_basic.cpp` — 3 tests: bypass, zfast-crt applied, and filter-chain toggle; golden image comparison with explicit tolerance contracts -- **Per-test TOML configs**: `tests/visual/configs/` — one config file per scenario driving `--config` CLI flag -- **Golden images**: `tests/golden/` directory (created by this change) with `.gitattributes` (Git LFS), a README, and two initial reference images (`shader_bypass_quadrant.png`, `shader_zfast_quadrant.png`) -- **Golden update script**: `scripts/task/update-golden.sh` — created by this change; regenerates golden images from current build via headless mode -- **CMake wiring**: extend `tests/visual/CMakeLists.txt` to build and register all new test executables with `visual` CTest label -- **Pixi task**: `update-golden` in `pixi.toml` - -## Non-goals - -- Phase 3 (RenderDoc/GPU state validation) — independent change -- Phase 4 (compositor/surface composition tests) — depends on Phase 2 patterns, separate change -- CI workflow updates (`.github/workflows/`) — Phase 6 -- SwiftShader integration — Phase 6 (determinism for CI); local runs use the installed Vulkan driver -- Shader effect tests beyond zfast-crt (CRT Royale, HSM Afterglow) — Phase 5 -- Runtime UI toggle via ImGui interaction in headless mode — not supported; toggle test uses config-based dual-run (see toggle test clarification below) - -## What Changes - -### New files (all created by this change — none pre-exist in the repo) - -| File | Purpose | -|------|---------| -| `tests/visual/test_aspect_ratio.cpp` | 8 parametrised Catch2 tests; mathematical pixel-region assertions, no golden images | -| `tests/visual/test_shader_basic.cpp` | 3 Catch2 tests: bypass, zfast-crt, toggle (config-based dual-run) | -| `tests/visual/configs/aspect_fit_letterbox.toml` | Config: `fit` mode, 640×480 source, 1920×1080 viewport | -| `tests/visual/configs/aspect_fit_pillarbox.toml` | Config: `fit` mode, 1920×1080 source, 800×600 viewport | -| `tests/visual/configs/aspect_fill.toml` | Config: `fill` mode, 640×480 → 1920×1080 | -| `tests/visual/configs/aspect_stretch.toml` | Config: `stretch` mode, 640×480 → 1920×1080 | -| `tests/visual/configs/aspect_integer_1x.toml` | Config: `integer` mode, scale=1, 320×240 → 1920×1080 | -| `tests/visual/configs/aspect_integer_2x.toml` | Config: `integer` mode, scale=2, 320×240 → 1920×1080 | -| `tests/visual/configs/aspect_integer_auto.toml` | Config: `integer` mode, scale=0 (auto), 320×240 → 1920×1080 | -| `tests/visual/configs/aspect_dynamic.toml` | Config: `dynamic` mode, 640×480 → 1920×1080 | -| `tests/visual/configs/shader_bypass.toml` | Config: no shader preset | -| `tests/visual/configs/shader_zfast.toml` | Config: zfast-crt preset | -| `tests/golden/.gitattributes` | `*.png filter=lfs diff=lfs merge=lfs -text` | -| `tests/golden/README.md` | Golden image workflow documentation | -| `tests/golden/shader_bypass_quadrant.png` | Generated reference — unshaded quadrant output (created by `update-golden`) | -| `tests/golden/shader_zfast_quadrant.png` | Generated reference — zfast-crt applied to quadrant (created by `update-golden`) | -| `scripts/task/update-golden.sh` | Regenerates all golden images via headless mode | - -### Modified files - -| File | Change | -|------|--------| -| `tests/visual/CMakeLists.txt` | Add `test_aspect_ratio` and `test_shader_basic` executables; register with CTest `visual` label | -| `pixi.toml` | Add `update-golden` task | - -## Toggle Test Clarification - -The filter-chain toggle test does **not** rely on runtime ImGui toggle (not accessible in headless mode). It is implemented as a **config-based dual-run**: - -1. Spawn `goggles --headless --frames 5 --output /tmp/toggle_on.png --config shader_zfast.toml -- quadrant_client` → capture output -2. Spawn `goggles --headless --frames 5 --output /tmp/toggle_off.png --config shader_bypass.toml -- quadrant_client` → capture output -3. Assert `toggle_on.png` matches `shader_zfast_quadrant.png` golden within zfast tolerance -4. Assert `toggle_off.png` matches `shader_bypass_quadrant.png` golden within bypass tolerance - -Each invocation is a fully independent process. There is no shared state between the two runs. This is deliberately deterministic: it validates that the two config paths produce correctly distinct outputs, not the runtime toggle mechanism (which is Phase 4 territory). - -## Tolerance Contract - -All comparisons use `compare_images(actual, reference, tolerance_per_channel)` from `tests/visual/image_compare.hpp`. Pass/fail is gated on `CompareResult.passed`. - -| Test | `tolerance_per_channel` | `max_failing_percentage` | Rationale | -|------|------------------------|--------------------------|-----------| -| Aspect ratio — border/black-bar sampling | `0.0` (exact) | 0% | CPU-rendered `wl_shm` source; black regions must be exactly (0,0,0) | -| Aspect ratio — content quadrant sampling | `2.0/255.0` (~0.008) | 0.5% | Allows ±1 LSB from bilinear filtering at content boundaries | -| Shader bypass golden comparison | `2.0/255.0` (~0.008) | 0.1% | No shader transform; output should be near-identical to reference | -| zfast-crt golden comparison | `0.05` (≈13/255) | 5.0% | CRT scanline effects vary slightly across GPU drivers | - -`max_failing_percentage` is enforced by asserting `CompareResult.failing_percentage <= threshold` in addition to `CompareResult.passed`. Both fields MUST satisfy their thresholds for a test to pass. - -## Spec-Delta Intent - -### `openspec/specs/visual-regression/spec.md` — additions - -**New Requirement block: Aspect ratio visual regression** - -Scenarios to add (one per scale mode): -- GIVEN `quadrant_client` rendering 640×480 with `fit` mode into 1920×1080 / WHEN headless capture runs / THEN top/bottom border pixels SHALL be exactly (0,0,0) AND content pixels at computed positions SHALL match expected quadrant colors within `2.0/255.0` per channel -- (Parallel scenarios for `fill`, `stretch`, `integer` 1×/2×/auto, `dynamic`) - -**New Requirement block: Shader visual regression** - -Scenarios to add: -- GIVEN bypass config / WHEN headless capture / THEN output SHALL match `shader_bypass_quadrant.png` with `tolerance_per_channel ≤ 2.0/255.0` AND `failing_percentage ≤ 0.1%` -- GIVEN zfast-crt config / WHEN headless capture / THEN output SHALL match `shader_zfast_quadrant.png` with `tolerance_per_channel ≤ 0.05` AND `failing_percentage ≤ 5.0%` -- GIVEN bypass then zfast-crt configs run independently / WHEN outputs compared / THEN each SHALL match its respective golden within its tolerance - -**New Requirement block: Golden image management** - -Scenarios to add: -- GIVEN `pixi run update-golden` executes / WHEN script completes / THEN all PNG files under `tests/golden/` SHALL be overwritten with fresh headless captures AND exit code SHALL be 0 -- GIVEN `tests/golden/*.png` tracked via Git LFS / WHEN repo is cloned / THEN `git lfs pull` SHALL produce valid PNG files - -### `openspec/specs/build-system/spec.md` — addition - -**New Requirement block: Golden image update task** - -Scenario to add: -- GIVEN a clean build at any preset / WHEN `pixi run update-golden` is invoked / THEN `scripts/task/update-golden.sh` SHALL execute, regenerating all files under `tests/golden/` and exiting 0 - -## CI Survivability - -CI workflow changes are deferred to Phase 6, but visual tests registered with the `visual` CTest label MUST NOT silently break existing CI. The containment strategy: - -**Label isolation is the gate.** The existing CI invocation pattern runs `ctest --preset test -L unit` and `ctest --preset test -L integration`. Tests with only the `visual` label are excluded from both of those invocations. The new test targets registered in this change MUST carry only the `visual` label — no `unit` or `integration` label is assigned. - -**Verification requirement (Validation Plan step 9 below):** Confirm that `ctest --preset test -L unit` and `ctest --preset test -L integration` complete successfully with no new tests included. This is a hard acceptance criterion for this change. - -**Risk**: If CI calls bare `ctest --preset test` (no label filter), visual tests will execute without GPU or golden images, failing. This is documented as a known gap; Phase 6 adds the label-gated CI job. If bare `ctest --preset test` is the current CI invocation, an `ENVIRONMENT` property on visual test targets (e.g., `GOGGLES_VISUAL_TESTS=1` required to run) must be added before merge. - -## Capabilities - -### New Capabilities - -- `visual-regression-aspect-ratio`: Automated validation of all 8 scale modes via mathematical pixel-region assertions against `quadrant_client` output — no golden image required for geometry tests -- `visual-regression-shader`: Automated comparison of bypass vs. zfast-crt render output against committed golden images; includes config-based toggle validation -- `golden-image-management`: Reproducible workflow for generating, reviewing, and updating reference images; tracked via Git LFS; regenerated by `pixi run update-golden` - -### Modified Capabilities - -- `visual-regression` (spec): Extended with aspect ratio requirements, shader requirements, golden image management requirement — see Spec-Delta Intent above -- `build-system` (spec): New `update-golden` pixi task requirement - -## Impact - -### Impacted modules / files - -- `tests/visual/` — new test sources and TOML configs (~2 new C++ files, ~12 TOML files) -- `tests/golden/` — new directory (created by this change) with LFS-tracked PNGs and README -- `scripts/task/update-golden.sh` — new script (created by this change) -- `tests/visual/CMakeLists.txt` — extended with two new test targets -- `pixi.toml` — one new task - -### Impacted OpenSpec specs - -- `visual-regression` — see Spec-Delta Intent above -- `build-system` — see Spec-Delta Intent above - -### Policy-sensitive impacts - -- **Error handling**: Catch2 test fixtures that run goggles as a subprocess check exit code; any non-zero exit is a hard `REQUIRE` failure, not a silent skip; temporary output paths are cleaned up via RAII scope guard -- **No threading**: Test code does not spawn threads; each subprocess is launched synchronously and awaited before the next -- **No raw new/delete**: All RAII; subprocess handles and temp-file paths scoped to test fixtures -- **No Vulkan API calls in test code**: Tests interact with goggles exclusively through the CLI interface (`--headless --frames N --output path --config path -- client`) -- **Tolerance documented per test**: Exact constants are specified in the Tolerance Contract table above; tolerance values are `constexpr` named constants in source, not magic numbers - -## Risks - -| Risk | Severity | Likelihood | Mitigation | -|------|----------|-----------|------------| -| Aspect ratio math differs on fractional viewport dimensions | MEDIUM | LOW | Tests use integer-divisible viewport/source pairs; pixel boundary assertions include ±1 px tolerance (`2.0/255.0`) for filtering artefacts | -| Golden images differ across GPU drivers (shader output varies) | MEDIUM | HIGH | zfast-crt tolerance set to `0.05` / 5%; Phase 6 replaces with SwiftShader-generated goldens for CI determinism | -| Bare `ctest --preset test` in CI picks up visual tests without GPU/goldens | HIGH | LOW | Confirmed by inspection of CI config before merge; if bare invocation exists, add `ENVIRONMENT GOGGLES_VISUAL_TESTS=1` guard (see CI Survivability) | -| `quadrant_client` frame not yet committed when goggles reads frame N | LOW | LOW | Clients commit 30 stable frames; `--frames 5` samples well within the stable window | -| Git LFS not configured in dev environment | LOW | LOW | README documents setup; missing LFS degrades to pointer files, not data corruption; goldens are regeneratable via `update-golden` | - -## Validation Plan - -1. **Build**: `cmake --preset debug && cmake --build --preset debug` — all targets build including `test_aspect_ratio` and `test_shader_basic` executables -2. **Generate goldens first**: `pixi run update-golden` — exits 0; `tests/golden/shader_bypass_quadrant.png` and `tests/golden/shader_zfast_quadrant.png` exist and are valid PNGs -3. **Aspect ratio tests**: `ctest --preset test -L visual -R aspect` — all 8 tests pass with exit 0 -4. **Shader tests**: `ctest --preset test -L visual -R shader` — all 3 tests pass with exit 0 -5. **Regression check — unit**: `ctest --preset test -L unit` — all existing unit tests pass; zero new tests included -6. **Regression check — integration**: `ctest --preset test -L integration` — headless smoke test passes; zero new tests included -7. **CI survivability confirmation**: verify CI invocation does not use bare `ctest --preset test`; if it does, add `ENVIRONMENT` guard before merge -8. **Quality gate**: `pixi run build -p quality` — clang-tidy WarningsAsErrors passes on all new C++ sources -9. **Format**: `pixi run format` — no formatting changes required diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase2/specs/visual-regression/spec.md b/openspec/changes/archive/2026-02-27-test-framework-phase2/specs/visual-regression/spec.md deleted file mode 100644 index 67777dae..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase2/specs/visual-regression/spec.md +++ /dev/null @@ -1,103 +0,0 @@ -## ADDED Requirements - -### Requirement: Aspect ratio visual tests -The system SHALL provide 8 Catch2 visual tests in `tests/visual/test_aspect_ratio.cpp` that assert correct pixel-region geometry for each scale mode using `quadrant_client` as the fixed 640×480 source. - -#### Scenario: fit — source narrower than viewport (letterbox) -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "fit"` and a 640×480 source -- **WHEN** the output PNG is captured -- **THEN** pixels at x<240 and x≥1680 SHALL be black (pillarbox side bars) -- **AND** the content rectangle [240, 0, 1440, 1080] SHALL contain the correct quadrant colors within `CONTENT_TOLERANCE = 2/255` - -#### Scenario: fit — source aspect ratio matches viewport (no bars) -- **GIVEN** goggles runs headless at 800×600 with `scale_mode = "fit"` and a 640×480 source -- **WHEN** the output PNG is captured -- **THEN** no black bars SHALL be present -- **AND** the full 800×600 output SHALL contain the correct quadrant colors - -#### Scenario: fill — content overflows viewport -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "fill"` and a 640×480 source -- **WHEN** the output PNG is captured -- **THEN** the center pixel (960, 540) SHALL NOT be black - -#### Scenario: stretch — entire viewport covered -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "stretch"` and a 640×480 source -- **WHEN** the output PNG is captured -- **THEN** the full 1920×1080 output SHALL contain the correct quadrant colors with no black bars - -#### Scenario: integer scale 1x -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "integer"`, `integer_scale = 1` -- **WHEN** the output PNG is captured -- **THEN** the content rectangle SHALL be 640×480 at offset (640, 300) -- **AND** black border pixels SHALL be present in all four surrounding regions - -#### Scenario: integer scale 2x -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "integer"`, `integer_scale = 2` -- **WHEN** the output PNG is captured -- **THEN** the content rectangle SHALL be 1280×960 at offset (320, 60) -- **AND** black border pixels SHALL be present in all four surrounding regions - -#### Scenario: integer scale auto -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "integer"`, `integer_scale = 0` (auto) -- **WHEN** the output PNG is captured -- **THEN** auto scale SHALL resolve to 2 (min(1920÷640, 1080÷480) = min(3,2) = 2) -- **AND** geometry SHALL match the integer 2x scenario - -#### Scenario: dynamic scale mode -- **GIVEN** goggles runs headless at 1920×1080 with `scale_mode = "dynamic"` and a 640×480 source -- **WHEN** the source resolution is stable (no mid-stream change) -- **THEN** dynamic SHALL fall back to fit behaviour -- **AND** geometry SHALL match the fit letterbox scenario (side bars at x<240, x≥1680) - -### Requirement: Shader visual tests with golden image comparison -The system SHALL provide 3 Catch2 visual tests in `tests/visual/test_shader_basic.cpp` that compare rendered output to golden reference images within explicit tolerance contracts. - -#### Scenario: bypass shader matches golden -- **GIVEN** golden `tests/golden/shader_bypass_quadrant.png` exists -- **WHEN** goggles runs headless with no shader preset and the output is compared to the golden -- **THEN** the comparison SHALL pass with `tolerance = 2/255` and `max_failing_pct ≤ 0.1%` - -#### Scenario: zfast-crt shader matches golden -- **GIVEN** golden `tests/golden/shader_zfast_quadrant.png` exists -- **WHEN** goggles runs headless with `zfast-crt.slangp` and the output is compared to the golden -- **THEN** the comparison SHALL pass with `tolerance = 0.05` and `max_failing_pct ≤ 5.0%` - -#### Scenario: filter-chain toggle produces distinct bypass and zfast outputs -- **GIVEN** both golden images exist -- **WHEN** goggles is run twice — once with bypass config and once with zfast config -- **THEN** the bypass run SHALL match the bypass golden within bypass tolerance -- **AND** the zfast run SHALL match the zfast golden within zfast tolerance - -#### Scenario: tests skip gracefully when goldens are absent -- **GIVEN** golden images have not been generated (e.g. fresh checkout without GPU) -- **WHEN** CTest runs the shader visual tests -- **THEN** each test SHALL emit a Catch2 SKIP (not FAIL) with a message directing the user to run `pixi run update-golden` - -### Requirement: Golden image update workflow -The system SHALL provide a reproducible mechanism to regenerate golden reference images. - -#### Scenario: update-golden script captures both goldens -- **GIVEN** the project is built (`pixi run build` completed) -- **WHEN** `pixi run update-golden` is executed on a machine with a GPU -- **THEN** `tests/golden/shader_bypass_quadrant.png` SHALL be overwritten with the current bypass render -- **AND** `tests/golden/shader_zfast_quadrant.png` SHALL be overwritten with the current zfast render - -#### Scenario: golden PNGs are tracked via Git LFS -- **GIVEN** Git LFS is configured in the repository -- **WHEN** `*.png` files are committed to `tests/golden/` -- **THEN** they SHALL be stored as LFS pointers per `tests/golden/.gitattributes` - -### Requirement: Visual test CTest label isolation -Visual tests SHALL carry only the `visual` CTest label; they MUST NOT carry `unit` or `integration` labels. - -#### Scenario: visual label selects new tests -- **WHEN** `ctest --preset test -L visual` is run -- **THEN** `test_aspect_ratio` and `test_shader_basic` SHALL be included - -#### Scenario: unit label excludes visual tests -- **WHEN** `ctest --preset test -L unit` is run -- **THEN** `test_aspect_ratio` and `test_shader_basic` SHALL NOT be included - -#### Scenario: integration label excludes visual tests -- **WHEN** `ctest --preset test -L integration` is run -- **THEN** `test_aspect_ratio` and `test_shader_basic` SHALL NOT be included diff --git a/openspec/changes/archive/2026-02-27-test-framework-phase2/tasks.md b/openspec/changes/archive/2026-02-27-test-framework-phase2/tasks.md deleted file mode 100644 index 54724594..00000000 --- a/openspec/changes/archive/2026-02-27-test-framework-phase2/tasks.md +++ /dev/null @@ -1,53 +0,0 @@ -## 1. Per-test TOML Configs - -- [x] 1.1 Create `tests/visual/configs/aspect_fit_letterbox.toml` — `scale_mode = "fit"`, logging warn -- [x] 1.2 Create `tests/visual/configs/aspect_fit_pillarbox.toml` — `scale_mode = "fit"`, logging warn -- [x] 1.3 Create `tests/visual/configs/aspect_fill.toml` — `scale_mode = "fill"`, logging warn -- [x] 1.4 Create `tests/visual/configs/aspect_stretch.toml` — `scale_mode = "stretch"`, logging warn -- [x] 1.5 Create `tests/visual/configs/aspect_integer_1x.toml` — `scale_mode = "integer"`, `integer_scale = 1`, logging warn -- [x] 1.6 Create `tests/visual/configs/aspect_integer_2x.toml` — `scale_mode = "integer"`, `integer_scale = 2`, logging warn -- [x] 1.7 Create `tests/visual/configs/aspect_integer_auto.toml` — `scale_mode = "integer"`, `integer_scale = 0`, logging warn -- [x] 1.8 Create `tests/visual/configs/aspect_dynamic.toml` — `scale_mode = "dynamic"`, logging warn -- [x] 1.9 Create `tests/visual/configs/shader_bypass.toml` — `scale_mode = "fit"`, no preset, logging warn -- [x] 1.10 Create `tests/visual/configs/shader_zfast.toml` — `scale_mode = "fit"`, `preset = "shaders/retroarch/crt/zfast-crt.slangp"`, logging warn - -## 2. Aspect Ratio Visual Tests - -- [x] 2.1 Implement `tests/visual/test_aspect_ratio.cpp` with 8 Catch2 test cases: - - `fit letterbox (1920×1080)` — checks side bars at x<240, x≥1680; quadrant content in 1440×1080 - - `fit pillarbox (800×600)` — perfect 4:3 match, no bars; full 800×600 quadrant check - - `fill (1920×1080)` — center pixel is non-black (content overflows, no bars visible) - - `stretch (1920×1080)` — full 1920×1080 quadrant check - - `integer 1x (1920×1080)` — bars around 640×480 content at offset (640,300) - - `integer 2x (1920×1080)` — bars around 1280×960 content at offset (320,60) - - `integer auto (1920×1080)` — auto=2, same geometry as 2x - - `dynamic (1920×1080)` — falls back to fit, same side bars as letterbox -- [x] 2.2 Use `fork`/`execvp`/`waitpid` subprocess helper; RAII `TempFile`; named `constexpr` tolerance constants -- [x] 2.3 Build: `cmake --build --preset test` compiles `test_aspect_ratio` without warnings - -## 3. Build System and Pixi Wiring - -- [x] 3.1 Extend `tests/visual/CMakeLists.txt` with `test_aspect_ratio` target: links `image_compare` + `Catch2::Catch2WithMain`, compile definitions for `GOGGLES_BINARY`, `QUADRANT_CLIENT_BINARY`, `VISUAL_CONFIGS_DIR` -- [x] 3.2 Register `test_aspect_ratio` CTest test with `LABELS "visual"`, `TIMEOUT 120`, `ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0"` -- [x] 3.3 Extend `tests/visual/CMakeLists.txt` with `test_shader_basic` target: links `image_compare` + `Catch2::Catch2WithMain`, compile definitions for `GOGGLES_BINARY`, `QUADRANT_CLIENT_BINARY`, `VISUAL_CONFIGS_DIR`, `GOLDEN_DIR` -- [x] 3.4 Register `test_shader_basic` CTest test with `LABELS "visual"`, `TIMEOUT 120`, `ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0"` -- [x] 3.5 Add `[tasks.update-golden]` to `pixi.toml` with `cmd = "bash scripts/task/update-golden.sh"` and `depends-on = ["build"]` - -## 4. Shader Tests and Golden Image Infrastructure - -- [x] 4.1 Create `tests/golden/` directory -- [x] 4.2 Create `tests/golden/.gitattributes` — Git LFS tracking for `*.png` -- [x] 4.3 Create `tests/golden/README.md` — documents golden generation, update workflow, LFS policy -- [x] 4.4 Create `scripts/task/update-golden.sh` (executable) — captures `shader_bypass_quadrant.png` and `shader_zfast_quadrant.png` via `goggles --headless` with `quadrant_client` -- [x] 4.5 Implement `tests/visual/test_shader_basic.cpp` with 3 Catch2 test cases: - - `shader bypass` — runs goggles with bypass config, compares to golden within `BYPASS_TOLERANCE=2/255`, `max_failing_pct=0.1%`; SKIP if golden absent - - `zfast-crt` — runs goggles with zfast config, compares to golden within `ZFAST_TOLERANCE=0.05`, `max_failing_pct=5.0%`; SKIP if golden absent - - `filter-chain toggle` — dual-run (bypass + zfast configs), validates each against respective golden; SKIP if either golden absent -- [x] 4.6 Build: `cmake --build --preset test` compiles `test_shader_basic` without warnings - -## 5. Verification - -- [x] 5.1 Confirm `cmake --build --preset test` (72/72 targets) succeeds with no warnings for new sources -- [x] 5.2 Confirm `ctest --preset test -N -L visual` lists exactly `test_aspect_ratio` and `test_shader_basic` -- [x] 5.3 Confirm `ctest --preset test -N -L unit` does NOT include `test_aspect_ratio` or `test_shader_basic` -- [x] 5.4 Confirm `ctest --preset test -N -L integration` does NOT include `test_aspect_ratio` or `test_shader_basic` diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/.openspec.yaml b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/.openspec.yaml deleted file mode 100644 index 565fad56..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-08 diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/design.md b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/design.md deleted file mode 100644 index 04195bca..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/design.md +++ /dev/null @@ -1,105 +0,0 @@ -## Context - -Filter-chain stage enablement is currently decided through multiple call sites and APIs: -- Application-side toggle resolution for global/per-surface/effect controls. -- Backend stage setters invoked separately for prechain and effect behavior. -- Filter-chain reinitialization and async chain swap paths that instantiate new chain objects. - -This split ownership makes stage state vulnerable to drift. During async preset reload or swapchain-triggered chain recreation, new `FilterChain` instances can run with default stage settings before runtime toggles are re-applied. The current prechain lifecycle also allows stale downsampling resources to survive toggle/resize transitions. - -The startup path has an additional race between three systems: -- Surface defaults are inferred while capture source can still be transitioning. -- Per-surface maximize/restore requests are issued before policy stabilization. -- Prechain UI initialization can latch target extent from the first observed frame. - -This race explains observed launch variance (`500x500`, `1920x1080`, and mixed states) across identical runs. - -Constraints: -- Keep current three user controls and semantics. -- Preserve compositor-style resize behavior when global/per-surface filtering is disabled. -- Preserve mandatory presentation via postchain output blit. -- Keep implementation aligned with current app/backend module boundaries. -- Keep one proposal/change-id; do not split startup determinism into a separate OpenSpec change. - -## Goals / Non-Goals - -**Goals:** -- Centralize effective stage resolution from the three GUI controls into one policy. -- Apply policy atomically to runtime stages (prechain/effect) through one backend API. -- Ensure policy persists across filter-chain recreation and async chain swaps. -- Remove prechain downsampling state ambiguity across toggle and resize transitions. -- Clarify and codify that postchain output blit remains active even when global filtering is disabled. - -**Non-Goals:** -- Changing UI layout or adding new end-user toggles. -- Replacing the existing async shader/preset load architecture. -- Reworking output-pass rendering into a separate non-filter-chain renderer. - -## Decisions - -- Decision: Introduce a resolved runtime policy object computed once per frame in `Application`. - - Policy fields: global enabled, per-surface enabled, effect checkbox enabled, effective prechain enabled, effective effect enabled. - - Rationale: a single resolver eliminates duplicated precedence logic and conflicting call order. - - Alternative considered: keep separate boolean setters and improve call ordering. - - Rejected because ordering fixes are fragile under future code paths. - -- Decision: Replace split backend stage toggles with one `set_filter_chain_policy(...)` backend entry point. - - Rationale: backend applies prechain/effect updates in one operation and owns current runtime policy. - - Alternative considered: keep `set_prechain_enabled` and `set_shader_enabled` but enforce pairing. - - Rejected because API design still permits drift and incomplete application. - -- Decision: Persist the active policy in backend state and reapply it on any chain object replacement (`init_filter_chain`, async swap). - - Rationale: guarantees newly created chains inherit active runtime gating before first render. - - Alternative considered: defer reapply to next frame from app loop. - - Rejected because it allows one-frame default behavior leakage. - -- Decision: Keep postchain output blit always active; global/per-surface OFF bypasses prechain and effect stage only. - - Rationale: output pass is the canonical presentation path and should remain stable regardless of filter effect routing. - - Alternative considered: fully bypass all stages including postchain. - - Rejected due to added renderer complexity and behavior divergence from current architecture. - -- Decision: Separate requested prechain resolution from resolved runtime prechain extent and rebuild prechain resources on basis changes. - - Rationale: prevents stale downsample targets and avoids mutating requested aspect-preserve semantics. - - Alternative considered: continue mutating one shared prechain resolution value. - - Rejected because it obscures source-of-truth and causes unstable resize/toggle outcomes. - -- Decision: Use the frame's actual source path when updating history inputs. - - Rationale: history should reflect executed stage output, not stale prechain resources. - -- Decision: Resolve and persist a stable session capture mode before per-surface defaulting and resize orchestration. - - Rationale: removes dependence on transient `has_frame()` timing during startup. - - Alternative considered: infer defaults from live frame availability each sync tick. - - Rejected because arrival order causes nondeterministic defaults. - -- Decision: In direct Vulkan capture sessions, default prechain target initialization uses viewer swapchain extent. - - Rationale: user-selected behavior `1` prefers stable desktop-size startup behavior. - - Alternative considered: default to first stable app/source extent. - - Rejected because it preserves startup race outcomes and native-size drift. - -- Decision: Apply compositor maximize/restore requests only on effective policy transitions (or surface topology changes), not every periodic sync pass. - - Rationale: prevents resize oscillation and contradictory requests while startup state settles. - -## Risks / Trade-offs - -- [Policy migration mistakes in call sites] -> Mitigation: remove/limit old setter paths and route all updates through one API. -- [One-frame regressions during chain swap] -> Mitigation: apply persisted policy immediately after new chain is installed. -- [Behavior confusion around "filter chain off" wording] -> Mitigation: specs explicitly state postchain output remains active. -- [Prechain rebuild frequency increases] -> Mitigation: gate rebuilds by precise resolution/basis changes only. -- [Session mode misclassification] -> Mitigation: establish explicit mode state and log one-time source-mode transition events. -- [Behavior change for users expecting native-size startup] -> Mitigation: document swapchain-extent default and keep manual prechain profile override unchanged. - -## Migration Plan - -1. Add policy type and resolver in `Application`; route existing toggle code through resolver. -2. Introduce stable session capture-mode state and use it for first-time per-surface defaults. -3. Add backend policy API and wire it to `FilterChain` stage controls. -4. Store/reapply policy in backend chain-init and chain-swap paths. -5. Refine prechain resolution/resource handling and history-source selection in `FilterChain`. -6. Make prechain startup default deterministic: initialize from swapchain extent in direct Vulkan sessions. -7. Gate compositor resize requests by effective policy transition/surface topology changes. -8. Validate with repeated startup runs (`pixi run start -p debug -- vkcube`) in addition to preset build/test flow. -9. Rollback strategy: revert to previous split setters if critical regressions occur; keep spec deltas to reattempt in a follow-up change. - -## Open Questions - -- None. diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/proposal.md b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/proposal.md deleted file mode 100644 index 4a092f86..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/proposal.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change: Update filter chain state management and stage gating - -## Why -Filter chain enablement is currently controlled from multiple entry points across `Application`, `VulkanBackend`, and `FilterChain`. This allows stage state to drift during async chain swaps and resize transitions, which causes incorrect behavior: global OFF still applying parts of the chain, re-enable paths reapplying unexpectedly, and unstable prechain downsampling results. - -Startup behavior is also non-deterministic in Vulkan-layer mode. The per-surface default, compositor maximize requests, and prechain target initialization currently depend on transient frame arrival order, which can produce inconsistent outcomes across identical launches (for example `500x500` vs `1920x1080` source/prechain combinations and scratched output). - -## What Changes -- Introduce a single resolved runtime policy for filter chain gating that combines the three GUI switches: - - `Application -> Window Management -> Filter Chain (All Surfaces)` (global) - - `Application -> Window Management -> Surface List` per-surface toggle - - `Shader Controls -> Effect Stage -> Enable Shader` (effect stage only) -- Route policy application through one backend API so prechain/effect state updates are atomic and consistent per frame. -- Reapply active policy after filter-chain reinitialization and async chain swaps so newly created chain instances never fall back to default stage state. -- Harden prechain resource/resolution management to avoid stale downsampling resources when toggling or resizing. -- Make startup deterministic by resolving capture mode/session policy before applying per-surface defaults and resize policy. -- In direct Vulkan capture sessions, initialize default prechain target from viewer swapchain extent (option `1`) instead of first observed source-frame extent. -- Gate compositor resize requests to policy transitions so maximize/restore does not oscillate during startup. -- Keep postchain output blit as the mandatory presentation path even when global filter chain is OFF; global OFF bypasses prechain and effect stage only. - -## Impact -- Affected specs: render-pipeline, app-window -- Affected code: `src/app/application.cpp`, `src/app/application.hpp`, `src/render/backend/vulkan_backend.cpp`, `src/render/backend/vulkan_backend.hpp`, `src/render/chain/filter_chain.cpp`, `src/render/chain/filter_chain.hpp`, `src/ui/imgui_layer.cpp`, `src/ui/imgui_layer.hpp` diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/app-window/spec.md b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/app-window/spec.md deleted file mode 100644 index 2e2cf66d..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/app-window/spec.md +++ /dev/null @@ -1,56 +0,0 @@ -## ADDED Requirements - -### Requirement: Filter Chain Control Scope and Precedence -The application UI SHALL expose three filter-related controls with distinct scope and precedence: -- `Application -> Window Management -> Filter Chain (All Surfaces)` controls global prechain/effect enablement. -- `Application -> Window Management -> Surface List` controls per-surface prechain/effect enablement. -- `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader` controls effect stage only. - -The application SHALL resolve an effective runtime policy that applies precedence as: -1) global toggle, -2) per-surface toggle, -3) effect-stage toggle. - -The application SHALL dispatch the resolved policy through a single runtime update path so prechain -and effect stage updates occur together. - -For first-time surface discovery, the application SHALL use deterministic defaulting rules: -- In direct Vulkan capture sessions, newly discovered active Vulkan-target surfaces default to - filter-chain enabled. -- Once the user toggles a surface, the user choice SHALL be preserved and SHALL NOT be overwritten - by subsequent auto-default evaluation. - -When a direct Vulkan capture session initializes prechain defaults and no explicit prechain target -is configured, the application SHALL initialize prechain target from viewer swapchain extent. - -#### Scenario: Global toggle disables all surfaces -- **GIVEN** the Window Management panel is visible -- **WHEN** the user disables `Filter Chain (All Surfaces)` -- **THEN** subsequent frames SHALL bypass prechain and effect stages for all surfaces - -#### Scenario: Per-surface toggle applies when global is enabled -- **GIVEN** `Filter Chain (All Surfaces)` is enabled -- **WHEN** the user disables a surface entry in Surface List -- **THEN** subsequent frames for that surface SHALL bypass prechain and effect stages -- **AND** other enabled surfaces SHALL continue using prechain/effect - -#### Scenario: Effect toggle does not disable prechain -- **GIVEN** global and per-surface toggles are enabled -- **WHEN** the user disables `Enable Shader` -- **THEN** subsequent frames SHALL bypass effect stage only -- **AND** prechain behavior SHALL remain controlled by global/per-surface toggles - -#### Scenario: Runtime updates are applied atomically -- **GIVEN** any toggle transition changes effective stage policy -- **WHEN** the application dispatches runtime state to the backend -- **THEN** prechain and effect stage updates SHALL be applied together in one policy update - -#### Scenario: First discovery defaults ON for direct Vulkan sessions -- **GIVEN** a direct Vulkan capture session and a newly discovered active surface -- **WHEN** the surface appears in Surface List without user override -- **THEN** its per-surface filter toggle SHALL default to enabled - -#### Scenario: User override remains authoritative -- **GIVEN** a surface has been manually toggled by the user -- **WHEN** the surface list is refreshed or source timing changes -- **THEN** the surface toggle SHALL keep the user-selected value diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/render-pipeline/spec.md deleted file mode 100644 index b8d5dc5d..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/render-pipeline/spec.md +++ /dev/null @@ -1,85 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Per-Surface Filter Chain Routing -The render pipeline SHALL honor a per-surface filter-chain enable flag and a session-wide global -enable flag when deciding whether to execute prechain and effect processing for a frame. - -The effect stage SHALL also respect `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader`. - -When the global flag is disabled, the pipeline SHALL bypass prechain and effect stages for all -surfaces and render captured surfaces using a compositor-style maximize resize so the client -re-renders at the window size (no stretch-blit). - -When the global flag is enabled but the per-surface flag is disabled, the pipeline SHALL bypass -prechain and effect stages for that surface and render it using a compositor-style maximize resize -so the client re-renders at the window size (no stretch-blit). - -The postchain output blit SHALL remain active for presentation in all modes, including global -bypass mode. - -The per-surface mode SHALL apply to the entire xdg_toplevel surface, including all popups and -subsurfaces belonging to that toplevel. - -The runtime SHALL resolve the effective stage policy once per frame and apply it atomically so -prechain/effect stage state cannot diverge during toggle transitions. - -The runtime SHALL preserve the active policy across filter-chain recreation and async chain swap so -new chain instances start with the same effective stage policy before rendering their first frame. - -The runtime SHALL avoid startup-order-dependent behavior between source capture arrival, compositor -resize requests, and prechain target initialization. - -In direct Vulkan capture sessions, the default prechain target initialization SHALL use viewer -swapchain extent unless the user/config explicitly sets a prechain resolution. - -Compositor maximize/restore requests tied to filter policy SHALL be emitted on effective policy -transitions (or surface topology changes), not as unconditional periodic requests. - -#### Scenario: Default uses filter chain -- **GIVEN** a surface has no explicit override -- **WHEN** a frame is rendered for that surface -- **THEN** prechain and effect stages SHALL execute for that frame - -#### Scenario: Bypass filter chain for a surface -- **GIVEN** a surface has filter-chain disabled -- **WHEN** a frame is rendered for that surface -- **THEN** prechain and effect stages SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit -- **AND** the postchain output blit SHALL still present the frame - -#### Scenario: Global bypass overrides per-surface -- **GIVEN** the global filter-chain flag is disabled -- **WHEN** a frame is rendered for any surface -- **THEN** prechain and effect stages SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit -- **AND** the postchain output blit SHALL still present the frame - -#### Scenario: Effect stage toggle only affects effect stage -- **GIVEN** global and per-surface filter-chain flags are enabled -- **AND** `Enable Shader` is disabled -- **WHEN** a frame is rendered -- **THEN** prechain SHALL execute -- **AND** effect stage SHALL be bypassed -- **AND** postchain output blit SHALL present the frame - -#### Scenario: Popup inherits parent mode -- **GIVEN** an xdg_toplevel surface has filter-chain disabled -- **WHEN** a popup or subsurface belonging to that toplevel is rendered -- **THEN** the popup SHALL be rendered with prechain/effect bypass and maximize-style resize without stretch-blit - -#### Scenario: Async chain swap keeps active policy -- **GIVEN** the runtime has an active effective stage policy -- **WHEN** an async shader reload completes and swaps in a new chain instance -- **THEN** the new chain SHALL use the active effective stage policy on its first rendered frame -- **AND** no frame SHALL be rendered with default stage policy values - -#### Scenario: Direct Vulkan startup is deterministic -- **GIVEN** the session uses direct Vulkan capture -- **WHEN** the application starts and filter chain is enabled -- **THEN** prechain default target initialization SHALL use viewer swapchain extent -- **AND** startup SHALL not depend on first-arrival source-frame extent - -#### Scenario: Resize requests do not oscillate during startup -- **GIVEN** startup state is settling and surfaces are enumerating -- **WHEN** effective filter policy for a surface has not changed -- **THEN** the runtime SHALL NOT repeatedly emit contradictory maximize/restore resize requests diff --git a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/tasks.md b/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/tasks.md deleted file mode 100644 index 1538c64c..00000000 --- a/openspec/changes/archive/2026-02-27-update-filter-chain-state-management/tasks.md +++ /dev/null @@ -1,26 +0,0 @@ -## 1. Runtime Policy Consolidation - -- [x] 1.1 Keep a centralized filter-chain policy resolver in `src/app/application.cpp` / `src/app/application.hpp` that combines global, per-surface, and effect-stage toggles. -- [x] 1.2 Remove remaining split stage-update paths and enforce a single backend policy update API in `src/render/backend/vulkan_backend.hpp` / `src/render/backend/vulkan_backend.cpp`. -- [x] 1.3 Ensure UI callbacks in `src/ui/imgui_layer.cpp` only mutate the three control inputs and never apply stage policy directly. - -## 2. Deterministic Startup Behavior - -- [x] 2.1 Add stable session capture-mode state in `Application` and stop inferring per-surface defaults from transient frame-arrival timing. -- [x] 2.2 In direct Vulkan capture sessions, initialize default prechain target from viewer swapchain extent when no explicit prechain target is configured. -- [x] 2.3 Preserve per-surface user override state so auto-default logic never overwrites manual toggle selections. -- [x] 2.4 Gate compositor maximize/restore requests by effective policy transitions (and surface topology changes) to prevent startup resize oscillation. - -## 3. Backend and FilterChain Correctness - -- [x] 3.1 Persist active filter-chain policy in backend runtime state and reapply it after `init_filter_chain()` and async chain swap paths. -- [x] 3.2 Keep requested vs resolved prechain resolution state separated in `src/render/chain/filter_chain.cpp` / `src/render/chain/filter_chain.hpp` and rebuild resources only on basis changes. -- [x] 3.3 Ensure frame-history source selection tracks the actually executed stage path to avoid stale prechain downsampling artifacts. - -## 4. Verification - -- [x] 4.1 Automated: `pixi run build -p debug && pixi run build -p test && ctest --preset test -R "filter_chain|application" --output-on-failure`. -- [x] 4.2 Manual: global OFF bypasses prechain/effect for all surfaces while postchain output still presents. -- [x] 4.3 Manual: global ON + per-surface OFF bypasses only selected surface; global ON + effect OFF bypasses effect stage only. -- [x] 4.4 Manual: run `pixi run start -p debug -- vkcube` repeatedly (>=10 launches); startup behavior is deterministic with consistent app extent/prechain target pairing. -- [x] 4.5 Manual: toggle during async preset reload and resize transitions does not reapply default stage state or produce prechain downsampling artifacts. diff --git a/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/proposal.md b/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/proposal.md deleted file mode 100644 index 5e7ea27b..00000000 --- a/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/proposal.md +++ /dev/null @@ -1,77 +0,0 @@ -# Change: Wayland-Native Frame Delivery - -## Why - -The current capture architecture uses a bespoke Unix socket IPC channel between the Vulkan layer -and the Goggles process to deliver DMA-BUF frame handles and timeline semaphore FDs. This requires -maintaining a custom wire protocol (`capture_protocol.hpp`), a socket client in the layer -(`LayerSocketClient`), a socket server in the application (`CaptureReceiver`), an async worker -thread in `CaptureManager`, and a `CmdCopyImage` blit from the game swapchain to a separate -export image on every frame. - -The same result can be achieved without any of this: the Vulkan layer can present game frames to -Goggles' compositor Wayland socket using standard Vulkan Wayland WSI. The game creates a real -`VkSwapchainKHR` backed by a `wl_surface` on Goggles' `WAYLAND_DISPLAY`. The Wayland driver -(Mesa) handles DMA-BUF export and `wl_surface.commit` internally. Goggles' compositor receives -each frame as a native Wayland surface commit — the same path already used for Qt/GTK windows — -with no separate IPC socket, no copy, and no bespoke protocol. - -To close the GPU synchronization gap that exists in the current compositor capture path (where -`wlr_render_pass_submit` is asynchronous and no explicit fence is propagated), the compositor -gains support for the `wp_linux_drm_syncobj_v1` explicit sync protocol. The wlroots backend -attaches acquire/release timeline points to committed buffers, giving the Vulkan backend proper -GPU-side ordering without polling or CPU stalls. - -This change depends on `2026-02-26-drop-wsi-proxy-simplify-capture` being completed first. - -## What Changes - -- **REPLACED:** Vulkan layer IPC mechanism — `CmdCopyImage` + Unix socket + `CaptureReceiver` — - with standard Vulkan Wayland WSI presenting to Goggles' compositor socket -- **REMOVED:** `LayerSocketClient` (`ipc_socket.hpp/cpp`) -- **REMOVED:** `CaptureReceiver` (`capture_receiver.hpp/cpp`) and its poll loop in `Application` -- **REMOVED:** `capture_protocol.hpp` wire format -- **REMOVED:** `CaptureManager` async worker thread and export image infrastructure - (`vk_capture.hpp/cpp`) -- **REMOVED:** Remaining Vulkan layer hook infrastructure: `vk_hooks.cpp/hpp`, - `vk_dispatch.cpp/hpp`, `layer_main.cpp`, `frame_dump.cpp/hpp`, `logging.hpp`, - `goggles_vklayer` build target and JSON manifest -- **REMOVED:** `handle_sync_semaphores()` and timeline semaphore import path in `Application` and - `VulkanBackend` -- **ADDED:** `wp_linux_drm_syncobj_v1` explicit sync protocol support in `CompositorServer` — - acquire timeline point extracted from committed surface state, release point signaled after - Goggles finishes reading the buffer -- **ADDED:** Direct buffer import path in `VulkanBackend`: committed `wl_buffer` DMA-BUF is - imported as a `VkImage` without going through `wlr_renderer` re-compositing for the primary - game surface -- **MODIFIED:** `Application::update_frame_sources()` — single compositor path only; no - `capture_receiver` priority logic -- **MODIFIED:** `CompositorServer::get_presented_frame()` — propagates explicit sync acquire - point alongside `ExternalImageFrame` so `VulkanBackend` can wait on the GPU fence before - sampling -- **MODIFIED:** `util::ExternalImageFrame` — add optional `sync_fd` field for explicit acquire - fence -- **MODIFIED:** `SessionCaptureMode` — remove `direct_vulkan` variant; compositor is the only - capture mode - -## Impact - -- Affected specs: `vk-layer-capture`, `render-pipeline` -- Affected code: - - `src/capture/vk_layer/` — entire directory removed (all remaining files after proposal 1) - - `src/capture/capture_receiver.hpp/cpp` — deleted - - `src/capture/capture_protocol.hpp` — deleted - - `src/capture/CMakeLists.txt` — remove `goggles_vklayer` target and manifest wiring - - `src/compositor/compositor_server.hpp/cpp` — add explicit sync protocol support, propagate - acquire fence in `get_presented_frame()` - - `src/util/external_image.hpp` — add optional `sync_fd` field - - `src/render/backend/vulkan_backend.hpp/cpp` — remove timeline semaphore import, add explicit - fence wait before sampling imported image; add direct `wl_buffer` import path - - `src/app/application.hpp/cpp` — remove `CaptureReceiver`, `handle_sync_semaphores()`, - `SessionCaptureMode::direct_vulkan`, `m_initial_resolution_sent` resolution relay - - `config/goggles.template.toml` and runtime config bootstrap/loading docs — remove `capture.backend` - config key references (runtime config resolves to `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`) -- **NEW DEPENDENCY:** `wp_linux_drm_syncobj_v1` Wayland protocol (part of wayland-protocols - ≥ 1.32; already available in the pixi environment) -- **BREAKING:** `GOGGLES_CAPTURE=1` layer no longer exists; games do not require any special - environment variable — they connect to `WAYLAND_DISPLAY` / `DISPLAY` as normal diff --git a/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/tasks.md b/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/tasks.md deleted file mode 100644 index 9fa754fb..00000000 --- a/openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/tasks.md +++ /dev/null @@ -1,82 +0,0 @@ -## 1. Remove Vulkan Layer Build Target - -- [x] 1.1 Remove `goggles_vklayer` CMake target from `src/capture/CMakeLists.txt` -- [x] 1.2 Remove layer manifest template `config/goggles_layer.json.in` and its configure step -- [x] 1.3 Remove `pixi run dev` manifest install step for the layer JSON -- [x] 1.4 Delete remaining `src/capture/vk_layer/` source files: `layer_main.cpp`, - `vk_hooks.cpp/hpp`, `vk_dispatch.cpp/hpp`, `vk_capture.cpp/hpp`, `ipc_socket.cpp/hpp`, - `frame_dump.cpp/hpp`, `logging.hpp` - -## 2. Remove IPC Protocol and Receiver - -- [x] 2.1 Delete `src/capture/capture_protocol.hpp` -- [x] 2.2 Delete `src/capture/capture_receiver.hpp` and `capture_receiver.cpp` -- [x] 2.3 Remove `CaptureReceiver` from `src/capture/CMakeLists.txt` source list -- [x] 2.4 Remove `goggles_capture` library target if it becomes empty - -## 3. Add Explicit Sync Protocol to `CompositorServer` - -- [x] 3.1 Add `wp_linux_drm_syncobj_v1` protocol XML to the wayland-protocols source set in - `CMakeLists.txt` (if not already present via wlroots) -- [x] 3.2 In `CompositorServer::Impl`, bind `wp_linux_drm_syncobj_manager_v1` global when the - wlroots backend reports DRM syncobj support -- [x] 3.3 Implement `wp_linux_drm_syncobj_surface_v1` resource per surface: store acquire and - release timeline point (syncobj FD + point value) in surface state -- [x] 3.4 In `render_surface_to_frame()`, extract the acquire timeline point from committed - surface state if present -- [x] 3.5 Propagate the acquire timeline point (syncobj FD + point) through - `util::ExternalImageFrame::sync_fd` to the application - -## 4. Extend `util::ExternalImageFrame` - -- [x] 4.1 Add `std::optional sync_fd` field to `ExternalImageFrame` - (`src/util/external_image.hpp`) -- [x] 4.2 Add `uint64_t sync_point = 0` field alongside `sync_fd` for the timeline point value -- [x] 4.3 Update `CompositorServer::get_presented_frame()` to dup and forward the acquire - timeline FD when present - -## 5. Add Explicit Fence Wait in `VulkanBackend` - -- [x] 5.1 Remove `import_sync_semaphores()`, `cleanup_sync_semaphores()`, and all - `m_frame_ready_sem` / `m_frame_consumed_sem` members and logic -- [x] 5.2 In the `render()` path, when `source_frame->sync_fd` is present: import as - `VkSemaphore` via `VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT` (binary semaphore) and - add as a wait semaphore in the queue submit before sampling the captured image -- [x] 5.3 When `source_frame->sync_fd` is absent (frames from compositor path without explicit - sync): proceed without additional wait (implicit DMA-BUF sync as current behavior) -- [x] 5.4 Signal a release fence after submit and write back the release timeline point to the - compositor surface via `wp_linux_drm_syncobj_surface_v1.set_release_point` - -## 6. Remove `CaptureReceiver` and `SessionCaptureMode` from `Application` - -- [x] 6.1 Remove `m_capture_receiver` member, `init_capture_receiver()`, and - `handle_sync_semaphores()` from `Application` -- [x] 6.2 Remove `SessionCaptureMode` enum and `m_session_capture_mode`; remove - `is_direct_vulkan_session()` helper -- [x] 6.3 Simplify `update_frame_sources()`: remove `poll_frame()`, resolution request sending, - semaphore handling; call only `compositor_server->get_presented_frame()` -- [x] 6.4 Simplify `render_frame()`: remove `capture_receiver` priority branch; single source - from `m_surface_frame` -- [x] 6.5 Remove `m_initial_resolution_sent` flag and its resolution relay logic -- [x] 6.6 Update `sync_surface_filters()`: remove `is_direct_vulkan_session()` branch from - `default_filter_enabled` - -## 7. Remove `capture.backend` Config Key - -- [x] 7.1 Remove `capture.backend` field from `util::Config` / `config.hpp` -- [x] 7.2 Remove parsing in `util::load_config()` -- [x] 7.3 Remove `capture.backend` from `config/goggles.template.toml`, runtime config - bootstrap/loading references for `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`, - and any CLI arg mapping - -## 8. Build and Quality Gates - -- [x] 8.1 `pixi run build -p debug` — zero compile errors -- [x] 8.2 `pixi run build -p quality` — zero clang-tidy warnings -- [x] 8.3 `pixi run test -p test` — all tests pass -- [x] 8.4 Confirm no remaining references to `CaptureReceiver`, `capture_protocol`, - `LayerSocketClient`, `CaptureManager`, `SessionCaptureMode::direct_vulkan`, - `frame_ready_sem`, `frame_consumed_sem` in `src/` -- [x] 8.5 Runtime smoke test: Proton Vulkan game launches inside Goggles compositor, frame - appears in filter chain with no tearing artefacts; Qt/GTK launcher window captured - alongside game window diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/.openspec.yaml b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/.openspec.yaml deleted file mode 100644 index 85cf50d8..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-03 diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/design.md b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/design.md deleted file mode 100644 index 5b616f30..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/design.md +++ /dev/null @@ -1,131 +0,0 @@ -## Context - -The current render path places filter-chain orchestration inside `src/render/chain`, but backend lifecycle and app/UI control flows still directly depend on concrete chain types. This increases coupling across `src/render/backend`, `src/app`, and `src/ui` and makes isolated testing and evolution harder. - -Project constraints that shape this design: -- fallible operations MUST remain `Result`-based and policy-compliant -- Vulkan result handling MUST stay explicit -- render/pipeline async work MUST remain on `util::JobSystem` -- behavior contracts for shader semantics and stage ordering MUST remain unchanged - -## Goals / Non-Goals - -**Goals:** -- Establish a standalone Goggles filter library boundary owning chain + shader + texture internals. -- Keep host backend responsibilities focused on swapchain/import/synchronization/present. -- Remove backend-public/app/UI exposure of concrete `FilterChain*` in favor of backend-facing facade methods. -- Remove reverse dependency from chain sources to backend helper headers. -- Name the new library `goggles-filter-chain`. - -**Non-Goals:** -- Changing filter output behavior or RetroArch semantic contracts. -- Replacing Vulkan backend host responsibilities. -- Multi-API runtime expansion in this change. - -## Decisions - -1. **Boundary extraction at filter-runtime level** - - Decision: The extracted boundary includes chain, shader, and texture internals together, including shader-runtime ownership and creation. - - Rationale: These components are currently co-dependent in preset load and pass materialization paths. - - Alternatives considered: - - Split only `chain/` and keep shader/texture outside: rejected due continued tight runtime coupling. - - Extract full `render/` stack: rejected as too broad for a safe incremental change. - -2. **Backend remains host/runtime owner for present path** - - Decision: Host backend keeps swapchain, external image import, queue submission, and present control. - - Rationale: These responsibilities are tied to window/surface lifecycle and platform integration. - - Alternatives considered: - - Move present/import logic into filter library: rejected due boundary blur and increased host integration risk. - -3. **Facade-based access instead of concrete chain pointer exposure** - - Decision: App/UI-facing code uses backend facade operations, not concrete `FilterChain*`. - - Rationale: Reduces cross-layer dependency leakage and supports isolated testing. - - Alternatives considered: - - Keep pointer exposure and document usage constraints: rejected as unenforceable and brittle. - -4. **Dependency direction cleanup in chain sources** - - Decision: Chain/shader/texture sources stop including backend helper headers and rely on boundary-safe utilities/contracts. - - Rationale: A standalone library cannot depend on host backend internals. - - Concrete boundary-safe migration steps: - - remove dead backend-helper includes where they are not needed - - relocate `VK_TRY` into a boundary-safe helper header with boundary-allowed dependencies only - - repoint remaining call sites to the boundary-safe header and enforce include guards - - Alternatives considered: - - Keep includes and duplicate backend helpers: rejected for circular dependency and maintenance cost. - -5. **Library name** - - Decision: The extracted library is named `goggles-filter-chain`. - - Rationale: Fixed project naming decision for this change. - -6. **Boundary control model** - - Decision: The boundary exposes curated control structs only; pass-level parameter metadata is not part of the stable boundary. - - Rationale: Keeps app/UI and downstream tests decoupled from pass internals while preserving behavior-level control. - - Boundary descriptor contract: - - stable fields include `control_id`, `stage`, `name`, optional `description`, `current_value`, `default_value`, `min_value`, `max_value`, and `step` - - descriptor contract covers effect-stage and prechain controls with a closed stage domain (`prechain`, `effect`) - - descriptor enumeration order is deterministic and stable for equivalent reloads - - set-value requests outside descriptor bounds are clamped to `[min_value, max_value]` - - UI fallback: when `description` is absent, UI uses `name` without tooltip text - - control mutation/callback surfaces use `control_id` and do not expose pass indices - - `control_id` semantics: - - MUST be unique within an active preset - - MUST remain stable across reloads of the same preset when control layout is unchanged - - MAY change when switching to a different preset with a different control layout - - pass indices, reflection internals, and descriptor-binding internals are excluded - - Alternatives considered: - - Expose pass-level metadata directly: rejected due direct leakage of internal pass graph details. - -7. **Stable downstream test surface** - - Decision: Downstream tests rely on a minimal facade API only. - - Stable API groups: - - lifecycle and preset: create/shutdown, load/reload preset, current preset path, chain-swapped signal - - frame submission: record/filter-frame entry with caller-provided frame context and stage policy - - controls: list controls, set control value by `control_id`, reset controls - - prechain and policy: set/get prechain resolution, set stage policy, prechain-control operations - - control descriptors: backend-safe descriptor list used by UI and downstream tests - - Not stable: - - concrete `FilterChain*` exposure and pass-level concrete types - - pass indices and internal pass ordering details within a stage - - internal async swap/deferred-destroy mechanics - - UI contracts tied to shader-internal metadata structs - -8. **Adapter ownership location** - - Decision: descriptor adapters from shader/chain internals to curated control descriptors live behind the `goggles-filter-chain` boundary. - - Rationale: Prevents backend/app/UI from depending on shader-internal metadata structs. - - Mapping rules: - - effect and prechain metadata sources must map to one descriptor schema - - if a source omits `current_value`, adapter emits runtime-effective value (or `default_value` when no runtime override exists) - -9. **VulkanContext boundary contract placement** - - Decision: host<->filter initialization uses a boundary-owned `VulkanContext` contract header with boundary-allowed includes only. - - Rationale: Prevents reverse dependency leaks where backend-only headers are dragged into filter-boundary APIs. - - Alternatives considered: - - Keep `VulkanContext` under backend headers: rejected due recurring boundary leakage risk. - -10. **Facade invocation safety invariant** - - Decision: facade methods MUST resolve the active chain reference at call time and MUST NOT cache `FilterChain*` across calls. - - Rationale: Preserves async reload/swap safety and avoids stale-pointer use across deferred-destroy windows. - -## Risks / Trade-offs - -- [Lifecycle regressions on reload/resize] -> Keep async reload/swap flows behavior-equivalent and validate with existing render/integration tests. -- [Residual direct chain access from app/UI] -> Introduce explicit facade methods and remove downstream dependency paths. -- [Policy drift during boundary move] -> Keep `Result`/logging/threading contracts explicit in tasks and reviews. -- [Build graph churn] -> Stage target/link changes incrementally and keep current runtime behavior as acceptance gate. -- [Ownership confusion for retired runtime] -> Document active-vs-retired dual ownership and gate with lifecycle tests covering success and reload-failure paths. - -## Migration Plan - -1. Establish boundary-safe utility contracts and relocate boundary-safe `VK_TRY` usage. -2. Place `VulkanContext` in a boundary-safe contract header and migrate host<->filter initialization to that contract. -3. Define curated control descriptor contract (including closed stage domain and deterministic ordering) and implement adapters behind `goggles-filter-chain`. -4. Introduce/complete facade operations with call-time active-chain resolution. -5. Remove/deprecate backend concrete-chain accessor exposure and migrate app/UI paths to facade-only usage. -6. Migrate UI control plumbing to curated descriptors and `control_id`-keyed callbacks. -7. Preserve and verify async reload success/failure behavior, swap one-shot signaling, and deferred-destroy safety invariants. -8. Align build targets/naming and wire chain/shader/texture ownership under `goggles-filter-chain`. -9. Validate structural boundaries via tests and source-audit checks (including shader-header isolation) without dedicated guard-script CI/local gating; keep rollback to prior link wiring if regressions appear. - -## Open Questions - -- None. diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/proposal.md b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/proposal.md deleted file mode 100644 index cacd312a..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/proposal.md +++ /dev/null @@ -1,80 +0,0 @@ -## Why - -The current filter chain is tightly coupled to backend lifecycle and app/UI control paths, which makes it hard to test and evolve independently. We need a clear extraction boundary so filter-chain behavior can be developed and validated as a standalone Goggles filter library while preserving existing runtime behavior. - -## Problem - -- `FilterChain` is exposed through backend APIs and called directly from app-layer wiring, so chain internals leak outside render boundaries. -- `src/render/chain` has reverse dependency on backend helper headers, which blocks clean library isolation. -- Shader/preset/texture responsibilities are interleaved with host backend ownership concerns, increasing regression risk for reload and resize paths. - -## Scope - -- Define a standalone Goggles filter library boundary that owns filter-chain, shader, and texture internals. -- Keep host backend responsibilities focused on swapchain, import, synchronization, and present. -- Remove direct exposure of concrete `FilterChain*` from backend public APIs and app-facing code by introducing backend-facing filter-chain boundary access. -- Remove reverse include coupling from chain to backend helper headers. - -## Non-goals - -- No change to user-facing shader semantics (`Source`, `OriginalHistory#`, `PassOutput#`, `PassFeedback#`, `Feedback`). -- No functional redesign of pre-chain/effect/post-chain behavior. -- No migration to non-Vulkan rendering APIs in this change. - -## What Changes - -- Introduce the `goggles-filter-chain` library boundary for filter runtime code. -- Re-scope ownership so chain+shader+texture internals are grouped under the filter library boundary, including shader-runtime ownership/creation. -- Add backend-facing filter-chain boundary methods for filter controls and parameter operations, replacing any external use of concrete `FilterChain*` and deprecating backend chain accessors that leak concrete internals. -- Add a boundary-safe `VulkanContext` contract placement decision and migration task so host<->filter initialization uses a boundary-owned header with boundary-allowed includes only. -- Add a boundary-safe control descriptor for both effect and prechain controls, with explicit stage domain (`prechain`, `effect`), deterministic enumeration order, stable `control_id` semantics, and optional `description` fallback behavior. -- Move control mutation/callback surfaces to `control_id` contracts (no `pass_index` leakage across backend/app/UI boundaries). -- Keep descriptor adapters behind the filter boundary so backend/app/UI do not depend on shader-internal metadata types. -- Document adapter mapping rules for effect and prechain controls, including asymmetries where one source lacks `current_value` at enumeration time. -- Remove backend helper header dependency from chain/shader/texture sources by introducing boundary-safe utility contracts and relocating boundary-safe `VK_TRY` usage. -- Add spec and task coverage for lifecycle (success and failure paths), stage-order, and semantic-binding parity invariants, plus stronger structural boundary guards. - -## Capabilities - -### New Capabilities -- `goggles-filter-chain`: Defines the standalone filter library boundary, ownership, public control surface, and fixed library name. - -### Modified Capabilities -- `render-pipeline`: Updates render ownership boundaries so backend host duties and filter library duties are explicitly separated without changing output behavior. - -## Risks - -- Reload/resize regressions if lifecycle handoff between backend host and filter library is incomplete. -- Hidden dependency leaks if app/UI code still depends on concrete chain types. -- Policy-sensitive drift in error propagation or logging when moving boundaries. - -## Validation Plan - -- Verify unchanged rendering behavior for existing presets and semantic bindings. -- Verify async reload success and failure behavior, swapchain recreation, and resize paths preserve current behavior. -- Verify app/UI code paths operate through backend-facing filter-chain boundary methods only, with control callbacks keyed by `control_id`. -- Verify descriptor contracts: closed stage domain (`prechain`, `effect`), deterministic enumeration order, and out-of-range set-value handling. -- Verify chain sources no longer include backend helper headers. -- Verify boundary constraints: no app/UI includes of `render/chain/filter_chain.hpp`, no app/UI includes of `render/shader/*`, no app/UI/backend-public concrete-chain accessor usage, and no backend helper includes in boundary code. -- Verify one-way build dependency direction and dependency audit for `goggles-filter-chain` link targets. -- Keep CI/local preset test flows free of dedicated `check-filter-boundary.sh` guard gating; rely on tests plus source-audit checks for boundary regressions. -- Run existing render and integration test suites relevant to parser/preprocessor/shader runtime/filter-chain logic. - -## Acceptance Criteria - -- No backend public API, app code, or UI code exposes concrete chain types or accessors. -- Semantic parity explicitly includes `Source`, `OriginalHistory#`, `PassOutput#`, `PassFeedback#`, and `Feedback`. -- Descriptor contract is deterministic and testable: known stage domain values, deterministic order, and `control_id`-based callbacks. -- Async reload success and failure semantics are both specified and tested, and swap-signal semantics remain one-shot. -- Boundary leak constraints are maintained without dedicated guard-script CI gating. -- `VulkanContext` placement is explicit, boundary-safe, and covered by include/dependency checks. - -## Impact - -- Impacted modules: `src/render/chain`, `src/render/shader`, `src/render/texture`, `src/render/backend`, `src/app`, and related render tests. -- Impacted specs: new `goggles-filter-chain`; modified `render-pipeline`. -- Policy-sensitive areas touched: - - error handling and propagation (`Result`/`GOGGLES_TRY` consistency) - - logging boundaries (avoid duplicate or suppressed logs) - - threading/reload behavior (`util::JobSystem` reload path) - - Vulkan API boundary split and resource ownership/lifetime semantics diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/goggles-filter-chain/spec.md b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/goggles-filter-chain/spec.md deleted file mode 100644 index a1e632ce..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,192 +0,0 @@ -## ADDED Requirements - -### Requirement: Standalone Filter Library Target -The extracted filter runtime SHALL build as a standalone target named `goggles-filter-chain` with one-way dependency direction between host backend and filter boundary. - -#### Scenario: Target dependency direction -- **GIVEN** build targets are configured for render and filter runtime -- **WHEN** dependency checks run for target link relationships -- **THEN** `goggles-filter-chain` SHALL compile and link without depending on host backend targets -- **AND** host backend targets SHALL depend on `goggles-filter-chain` for filter execution - -#### Scenario: Target dependency audit -- **GIVEN** the `goggles-filter-chain` target link dependency list -- **WHEN** dependency audit checks execute -- **THEN** `goggles-filter-chain` SHALL NOT link app- or UI-only targets -- **AND** `goggles-filter-chain` SHALL link only dependencies required for chain/shader/texture runtime behavior - -### Requirement: Complete Filter Runtime Ownership Boundary -The filter boundary SHALL own filter-chain orchestration, shader runtime ownership/creation, shader processing, and preset texture loading internals. - -#### Scenario: Runtime ownership initialization -- **GIVEN** the renderer initializes filter processing -- **WHEN** filter runtime components are created -- **THEN** chain orchestration, shader runtime, and preset texture loading SHALL be created and owned within `goggles-filter-chain` -- **AND** host backend code SHALL consume runtime behavior through boundary-facing contracts - -#### Scenario: Shader and texture wiring ownership -- **GIVEN** build wiring for chain/shader/texture modules -- **WHEN** `goggles-filter-chain` is configured -- **THEN** shader and texture module wiring SHALL be included under the `goggles-filter-chain` ownership boundary -- **AND** host backend code SHALL NOT own shader-runtime creation paths directly - -### Requirement: Host Backend Responsibility Boundary -The host backend SHALL remain responsible for swapchain lifecycle, external image import, synchronization, queue submission, and present. - -#### Scenario: Frame submission boundary -- **GIVEN** the host backend submits a frame context for filtering -- **WHEN** filter processing commands are recorded -- **THEN** `goggles-filter-chain` SHALL record filter-processing work from caller-provided frame context -- **AND** the host backend SHALL remain responsible for present-path submission and display ownership - -#### Scenario: Resize and recreation handoff -- **GIVEN** swapchain extent or format changes require recreation -- **WHEN** render resources are recreated -- **THEN** host backend code SHALL drive swapchain and present resource recreation -- **AND** filter runtime resize/recreation SHALL be invoked through boundary-facing contracts - -### Requirement: Boundary-safe VulkanContext Contract Placement -Host<->filter initialization contracts SHALL use a boundary-owned `VulkanContext` definition that does not pull backend internals into the filter boundary. - -#### Scenario: VulkanContext ownership and include safety -- **GIVEN** headers used to define `VulkanContext` for host<->filter initialization -- **WHEN** include/dependency checks run -- **THEN** the `VulkanContext` type SHALL be declared in a boundary-owned header -- **AND** that header SHALL include only boundary-allowed dependencies and SHALL NOT include backend-only helper headers - -#### Scenario: Host/backend consumption of VulkanContext contract -- **GIVEN** backend and filter runtime initialization paths -- **WHEN** host code passes initialization context into `goggles-filter-chain` -- **THEN** host code SHALL consume the boundary-owned `VulkanContext` contract -- **AND** backend public headers SHALL NOT expose filter-boundary internals beyond this contract - -### Requirement: Boundary-safe Vulkan Result Utility Contracts -Filter boundary code SHALL use boundary-safe Vulkan result utilities and SHALL NOT include backend-only helper headers. - -#### Scenario: Backend helper include removal -- **GIVEN** chain/shader/texture boundary source files -- **WHEN** include dependency checks are executed -- **THEN** backend helper headers SHALL NOT be included from boundary sources - -#### Scenario: Boundary-safe `VK_TRY` relocation -- **GIVEN** Vulkan result-checking macros used by boundary code -- **WHEN** boundary-safe utility contracts are applied -- **THEN** boundary call sites SHALL include `VK_TRY` from a boundary-safe helper header -- **AND** that helper header SHALL depend only on boundary-allowed headers - -### Requirement: Boundary-safe Control Descriptor Contract -The filter boundary SHALL expose curated control descriptors for both effect and prechain controls with a closed, deterministic stage contract. - -#### Scenario: Control descriptor enumeration -- **GIVEN** a preset with effect-stage and prechain controls is loaded -- **WHEN** controls are enumerated through the boundary API -- **THEN** each descriptor SHALL include `control_id`, `stage`, `name`, `current_value`, `default_value`, `min_value`, `max_value`, and `step` -- **AND** descriptors SHALL represent both effect and prechain controls through the same boundary-safe contract - -#### Scenario: Stage domain is explicit and closed -- **GIVEN** control descriptors returned by the boundary API -- **WHEN** descriptor stage values are validated -- **THEN** each descriptor `stage` SHALL be one of `prechain` or `effect` -- **AND** unknown stage values SHALL NOT be emitted without an explicit spec update - -#### Scenario: Deterministic descriptor ordering -- **GIVEN** the same preset is enumerated repeatedly without control-layout changes -- **WHEN** control descriptors are listed through the boundary API -- **THEN** descriptor order SHALL be deterministic across runs and equivalent reloads -- **AND** ordering SHALL group `prechain` controls before `effect` controls while preserving stable per-stage ordering - -#### Scenario: Optional description fallback -- **GIVEN** a control descriptor may omit `description` -- **WHEN** UI renders control metadata -- **THEN** UI SHALL render a control label from `name` -- **AND** UI SHALL apply no tooltip text when `description` is absent - -### Requirement: Control Identifier Semantics -The control identifier contract SHALL define uniqueness and stability rules for `control_id`. - -#### Scenario: Uniqueness within active preset -- **GIVEN** controls for a loaded preset are enumerated -- **WHEN** the boundary returns control descriptors -- **THEN** each `control_id` SHALL be unique within the active preset - -#### Scenario: Stability across equivalent reload -- **GIVEN** the same preset is reloaded without control-layout changes -- **WHEN** controls are enumerated after reload -- **THEN** `control_id` values for matching controls SHALL remain stable across reload - -#### Scenario: Different preset layouts -- **GIVEN** a different preset with different control layout is loaded -- **WHEN** controls are enumerated for the new preset -- **THEN** `control_id` values MAY differ from the previous preset - -### Requirement: Control Mutation Contract -Control mutation and callback contracts SHALL use `control_id` and SHALL define deterministic out-of-range handling. - -#### Scenario: Control mutation is `control_id`-only -- **GIVEN** boundary consumers set or reset control values -- **WHEN** control mutation APIs are invoked -- **THEN** operations SHALL address controls by `control_id` -- **AND** boundary surfaces SHALL NOT expose pass indices as mutation keys - -#### Scenario: Out-of-range value handling -- **GIVEN** a control descriptor defines `min_value` and `max_value` -- **WHEN** a set-value request provides a value outside `[min_value, max_value]` -- **THEN** the boundary SHALL clamp the request to the nearest valid bound before applying it -- **AND** subsequent control enumeration SHALL report the clamped `current_value` - -### Requirement: Adapter Ownership Isolation -Adapters from shader-internal metadata to curated control descriptors SHALL live behind the `goggles-filter-chain` boundary. - -#### Scenario: Adapter dependency boundary -- **GIVEN** backend, app, and UI modules consume boundary descriptors -- **WHEN** control metadata adaptation is performed -- **THEN** adaptation from shader-internal metadata SHALL occur inside `goggles-filter-chain` -- **AND** backend/app/UI modules SHALL NOT include shader-internal metadata types for control enumeration - -#### Scenario: Adapter mapping parity across effect and prechain sources -- **GIVEN** control metadata comes from effect-stage and prechain-stage sources with different field availability -- **WHEN** adapters build curated descriptors -- **THEN** both sources SHALL map to the same descriptor schema with documented, deterministic field mapping rules -- **AND** when a source omits `current_value`, adapters SHALL emit `current_value` equal to the effective runtime value (or `default_value` when no runtime override exists) - -#### Scenario: UI/include isolation from shader internals -- **GIVEN** non-boundary consumer paths such as `src/ui` and `src/app` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** non-boundary consumers SHALL NOT include `render/shader/*` headers for control metadata access - -### Requirement: No Concrete FilterChain Type Exposure Outside Boundary -Backend public APIs, app code, and UI code MUST NOT depend on concrete chain headers, concrete `FilterChain*` types, or chain accessors that expose concrete internals. - -#### Scenario: Include guard in app and UI -- **GIVEN** source files under `src/app` and `src/ui` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** files under `src/app` and `src/ui` SHALL NOT include `render/chain/filter_chain.hpp` - -#### Scenario: Backend public header guard -- **GIVEN** backend public headers consumed by app/UI and downstream tests -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** backend public headers SHALL NOT expose concrete `FilterChain` types or accessors returning concrete chain internals - -#### Scenario: Type and accessor guard in app and UI -- **GIVEN** source files under `src/app` and `src/ui` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** no direct references to concrete `FilterChain*` SHALL exist in app or UI code -- **AND** app/UI code SHALL NOT call backend chain-accessor methods that expose concrete chain internals - -### Requirement: Stable Facade API Groups for Downstream Tests -The stable downstream test surface SHALL be limited to boundary facade groups for lifecycle/preset, frame submission, controls, and prechain/policy operations. - -#### Scenario: Downstream test compile surface -- **GIVEN** downstream contract tests compile against the boundary facade -- **WHEN** tests include boundary-facing headers only -- **THEN** tests SHALL be able to exercise lifecycle/preset, frame submission, controls, and prechain/policy operations -- **AND** tests SHALL NOT require concrete chain, pass, shader-runtime, or deferred-destroy internal types - -### Requirement: Facade Active-Chain Invocation Safety -Boundary facade methods SHALL resolve active chain/runtime references at call time and SHALL NOT cache concrete chain pointers across calls. - -#### Scenario: Async swap with subsequent facade calls -- **GIVEN** an async preset reload swaps in a new active chain/runtime -- **WHEN** subsequent facade operations are invoked -- **THEN** each facade call SHALL target the current active chain/runtime reference -- **AND** facade operations SHALL NOT use stale cached concrete chain pointers from prior calls diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/render-pipeline/spec.md deleted file mode 100644 index 94431b7d..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/specs/render-pipeline/spec.md +++ /dev/null @@ -1,82 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Pipeline Extensibility - -The render architecture SHALL support future multi-pass shader processing through a modular structure with explicit host-backend and filter-library boundaries. - -#### Scenario: Module organization - -- **GIVEN** the render module structure -- **WHEN** pipeline responsibilities are assigned -- **THEN** host backend code SHALL own swapchain, external image import, synchronization, and present -- **AND** the Goggles filter library boundary SHALL own filter-chain orchestration, shader processing, and preset texture loading internals -- **AND** app-facing filter operations SHALL be accessed via backend-facing facade methods rather than exposing concrete chain types - -#### Scenario: Stage ordering invariance - -- **GIVEN** the filter runtime executes pre-chain, effect, and post-chain stages -- **WHEN** filter boundary extraction changes ownership and call paths -- **THEN** stage execution order SHALL remain pre-chain -> effect -> post-chain - -#### Scenario: Semantic binding invariance - -- **GIVEN** shaders relying on established semantic texture bindings -- **WHEN** filter processing runs after boundary extraction -- **THEN** semantic bindings SHALL remain unchanged for `Source`, `OriginalHistory#`, `PassOutput#`, `PassFeedback#`, and `Feedback` - -#### Scenario: Error handling macros - -- **GIVEN** Vulkan API calls that return `vk::Result` -- **WHEN** error checking is needed -- **THEN** `VK_TRY(call, code, msg)` macro SHALL be used for early return -- **AND** error message SHALL include the Vulkan result string - -#### Scenario: Result propagation - -- **GIVEN** internal functions that return `Result` -- **WHEN** the result needs to be propagated to the caller -- **THEN** `GOGGLES_TRY(expr)` macro SHALL be used for early return - -## ADDED Requirements - -### Requirement: Async Filter Lifecycle Safety - -The render pipeline SHALL preserve async preset reload, chain swap, and resize safety behavior after introducing the `goggles-filter-chain` boundary. - -#### Scenario: Async reload and swap notification ordering - -- **GIVEN** a preset reload is executed asynchronously -- **WHEN** the new chain becomes active for rendering -- **THEN** swap notification SHALL be emitted only after activation of the new chain -- **AND** consumers of swap notification SHALL observe the new chain as active state - -#### Scenario: Chain-swapped signal observability - -- **GIVEN** an async reload completes and activates a new chain/runtime -- **WHEN** host code consumes the chain-swapped signal -- **THEN** the signal SHALL indicate swap completion only after the new chain/runtime is active -- **AND** the first successful consumption SHALL clear the current swap indication -- **AND** additional consumption attempts before the next successful activation SHALL report no swap-complete indication - -#### Scenario: Async reload activation failure behavior - -- **GIVEN** an async reload attempt fails before activating a replacement chain/runtime -- **WHEN** host code checks active runtime state and chain-swapped signal state -- **THEN** the previously active chain/runtime SHALL remain active for rendering -- **AND** the chain-swapped signal SHALL remain unset and SHALL NOT report swap completion -- **AND** repeated signal consumption attempts before the next successful activation SHALL continue reporting no swap-complete indication - -#### Scenario: Deferred destroy safety after chain swap - -- **GIVEN** a chain swap replaces active filter runtime objects -- **WHEN** previous runtime resources are retired -- **THEN** retired resources SHALL be deferred until safe destruction criteria are met -- **AND** rendering SHALL NOT access retired chain resources after retirement begins -- **AND** active runtime ownership SHALL remain in the filter boundary while host-side retirement ownership is temporary and limited to GPU-drain-safe destruction - -#### Scenario: Resize handoff with boundary split - -- **GIVEN** resize or format changes trigger swapchain recreation -- **WHEN** host backend recreates present-path resources -- **THEN** filter runtime resize/recreation SHALL occur through boundary-facing operations -- **AND** app and UI modules SHALL NOT directly recreate concrete chain resources diff --git a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/tasks.md b/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/tasks.md deleted file mode 100644 index 046450cb..00000000 --- a/openspec/changes/archive/2026-03-05-decouple-goggles-filter-library/tasks.md +++ /dev/null @@ -1,45 +0,0 @@ -## 1. Boundary-safe Utility Contracts - -- [x] 1.1 Introduce a boundary-safe Vulkan result helper contract (including `VK_TRY`) for filter-boundary code with boundary-allowed includes only. -- [x] 1.2 Remove dead backend-helper includes from chain/shader/texture boundary sources. -- [x] 1.3 Repoint remaining boundary call sites to the boundary-safe helper contract and enforce that backend helper headers are not included from boundary code. - -## 2. Ownership and Descriptor Contract - -- [x] 2.1 Define and implement boundary-safe `VulkanContext` placement for host<->filter initialization (boundary-owned header, backend-safe includes only). -- [x] 2.2 Migrate shader-runtime ownership/creation from backend-owned paths into the `goggles-filter-chain` boundary (depends on 1.1-1.3). -- [x] 2.3 Wire `src/render/shader` and `src/render/texture` targets under `goggles-filter-chain` ownership and remove backend ownership of those internals (depends on 2.2). -- [x] 2.4 Define a boundary-safe control descriptor contract for both effect and prechain controls with explicit stage domain (`prechain`, `effect`), deterministic enumeration ordering, `control_id` uniqueness/stability, and out-of-range set-value behavior. -- [x] 2.5 Implement descriptor adapters behind `goggles-filter-chain`, including explicit effect/prechain mapping rules for metadata asymmetries (for example missing `current_value` source fields) and adapter-focused tests. - -## 3. Facade and Consumer Migration - -- [x] 3.1 Introduce backend-facing facade operations for lifecycle/preset, frame submission, controls, and prechain/policy access (depends on 2.3-2.5). -- [x] 3.2 Enforce facade invariant: each invocation resolves the active chain/runtime at call time and MUST NOT cache `FilterChain*` across calls. -- [x] 3.3 Remove/deprecate backend chain accessor surfaces that expose concrete internals (for example `VulkanBackend::filter_chain()`) from backend public headers, then migrate all consumers to facade APIs. -- [x] 3.4 Migrate app-layer filter call paths to facade-only usage and remove direct concrete-chain access in `src/app/application.cpp`. -- [x] 3.5 Migrate UI types to curated control descriptors and remove direct shader/preprocessor metadata dependencies in `src/ui/imgui_layer.hpp` and `src/ui/imgui_layer.cpp`. -- [x] 3.6 Replace pass-index-based callback contracts with `control_id`-based callback contracts across UI/app/backend wiring. - -## 4. Async Lifecycle and Build Boundary - -- [x] 4.1 Preserve async reload/swap notification ordering and deferred-destroy safety after facade migration (depends on 3.1-3.6). -- [x] 4.2 Define and implement async reload failure semantics: if activation fails, old chain/runtime remains active and swap-complete signal remains unset. -- [x] 4.3 Keep chain-swapped signal semantics one-shot in success and failure paths (consumption clears success signal; failure emits no completion signal). -- [x] 4.4 Align build target naming to `goggles-filter-chain` and enforce one-way dependency direction: backend links `goggles-filter-chain`, but `goggles-filter-chain` does not link backend targets. -- [x] 4.5 Audit `goggles-filter-chain` link dependencies, explicitly preventing unnecessary SDL3/app/UI-only linkage when not required by chain/shader/texture runtime. -- [x] 4.6 Document and enforce deferred-destroy dual-ownership semantics (active runtime owned by filter boundary; retired runtime retained by host only until GPU-drain-safe destruction). - -## 5. Structural Boundary Verification - -- [x] 5.1 Enforce boundary constraints in implementation: no `render/chain/filter_chain.hpp` includes from `src/app`/`src/ui` and no concrete chain accessor exposure in backend public headers. -- [x] 5.2 Enforce include isolation in implementation: no `render/shader/*` includes from non-boundary consumers and no backend helper includes from chain/shader/texture boundary code. -- [x] 5.3 Remove `check-filter-boundary.sh`-based guard gating from local test flow and CI per project-direction update. -- [x] 5.4 Keep boundary regression coverage through render/boundary tests and review-time source audits rather than dedicated guard scripts. -- [x] 5.5 Record the guard-strategy change in proposal/design/spec artifacts to keep OpenSpec in sync with implemented workflow. -- [x] 5.6 Add or extend tests for stage-order parity (`pre-chain -> effect -> post-chain`) and semantic-binding parity (`Source`, `OriginalHistory#`, `PassOutput#`, `PassFeedback#`, `Feedback`). -- [x] 5.7 Add boundary API integration coverage for control enumeration/set/reset through facade APIs only, including stage-domain validation, deterministic ordering, `control_id` callbacks, and out-of-range set behavior. -- [x] 5.8 Add targeted integration coverage for async reload success/failure/resize behavior, including one-shot swap-signal correctness and deferred-destroy safety expectations. -- [x] 5.9 Keep OpenSpec deltas synchronized with implementation outcomes for `specs/goggles-filter-chain/spec.md` and `specs/render-pipeline/spec.md`, then mark completed tasks in this checklist. -- [x] 5.10 Run render-related unit coverage using presets: `ctest --preset test -R "goggles_unit_tests" --output-on-failure`. -- [x] 5.11 Run CI-parity validation for final changes: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. diff --git a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/.openspec.yaml b/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/.openspec.yaml deleted file mode 100644 index 5aae5cfa..00000000 --- a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-04 diff --git a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/design.md b/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/design.md deleted file mode 100644 index 8e3bc72b..00000000 --- a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/design.md +++ /dev/null @@ -1,115 +0,0 @@ -## Context - -Goggles currently exposes filter-chain behavior through internal C++ boundaries in `src/render/chain/` and uses those boundaries directly from the viewer render path. The change introduces a stable public C ABI for Vulkan-based embedding and FFI while preserving runtime behavior, ordering, and ownership semantics already relied on by the renderer. - -Key constraints: -- Public contract MUST be ABI-stable across v1.x with additive-only minor evolution. -- Runtime failures MUST remain explicit status returns; no exception-based public flow. -- C API MUST not expose internal pass graph/shader runtime types. -- Host integration MUST keep ownership of host Vulkan objects and synchronization lifecycle. - -Stakeholders: -- Goggles renderer maintainers (parity and maintainability) -- embedding/engine integrators (stable lifecycle + record contract) -- FFI authors (string/length, scalar widths, handle ownership, deterministic failure behavior) - -## Goals / Non-Goals - -**Goals:** -- Define and ship a Vulkan-only v1 public C ABI in `include/goggles_filter_chain.h`. -- Preserve behavior parity with existing filter-chain lifecycle, controls, and record semantics. -- Lock contracts for nullability, ownership, out-parameter behavior, threading, and diagnostics. -- Provide FFI-friendly APIs (`*_ex` length-based paths) and deterministic ABI typing. -- Add conformance tests that freeze v1 contracts and detect regression. - -**Non-Goals:** -- Dynamic loader-table API in v1. -- Non-Vulkan backend support in v1. -- Exposing pass-graph internals or shader runtime internals publicly. -- Introducing hidden background worker threads or implicit async preset reload. -- Performing submission/presentation from the record API. - -## Decisions - -1. **Single-header C ABI with mandatory symbol export** - - Decision: Export all v1 declarations from `include/goggles_filter_chain.h` using `GOGGLES_CHAIN_API` and `GOGGLES_CHAIN_CALL`, with `goggles_chain_` prefix throughout. - - Rationale: Reduces integration ambiguity and locks ABI/linkage expectations for static/shared users. - - Alternatives considered: - - Loader-table API in v1: rejected to avoid duplicated negotiation surface and avoid symbol-presence ambiguity. - - Multi-header split by feature: rejected to reduce discoverability and stability. - -2. **Status-first error model with optional structured diagnostics** - - Decision: Fallible functions return `goggles_chain_status_t`; `goggles_chain_error_last_info_get(...)` provides optional numeric diagnostics (`status`, `vk_result`, `subsystem_code`). - - Rationale: Keeps C ABI deterministic and FFI-friendly while preserving rich troubleshooting without runtime-owned dynamic strings. - - Alternatives considered: - - Exception/abort semantics: rejected due incompatible C/FFI behavior. - - Runtime-owned mutable error strings: rejected due ownership and thread-safety complexity. - -3. **Fixed-width scalar typedefs + explicit struct extensibility** - - Decision: Enum-like API scalars use `uint32_t` typedefs with named constants; extensible structs use `struct_size` prefix contract; `goggles_chain_control_desc_t` layout remains fixed for v1.x. - - Rationale: Provides deterministic ABI layout and forward-compatible struct growth. - - Alternatives considered: - - Native C enums as ABI type: rejected due compiler-dependent width. - - Extensible control descriptor struct in v1.x: rejected to avoid breaking snapshot layout guarantees. - -4. **First-class three-stage model in v1** - - Decision: Stage domain includes `prechain`, `effect`, and `postchain` now, with order fixed as `prechain -> effect -> postchain`. - - Rationale: Prevents ABI churn when postchain behavior evolves and preserves deterministic execution semantics. - - Alternatives considered: - - Keep postchain implicit/deferred: rejected due likely ABI break in later introduction. - -5. **Snapshot-based controls API with explicit ownership** - - Decision: Expose control lists as owned snapshots and borrowed descriptor pointers valid only for snapshot lifetime. - - Rationale: Gives stable iteration semantics across FFI boundaries and clear release pairing. - - Alternatives considered: - - Iterator/callback listing: rejected due callback reentrancy, ABI callback shape, and language binding friction. - -6. **Host-managed synchronization and synchronous preset mutation** - - Decision: Runtime provides no internal synchronization for a runtime instance; host serializes per-instance calls; preset load remains synchronous and host-managed. - - Rationale: Preserves current integration model and avoids hidden mutation threads or contention surprises. - - Alternatives considered: - - Internal worker threads for reload: rejected due state-race complexity and harder deterministic contracts. - -7. **FFI-friendly string surface with `_ex` variants** - - Decision: Keep C-string APIs and add length-based `_ex` variants for create-time and preset paths with UTF-8 + embedded-NUL validation. - - Rationale: Supports languages without stable NUL-terminated ownership while retaining ergonomic C call sites. - - Alternatives considered: - - C-string-only API: rejected due FFI friction and unsafe buffer assumptions. - -8. **Record API as command-record only with hot-path bounds** - - Decision: `goggles_chain_record_vk(...)` records commands only, does not submit/present, validates frame index, and preserves hot-path constraints (no allocation/I/O/blocking work). - - Rationale: Keeps host in control of queue/presentation lifecycle and protects frame-time predictability. - - Alternatives considered: - - Internal submit/present convenience: rejected due backend coupling and ownership confusion. - -9. **Thin C shim over existing C++ internals** - - Decision: Implement C entrypoints as an adapter layer over current filter-chain boundary behavior, preserving semantics while introducing API contract checks. - - Rationale: Reduces migration risk and keeps renderer parity while enabling standalone library extraction. - - Alternatives considered: - - Rewrite runtime internals first: rejected due larger blast radius and longer parity validation cycle. - -## Risks / Trade-offs - -- [Risk] ABI drift from header/type/symbol mismatch across toolchains -> Mitigation: add ABI checks (`sizeof`, `offsetof`, typedef widths) plus cross-compiler smoke tests for static/shared linking. -- [Risk] Behavior mismatch between C shim and existing C++ call paths -> Mitigation: parity tests for lifecycle, stage ordering, control listing/clamping, and resize/record flows. -- [Risk] Host misuse of preconditions (layout/frame index/serialization/lifetime) -> Mitigation: strict input validation, explicit failure codes, and contract-focused docs/tests. -- [Risk] Optional diagnostics implemented inconsistently -> Mitigation: capability-gated contract tests for supported/unsupported paths and stable numeric fields. -- [Trade-off] Synchronous preset load favors determinism over background reload latency -> Mitigation: preserve host-managed async orchestration outside ABI and revisit in future additive release. - -## Migration Plan - -1. Add `include/goggles_filter_chain.h` with v1 ABI types/constants/macros and exported prototypes. -2. Implement C shim mapping from exported functions to existing filter-chain boundary behavior. -3. Keep backend behavior unchanged while switching internal integration call sites to the C shim. -4. Implement `_ex` create/load APIs, capability query, and last-error info query. -5. Add/expand tests for lifecycle matrix, input validation, out-parameter semantics, control contract, and stage ordering. -6. Add record-path contract tests for frame-index bounds and invalid-argument no-command behavior. -7. Add ABI conformance checks and packaging/export verification for shared/static distribution. -8. Freeze v1 ABI and publish integration guidance for hosts and binding authors. - -Rollback strategy: -- If compatibility regressions are found before release freeze, disable new C ABI packaging/export and keep internal C++ boundary active while retaining test coverage for discovered gaps. - -## Open Questions - -- None for v1 scope. Future additive discussions (for later changes) include optional telemetry callbacks and additional `_ex` variants for new string-bearing APIs. diff --git a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/proposal.md b/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/proposal.md deleted file mode 100644 index 825c1363..00000000 --- a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/proposal.md +++ /dev/null @@ -1,58 +0,0 @@ -## Why - -Goggles needs a stable, embeddable filter-chain API that non-C++ hosts can consume without coupling to internal C++ types. Delivering a Vulkan-only v1 C ABI now enables engine and FFI integrations while preserving runtime behavior parity and minimizing migration risk. - -## Problem - -The current boundary is C++-only and does not define a public, versioned ABI contract for ownership, nullability, error semantics, or calling convention. This blocks straightforward static/shared linking by C, Rust, Python, and C# hosts and makes cross-version compatibility guarantees unclear. - -## Scope - -This change defines and ships a v1 public C API surface for filter chain runtime creation, preset loading, frame recording, stage policy, control snapshots/mutation, and runtime diagnostics. It includes a thin C shim over existing filter-chain internals and contract tests that lock lifecycle, validation, ownership, and ABI behavior required for v1.x stability. - -## What Changes - -- Add a single public header `include/goggles_filter_chain.h` with explicit export/calling-convention macros, opaque handles, fixed-width enum-like scalar typedefs, and `struct_size`-gated extensible structs. -- Add mandatory v1 exported symbols for version/capability queries, runtime lifecycle (`create/destroy`), preset APIs (`*_load`, `*_load_ex`), resize/record APIs, stage policy and prechain resolution APIs, control snapshot/list APIs, control mutation/reset APIs, and status/diagnostics APIs. -- Introduce first-class stage domain values and masks for `prechain`, `effect`, and `postchain` while preserving deterministic execution and list ordering semantics. -- Define strict contract behavior for nullability, out-parameter writes on failure, ownership/lifetime, non-finite control rejection, UTF-8 path validation, `frame_index` bounds, and status-first error handling. -- Keep host-managed async reload/synchronization model and keep record API limited to command recording (no implicit submit/present). -- Add conformance and integration tests that lock ABI-visible structure/layout assumptions and behavioral guarantees for v1. - -## Capabilities - -### New Capabilities -- `filter-chain-c-api`: Public Vulkan v1 C ABI for filter-chain runtime lifecycle, recording, controls, diagnostics, and compatibility guarantees. - -### Modified Capabilities -- None. - -## Non-goals - -- Dynamic loader-table API in v1. -- Non-Vulkan backend support in v1. -- Exposing pass-graph/shader-runtime internals in public ABI. -- Hidden background threads, implicit async preset reload, or global mutable runtime singleton. -- Implicit command submission or presentation from record APIs. - -## Risks - -- ABI freeze risk if symbol names/types/calling convention are not locked correctly in v1. -- Integration regressions if C shim diverges from existing C++ behavior for lifecycle, stage ordering, clamping, or resize/record sequencing. -- Host misuse risk around command-buffer/layout preconditions, pointer lifetime, or serialization when contracts are under-specified. -- Cross-compiler/packaging drift risk for shared/static builds if export requirements and layout checks are incomplete. - -## Validation Plan - -- Add contract tests for lifecycle state matrix, invalid call states, and null-safe/idempotent destroy behavior. -- Add validation tests for null pointers, invalid enum/scalar values, zero/invalid extents, invalid stage masks, malformed UTF-8/length inputs, and non-finite control values. -- Add tests for out-parameter guarantees, including unchanged-on-failure behavior and forced-null exceptions for create/list APIs. -- Add tests for record contract (`frame_index < num_sync_indices`, invalid-argument no-command-recorded behavior, and layout pre/post expectations). -- Add ownership/lifetime tests for snapshot data and descriptor-string validity windows. -- Add ABI durability checks (`sizeof`/`offsetof` and typedef widths) and cross-compiler shared/static smoke coverage. - -## Impact - -- **Impacted modules/files**: `include/goggles_filter_chain.h`, new C shim implementation under render-chain boundary, backend call sites that bind to filter-chain API, packaging/export definitions, and filter-chain test suites (unit/integration/FFI-oriented contract tests). -- **Impacted OpenSpec specs**: new capability spec at `openspec/changes/filter-chain-c-api-v1/specs/filter-chain-c-api/spec.md`. -- **Policy-sensitive impacts**: status-code-first failures (no exceptions), explicit ownership/lifetime boundaries, no hidden render-path threading, explicit Vulkan API boundary semantics, and no silent failure/logging ambiguity. diff --git a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/specs/filter-chain-c-api/spec.md b/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/specs/filter-chain-c-api/spec.md deleted file mode 100644 index cdf613dd..00000000 --- a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/specs/filter-chain-c-api/spec.md +++ /dev/null @@ -1,137 +0,0 @@ -## ADDED Requirements - -### Requirement: Public Header and Export Surface -The filter-chain C API MUST be defined in a single public header named `include/goggles_filter_chain.h`. All public types and functions in ABI v1 MUST use the `goggles_chain_` prefix, all exported functions MUST use `GOGGLES_CHAIN_CALL`, and all symbols declared in the v1 header MUST be exported when `goggles_chain_abi_version()` returns `GOGGLES_CHAIN_ABI_VERSION`. - -#### Scenario: Host links against ABI v1 library -- **GIVEN** a host compiles against `include/goggles_filter_chain.h` -- **WHEN** the host links against a library reporting ABI v1 -- **THEN** every function symbol declared by the v1 header resolves without requiring an optional loader table - -### Requirement: Version and Capability Negotiation -The API MUST expose `goggles_chain_api_version()`, `goggles_chain_abi_version()`, and `goggles_chain_capabilities_get(...)` for negotiation. `goggles_chain_api_version()` MUST return packed semantic version bits (`major << 22 | minor << 12 | patch`), `goggles_chain_abi_version()` MUST return ABI major, and capability flags MUST describe optional behavior only and MUST NOT indicate symbol presence. - -#### Scenario: Host checks runtime support -- **GIVEN** a host initializes capability negotiation before creating a runtime -- **WHEN** it calls version and capability queries -- **THEN** it can determine ABI compatibility and optional feature availability without probing symbols dynamically - -### Requirement: Fixed-Width Public Scalar Types -Enum-like public API scalars MUST use fixed-width `uint32_t` typedefs with named constants, including status, stage, stage mask, scale mode, and capability flags. The C ABI MUST keep these scalar widths stable across all v1.x releases. - -#### Scenario: FFI binding validates scalar ABI -- **GIVEN** a language binding maps public scalar typedefs from the header -- **WHEN** the binding validates type widths at load time -- **THEN** each enum-like scalar maps to 32-bit unsigned storage and remains stable across v1.x - -### Requirement: Runtime Lifecycle and State Semantics -`goggles_chain_t` MUST follow a deterministic state model: successful create enters `CREATED`, successful preset load enters or remains `READY`, and destroy transitions to `DEAD` by nulling the public handle. APIs that require an initialized preset (`record`, control listing, control mutation/reset) MUST return `GOGGLES_CHAIN_STATUS_NOT_INITIALIZED` when invoked before READY. `goggles_chain_destroy(...)` MUST be null-safe, idempotent, and return `GOGGLES_CHAIN_STATUS_OK` for `NULL`, `&NULL`, or live handles. - -#### Scenario: Record called before preset load -- **GIVEN** a runtime that has been created but has never completed a preset load -- **WHEN** the host calls `goggles_chain_record_vk(...)` -- **THEN** the function returns `GOGGLES_CHAIN_STATUS_NOT_INITIALIZED` and the runtime remains usable - -### Requirement: Error Model and Diagnostics Contract -All fallible APIs MUST return `goggles_chain_status_t` and MUST NOT surface exceptions as part of the public contract. `goggles_chain_status_to_string(...)` MUST return a stable static string and unknown status values MUST map to `"UNKNOWN_STATUS"`. Optional structured diagnostics MUST be queried through `goggles_chain_error_last_info_get(...)`; when unsupported, the API MUST return `GOGGLES_CHAIN_STATUS_NOT_SUPPORTED`. - -#### Scenario: Host inspects unknown status code -- **GIVEN** a status value outside known v1 constants -- **WHEN** the host calls `goggles_chain_status_to_string(...)` -- **THEN** the function returns the stable token `"UNKNOWN_STATUS"` without heap allocation - -### Requirement: Out-Parameter Failure Semantics -On success, output parameters MUST be fully initialized as documented. On failure, outputs MUST remain unchanged except creator/list APIs, which MUST set owned outputs to `NULL` when an output pointer is provided (`goggles_chain_create_*` sets `*out_chain = NULL`; `goggles_chain_control_list*` sets `*out_snapshot = NULL`). - -#### Scenario: Create fails validation -- **GIVEN** a non-null `out_chain` initialized to a sentinel value -- **WHEN** `goggles_chain_create_vk(...)` returns failure -- **THEN** `*out_chain` is set to `NULL` - -### Requirement: Struct Size Extensibility Contract -Each extensible public struct input/output gated by `struct_size` MUST reject `struct_size < sizeof(v1_type)` with `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`. For `struct_size >= sizeof(v1_type)`, implementations MUST read/write only the known v1 prefix and MUST leave unknown output tail bytes untouched. - -#### Scenario: Future-tail struct passed to v1 runtime -- **GIVEN** a caller passes a larger struct with v1-compatible prefix and extra tail bytes -- **WHEN** the runtime processes the call -- **THEN** behavior is based only on the v1 prefix and unknown tail bytes are not interpreted - -### Requirement: Vulkan Create Input Validation -Create APIs MUST validate required Vulkan context and create-info inputs, including non-null required pointers, `num_sync_indices >= 1`, `num_sync_indices <= max_sync_indices`, required non-empty shader directory input, and positive `initial_prechain_resolution` dimensions. Invalid inputs MUST return `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`. - -#### Scenario: Invalid sync index count -- **GIVEN** create info with `num_sync_indices` equal to zero -- **WHEN** the host calls `goggles_chain_create_vk(...)` -- **THEN** creation fails with `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` - -### Requirement: UTF-8 Path and Length-Based API Contract -The API MUST provide both C-string and length-based variants for create and preset loading (`*_ex` variants). `_ex` path inputs MUST accept explicit byte lengths, MUST reject invalid UTF-8 and embedded NUL bytes, and MUST require non-null pointers when length is non-zero. `goggles_chain_preset_load(...)` MUST be equivalent to `_ex(path, strlen(path))` for valid NUL-terminated UTF-8 input. - -#### Scenario: Preset load with malformed UTF-8 -- **GIVEN** a byte span containing invalid UTF-8 for preset path input -- **WHEN** the host calls `goggles_chain_preset_load_ex(...)` -- **THEN** the function returns `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` without mutating active runtime state - -### Requirement: Frame Recording Contract and Performance Bounds -`goggles_chain_record_vk(...)` MUST record commands only and MUST NOT submit or present. The caller MUST provide a command buffer already in recording state, valid source/target views, and `frame_index < num_sync_indices`. On `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`, no commands MUST be recorded. In v1 hot path, record MUST perform no heap allocation, file I/O, shader compilation, or blocking waits. - -#### Scenario: Invalid frame index -- **GIVEN** a runtime created with `num_sync_indices = 2` -- **WHEN** the host calls `goggles_chain_record_vk(...)` with `frame_index = 2` -- **THEN** the function returns `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` and records no commands - -### Requirement: Stage Model and Policy Contract -The v1 stage model MUST include `prechain`, `effect`, and `postchain` as first-class stage values and masks. `goggles_chain_stage_policy_set(...)` MUST reject unknown stage bits and zero mask with `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`. Runtime execution order MUST remain `prechain -> effect -> postchain`. - -#### Scenario: Unknown stage bit in policy -- **GIVEN** a stage policy mask containing bits outside known v1 stage-mask constants -- **WHEN** the host calls `goggles_chain_stage_policy_set(...)` -- **THEN** the function returns `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` - -### Requirement: Prechain Resolution Contract -`goggles_chain_prechain_resolution_set(...)` and `goggles_chain_prechain_resolution_get(...)` MUST be valid in both `CREATED` and `READY` states. Resolution values MUST require positive width and height and invalid extents MUST return `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`. - -#### Scenario: Zero-height prechain resolution -- **GIVEN** a live runtime instance -- **WHEN** the host sets prechain resolution with `height = 0` -- **THEN** the API returns `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` - -### Requirement: Snapshot-Based Control Listing Contract -Control exposure MUST be snapshot-based through `goggles_chain_control_snapshot_t` ownership APIs. `goggles_chain_control_list(...)` ordering MUST be deterministic as prechain, then effect, then postchain. `goggles_chain_control_list_stage(..., GOGGLES_CHAIN_STAGE_POSTCHAIN, ...)` MUST return a valid empty snapshot with `GOGGLES_CHAIN_STATUS_OK` in v1. Snapshot getters for null snapshot MUST return `0` count and `NULL` data. - -#### Scenario: Postchain stage list in v1 -- **GIVEN** a READY runtime with no postchain controls -- **WHEN** the host requests `goggles_chain_control_list_stage(..., GOGGLES_CHAIN_STAGE_POSTCHAIN, ...)` -- **THEN** the API succeeds and returns an owned snapshot whose count is zero - -### Requirement: Control Descriptor Lifetime and Mutation Semantics -Control descriptors MUST expose stable `control_id` mutation keys and borrowed string/data pointers valid only while the owning snapshot is alive. `goggles_chain_control_set_value(...)` MUST clamp finite values to descriptor bounds before apply, MUST reject non-finite values (`NaN`, `+Inf`, `-Inf`) with `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT`, and MUST leave control state unchanged on non-finite rejection. - -#### Scenario: Non-finite control input -- **GIVEN** a READY runtime and a valid control identifier -- **WHEN** the host calls `goggles_chain_control_set_value(...)` with `NaN` -- **THEN** the API returns `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` and does not modify the control value - -### Requirement: Ownership, Handle Provenance, and Pointer Retention -The runtime MUST NOT retain pointers to caller-provided input structs or path buffers after calls return. Vulkan handles in create context (`VkDevice`, `VkPhysicalDevice`, `VkQueue`) MUST remain valid for runtime lifetime, while record-time Vulkan handles are borrowed for call duration only. Non-null handles passed to API entrypoints MUST originate from corresponding successful create/list APIs in the same process and remain live. - -#### Scenario: Caller frees path buffer after call -- **GIVEN** a host passes valid path bytes to an API call -- **WHEN** the call returns -- **THEN** the host may free or mutate the path buffer without affecting runtime memory safety - -### Requirement: Threading and Reentrancy Contract -The API MUST provide no internal synchronization for a single `goggles_chain_t` instance, and callers MUST externally serialize all calls (including getters/listing) per runtime instance. Calls on different runtime instances are permitted to proceed concurrently. Global version/capability/status-string APIs MUST be thread-safe and reentrant. - -#### Scenario: Concurrent usage across independent runtimes -- **GIVEN** two distinct runtime instances -- **WHEN** two threads call APIs on different instances concurrently -- **THEN** concurrent usage is permitted as long as each instance is internally serialized by its caller - -### Requirement: Compatibility and Evolution Policy for v1.x -Across v1.x, patch releases MUST avoid source/ABI breaking changes and minor releases MUST be additive only. Incompatible layout/signature/calling-convention changes MUST require an ABI major bump. Deprecated declarations MUST use `GOGGLES_CHAIN_DEPRECATED(...)` when deprecation is introduced, and capability flags MUST gate optional behavior rather than symbol availability. - -#### Scenario: Additive minor release behavior -- **GIVEN** a host compiled against v1.0 header -- **WHEN** it links against a v1.x minor release -- **THEN** existing v1.0 symbols and contracts continue to work without source-breaking changes diff --git a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/tasks.md b/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/tasks.md deleted file mode 100644 index 76fde443..00000000 --- a/openspec/changes/archive/2026-03-05-filter-chain-c-api-v1/tasks.md +++ /dev/null @@ -1,44 +0,0 @@ -## 1. Public C ABI Surface and Build Wiring - -- [x] 1.1 Add `include/goggles_filter_chain.h` with v1 macros, opaque handles, fixed-width scalar typedefs/constants, struct definitions, and inline init helpers. -- [x] 1.2 Wire header install/export packaging so v1 symbols are visible for shared/static builds on supported toolchains. -- [x] 1.3 Implement global metadata APIs (`goggles_chain_api_version`, `goggles_chain_abi_version`, `goggles_chain_capabilities_get`, `goggles_chain_status_to_string`) with ABI-locked values. - -## 2. Runtime Lifecycle and Preset/Resize APIs - -- [x] 2.1 Implement C runtime handle wrapper and lifecycle state machine (`CREATED`, `READY`, `DEAD`) with null-safe idempotent destroy semantics. -- [x] 2.2 Implement `goggles_chain_create_vk` and `goggles_chain_create_vk_ex` with required input validation, `struct_size` enforcement, and out-parameter failure guarantees. -- [x] 2.3 Implement `goggles_chain_preset_load` and `goggles_chain_preset_load_ex` with UTF-8/length validation and state-preserving failure behavior. -- [x] 2.4 Implement `goggles_chain_handle_resize` and prechain resolution get/set APIs with extent validation in `CREATED` and `READY` states. - -## 3. Record API and Stage Policy - -- [x] 3.1 Implement `goggles_chain_record_vk` validation for command buffer, source/target inputs, scale/integer rules, and `frame_index < num_sync_indices`. -- [x] 3.2 Preserve execution order and stage semantics (`prechain -> effect -> postchain`) while keeping record path command-record-only. -- [x] 3.3 Implement `goggles_chain_stage_policy_set/get` with strict stage-mask validation (reject zero and unknown bits). - -## 4. Control Snapshot and Mutation APIs - -- [x] 4.1 Implement snapshot ownership APIs (`control_list`, `control_list_stage`, `snapshot_get_count`, `snapshot_get_data`, `snapshot_destroy`) with deterministic ordering and valid empty postchain snapshot behavior. -- [x] 4.2 Implement control mutation APIs (`control_set_value`, `control_reset_value`, `control_reset_all`) using stable `control_id` keys and finite-value clamping rules. -- [x] 4.3 Ensure non-finite control values (`NaN`/`Inf`) are rejected with `GOGGLES_CHAIN_STATUS_INVALID_ARGUMENT` and do not mutate runtime control state. - -## 5. Diagnostics, Ownership, and Integration - -- [x] 5.1 Implement per-runtime last-error diagnostics (`goggles_chain_error_last_info_get`) with capability-gated `NOT_SUPPORTED` behavior. -- [x] 5.2 Ensure no retention of caller-provided path/struct pointers after call return and enforce handle provenance checks where detectable. -- [x] 5.3 Update internal render/backend integration to use the C shim while preserving existing runtime behavior and synchronization ownership. - -## 6. Contract and Conformance Testing - -- [x] 6.1 Add unit tests for lifecycle matrix, invalid call-state behavior, null-safe/idempotent destroy, and out-parameter-on-failure rules. -- [x] 6.2 Add tests for `struct_size` matrix, enum/scalar/path/extents validation, UTF-8 `_ex` path rules, and non-finite control rejection. -- [x] 6.3 Add control snapshot tests for ordering, lifetime/borrowed-pointer validity, and empty postchain snapshot contract. -- [x] 6.4 Add record-path tests for frame-index bounds and invalid-argument no-command-recorded behavior; add integration coverage for create->load->record->destroy flow. -- [x] 6.5 Add ABI durability checks for public struct layout and scalar widths, plus shared/static cross-compiler smoke coverage in local test lanes. - -## 7. Verification and Spec Alignment - -- [x] 7.1 Run `pixi run build -p asan` and fix all build/lint/compile issues introduced by the change. -- [x] 7.2 Run `pixi run test -p asan` and `ctest --preset test -R "goggles_unit_tests" --output-on-failure` for contract and integration validation. -- [x] 7.3 Run `pixi run build -p quality` and resolve quality-gate findings. diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml deleted file mode 100644 index f1842c5f..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-07 diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md deleted file mode 100644 index f455e143..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md +++ /dev/null @@ -1,229 +0,0 @@ -## Context - -The nested compositor in `src/compositor/` is currently centered on `compositor_server.cpp` and `compositor_server.hpp`. The implementation mixes wlroots bootstrap, compositor thread lifecycle, XDG/XWayland/layer-shell listeners, input queue draining, hit-testing and focus switching, pointer constraints, cursor state/rendering, frame presentation/export, and teardown in one large translation unit. - -This is a brownfield maintainability change, not a behavior redesign. The proposal requires a responsibility-oriented split that improves editing locality while preserving compositor startup/shutdown order, protocol semantics, input routing, focus behavior, layer-shell behavior, pointer-constraint behavior, cursor behavior, presented-frame behavior, export behavior, and existing workarounds. - -Current code characteristics that shape the design: - -- `CompositorServer` already acts as the public entrypoint used outside `src/compositor/`. -- An internal `Impl` object currently centralizes wlroots handles, hook storage, synchronization primitives, queues, presentation state, focus/cursor metadata, and pointer-constraint state. -- Several behaviors depend on localized quirks and comments, especially around XWayland activation, destroy-listener constraints, stable hook allocation, and layer-shell popup forwarding into XDG popup handling. -- Input, focus, pointer constraints, cursor, and presentation paths are coupled through shared state and compositor-thread ordering, so a purely mechanical extraction is risky. - -Policy constraints from `docs/project_policies.md` apply, along with the existing compositor-local runtime conventions already present in `src/compositor/`: - -- Expected runtime failures MUST keep `Result`-style propagation and MUST NOT switch to exception-based handling. -- Ownership/lifetime MUST remain RAII-safe and easy to audit. -- Build/test workflows MUST use Pixi tasks and CMake/CTest presets. -- The compositor event loop MUST remain non-blocking with thread coordination consistent with current queue/eventfd patterns. - -## Goals / Non-Goals - -**Goals:** - -- Reduce `compositor_server.cpp` to facade/high-level orchestration. -- Split compositor implementation into a small set of subsystem-oriented modules with stable, self-describing responsibilities. -- Preserve one central implementation state object as the compositor single source of truth. -- Keep cross-cutting quirks and constraints near the subsystem logic they govern. -- Provide an implementation plan and verification map that can be executed from the repository artifacts alone. - -**Non-Goals:** - -- Changing external `CompositorServer` behavior or protocol semantics. -- Replacing wl_listener-based lifecycle wiring with a new event framework. -- Introducing generic utility buckets divorced from subsystem ownership. -- Splitting code so aggressively that one event flow is scattered across many tiny files. -- Reworking compositor ownership, threading, rendering, or protocol models beyond what safe extraction requires. - -## Decisions - -### 1) Keep `CompositorServer` as the only public facade - -Decision: - -- `compositor_server.hpp` remains the public API surface. -- `compositor_server.cpp` keeps factory/create/start/stop/public method entrypoints and delegates subsystem work through internal module functions. -- Any signature change MUST be internal-only and minimal. - -Rationale: - -- Preserves external integration points and keeps the public surface easy to reason about. -- Gives future editors a stable top-level entrypoint without mixing protocol or rendering details into the facade. - -Alternatives considered: - -- Promote subsystem headers into new semi-public API: rejected because it broadens the integration surface and weakens locality. -- Split public API by protocol domain: rejected because external callers still conceptually use one compositor service. - -### 2) Preserve one central compositor state authority - -Decision: - -- Keep one internal implementation state object (`Impl` or an equivalent renamed type) that owns wlroots objects, listener storage, event queues, focus/cursor state, pointer-constraint state, and presented-frame/export state. -- Subsystem files operate on that central state by reference/pointer; they MUST NOT duplicate ownership of compositor-global resources. - -Rationale: - -- Existing behavior relies on tightly shared state across protocol lifecycle, input dispatch, focus targeting, pointer constraints, cursor handling, and presentation. -- A single state authority keeps teardown ordering, synchronization, and ownership audits tractable. - -Alternatives considered: - -- Per-subsystem owner objects with distributed lifetime: rejected because it increases teardown complexity and risks behavior drift. -- A broad service-locator internal API: rejected because it hides ownership rather than clarifying it. - -### 3) Module boundaries follow responsibilities, not helper extraction - -Decision: - -- Extract subsystem modules aligned to behavior domains: - - `compositor_core.*` - - `compositor_input.*` - - `compositor_focus.*` - - `compositor_cursor.*` - - `compositor_present.*` - - `compositor_xdg.*` - - `compositor_xwayland.*` - - `compositor_layer_shell.*` -- Shared declarations are permitted only through narrow internal headers needed by a specific subsystem boundary. -- Helpers meaningful only inside one subsystem stay in that subsystem file. -- `compositor_focus.*` owns pointer-constraint listener lifecycle, activation/deactivation, confinement, and cursor-hint application; `compositor_input.*` only consults that state when dispatching motion/button/axis events. -- `compositor_layer_shell.*` owns layer-surface lifecycle and render-order integration, while `compositor_xdg.*` remains the sole owner of XDG popup hook creation/destruction, including popups forwarded from layer-shell surfaces. -- `compositor_core.*` MUST stay limited to startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown; extracted helpers that do not fit that scope MUST belong to a concrete subsystem or a narrow internal header owned by that subsystem. - -Rationale: - -- Responsibility-oriented files minimize context bloat for future edits. -- Future maintenance benefits when one task maps to one conceptual file instead of requiring a giant mixed-context unit. - -Alternatives considered: - -- Split by helper category (`listeners`, `render_helpers`, `state_utils`): rejected because it still spreads one behavior across many files. -- Extract only protocol-specific code and leave the rest in `compositor_server.cpp`: rejected because input/focus/cursor/presentation would remain context-heavy. - -### 4) Establish a compile-safe declaration seam before behavior extraction - -Decision: - -- Create `src/compositor/compositor_state.hpp` for the central compositor state declaration and only the shared POD/state members stored on that object. -- Create `src/compositor/compositor_targets.hpp` for `InputTarget` plus cursor/surface-coordinate helper declarations shared by input/focus/cursor/present. -- Create `src/compositor/compositor_protocol_hooks.hpp` for `XdgToplevelHooks`, `XdgPopupHooks`, `XWaylandSurfaceHooks`, `LayerSurfaceHooks`, and `ConstraintHooks` listener structs. -- Update `src/compositor/CMakeLists.txt` as each new compositor translation unit is introduced so the build graph stays compile-complete throughout extraction. - -Rationale: - -- The current `Impl` declaration block is too monolithic for safe early extraction of XDG/XWayland without first moving shared declarations into narrow internal headers. -- Naming the exact internal headers prevents implementation from inventing a broad catch-all internal header. - -Alternatives considered: - -- Defer header work until after the first source split: rejected because the early extraction waves would not be compile-safe. -- Use one `compositor_internal.hpp` catch-all header: rejected because it recreates the context-bloat problem in header form. - -### 5) Extract in risk-ordered migration waves - -Decision: - -- Follow this migration order: - 0. Establish the compile-safe declaration seam and update `src/compositor/CMakeLists.txt`. - 1. XDG lifecycle. - 2. XWayland lifecycle. - 3. Input dispatch and focus/hit-test split, including pointer constraints. - 4. Layer-shell lifecycle/render integration after XDG popup ownership is isolated. - 5. Cursor and presentation/export paths. - 6. Core bootstrap/teardown consolidation and final facade reduction. - -Rationale: - -- The declaration seam prevents phase-1/phase-2 extraction from failing to compile once method/type declarations leave the monolithic `Impl` block. -- XDG and XWayland lifecycle blocks are large, self-identifying seams with clear listener ownership. -- Input/focus can be split after protocol handlers are isolated, reducing concurrent churn and clarifying pointer-constraint ownership. -- Layer-shell extraction becomes safer once XDG popup ownership is explicit and input/focus seams are stable. -- Cursor and presentation depend on stabilized focus/targeting behavior. -- Core consolidation last avoids premature churn in wiring while subsystem extraction is still moving. - -Alternatives considered: - -- Start with bootstrap/teardown first: rejected because it risks destabilizing all later extractions. -- Extract cursor/presentation before input/focus: rejected because those paths depend on stable targeting semantics. - -### 6) Preserve behavior through subsystem-local quirk retention - -Decision: - -- Existing comments that capture non-obvious constraints MUST move with the subsystem logic they explain. -- XWayland-specific re-activation and destroy-listener restrictions stay in `compositor_xwayland.*`. -- Stable hook allocation notes stay with the hook-owning subsystem (`compositor_xdg.*`, `compositor_xwayland.*`, `compositor_layer_shell.*`). -- Pointer-constraint confinement/cursor-hint rationale stays in `compositor_focus.*`. -- Layer-shell popup forwarding rationale stays split as one explicit ownership rule: `compositor_layer_shell.*` forwards `new_popup` events, and `compositor_xdg.*` owns the popup hook lifecycle created from those events. - -Rationale: - -- These comments explain behavior-critical constraints, not optional narration. -- Separating the quirk from the code increases the chance of accidental regression in future edits. - -Alternatives considered: - -- Consolidate quirks into one internal notes header: rejected because it disconnects rationale from behavior. - -### 7) Verification is explicit and behavior-preserving - -Decision: - -- Implementation tasks MUST include both structural checks (module boundaries, facade reduction, central state retention, `CMakeLists.txt` coverage, and `compositor_core.*` scope) and runtime-oriented checks (build/test/static commands plus a behavior checklist). -- Verification remains preset-driven and project-policy aligned. - -Verification evidence is expected to include: - -- `ctest --preset asan -R "goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit" --output-on-failure` for environment-agnostic automated coverage prepared by the ASAN build tree. -- `pixi run test -p asan` when the full ASAN suite is supported by the local runtime environment. -- `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure` for XWayland/Wayland input routing when the environment supports those compositor tests. -- `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure` whenever presented-frame or DMA-BUF export code is touched; completion is blocked until an environment capable of running that check is available. -- Manual fallback evidence for `goggles_auto_input_forwarding_x11` / `goggles_auto_input_forwarding_wayland` only when automated CTest coverage is unavailable but an equivalent interactive runtime exists, recorded alongside prerequisites, observations, and stored proof, using repo-root commands: - - `./build/asan/tests/goggles_manual_input_x11` - - `./build/asan/tests/goggles_manual_input_wayland` - - `./build/asan/tests/goggles_manual_surface_selector_x11` - - `./build/asan/tests/goggles_manual_surface_selector_wayland` -- No manual fallback for `goggles_headless_integration*` when presented-frame or DMA-BUF export code is touched. -- Quirk-level checks for XWayland re-activation before each input event, the no-destroy-listener rule for `xsurface->surface`, stable hook allocation for XDG/XWayland/layer-shell hook containers, and layer-shell-to-XDG popup forwarding. - -Rationale: - -- This refactor is successful only if locality improves without behavior drift. -- Implementation needs concrete verification evidence, not an implied “no behavior change” claim. - -Alternatives considered: - -- Rely only on compile/test gates: rejected because structural acceptance criteria and quirk preservation would remain under-specified. - -## Risks / Trade-offs - -- [Risk] Input/focus/cursor/pointer-constraint logic remains too entangled for clean boundaries -> Mitigation: keep focus resolution and pointer constraints in `compositor_focus.*`, keep input dispatch in `compositor_input.*`, and allow narrow internal declarations rather than generic helpers. -- [Risk] Extraction changes listener registration or teardown order -> Mitigation: preserve one state authority, document ordering decisions, and verify startup/shutdown plus protocol behavior explicitly. -- [Risk] `compositor_core.*` becomes a second dumping ground -> Mitigation: keep it limited to bootstrap, thread lifecycle, backend/output/event-loop orchestration, and teardown only. -- [Risk] Layer-shell/XDG popup ownership drifts across two modules -> Mitigation: enforce one popup ownership rule (`compositor_xdg.*`) and keep layer-shell at event-forwarding/lifecycle/render responsibilities only. -- [Risk] A module boundary proves slightly different in code than planned -> Mitigation: allow a documented close variant only when responsibility ownership stays clear and external behavior remains unchanged. -- [Trade-off] Keeping one central state object retains some coupling -> Mitigation: accept this coupling to preserve behavior and ownership clarity while still improving file-level locality. - -## Migration Plan - -1. Introduce `compositor_state.hpp`, `compositor_targets.hpp`, and `compositor_protocol_hooks.hpp`, and move only the declarations needed to make multi-file extraction compile-safe. -2. Update `src/compositor/CMakeLists.txt` to add each new compositor translation unit as it lands. -3. Extract XDG setup, toplevel handlers, and popup handlers into `compositor_xdg.*`. -4. Extract XWayland setup and lifecycle handlers into `compositor_xwayland.*`, preserving X11-specific quirks and comments in the new subsystem. -5. Separate input queue injection/dispatch into `compositor_input.*` and target resolution/focus/pointer-constraint behavior into `compositor_focus.*`. -6. Extract layer-shell lifecycle/render-order integration into `compositor_layer_shell.*`, with popup events forwarded to XDG-owned popup hook creation. -7. Extract cursor setup/update/render logic and presented-frame/export logic into `compositor_cursor.*` and `compositor_present.*`. -8. Consolidate bootstrap, backend/output setup, compositor-thread lifecycle, and teardown into `compositor_core.*`, leaving `compositor_server.cpp` as facade/orchestration only. -9. Run structural and behavioral verification commands/checklists after the split. -10. If implementation uncovers a required behavior change, stop and reconcile proposal/spec/design artifacts before continuing. - -Rollback strategy: - -- Revert the module extraction while restoring the previous single-file implementation shape if the split causes behavior regressions. -- Do not keep a partially duplicated ownership model during rollback. - -## Open Questions - -- Whether additional targeted compositor tests should be added if the existing `goggles_auto_input_forwarding_*` and `goggles_headless_integration*` coverage proves insufficient for presentation/export preservation. diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md deleted file mode 100644 index 228657b6..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md +++ /dev/null @@ -1,80 +0,0 @@ -# Implementation Handoff - -Change: `refactor-compositor-server-modules` - -## Touched compositor files - -- `src/compositor/CMakeLists.txt` -- `src/compositor/compositor_server.cpp` -- `src/compositor/compositor_server.hpp` -- `src/compositor/compositor_state.hpp` -- `src/compositor/compositor_targets.hpp` -- `src/compositor/compositor_protocol_hooks.hpp` -- `src/compositor/compositor_core.cpp` -- `src/compositor/compositor_cursor.cpp` -- `src/compositor/compositor_focus.cpp` -- `src/compositor/compositor_input.cpp` -- `src/compositor/compositor_layer_shell.cpp` -- `src/compositor/compositor_present.cpp` -- `src/compositor/compositor_xdg.cpp` -- `src/compositor/compositor_xwayland.cpp` - -## Approved boundaries and close variants - -- `compositor_server.*` is reduced to the public facade and startup delegation. -- `compositor_core.*` contains startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown. -- `compositor_xdg.*`, `compositor_xwayland.*`, `compositor_input.*`, `compositor_focus.*`, `compositor_cursor.*`, `compositor_present.*`, and `compositor_layer_shell.*` hold the extracted subsystem logic. -- Shared ownership remains centralized on `CompositorState` in `src/compositor/compositor_state.hpp`. -- No materially different module boundary was required; no close-variant divergence needs spec/design reconciliation. - -## Preserved quirks and workarounds - -- XWayland input re-activation before each key/pointer event remains in `src/compositor/compositor_xwayland.cpp:94`. -- The no-destroy-listener rule on `xsurface->surface` remains documented and enforced in `src/compositor/compositor_xwayland.cpp:301`. -- Stable hook allocation for XDG, XWayland, and layer-shell listeners remains adjacent to each subsystem in: - - `src/compositor/compositor_xdg.cpp:53` - - `src/compositor/compositor_xdg.cpp:116` - - `src/compositor/compositor_xwayland.cpp:206` - - `src/compositor/compositor_layer_shell.cpp:101` -- Layer-shell `new_popup` forwarding into XDG-owned popup hooks remains in `src/compositor/compositor_layer_shell.cpp:155`. -- Layer-shell forwarded popup owner-root unconstraining remains in `src/compositor/compositor_xdg.cpp:179`. - -## Behavior-preservation checklist - -- startup/shutdown order: `src/compositor/compositor_server.cpp:28`, `src/compositor/compositor_core.cpp:286` -- Wayland client handling: `src/compositor/compositor_xdg.cpp:17`, `src/compositor/compositor_xdg.cpp:43` -- XWayland client handling: `src/compositor/compositor_xwayland.cpp:119`, `src/compositor/compositor_xwayland.cpp:202` -- input forwarding: `src/compositor/compositor_input.cpp:364`, `src/compositor/compositor_input.cpp:378`, `src/compositor/compositor_input.cpp:411`, `src/compositor/compositor_input.cpp:433` -- focus targeting: `src/compositor/compositor_focus.cpp:215`, `src/compositor/compositor_focus.cpp:268`, `src/compositor/compositor_focus.cpp:673` -- pointer constraints (activation, confine, cursor hints): `src/compositor/compositor_focus.cpp:188`, `src/compositor/compositor_focus.cpp:204` -- layer-shell render ordering, popup routing, and exclusive keyboard focus: `src/compositor/compositor_layer_shell.cpp:155`, `src/compositor/compositor_focus.cpp:427`, `src/compositor/compositor_focus.cpp:529` -- cursor visibility/behavior: `src/compositor/compositor_cursor.cpp:92`, `src/compositor/compositor_cursor.cpp:202` -- presented-frame acquisition/export: `src/compositor/compositor_present.cpp:93`, `src/compositor/compositor_present.cpp:190` - -## Verification results - -- `pixi run build -p debug`: pass -- `pixi run build -p asan`: pass -- `pixi run test -p asan`: pass -- `pixi run build -p quality`: pass -- `ctest --preset asan -R "goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit" --output-on-failure`: pass -- `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure`: pass -- `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure`: pass -- `grep -R --line-number '\bthrow\b' src/compositor --include='*.cpp' --include='*.hpp'`: no matches -- `grep -R --line-number 'std::thread\|std::jthread' src/compositor --include='*.cpp' --include='*.hpp'`: only `std::jthread` in `src/compositor/compositor_core.cpp:280` and `src/compositor/compositor_state.hpp:153` -- `grep -R --line-number 'Vk[A-Za-z0-9_]*' src/compositor --include='*.cpp' --include='*.hpp'`: no matches -- `lsp_diagnostics` on touched compositor sources: no diagnostics - -## Skipped checks and fallback evidence - -- No checks were skipped. -- Manual fallback evidence was not needed because both automated input-forwarding checks ran successfully. - -## Quirk-to-subsystem proof map - -- XWayland re-activation before input: `src/compositor/compositor_xwayland.cpp:94` -- No destroy listener on `xsurface->surface`: `src/compositor/compositor_xwayland.cpp:301` -- Stable XDG hook allocation: `src/compositor/compositor_xdg.cpp:53`, `src/compositor/compositor_xdg.cpp:116` -- Stable XWayland hook allocation: `src/compositor/compositor_xwayland.cpp:206` -- Stable layer-shell hook allocation: `src/compositor/compositor_layer_shell.cpp:101` -- Layer-shell popup forwarding into XDG-owned popup hooks: `src/compositor/compositor_layer_shell.cpp:155` diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md deleted file mode 100644 index e7ffe41d..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md +++ /dev/null @@ -1,110 +0,0 @@ -## Why - -`src/compositor/compositor_server.cpp` currently concentrates wlroots bootstrap, XDG/XWayland/layer-shell lifecycle, input dispatch, focus targeting, cursor handling, frame presentation/export, pointer constraints, and teardown in one implementation unit. That concentration makes future edits expensive because a localized change requires loading unrelated compositor domains and increases the risk of cross-domain drift. - -## Problem - -- The nested compositor implementation is too large and responsibility-mixed for safe, local editing. -- Protocol lifecycle, input routing, focus logic, cursor behavior, layer-shell behavior, pointer-constraint behavior, and presentation/export logic currently share one translation unit, so narrowly scoped changes still require broad context loading. -- Important compositor quirks and wlroots/XWayland workarounds are harder to audit because subsystem logic is not isolated near the behavior it constrains. - -## Scope - -This change is a behavior-preserving internal refactor of `src/compositor/` that introduces a stable, responsibility-oriented module layout for the compositor implementation. - -- Keep `CompositorServer` public API stable except for a very small internal-only adjustment if extraction requires it. -- Split the implementation into subsystem-oriented files centered on: facade, core lifecycle, input, focus, cursor, presentation/export, XDG lifecycle, XWayland lifecycle, and layer-shell lifecycle. -- Preserve one central compositor implementation state object (or equivalent single source of truth) so ownership and cross-thread state remain easy to audit. -- Preserve existing startup/shutdown order, protocol behavior, input behavior, focus behavior, layer-shell behavior, cursor behavior, pointer-constraint behavior, presented-frame/export behavior, and documented quirks/workarounds. -- Follow migration order that first establishes a compile-safe internal declaration seam and updates `src/compositor/CMakeLists.txt` incrementally as each new translation unit lands, then extracts XDG, XWayland, input/focus (including pointer constraints), layer-shell, cursor/presentation, and finally bootstrap/teardown consolidation unless a smaller compile-safety step is required. - -## Non-goals - -- Redesigning the compositor architecture beyond file/module decomposition. -- Replacing wl_listener patterns with a new framework except for a very small local cleanup required to complete extraction safely. -- Introducing a generic `misc`, `helpers`, or `utils` dumping-ground module. -- Over-fragmenting event flows into many micro-files. -- Changing protocol semantics, rendering semantics, or resource ownership unless required for safe extraction and explicitly documented. - -## What Changes - -- Reduce `compositor_server.hpp` / `compositor_server.cpp` to facade and public API responsibilities, with high-level create/start/stop delegation only. -- Introduce stable compositor implementation modules close to these target boundaries: - - `compositor_core.*`: wlroots bootstrap, backend/output/event loop setup, compositor thread lifecycle, teardown. - - `compositor_input.*`: event queue injection, compositor-thread input processing, keyboard/pointer/button/axis dispatch, resize/focus wakeups. - - `compositor_focus.*`: target resolution, focus switching, hit-testing, pointer-constraint activation/deactivation, confinement, cursor-hint application, and cursor-local coordinate helpers tied to targeting. - - `compositor_cursor.*`: theme setup, fallback cursor generation, cursor frame lookup, visibility/reset/hint/update/render overlay. - - `compositor_present.*`: presented-frame tracking, refresh/reset/export, render-to-frame path and related presentation logic. - - `compositor_xdg.*`: XDG toplevel/popup lifecycle hooks and handlers. - - `compositor_xwayland.*`: XWayland setup plus associate/map/commit/destroy handling and X11-specific quirks. -- - `compositor_layer_shell.*`: layer-shell hooks, lifecycle, render-order integration, and forwarding into XDG-owned popup hook lifecycle handling. -- Define narrowly scoped internal headers up front so implementation work does not invent a broad internal catch-all header during extraction. -- Keep important subsystem comments and workarounds next to the logic they constrain, especially XWayland activation quirks, destroy-listener restrictions, stable hook allocation requirements, and layer-shell popup forwarding rules. -- Add implementation-facing design and task traceability so the change can be executed from the repository artifacts alone. - -## Capabilities - -### New Capabilities -- `compositor-module-layout`: Responsibility-oriented compositor module boundaries that preserve current nested compositor behavior while improving editing locality and state auditability. - -### Modified Capabilities -- None. - -## Risks - -- Extraction could accidentally change startup/shutdown sequencing or listener registration order. -- Shared state could fragment if module boundaries duplicate ownership instead of delegating through one implementation state object. -- Input/focus/cursor/pointer-constraint flows have tightly coupled behavior; an overly mechanical split could break routing or hide important quirks. -- Layer-shell routing could regress if popup forwarding or exclusive keyboard-focus handling drifts during extraction. -- Presentation/export paths could regress if render ordering, retained-frame logic, or cursor overlay integration drift during extraction. - -## Validation Plan - -- Structural verification: - - Confirm `src/compositor/compositor_server.cpp` is reduced to facade/high-level delegation and no longer contains all compositor subsystems. - - Confirm the new subsystem files exist and align with the approved module boundaries or documented close variants. - - Confirm `src/compositor/CMakeLists.txt` lists each new compositor translation unit. - - Confirm `compositor_core.*` only owns startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown responsibilities. -- Build and static verification: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` - - `ctest --preset asan -R "goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit" --output-on-failure` - - `pixi run test -p asan` when the full ASAN suite is supported by the local runtime environment - - `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure` - - `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure` -- Behavioral preservation checklist: - - Compositor startup/shutdown order remains unchanged. - - Wayland client handling remains unchanged for XDG toplevels and popups. - - XWayland client handling remains unchanged, including X11-specific quirks/workarounds. - - Input forwarding and event wakeups remain unchanged. - - Focus targeting and hit-testing remain unchanged. - - Pointer-constraint activation, confinement, and cursor-hint behavior remain unchanged. - - Layer-shell render ordering, popup routing, and exclusive keyboard-interactivity behavior remain unchanged. - - Cursor visibility, positioning, lock/confine, and overlay rendering remain unchanged. - - Presented frame acquisition, retained-frame behavior, and DMA-BUF export remain unchanged. - - When `goggles_auto_input_forwarding_x11` or `goggles_auto_input_forwarding_wayland` cannot run but an equivalent interactive runtime exists, capture fallback evidence and record the prerequisites, observations, and stored proof using: - - `./build/asan/tests/goggles_manual_input_x11` - - `./build/asan/tests/goggles_manual_input_wayland` - - `./build/asan/tests/goggles_manual_surface_selector_x11` - - `./build/asan/tests/goggles_manual_surface_selector_wayland` - - When presented-frame or DMA-BUF export code is touched, `goggles_headless_integration*` remains mandatory; do not replace it with manual fallback. - -## Divergence Handling - -- If extraction uncovers a required behavior change, implementation MUST stop and reconcile proposal/spec/design artifacts before implementation continues. -- If a target module boundary proves unsafe, implementation MAY use a documented close variant only when it preserves responsibility-oriented locality, keeps a single state authority, and records the rationale in the implementation handoff. - -## Impact - -- **Code modules/files**: `src/compositor/compositor_server.hpp`, `src/compositor/compositor_server.cpp`, `src/compositor/CMakeLists.txt`, plus new `src/compositor/compositor_*.hpp/.cpp` internal modules. -- **Runtime systems**: wlroots bootstrap, XDG shell lifecycle, XWayland lifecycle, layer-shell handling, pointer constraints, input forwarding, focus resolution, cursor rendering, and compositor-presented frame export. -- **Public API**: `CompositorServer` surface is expected to remain stable. -- **Tests/verification**: compositor-focused unit/integration coverage and preset-driven build/test/static checks, with explicit fallback manual evidence when environment-sensitive tests cannot run. -- **OpenSpec artifacts impacted**: - - Delta spec introduced by this change: `openspec/changes/refactor-compositor-server-modules/specs/compositor-module-layout/spec.md` - - Proposed living-spec sync target after archive/sync: `openspec/specs/compositor-module-layout/spec.md` -- **Policy-sensitive areas**: - - Error handling and logging boundaries MUST remain unchanged unless explicitly required. - - Ownership/lifetime MUST remain centralized and RAII-safe. - - Render/presentation behavior MUST preserve existing threading constraints and resource ownership rules. diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/specs/compositor-module-layout/spec.md b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/specs/compositor-module-layout/spec.md deleted file mode 100644 index 705b6f04..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/specs/compositor-module-layout/spec.md +++ /dev/null @@ -1,131 +0,0 @@ -## ADDED Requirements - -### Requirement: Compositor Server Facade Remains Stable -The compositor implementation SHALL preserve `CompositorServer` as the public integration facade while moving subsystem logic out of `compositor_server.cpp`. - -The refactor SHALL: - -- Keep `compositor_server.hpp` as the public API declaration surface. -- Keep `compositor_server.cpp` limited to public method entrypoints and high-level delegation. -- Preserve existing `CompositorServer` externally observable behavior unless a very small internal-only adjustment is explicitly documented in the change artifacts. - -#### Scenario: Public facade preserved after split -- **GIVEN** the compositor refactor is complete -- **WHEN** external callers integrate the nested compositor -- **THEN** external callers still integrate through `CompositorServer` -- **AND** compositor subsystem implementations no longer remain concentrated in one giant `compositor_server.cpp` - -### Requirement: Single Compositor State Authority -The compositor implementation SHALL retain one central implementation state object as the single source of truth for wlroots resources, synchronization primitives, listener storage, focus state, cursor state, pointer-constraint state, and presented-frame/export state. - -Subsystem modules SHALL operate on that central state and SHALL NOT duplicate ownership of compositor-global resources across separate subsystem owners. - -#### Scenario: Ownership remains centralized after extraction -- **GIVEN** subsystem code is split across multiple files -- **WHEN** ownership of compositor-global resources is inspected -- **THEN** wlroots handles, listener containers, input queues, focus metadata, cursor metadata, pointer-constraint state, and presented-frame state still resolve to one compositor state authority -- **AND** teardown ordering remains auditable from that central state - -### Requirement: Responsibility-Oriented Compositor Modules -The compositor implementation SHALL organize subsystem logic into responsibility-oriented modules so future edits can stay local to one compositor concern. - -The split SHALL provide module boundaries that match or closely approximate these responsibilities: - -- facade/public API in `compositor_server.*` -- bootstrap/thread lifecycle/teardown in `compositor_core.*` -- input queue injection and dispatch in `compositor_input.*` -- focus switching, hit-testing, and pointer-constraint ownership in `compositor_focus.*` -- cursor setup/update/rendering in `compositor_cursor.*` -- presented-frame/render/export logic in `compositor_present.*` -- XDG lifecycle in `compositor_xdg.*` -- XWayland lifecycle in `compositor_xwayland.*` -- layer-shell lifecycle/render integration in `compositor_layer_shell.*` - -The implementation SHALL NOT introduce a generic `misc`, `helpers`, or `utils` dumping-ground module for compositor extraction. -`compositor_core.*` SHALL contain only startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown responsibilities. -Layer-shell-originated `xdg_popup` hook creation and destruction SHALL remain owned by `compositor_xdg.*`, with `compositor_layer_shell.*` limited to forwarding popup creation events into that XDG-owned path. - -#### Scenario: Localized edit surface for protocol lifecycle -- **GIVEN** a future change only affects XWayland lifecycle behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `compositor_xwayland.*` -- **AND** unrelated input, cursor, and presentation logic does not need to remain in the same translation unit - -#### Scenario: Localized edit surface for input targeting -- **GIVEN** a future change only affects hit-testing, focus targeting, or pointer-constraint behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `compositor_focus.*` -- **AND** protocol lifecycle code does not need to be loaded to make that focused change - -### Requirement: Extraction Contract Is Explicit for Apply -The change artifacts SHALL define a compile-safe extraction order and verification plan that minimize behavior drift during apply. - -The change artifacts SHALL specify: - -- An initial declaration-seam step that creates only the narrow internal headers needed to make multi-file extraction compile-safe. -- Updating `src/compositor/CMakeLists.txt` as each compositor translation unit lands so the migration remains compile-complete. -- Extract XDG lifecycle before XWayland lifecycle. -- Extract XWayland lifecycle before splitting input dispatch from focus/hit-testing. -- Extract layer-shell after XDG popup ownership is isolated and after the input/focus split is stable, and before final core/facade cleanup. -- Extract cursor and presentation/export logic after protocol and input/focus seams are stable. -- Leave bootstrap/teardown consolidation for the end unless an earlier minimal core split is required for safe extraction. -- Include verification commands and a concrete checklist covering startup/shutdown, Wayland clients, XWayland clients, input forwarding, focus targeting, pointer constraints, layer-shell behavior, cursor behavior, and presented-frame acquisition/export. - -#### Scenario: Migration order is explicit for implementation -- **GIVEN** implementation starts from the repository artifacts alone -- **WHEN** the artifacts are read before editing code -- **THEN** the OpenSpec artifacts specify the required extraction order -- **AND** the implementation does not depend on undocumented external context to determine safe sequencing - -#### Scenario: Behavior preservation is verified explicitly -- **GIVEN** the refactor is implemented -- **WHEN** verification evidence is recorded -- **THEN** the recorded verification evidence includes preset-driven build/test/static checks -- **AND** it includes a compositor behavior checklist covering startup/shutdown, Wayland/XWayland handling, input, focus, pointer constraints, layer-shell behavior, cursor, and presentation/export preservation - -#### Scenario: Input-routing fallback is explicit and bounded -- **GIVEN** automated execution of `goggles_auto_input_forwarding_x11` or `goggles_auto_input_forwarding_wayland` is unavailable -- **WHEN** equivalent interactive runtime conditions exist -- **THEN** the verification plan allows manual fallback only for those unavailable input-routing checks -- **AND** it requires recorded prerequisites, observations, and stored proof for the fallback run - -#### Scenario: Presentation and export verification stays mandatory -- **GIVEN** implementation touches presented-frame or DMA-BUF export code -- **WHEN** verification is executed -- **THEN** `goggles_headless_integration*` remains a required check -- **AND** the verification plan does not replace that check with manual fallback - -### Requirement: Behavior Is Preserved Across the Module Split -The compositor refactor SHALL preserve existing compositor behavior while changing only the internal module layout. - -The preserved behavior SHALL include: - -- startup and shutdown ordering -- Wayland XDG toplevel and popup lifecycle behavior -- XWayland lifecycle behavior and X11-specific quirks -- input forwarding and focus targeting behavior -- pointer-constraint activation, confinement, and cursor-hint behavior -- layer-shell render ordering, popup forwarding, and exclusive keyboard-focus behavior -- cursor visibility, positioning, and overlay rendering behavior -- presented-frame acquisition, retained-frame refresh, and DMA-BUF export behavior - -#### Scenario: Protocol and input behavior remain unchanged -- **GIVEN** the compositor implementation has been split into subsystem-oriented files -- **WHEN** Wayland clients, XWayland clients, and forwarded input are exercised through the defined verification plan -- **THEN** their externally observable behavior matches the pre-refactor compositor behavior - -#### Scenario: Presentation and export behavior remain unchanged -- **GIVEN** the compositor implementation has been split into subsystem-oriented files -- **WHEN** the presented-frame path and export path are exercised through the defined verification plan -- **THEN** retained-frame behavior, cursor overlay behavior, and DMA-BUF export behavior remain unchanged - -### Requirement: Behavior-Critical Quirks Stay With Their Subsystems -The compositor refactor SHALL preserve existing behavior-critical quirks, workarounds, and constraint comments adjacent to the subsystem logic they govern. - -This includes XWayland-specific activation/input quirks, destroy-listener restrictions, pointer-constraint confinement/cursor-hint rules, layer-shell popup forwarding ownership, and hook-allocation/lifetime constraints for protocol handlers. - -#### Scenario: XWayland quirks remain isolated and documented -- **GIVEN** XWayland handling is extracted -- **WHEN** the subsystem ownership is inspected -- **THEN** XWayland-specific activation and destroy-listener constraints remain documented next to the XWayland lifecycle logic -- **AND** those constraints are not moved into an unrelated shared helper or omitted during extraction diff --git a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md b/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md deleted file mode 100644 index c1bdeb55..00000000 --- a/openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md +++ /dev/null @@ -1,109 +0,0 @@ -## 1. Proposal Contract Lock and Compile-Safe Baseline - -- [x] 1.1 Confirm the proposal/design/spec artifacts remain the source of truth for module boundaries, migration order, behavior-preservation scope, and divergence handling before touching product code. -- [x] 1.2 Map the current `src/compositor/compositor_server.cpp` responsibilities to concrete extraction targets (`xdg`, `xwayland`, `input`, `focus`, `cursor`, `present`, `layer_shell`, `core`) and record any shared state or quirks that must remain centralized. -- [x] 1.3 Create the compile-safe declaration seam before behavior extraction: - - `src/compositor/compositor_state.hpp` for the central compositor state declaration and only the shared POD/state members stored on that object. - - `src/compositor/compositor_targets.hpp` for `InputTarget` plus cursor/surface-coordinate helper declarations shared by input/focus/cursor/present. - - `src/compositor/compositor_protocol_hooks.hpp` for `XdgToplevelHooks`, `XdgPopupHooks`, `XWaylandSurfaceHooks`, `LayerSurfaceHooks`, and `ConstraintHooks` listener structs. -- [x] 1.4 Update `src/compositor/CMakeLists.txt` as each new compositor translation unit is introduced so the build graph stays compile-complete throughout extraction. - -## 2. XDG and XWayland Lifecycle Extraction - -- [x] 2.1 Extract XDG toplevel and popup hook structs plus lifecycle handlers into `src/compositor/compositor_xdg.*`, preserving current listener registration, mapping, commit, and destroy behavior. -- [x] 2.2 Extract XWayland setup and lifecycle handlers into `src/compositor/compositor_xwayland.*`, preserving association/map/commit/destroy behavior and X11-specific workarounds/comments. -- [x] 2.3 Keep subsystem code operating on one central compositor implementation state object rather than duplicating ownership across extracted lifecycle modules. -- [x] 2.4 Keep XDG popup hook ownership authoritative in `src/compositor/compositor_xdg.*`, including popup hooks created from layer-shell `new_popup` forwarding. - -## 3. Input and Focus Separation - -- [x] 3.1 Extract input queue injection, compositor-thread event draining, and keyboard/pointer/button/axis dispatch into `src/compositor/compositor_input.*`. -- [x] 3.2 Extract input target resolution, focus switching, hit-testing, pointer-constraint activation/deactivation, confinement, cursor-hint application, and cursor-local coordinate helpers into `src/compositor/compositor_focus.*`. -- [x] 3.3 Preserve focus wakeups, resize request wakeups, auto/manual targeting behavior, pointer-confine/pointer-lock behavior, and layer/XWayland interactions while separating input flow from target resolution. - -## 4. Layer Shell Extraction - -- [x] 4.1 Extract layer-shell hook structs, lifecycle handlers, popup forwarding, keyboard-interactivity handling, and render-order integration into `src/compositor/compositor_layer_shell.*` after XDG popup ownership is isolated. -- [x] 4.2 Preserve layer-shell render ordering, popup routing into the XDG popup path, and exclusive keyboard-focus restoration behavior during extraction. - -## 5. Cursor and Presentation Extraction - -- [x] 5.1 Extract cursor theme setup, fallback cursor generation, cursor frame lookup, visibility/reset/hint/update, and overlay rendering into `src/compositor/compositor_cursor.*`. -- [x] 5.2 Extract presented-frame tracking, retained-frame refresh/reset, render-to-frame flow, and DMA-BUF export logic into `src/compositor/compositor_present.*`. -- [x] 5.3 Keep cursor and presentation modules wired through the same central compositor state object and preserve existing cursor/focus/presentation interactions. - -## 6. Core Consolidation and Facade Reduction - -- [x] 6.1 Consolidate wlroots bootstrap, backend/output/event loop setup, compositor thread lifecycle, and teardown into `src/compositor/compositor_core.*`. -- [x] 6.2 Keep `src/compositor/compositor_core.*` limited to startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown; assign every other extracted helper to a concrete subsystem or subsystem-owned narrow header. -- [x] 6.3 Reduce `src/compositor/compositor_server.cpp` and `src/compositor/compositor_server.hpp` to facade/public API responsibilities and high-level delegation only. - -## 7. Structural Verification and Behavior Preservation - -- [x] 7.1 Verify the structural acceptance criteria: - - `src/compositor/compositor_server.cpp` no longer contains all compositor subsystems. - - The extracted subsystem files exist and match the approved boundaries or documented close variants. - - `src/compositor/CMakeLists.txt` lists each new compositor translation unit. - - `src/compositor/compositor_core.*` contains only startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown responsibilities. - - No compositor `misc/helpers/utils` dumping-ground files were introduced. -- [x] 7.2 Run project-aligned build/test/static commands: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run test -p asan` when the full ASAN suite is supported by the local runtime environment - - `pixi run build -p quality` - - `ctest --preset asan -R "goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit" --output-on-failure` -- [x] 7.3 Run environment-sensitive compositor integration checks when supported: - - `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure` - - `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure` whenever `src/compositor/compositor_present.*` or DMA-BUF export code is touched; this check is mandatory and has no manual fallback -- [x] 7.4 When `goggles_auto_input_forwarding_x11` or `goggles_auto_input_forwarding_wayland` cannot run because automated CTest coverage is unavailable but an equivalent interactive runtime exists, record fallback manual evidence from the repository root with exact prerequisites, observations, and proof: - - prerequisite: active compositor-compatible X11 runtime for `./build/asan/tests/goggles_manual_input_x11` - - prerequisite: active compositor-compatible Wayland runtime with `WAYLAND_DISPLAY` available for `./build/asan/tests/goggles_manual_input_wayland` - - prerequisite: active compositor-compatible X11 runtime for `./build/asan/tests/goggles_manual_surface_selector_x11` - - prerequisite: active compositor-compatible Wayland runtime with `WAYLAND_DISPLAY` available for `./build/asan/tests/goggles_manual_surface_selector_wayland` - - record which automated check was skipped, why it was unavailable, what was observed, and where the evidence was stored -- [x] 7.5 Record a compositor behavior-preservation checklist covering: - - startup/shutdown order - - Wayland client handling - - XWayland client handling - - input forwarding - - focus targeting - - pointer constraints (activation, confine, cursor hints) - - layer-shell render ordering, popup routing, and exclusive keyboard focus - - cursor visibility/behavior - - presented-frame acquisition/export -- [x] 7.6 Run quirk-preservation checks and record the proving file for each one: - - XWayland re-activation before each key/pointer event - - the no-destroy-listener rule on `xsurface->surface` - - stable allocation for XDG, XWayland, and layer-surface hook containers - - layer-shell `new_popup` forwarding into XDG-owned popup hooks -- [x] 7.7 Run static spot checks for scope drift and policy-sensitive regressions in touched compositor files with explicit expectations: - - `grep -R --line-number '\bthrow\b' src/compositor --include='*.cpp' --include='*.hpp'` - - expected result: no new expected-failure exception paths in touched files - - `grep -R --line-number 'std::thread\|std::jthread' src/compositor --include='*.cpp' --include='*.hpp'` - - expected result: relocation of the existing compositor thread primitive into `compositor_core.*` is allowed, but touched files introduce no net-new thread owners, no additional thread primitives, and no new `std::thread` usage - - `grep -R --line-number 'Vk[A-Za-z0-9_]*' src/compositor --include='*.cpp' --include='*.hpp'` - - expected result: no new raw `Vk*` usage appears in touched compositor sources or headers - -## 8. Spec/Design Consistency and Apply Handoff - -- [x] 8.1 Confirm implementation matches `openspec/changes/refactor-compositor-server-modules/specs/compositor-module-layout/spec.md` and `openspec/changes/refactor-compositor-server-modules/design.md`. -- [x] 8.2 If extraction requires behavior divergence or a materially different module boundary, stop apply work and reconcile proposal/spec/design artifacts before continuing. -- [x] 8.3 Prepare `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` listing touched compositor files, preserved quirks/workarounds, any documented close-variant boundaries, exact verification results, skipped checks, fallback evidence locations, and the file proving each quirk remained adjacent to its subsystem. - -## 9. Requirement Traceability - -- [x] 9.1 Keep this traceability matrix current during apply so every requirement/scenario maps to implementation tasks and verification evidence. - -| Requirement / Scenario | Task IDs | Verification commands / evidence | -| --- | --- | --- | -| Compositor Server Facade Remains Stable / Public facade preserved after split | 6.1, 6.3, 7.1, 8.3 | 7.1 checklist item citing `src/compositor/compositor_server.cpp`, `src/compositor/compositor_server.hpp`, and `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md`; `pixi run build -p debug` | -| Single Compositor State Authority / Ownership remains centralized after extraction | 1.2, 1.3, 2.3, 5.3, 6.1, 8.3 | 7.1 checklist item citing `src/compositor/compositor_state.hpp` and extracted subsystem files in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md`; `pixi run build -p quality` | -| Responsibility-Oriented Compositor Modules / Localized edit surface for protocol lifecycle | 1.2, 1.3, 2.2, 6.2, 7.1, 8.3 | 7.1 checklist item citing `src/compositor/compositor_xwayland.*`, `src/compositor/compositor_core.*`, and `src/compositor/CMakeLists.txt` in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md`; `pixi run build -p debug` | -| Responsibility-Oriented Compositor Modules / Localized edit surface for input targeting | 1.2, 1.3, 3.1, 3.2, 6.2, 7.1, 8.3 | 7.1 checklist item citing `src/compositor/compositor_input.*`, `src/compositor/compositor_focus.*`, and `src/compositor/compositor_targets.hpp` in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md`; `pixi run build -p debug` | -| Extraction Contract Is Explicit for Apply / Migration order is explicit for implementation | 1.1, 1.3, 1.4, 2.1, 2.2, 3.1, 3.2, 4.1, 5.1, 5.2, 6.1, 7.1, 7.2, 8.2 | 7.1 checklist item citing `src/compositor/CMakeLists.txt` plus the ordered subsystem files in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md`; `pixi run build -p asan`; `pixi run build -p quality`; `ctest --preset asan -R "goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit" --output-on-failure` | -| Extraction Contract Is Explicit for Apply / Behavior preservation is verified explicitly | 7.2, 7.3, 7.4, 7.5, 8.3 | `pixi run test -p asan` when supported; `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure`; `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure`; fallback notes in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | -| Extraction Contract Is Explicit for Apply / Input-routing fallback is explicit and bounded | 7.3, 7.4, 8.3 | repo-root manual fallback commands from 7.4 plus prerequisites, observations, and proof recorded in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | -| Extraction Contract Is Explicit for Apply / Presentation and export verification stays mandatory | 5.1, 5.2, 7.3, 8.3 | `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure`; mandatory-check note recorded in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | -| Behavior Is Preserved Across the Module Split / Protocol and input behavior remain unchanged | 2.1, 2.2, 3.1, 3.2, 4.1, 4.2, 7.3, 7.4, 7.5, 8.3 | `ctest --preset asan -R "goggles_auto_input_forwarding_(x11|wayland)" --output-on-failure`; repo-root manual fallback commands from 7.4 when allowed; preserved-behavior checklist and observations in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | -| Behavior Is Preserved Across the Module Split / Presentation and export behavior remain unchanged | 5.1, 5.2, 7.3, 7.5, 8.3 | `ctest --preset asan -R "goggles_headless_integration(_png_exists)?" --output-on-failure`; preserved-behavior checklist and results in `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | -| Behavior-Critical Quirks Stay With Their Subsystems / XWayland quirks remain isolated and documented | 1.2, 2.2, 3.2, 4.1, 7.6, 8.3 | 7.6 checklist with proving files for XWayland, XDG, and layer-shell hook ownership plus `openspec/changes/refactor-compositor-server-modules/implementation-handoff.md` | diff --git a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/.openspec.yaml b/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/.openspec.yaml deleted file mode 100644 index 8f0b8699..00000000 --- a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-05 diff --git a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/design.md b/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/design.md deleted file mode 100644 index 17ea93bf..00000000 --- a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/design.md +++ /dev/null @@ -1,163 +0,0 @@ -## Context - -Goggles exposes a filter-chain C ABI in `src/render/chain/include/goggles_filter_chain.h` and currently consumes that header directly in internal C++ runtime code (notably backend integration). The goggles-interview plan requires a C++20-native API surface for internal/public C++ usage while preserving the C ABI boundary and C contract tests. - -Policy constraints from `docs/project_policies.md` apply: - -- Expected runtime failures MUST use `Result`-style propagation, not exceptions. -- App Vulkan code MUST keep existing `vk::` conventions. -- Ownership and lifetime must remain explicit and RAII-friendly. - -Wave 1 is intentionally narrow: migrate runtime lifecycle/runtime callsites while preserving ABI boundary files and C ABI contract tests. - -## Goals / Non-Goals - -**Goals:** - -- Provide a C++20 wrapper header for filter-chain lifecycle/runtime operations with strong typing and RAII ownership. -- Remove direct internal runtime includes/usages of `goggles_filter_chain.h` in wave-1 scope. -- Keep C ABI behavior and test coverage stable. -- Enforce no C-style surface leakage in migrated C++ callsites. - -**Non-Goals:** - -- Full C API parity in wave 1 (advanced controls/snapshot APIs may remain for wave 2). -- Removing or changing the C ABI contract. -- Reworking unrelated render pipeline behavior. - -## Decisions - -### 1) C++20 wrapper shape is value/RAII-first - -Decision: - -- Introduce a dedicated C++ wrapper header in render chain include surface. -- Expose a move-only chain handle type that owns the underlying `goggles_chain_t*` and destroys it in destructor. -- Provide lifecycle/runtime operations for wave 1: create, preset load, resize, record, stage policy get/set. - -Rationale: - -- Eliminates pointer-to-pointer lifecycle patterns at C++ callsites. -- Makes ownership and destruction deterministic. - -Alternatives considered: - -- Thin free-function wrapper around C ABI: rejected (still C-shaped API ergonomics). -- Full API parity in wave 1: deferred to reduce risk and migration churn. - -### 2) Error model mirrors project policy (`Result`, no exception-based expected failures) - -Decision: - -- Every fallible wrapper operation MUST return project `Result` (`tl::expected` or project alias); sentinel-only status-object APIs are not permitted for wrapper-facing operations. -- Wrapper APIs MUST NOT require exception handling for expected runtime failures. -- Errors MUST be handled or propagated, and logging MUST occur at subsystem boundaries without duplicate cascading logs. - -Rationale: - -- Aligns with repo-wide error handling policy and callsite patterns. - -Alternatives considered: - -- Exception-based wrapper API: rejected by policy and consistency requirements. - -### 3) Strongly typed enums/flags replace raw integer protocol in C++ surface - -Decision: - -- Define C++ enum classes / typed flag wrappers for stage and scale modes in wrapper-facing API. -- Hide C macro/status constants from normal C++ consumer paths. -- Wrapper-facing Vulkan signatures in app/runtime C++ paths MUST use `vk::` types and MUST NOT expose raw `Vk*` handles. - -Rationale: - -- Improves misuse resistance and readability. - -Alternatives considered: - -- Re-export C integer constants in C++ namespace: rejected as insufficiently typed. - -### 4) Boundary isolation is strict - -Decision: - -- C header remains only at ABI boundary implementation and C ABI contract tests in wave 1. -- Internal runtime code MUST include/use the C++ wrapper, not the C header. -- The wrapper header MUST NOT require direct runtime inclusion of `goggles_filter_chain.h` by C++ consumer modules. - -Rationale: - -- Preserves ABI while enabling C++ API evolution. - -Alternatives considered: - -- Temporary mixed runtime includes: rejected as it weakens migration guarantees. - -### 5) Blocker fallback uses adapter/shim, never runtime boundary regression - -Decision: - -- If hidden blocker callsites appear, introduce adapter/shim at boundary-facing edges while keeping runtime C++ wrapper contract intact. -- Direct reintroduction of raw C-header runtime use is not allowed. - -Rationale: - -- Maintains architectural direction under delivery pressure. - -Alternatives considered: - -- Temporary waiver to direct C-header runtime usage: rejected. - -### 6) Stage-order invariants are preserved - -Decision: - -- Wrapper stage policy APIs only control stage enablement and MUST preserve execution order invariants: `pre-chain -> effect chain -> output pass`. - -Rationale: - -- Prevents API migration from altering render pipeline semantics. - -Alternatives considered: - -- Allowing stage reordering through wrapper control: rejected as out of scope and incompatible with pipeline invariants. - -### 7) Enforcement strategy is explicit for wave 1 - -Decision: - -- Wave-1 enforcement SHALL use concrete command checks and test gates defined in `tasks.md` (include-boundary grep checks, policy grep checks, and preset-driven `ctest`/Pixi verification). -- A separate custom lint target is deferred unless wave-1 verification proves insufficient. - -Rationale: - -- Keeps enforcement deterministic for `/goggles-apply` without adding tooling scope in this change. - -Alternatives considered: - -- Introduce a dedicated lint target in wave 1: deferred to avoid expanding this proposal beyond wrapper migration scope. - -## Risks / Trade-offs - -- [Risk] Wrapper drifts into naming shim only -> Mitigation: spec-level requirement for typed/RAII/no-leakage API and review checks. -- [Risk] Hidden callsites increase migration scope -> Mitigation: explicit wave-1 scope lock + adapter/shim fallback rule. -- [Risk] Behavioral drift vs C ABI -> Mitigation: preserve C ABI contract tests and require parity checks in wave-1 validation. -- [Trade-off] Deferring advanced APIs to wave 2 leaves temporary dual-surface state -> Mitigation: explicitly document deferred scope and required follow-up. - -## Migration Plan - -1. Add wrapper header and minimal support plumbing for lifecycle/runtime operations. -2. Switch wave-1 runtime callsites from direct C header to wrapper surface. -3. Keep ABI boundary and C API contract tests unchanged. -4. Add include-boundary verification checks and run required build/test presets. -5. If blocker callsites are discovered, apply boundary-safe adapter/shim first and continue migration without runtime boundary regression. -6. If behavior/spec divergence is still required after adapter/shim attempt, halt apply work and reconcile proposal/design/spec artifacts before further implementation. - -Rollback strategy: - -- Revert wrapper adoption in affected runtime callsites while preserving unchanged C ABI contract behavior. -- Do not remove/alter C ABI files in rollback. - -## Open Questions - -- Which advanced APIs (controls/snapshots/diagnostics) should be promoted in wave 2 first? diff --git a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/proposal.md b/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/proposal.md deleted file mode 100644 index 975512e7..00000000 --- a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/proposal.md +++ /dev/null @@ -1,80 +0,0 @@ -## Why - -Internal C++ runtime code currently consumes the filter-chain C ABI directly via `goggles_filter_chain.h`, which exposes C-style ownership, status/macro ceremony, and weak typing in C++ callsites. We need a C++20-native public interface for Goggles C++ code so runtime integration is idiomatic, safer, and easier to maintain without changing the C ABI boundary. - -## Problem - -- C++ runtime callsites rely on C ABI signatures and macros (`struct_size`, status codes, pointer-to-pointer ownership), reducing readability and type safety. -- The current internal API shape does not express C++ ownership and misuse-resistant patterns. -- A direct C-header dependency in internal C++ code makes future C++-first API evolution harder. - -## Scope - -Wave 1 scope is intentionally narrow and brownfield-safe: - -- Add a C++20 wrapper header for the filter-chain API with strong typing and RAII-friendly ownership. -- Keep `goggles_filter_chain.h` as ABI boundary contract for C-facing implementation/tests. -- Migrate runtime internal C++ callsites (anchored at `src/render/backend/vulkan_backend.hpp`) away from direct C-header usage. -- Keep C API contract tests on the C header. - -## Non-goals - -- Replacing or removing the C ABI (`goggles_filter_chain.h`) in this change. -- Rewriting C API contract tests to use the C++ wrapper. -- Shipping full C API parity in wave 1 (advanced controls/snapshot APIs can be deferred). -- Introducing behavioral or rendering-path feature changes unrelated to API shape/migration. - -## What Changes - -- Add a C++20 header interface for filter-chain lifecycle/runtime operations (create, preset load, resize, record, stage policy) with strong C++ types and no C-style callsite ceremony. -- Define explicit ownership model (RAII handle) so C++ callsites do not use pointer-to-pointer lifecycle operations. -- Replace internal C++ runtime includes/usages of `goggles_filter_chain.h` within wave-1 scope. -- Preserve C ABI boundary implementation in `src/render/chain/filter_chain_c_api.cpp` and ABI contract tests. -- Define migration fallback policy: if blockers appear, use adapter/shim patterns without reintroducing direct runtime C-header usage. - -## Capabilities - -### New Capabilities -- `filter-chain-cpp-wrapper`: C++20-native wrapper contract for Goggles internal/public C++ usage of filter-chain runtime operations while preserving C ABI boundary. - -### Modified Capabilities -- None. - -## Risks - -- Wrapper design could drift into a naming-only shim if strong-typing/ownership constraints are not enforced. -- Migration may uncover hidden callsites or coupling that pressure temporary regressions to direct C-header usage. -- C++ wrapper and C ABI behavior could diverge without parity validation. - -## Validation Plan - -- Verify include boundary directly: - - `grep -R --line-number '#include "goggles_filter_chain.h"' src/render/backend` - - `grep -R --line-number '#include ' src/render/backend` - - Expected result after migration: no matches in runtime backend paths. -- Run policy-aligned verification commands: - - `pixi run build -p asan` - - `pixi run test -p asan` - - `pixi run build -p quality` -- Verify C ABI contract coverage remains green: - - `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` -- Add/adjust C++-focused checks validating no C-style surface leakage in wrapper-facing callsites. - -## Divergence Handling - -- Default path for hidden callsite blockers is adapter/shim remediation that preserves C++ runtime boundaries. -- If behavioral/spec divergence is required, apply work MUST halt and proposal/design/spec artifacts MUST be reconciled before implementation continues. - -## Impact - -- **Code modules/files**: `src/render/chain/include/` (new C++ wrapper header), `src/render/backend/` runtime callsites, potential wrapper support code in `src/render/chain/`. -- **C ABI boundary**: `src/render/chain/filter_chain_c_api.cpp` remains authoritative bridge and must preserve behavior. -- **Tests**: `tests/render/test_filter_chain_c_api_contracts.cpp` stays C-header based; additional C++ wrapper tests may be added for migrated surface. -- **Build/install**: render include/install rules may need to export/install the new C++ header alongside existing C header. -- **OpenSpec artifacts impacted**: - - Delta spec introduced by this change: `openspec/changes/cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md` - - Proposed living-spec sync target after archive/sync: `openspec/specs/filter-chain-cpp-wrapper/spec.md` -- **Policy-sensitive areas**: - - Error handling MUST remain `Result`-style for expected failures (no exception-driven runtime failure model). - - Ownership/lifetime semantics MUST be explicit and RAII-friendly. - - Vulkan API split and existing threading policies MUST remain unchanged. diff --git a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md b/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md deleted file mode 100644 index 2f5506f3..00000000 --- a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -## ADDED Requirements - -### Requirement: C++20 Wrapper Header for Filter Chain - -The project SHALL provide a C++20 header for filter-chain runtime integration that wraps the C ABI in `src/render/chain/include/goggles_filter_chain.h` for C++ consumers. - -#### Scenario: Wrapper header exists in public include surface -- **GIVEN** a wave-1 runtime C++ module integrates filter-chain operations -- **WHEN** a C++ runtime module needs filter-chain integration -- **THEN** it SHALL consume the C++ wrapper header instead of directly including `goggles_filter_chain.h` - -### Requirement: RAII Ownership for Chain Handle - -The C++ wrapper SHALL expose RAII ownership for the runtime chain handle so C++ callsites do not manage pointer-to-pointer destruction flows. - -#### Scenario: Handle destruction -- **GIVEN** a wrapper-owned chain instance holds a valid underlying C ABI handle -- **WHEN** a wrapper-owned chain instance goes out of scope -- **THEN** the underlying C ABI chain handle SHALL be destroyed exactly once -- **AND** no explicit pointer-to-pointer destroy call SHALL be required by normal C++ callsites - -### Requirement: Strongly Typed C++ Runtime Surface - -The C++ wrapper SHALL provide strongly typed C++ interfaces for wave-1 operations (create, preset load, resize, record, stage policy), and SHALL NOT expose C-style macro/size ceremony in normal usage. - -#### Scenario: Runtime operation callsite shape -- **GIVEN** wave-1 wrapper lifecycle/runtime APIs are available to runtime backend code -- **WHEN** a C++ runtime caller invokes wave-1 filter-chain operations -- **THEN** the callsite SHALL use typed C++ arguments/results -- **AND** it SHALL NOT require `struct_size` initialization macros, raw status-code switch logic, or pointer-to-pointer out-parameter ownership patterns - -### Requirement: Result-Based Error Propagation - -Every fallible wrapper operation SHALL use project `Result` (`tl::expected` or project alias) and SHALL NOT require exceptions for expected runtime failures. - -#### Scenario: Wrapper failure path -- **GIVEN** a wrapper operation reaches an expected runtime-failure condition -- **WHEN** a wrapper operation encounters an expected runtime failure -- **THEN** it SHALL return a failed project `Result` -- **AND** it SHALL NOT throw exceptions for that expected failure - -### Requirement: Single-Boundary Error Logging - -Wrapper migration paths SHALL ensure each error is handled or propagated, and duplicate cascading logs across wrapper/runtime stack layers SHALL NOT occur. - -#### Scenario: Error crosses wrapper/runtime boundary -- **GIVEN** a lower-layer error is propagated through wrapper/runtime call paths -- **WHEN** a wrapper operation fails and the error is propagated to a subsystem boundary -- **THEN** the error SHALL emit exactly one boundary log event with actionable context -- **AND** lower layers SHALL emit zero duplicate logs for the same failure event - -### Requirement: App-Side Vulkan Type Conventions - -Wrapper-facing app/runtime C++ signatures SHALL use `vk::` types and SHALL NOT expose raw `Vk*` handles to C++ consumers. - -#### Scenario: Wrapper API signature conventions -- **GIVEN** wrapper-facing app/runtime API declarations are authored for wave 1 -- **WHEN** wave-1 wrapper interfaces are declared -- **THEN** app/runtime-facing Vulkan parameters and return values SHALL use `vk::` conventions -- **AND** raw `Vk*` handle types SHALL remain confined to C ABI boundary internals -- **AND** wrapper-facing public headers SHALL contain no raw `Vk*` parameter or return signatures - -### Requirement: Boundary Isolation - -The C header SHALL remain the ABI boundary contract only. Internal runtime C++ modules in wave-1 scope SHALL NOT directly include or call the C ABI header, and wrapper adoption SHALL avoid re-exporting C-header ceremony into runtime callsites. - -#### Scenario: Internal runtime include boundary -- **GIVEN** runtime backend migration scope is `src/render/backend/` -- **WHEN** wave-1 migration is complete -- **THEN** runtime backend paths under `src/render/backend/` SHALL include the C++ wrapper header for filter-chain integration -- **AND** direct inclusion of `goggles_filter_chain.h` SHALL be absent from `src/render/backend/` for both quote and angle-bracket forms -- **AND** include-boundary verification command output SHALL show zero matches in that path - -### Requirement: Stage Policy Preserves Pipeline Order Invariants - -Wrapper stage policy controls SHALL only enable or disable known stages and SHALL preserve invariant stage order `pre-chain -> effect chain -> output pass`. - -#### Scenario: Stage policy update -- **GIVEN** wrapper stage policy APIs receive a supported stage-mask configuration -- **WHEN** a caller applies stage policy through the wrapper -- **THEN** the policy SHALL affect enablement only for supported stage bits -- **AND** execution order SHALL remain `pre-chain -> effect chain -> output pass` -- **AND** regression tests SHALL assert this ordering under both all-enabled and subset-enabled stage masks - -### Requirement: C ABI Continuity During Migration - -Wave-1 migration SHALL preserve C ABI behavior and validation coverage. - -#### Scenario: C ABI contract test continuity -- **GIVEN** C ABI contract tests are already present for `goggles_filter_chain.h` -- **WHEN** wave-1 wrapper migration is integrated -- **THEN** C ABI contract tests SHALL continue to compile and pass using `goggles_filter_chain.h` -- **AND** ABI boundary implementation files SHALL remain C-header based - -### Requirement: Non-Regressive Blocker Handling - -If migration encounters hidden blocker callsites, remediation SHALL preserve the C++ boundary in runtime code. - -#### Scenario: Unexpected blocker callsite -- **GIVEN** a hidden runtime callsite blocks immediate wrapper adoption in wave-1 migration -- **WHEN** a blocker prevents immediate direct wrapper adoption in runtime flow -- **THEN** migration SHALL use an adapter/shim approach that keeps runtime callsites on C++ wrapper boundaries -- **AND** it SHALL NOT reintroduce direct runtime dependence on `goggles_filter_chain.h` -- **AND** implementation evidence SHALL record blocker path, shim/adaptor entrypoint, and follow-up removal task identifier diff --git a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/tasks.md b/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/tasks.md deleted file mode 100644 index 2c2fa24b..00000000 --- a/openspec/changes/archive/2026-03-08-cpp20-filter-chain-hpp-wrapper/tasks.md +++ /dev/null @@ -1,63 +0,0 @@ -## 1. Wrapper API Surface (Wave 1) - -- [x] 1.1 Add C++20 wrapper header in `src/render/chain/include/` for lifecycle/runtime operations (create, preset load, resize, record, stage policy) with typed enums and RAII ownership. -- [x] 1.2 Implement wrapper support code in `src/render/chain/` that bridges to existing C ABI calls without changing ABI behavior. -- [x] 1.3 Ensure every fallible wrapper operation returns project `Result` (`tl::expected` alias), uses no expected-failure exceptions, and follows boundary logging rules (no duplicate cascading logs). -- [x] 1.4 Ensure wrapper-facing runtime signatures use `vk::` conventions and do not expose raw `Vk*` handles or C-header ceremony to C++ consumers. - -## 2. Runtime Integration Migration - -- [x] 2.1 Migrate wave-1 internal runtime callsites (starting from `src/render/backend/vulkan_backend.hpp` and related backend integration points) to include/use the new C++ wrapper. -- [x] 2.2 Keep `src/render/chain/filter_chain_c_api.cpp` as C ABI boundary on `goggles_filter_chain.h`. -- [x] 2.3 Keep `tests/render/test_filter_chain_c_api_contracts.cpp` on C header to preserve ABI contract validation. -- [x] 2.4 If hidden blocker callsites appear, apply adapter/shim strategy that preserves runtime C++ wrapper boundary (no direct runtime C-header regression) and record evidence fields: blocker path, shim/adaptor entrypoint, and follow-up removal task ID. -- [x] 2.5 Preserve stage policy behavior so wrapper migration does not alter invariant execution order (`pre-chain -> effect chain -> output pass`). - -## 3. Boundary Enforcement and Verification - -- [x] 3.1 Add/update include-boundary checks, with explicit command and expectation: - - `grep -R --line-number '#include "goggles_filter_chain.h"' src/render/backend` - - `grep -R --line-number '#include ' src/render/backend` - - Expected result after migration: no matches in `src/render/backend`. -- [x] 3.2 Run policy-aligned build/test verification: - - `pixi run build -p asan` - - `pixi run test -p asan` - - `pixi run build -p quality` -- [x] 3.3 Verify C ABI contract tests with explicit commands: - - `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` -- [x] 3.4 Add/update static policy checks with explicit commands: - - `grep -R --line-number '\bthrow\b' src/render/backend src/render/chain/include src/render/chain` - - `grep -R --line-number 'std::thread\|std::jthread' src/render/backend src/render/chain` - - Expected result after migration: no new expected-failure exception paths and no render-path thread primitives in migrated scope. -- [x] 3.5 Add/update wrapper-focused tests (for example under `tests/render/`) validating RAII destruction behavior and no pointer-to-pointer lifecycle leakage, and run: - - `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` -- [x] 3.6 Add/update wrapper logging tests that validate exactly one boundary log event and zero duplicate lower-layer logs for representative wrapper failure paths. - - `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` -- [x] 3.7 Add/update static signature checks for wrapper-facing headers: - - `grep -R --line-number 'Vk[A-Za-z0-9_]*' src/render/chain/include --include='*.hpp'` - - Expected result after migration: wrapper-facing filter-chain API surface has no raw `Vk*` parameter/return signatures. -- [x] 3.8 Add/update stage-policy regression tests asserting invariant order (`pre-chain -> effect chain -> output pass`) under all-enabled and subset-enabled masks. - - `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` - -## 4. Spec/Design Consistency and Handoff - -- [x] 4.1 Confirm implementation matches `openspec/changes/cpp20-filter-chain-hpp-wrapper/specs/filter-chain-cpp-wrapper/spec.md` scenarios and `design.md` decisions. -- [x] 4.2 If implementation requires behavior divergence, `/goggles-apply` MUST stop and wait for proposal/spec/design reconciliation before any further implementation; automatic implementation-side spec rewrites are forbidden. -- [x] 4.3 Prepare apply handoff note with touched files, verification evidence, and deferred wave-2 scope. - -## 5. Requirement Traceability - -- [x] 5.1 Keep this mapping updated during apply so each requirement/scenario has at least one implementation task and one verification command. - -| Requirement (spec.md) | Task IDs | Verification commands | -| --- | --- | --- | -| C++20 Wrapper Header for Filter Chain | 1.1, 2.1 | `pixi run build -p asan` | -| RAII Ownership for Chain Handle | 1.1, 1.2, 3.5 | `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` | -| Strongly Typed C++ Runtime Surface | 1.1, 1.4, 2.1, 3.5 | `ctest --preset asan -R "goggles_unit_tests" --output-on-failure`; `pixi run build -p quality` | -| Result-Based Error Propagation | 1.3, 3.4 | `grep -R --line-number '\bthrow\b' src/render/backend src/render/chain/include src/render/chain` | -| Single-Boundary Error Logging | 1.3, 3.6, 4.1 | `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` | -| App-Side Vulkan Type Conventions | 1.4, 3.7, 4.1 | `grep -R --line-number 'Vk[A-Za-z0-9_]*' src/render/chain/include --include='*.hpp'` | -| Boundary Isolation | 2.1, 2.2, 3.1 | `grep -R --line-number '#include "goggles_filter_chain.h"' src/render/backend`; `grep -R --line-number '#include ' src/render/backend` | -| Stage Policy Preserves Pipeline Order Invariants | 2.5, 3.8, 4.1 | `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` | -| C ABI Continuity During Migration | 2.2, 2.3, 3.3 | `ctest --preset asan -R "goggles_unit_tests" --output-on-failure` | -| Non-Regressive Blocker Handling | 2.4, 4.2, 4.3 | `grep -R --line-number 'blocker path, shim/adaptor entrypoint, and follow-up removal task ID' openspec/changes/cpp20-filter-chain-hpp-wrapper/tasks.md` | diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/.openspec.yaml b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/.openspec.yaml deleted file mode 100644 index 4b423f3a..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-08 diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/design.md b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/design.md deleted file mode 100644 index 71614ddf..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/design.md +++ /dev/null @@ -1,94 +0,0 @@ -## Context -The repository already has a `static-analysis` CI job in `.github/workflows/ci.yml` that runs `pixi run build -p quality`. There is no Semgrep dependency, no checked-in Semgrep config, and no local task that guarantees the same static-pattern checks run in both developer workflows and CI. - -The proposal constrains Semgrep tightly: it must be a blocking gate, it must be repo-controlled, and it must stay limited to policy clauses from `docs/project_policies.md` that are realistically Semgrep-enforceable. Formatting, naming, include ordering, many clang-tidy-style semantic checks, preset/lockfile checks, and runtime validation remain with the tools that already own them. - -## Goals / Non-Goals - -**Goals:** -- Add the deterministic `pixi run semgrep` entrypoint that local developers and CI both use. -- Keep all Semgrep configuration and rules checked into the repository. -- Encode only narrow, high-signal policy bans that Semgrep can enforce reliably. -- Prove each claimed Semgrep-enforced policy clause with tracked positive and negative verification inputs. -- Preserve the existing `pixi run build -p quality` gate instead of collapsing multiple tool responsibilities into one scanner. - -**Non-Goals:** -- No Semgrep registry or hosted rule dependencies. -- No broad “scan everything” policy sweep. -- No replacement of clang-tidy, formatting checks, lockfile checks, or runtime validation. -- No policy rewrite in this change. - -## Decisions -- Decision: Use a checked-in Semgrep config and checked-in local rule directory. - - The change will keep Semgrep configuration under repository control so local and CI behavior derive from the same committed inputs. - - Rationale: this satisfies the repo-controlled constraint and keeps proposal/apply verification observable. - - Alternative considered: use Semgrep registry defaults or a hosted policy. - - Rejected: that would make rule contents drift outside the repository and break deterministic review. - -- Decision: Run Semgrep through a Pixi-managed entrypoint and keep it inside the existing static-analysis lane. - - CI will invoke `pixi run semgrep`, the same repository-defined command that developers run locally, and the existing `pixi run build -p quality` step remains part of the lane. - - Rationale: one shared entrypoint reduces environment drift, while keeping clang-tidy coverage intact. - - Alternative considered: run Semgrep via a standalone GitHub Action or ad hoc binary install. - - Rejected: that would weaken determinism and duplicate environment management outside Pixi. - -- Decision: Admit Semgrep through Pixi source-of-truth files and record dependency-governance checks in the change. - - The apply work MUST update `pixi.toml` and `pixi.lock` together, and the change artifacts MUST make Semgrep rationale, license compatibility review, maintenance assessment, and team agreement explicit. - - Rationale: `docs/project_policies.md` requires Pixi to remain the dependency source of truth and requires new dependencies to carry admission evidence. - - Alternative considered: rely on transitive or environment-local Semgrep installation without lockfile or review metadata. - - Rejected: that would violate deterministic environment policy and make dependency admission non-reviewable. - -- Decision: Keep the initial Semgrep ruleset narrow and policy-derived. - - Initial rule coverage should focus on the approved high-signal bans from `docs/project_policies.md`: banned subsystem logging APIs, `using namespace` in headers, raw `new`/`delete`, raw `Vk*` in app code, `vk::Unique*`/`vk::raii::*` in app code, `static_cast(...)` on Vulkan result-returning calls, and direct `std::thread`/`std::jthread` in render or pipeline paths. - - The initial scan surface should be repository-managed C/C++ sources under `src/` and `tests/`, with per-rule path filters for narrower policy scopes. - - Claimed coverage must be backed by tracked verification inputs that exercise representative repo-style matches, non-matches, and documented exception paths. - - Rationale: these patterns are specific enough to gate automatically without trying to replace review judgment. - - Alternative considered: add every clause that might be technically toolable. - - Rejected: broad scope would increase noise, duplicate stronger tooling, and make ownership of failures unclear. - -- Decision: Path-scope subsystem-sensitive rules instead of pretending policy is uniform everywhere. - - Rules that depend on policy scope, such as raw `Vk*` bans or render-path threading bans, must target only the directories where the policy applies and exclude known exceptions such as `src/capture/vk_layer/`. - - Rationale: subsystem-specific policy is already part of the repository contract and must remain explicit in the static checks. -- Alternative considered: apply the same rule to all C/C++ code. - - Rejected: that would create false positives against allowed subsystem-specific usage. - -## Verification contract -- Baseline gates: - - `pixi run semgrep` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `openspec validate static-check-add-semgrep --strict` - - `pixi run bash -c 'command -v semgrep && semgrep --version'` -- Required rule-verification inputs: - - tracked positive and negative verification inputs for each claimed policy clause - - representative repo-style patterns for targeted code shapes, not only toy single-line snippets - - scoped verification inputs covering intended directories and documented exception paths -- Environment-sensitive checks: - - None. -- Manual fallback: - - None for the static Semgrep gate. -- Mandatory checks with no fallback: - - `pixi run semgrep` - - `pixi run build -p quality` -- Pass criteria: - - `pixi run bash -c 'command -v semgrep && semgrep --version'` reports a Semgrep binary from the Pixi-managed environment and a version surface that matches the locked dependency. - - `pixi run semgrep` exits successfully using only checked-in Semgrep configuration and rules. - - Each claimed policy clause is proven by tracked positive inputs that fail when the rule is active and tracked negative inputs that remain clean. - - Positive verification inputs cover representative repo-style code shapes for the claimed policy clause. - - CI records the Semgrep path provenance and version surface before running the blocking scan. - - The static-analysis lane still includes `pixi run build -p quality` after Semgrep is added. - - Scoped rules do not apply to directories outside their policy boundary. - -## Risks / Trade-offs -- False-positive drift from path mistakes or overly generic patterns -> mitigate by keeping the initial rule set small and path-scoped. -- Added CI cost in the static-analysis lane -> mitigate by reusing the existing job and keeping Semgrep limited to targeted code roots. -- Semgrep may still miss semantic policy violations that humans catch -> accept this boundary explicitly and keep review-only rules out of scope. - -## Migration Plan -1. Add proposal artifacts for `ci` and `build-system` behavior changes. -2. Add repo-controlled Semgrep dependency/configuration through `pixi.toml` and `pixi.lock`, and document dependency-admission evidence. -3. Wire the existing static-analysis lane to run the Semgrep gate alongside the current quality build. -4. Add tracked verification inputs that prove the checked-in rules catch the claimed policy violations, allow approved counterexamples, and respect documented scope exceptions. -5. Verify the checked-in rules only cover the approved high-signal policy bans and do not replace stronger existing tooling. - -## Open Questions -- None blocking. Exact rule file names and final path globs can be decided during apply as long as they preserve the scoped rule categories and deterministic entrypoint contract. diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/proposal.md b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/proposal.md deleted file mode 100644 index 9a0d9232..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/proposal.md +++ /dev/null @@ -1,78 +0,0 @@ -# Change: Add Repo-Controlled Semgrep to the Static Analysis Workflow - -## Why -The current static-analysis lane only enforces the Pixi-managed `build -p quality` flow, which leaves several high-signal policy bans dependent on manual review even though they are pattern-checkable. That gap makes enforcement inconsistent and slows review on rules that should fail deterministically before humans look at the patch. - -The requested change adds a repo-controlled Semgrep gate that complements, rather than replaces, the existing clang-tidy and build/test gates. It keeps the scan narrow so Semgrep only owns policy clauses it can enforce reliably. - -## Problem -- The repository has no Semgrep configuration, task entrypoint, or CI gate today. -- Several policy bans in `docs/project_policies.md` are straightforward static-pattern checks, but they are not enforced consistently by the current toolchain. -- Without a shared repo-controlled entrypoint, local and CI behavior would drift if Semgrep were added ad hoc. - -## Scope -- Add a checked-in Semgrep ruleset and repository-controlled config. -- Add the Pixi-managed `pixi run semgrep` entrypoint so local and CI usage share the same tool and config surface. -- Add Semgrep as a blocking step in the existing static-analysis workflow without removing the current `pixi run build -p quality` gate. -- Limit Semgrep to high-signal policy bans that are realistically Semgrep-enforceable. - -## What Changes -- Add a repo-controlled Semgrep ruleset for approved high-signal policy bans. -- Extend the static-analysis workflow to run Semgrep as a blocking CI gate. -- Provide the deterministic local `pixi run semgrep` entrypoint that uses the same checked-in config and rules as CI. -- Add tracked verification inputs that prove each claimed Semgrep-enforced policy clause both matches representative violating code and does not flag approved in-scope or exception cases. -- Admit Semgrep through Pixi source-of-truth files, keeping `pixi.toml` and `pixi.lock` in sync. -- Scope Semgrep to policy-derived checks such as banned logging APIs, banned ownership patterns, banned Vulkan wrapper usage, and banned render-path thread primitives. -- Keep formatting, naming, include ordering, lockfile/preset checks, and runtime-validation rules with their existing tools instead of duplicating them in Semgrep. - -## Capabilities - -### New Capabilities -- None. - -### Modified Capabilities -- `ci`: static-analysis behavior changes to include a blocking, repo-controlled Semgrep gate alongside the existing quality build. -- `build-system`: repository tooling changes to provide deterministic Semgrep execution and checked-in rule sources for both local and CI use. - -## Non-Goals -- Replacing `clang-format`, `clang-tidy`, preset builds, or test gates with Semgrep. -- Using Semgrep registry defaults, hosted defaults, or any non-repository rule source. -- Turning Semgrep into a broad generic lint pass for every tool-enforceable policy clause. -- Encoding review-only, runtime-only, or heavily semantic policy rules into static pattern checks. - -## Risks -- Over-broad rules could create noisy failures and reduce trust in the gate. -- Poor path scoping could flag allowed code in `src/capture/vk_layer/` or other exceptions where policy differs by subsystem. -- CI runtime will increase if the scan is not kept targeted. - -## Dependency Admission -- Rationale: Semgrep is admitted to close a deterministic enforcement gap for high-signal policy bans that are currently review-only in practice. -- License compatibility check: the change MUST confirm the Semgrep package/license is acceptable for repository use before apply is complete. -- Maintenance assessment: the change MUST confirm the selected Semgrep package source is actively maintained enough for a blocking CI gate. -- Team agreement: the dependency addition MUST be explicit in code review before apply is considered complete. - -## Validation Plan -- `openspec validate static-check-add-semgrep --strict` -- update `pixi.lock` in sync with `pixi.toml` -- `pixi run bash -c 'command -v semgrep && semgrep --version'` -- verify each claimed policy clause has tracked positive and negative Semgrep verification inputs checked into the repository -- verify positive inputs include representative repo-style patterns for the targeted code shape, not only toy single-line snippets -- verify scoped rules cover the directories they claim to govern and do not flag documented exception paths -- `pixi run semgrep` -- `pixi run build -p quality` -- confirm CI logs the Semgrep path provenance and version surface before the blocking scan runs -- confirm task success is based on the claimed rule coverage passing those verification inputs, not only on the aggregate Semgrep command exiting successfully - -## Impact -- Affected specs: - - `ci` - - `build-system` -- Affected files/modules (expected): - - `.github/workflows/ci.yml` - - `pixi.toml` - - `.semgrep.yml` - - `.semgrep/rules/` - - tracked Semgrep verification inputs under `tests/` -- Policy-sensitive impacts: - - logging, ownership, Vulkan API split, Vulkan lifetime helpers, and render-path threading are touched only as static rule sources - - no runtime behavior or subsystem ownership model changes are intended diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/build-system/spec.md b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/build-system/spec.md deleted file mode 100644 index e3e4556b..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/build-system/spec.md +++ /dev/null @@ -1,49 +0,0 @@ -# build-system Spec Delta - -## ADDED Requirements - -### Requirement: Deterministic Semgrep Tooling - -The build system SHALL provide a Pixi-managed Semgrep toolchain and checked-in rule source so local and CI static analysis use the same deterministic inputs. - -#### Scenario: Pixi provides Semgrep for local and CI execution -- **GIVEN** the repository defines lint and developer workflow tooling in `pixi.toml` -- **WHEN** contributors or CI invoke the Semgrep entrypoint -- **THEN** the Semgrep binary SHALL come from the repository-managed Pixi environment -- **AND** the same Semgrep version surface SHALL be used locally and in CI - -#### Scenario: Semgrep provenance is observable during verification -- **GIVEN** the repository verifies the Semgrep tool surface before enforcing the gate -- **WHEN** maintainers inspect the Semgrep path and version under the repository-managed workflow -- **THEN** the resolved Semgrep executable SHALL originate from the Pixi-managed environment -- **AND** the reported version SHALL match the locked local and CI Semgrep surface - -#### Scenario: Pixi source-of-truth files stay synchronized -- **GIVEN** the repository adds Semgrep to the Pixi-managed tool surface -- **WHEN** the change updates Semgrep dependency configuration -- **THEN** `pixi.toml` SHALL declare the dependency version surface -- **AND** `pixi.lock` SHALL be updated in sync with that change - -#### Scenario: Dependency admission remains reviewable -- **GIVEN** the repository adds Semgrep as a new dependency for the static-analysis workflow -- **WHEN** the proposal and apply artifacts are reviewed -- **THEN** they SHALL include dependency rationale, license compatibility review, maintenance assessment, and team agreement evidence -- **AND** the dependency SHALL NOT be treated as implicitly admitted just because the tool is easy to install - -#### Scenario: Initial scan roots stay limited to repository-managed C and C++ code -- **GIVEN** the repository enables Semgrep policy checks -- **WHEN** the `pixi run semgrep` entrypoint runs in its initial configuration -- **THEN** it SHALL scan repository-managed code under `src/` and `tests/` -- **AND** it SHALL use narrower path filters for rules that apply only to selected subsystems - -#### Scenario: Semgrep rule sources are checked into the repository -- **GIVEN** the repository enables Semgrep policy checks -- **WHEN** the Semgrep entrypoint runs -- **THEN** it SHALL load configuration and rules from checked-in repository files -- **AND** it SHALL NOT depend on registry defaults or hosted rule configuration - -#### Scenario: Subsystem-sensitive rules stay path-scoped -- **GIVEN** some policy bans only apply to selected Goggles subsystems -- **WHEN** the repository defines Semgrep rules for Vulkan API split or render-path threading -- **THEN** those rules SHALL scope to the directories where the policy applies -- **AND** they SHALL exclude directories with explicit policy exceptions such as `src/capture/vk_layer/` diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/ci/spec.md b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/ci/spec.md deleted file mode 100644 index cab301f1..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/specs/ci/spec.md +++ /dev/null @@ -1,34 +0,0 @@ -# ci Spec Delta - -## Purpose -Define the CI behavior change that adds Semgrep as a repo-controlled blocking gate in the existing static-analysis workflow. - -## ADDED Requirements - -### Requirement: Repo-Controlled Semgrep Gate - -The CI system SHALL run a repo-controlled Semgrep scan as a blocking step in the static-analysis workflow. - -#### Scenario: Static-analysis job runs checked-in Semgrep rules -- **GIVEN** the repository defines a Semgrep configuration and local ruleset -- **WHEN** the static-analysis workflow runs in CI -- **THEN** it SHALL execute Semgrep from repository-checked-in configuration -- **AND** it SHALL fail the job when Semgrep reports a blocking finding - -#### Scenario: Local and CI Semgrep use the same repository entrypoint -- **GIVEN** the repository exposes `pixi run semgrep` -- **WHEN** contributors run the local command and CI runs the static-analysis job -- **THEN** both flows SHALL use the same repository-defined entrypoint -- **AND** both flows SHALL evaluate the same checked-in ruleset - -#### Scenario: Semgrep complements the existing quality gate -- **GIVEN** the static-analysis workflow already runs `pixi run build -p quality` -- **WHEN** Semgrep is added to the workflow -- **THEN** the workflow SHALL retain the existing quality build gate -- **AND** Semgrep SHALL complement rather than replace that gate - -#### Scenario: Semgrep scope stays limited to approved policy bans -- **GIVEN** the repository policy identifies both tool-enforceable and review-only rules -- **WHEN** the CI Semgrep gate evaluates the repository -- **THEN** it SHALL cover only the approved high-signal policy bans selected for Semgrep -- **AND** it SHALL NOT duplicate formatting, naming, include-order, lockfile/preset, or runtime-validation checks that are owned by other tools diff --git a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/tasks.md b/openspec/changes/archive/2026-03-08-static-check-add-semgrep/tasks.md deleted file mode 100644 index bca9bf90..00000000 --- a/openspec/changes/archive/2026-03-08-static-check-add-semgrep/tasks.md +++ /dev/null @@ -1,29 +0,0 @@ -## 1. Spec & Design - -- [x] 1.1 Add `ci` delta requirements for a repo-controlled blocking Semgrep gate in the existing static-analysis workflow. -- [x] 1.2 Add `build-system` delta requirements for Pixi-managed deterministic Semgrep tooling and path-scoped rule sources. -- [x] 1.3 Keep `design.md` aligned with the narrow Semgrep boundary: checked-in rules only, approved high-signal bans only, and no duplication of stronger existing tooling. - -## 2. Workflow & Configuration - -- [x] 2.1 Add a checked-in Semgrep configuration and local rule set for the approved policy-derived bans. -- [x] 2.2 Update `pixi.toml` and `pixi.lock` together to provide `pixi run semgrep` for local and CI use. -- [x] 2.3 Update `.github/workflows/ci.yml` so the `static-analysis` job runs the Semgrep gate without removing `pixi run build -p quality`. -- [x] 2.4 Limit the initial Semgrep scan to `src/` and `tests/`, then scope subsystem-sensitive rules to the correct directories and exclude known policy exceptions such as `src/capture/vk_layer/`. - -## 3. Dependency Governance - -- [x] 3.1 Record Semgrep dependency rationale, license compatibility review, maintenance assessment, and team agreement in the implementation or PR evidence. -- [x] 3.2 Verify the selected Semgrep package is admitted through Pixi source-of-truth files rather than an environment-local install path. - -## 4. Verification - -- [x] 4.1 Run `openspec validate static-check-add-semgrep --strict`. -- [x] 4.2 Run `pixi run bash -c 'command -v semgrep && semgrep --version'` and confirm the resolved binary comes from the Pixi-managed environment. -- [x] 4.3 Add tracked positive and negative Semgrep verification inputs for each claimed policy clause. -- [x] 4.4 Verify the positive inputs cover representative repo-style patterns for the targeted code shapes, not only toy single-line snippets. -- [x] 4.5 Verify scoped rules cover the directories they claim to govern and do not flag documented exception paths. -- [x] 4.6 Run `pixi run semgrep` locally and confirm checked-in rules catch the positive inputs, allow the negative inputs, and use checked-in rules only. -- [x] 4.7 Run `pixi run build -p quality` and confirm the Semgrep addition does not replace the existing quality gate. -- [x] 4.8 Record CI evidence showing the same Semgrep version surface and path provenance before the blocking scan runs. -- [x] 4.9 Record any no-fallback static-check expectations and rule-coverage evidence in the implementation notes or PR description when the change is applied. diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/.openspec.yaml b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/.openspec.yaml deleted file mode 100644 index 4b423f3a..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-08 diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/design.md b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/design.md deleted file mode 100644 index 85dfbf3a..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/design.md +++ /dev/null @@ -1,115 +0,0 @@ -## Context - -Goggles moved from the old layer-oriented performance interpretation to a pure compositor capture path, -but the Application performance panel still derives its numbers from ImGui-layer frame deltas and a -viewer-sampled source update hook. That architecture mismatch is why the UI can report `60 FPS` -while the captured game is presenting much faster. - -This change crosses `src/ui`, `src/app`, and `src/compositor`, so the design MUST establish one -authoritative metric source that matches the compositor capture model and removes the obsolete -layer-era timing path entirely. - -## Goals / Non-Goals - -**Goals:** -- Make `Game FPS` reflect active captured game-surface presents or commits only. -- Make `Compositor Latency` reflect commit-to-capture delay. -- Remove the old `Render` / `Source` FPS bookkeeping, plots, and hooks once the new metrics are in - place. -- Keep the new metric path simple enough that `/goggles-apply` can implement it without transitional - compatibility code. - -**Non-Goals:** -- Measuring Goggles viewer FPS. -- Measuring end-to-end display latency or input latency. -- Introducing new external dependencies, packaging changes, or persistent telemetry systems. - -## Decisions - -### Decision: Move metric source-of-truth to compositor-side capture telemetry - -The active metric source-of-truth SHALL move out of `ImGuiLayer` timing buffers and into the -compositor capture path. - -Rationale: -- The compositor already observes the active surface commit boundary and the capture publication - boundary. -- The UI cannot observe game cadence honestly from its own render loop. - -Alternatives considered: -- Keep computing metrics in `ImGuiLayer`: rejected because it preserves the misleading viewer-loop - sampling model. -- Compute metrics in `Application` from `get_presented_frame()` polling: rejected because it only - sees retained frames after sampling loss. - -### Decision: Publish a bounded runtime metrics snapshot from compositor to app/UI - -The compositor path SHALL maintain the rolling state needed for `Game FPS` and `Compositor -Latency`, and the application/UI path SHALL consume a compact snapshot rather than recomputing from -legacy frame histories. - -That snapshot contract SHALL be owned by a compositor-to-application runtime boundary outside -`src/ui`. The UI SHALL only render already-computed values and SHALL NOT depend on compositor event -types, commit semantics, or compositor-internal storage details. - -Rationale: -- A single bounded snapshot prevents duplicate timing logic across compositor, app, and UI. -- It keeps ownership aligned with where the authoritative events occur. - -Alternatives considered: -- Expose raw event streams to the UI: rejected because it recreates timing logic in the wrong layer. -- Push values directly into `ImGuiLayer` through ad-hoc callbacks: rejected because it couples UI to - compositor internals without a stable runtime contract. -- Reuse a UI-shaped data model as the shared contract: rejected because it makes compositor/app - boundaries depend on presentation concerns. - -### Decision: Count only active captured game-surface commits toward `Game FPS` - -`Game FPS` SHALL ignore viewer redraw cadence, secondary surfaces, and non-game refresh paths such -as unrelated compositor updates. - -Rationale: -- The user explicitly wants a gamer-facing metric. -- Counting secondary or viewer-side activity would reintroduce the same semantic drift the change is - trying to remove. - -Alternatives considered: -- Count every compositor-captured frame: rejected because overlay, cursor, or other compositor - refreshes can inflate the metric. - -### Decision: Remove legacy metric plumbing completely after replacement - -The implementation SHALL delete the old `Render` / `Source` FPS plots, timing buffers, -`notify_source_frame` path, and any other now-unused code instead of hiding them. - -Rationale: -- The user requested a direct replacement with no deprecation note or compatibility fallback. -- Keeping the old path would preserve misleading terminology and dead maintenance burden. - -Alternatives considered: -- Keep a hidden fallback: rejected because it violates the no-legacy-remnants constraint. - -## Risks / Trade-offs - -- [Active-surface ambiguity] -> Require the metric source to key off the currently captured game - surface only and define that in the delta specs. -- [Indirect dead-code survivors] -> Require tasks to remove old labels, plots, timing buffers, and - update hooks together, then verify no legacy references remain. -- [Metric instability from very short sampling] -> Use a bounded rolling aggregation contract owned - by the compositor metric snapshot rather than UI-frame deltas. - -## Migration Plan - -1. Add compositor-side metric collection for active-surface cadence and commit-to-capture latency. -2. Publish the runtime metrics snapshot to the application/UI path. -3. Replace the Application performance panel labels and displayed values. -4. Remove legacy `Render` / `Source` metric state, plots, and update hooks. -5. Verify the new panel behavior and confirm legacy metric paths are gone. - -Rollback strategy: -- Revert the change as one unit if the new metric contract proves incorrect; do not preserve a dual - path in-tree. - -## Open Questions - -- None. diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/proposal.md b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/proposal.md deleted file mode 100644 index 9a204562..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/proposal.md +++ /dev/null @@ -1,93 +0,0 @@ -## Why - -The current `Application -> Performance` panel still reports legacy `Render` and `Source` FPS -metrics that were designed for the old layer-based approach. In the pure compositor path those -numbers no longer describe what players care about, so the UI is misleading exactly when users use -Goggles as intended. - -This change updates the performance panel now so it reports gamer-facing metrics aligned with the - current compositor architecture and removes the obsolete metric plumbing instead of carrying both - systems forward. - -## Problem - -- The current `Render` FPS reports Goggles viewer cadence, not game cadence. -- The current `Source` FPS reports viewer-sampled source updates, not active game presents. -- Legacy frame-history plots and timing hooks keep layer-era measurement code alive after the - underlying product model changed. - -## Scope - -- Replace the legacy `Render` / `Source` FPS panel entries with `Game FPS` and - `Compositor Latency`. -- Define `Game FPS` as presents or commits from the currently captured game surface only. -- Define `Compositor Latency` as commit-to-capture delay. -- Remove the old plots, timing buffers, source-frame notify path, and any dead code that exists - only for the retired metrics. - -## What Changes - -- Replace the visible Application performance metrics with gamer-facing labels and semantics. -- Source `Game FPS` from the active captured surface commit/present cadence instead of the viewer - loop. -- Source `Compositor Latency` from compositor commit-to-capture timing instead of layer-era frame - delta sampling. -- Delete obsolete `Render` / `Source` FPS plots and their supporting timing state and update hooks. -- Keep the new metric path direct and final: no deprecation banner, no hidden compatibility mode, - and no legacy fallback path. - -## Capabilities - -### New Capabilities -- None. - -### Modified Capabilities -- `app-window`: the Application performance panel requirements change from legacy viewer/source FPS - reporting to `Game FPS` and `Compositor Latency` with gamer-facing semantics. -- `compositor-capture`: the compositor capture path requirements change to expose the timing data - needed for active-surface present cadence and commit-to-capture latency reporting. - -## Non-goals - -- Keep legacy `Render` / `Source` FPS metrics available anywhere in the UI or code path. -- Rebrand Goggles viewer FPS as `Game FPS`. -- Expand `Compositor Latency` into end-to-end display, presentation, or input latency. -- Add migration banners, deprecation notes, or hidden fallback behavior for the old metrics. - -## Impact - -- Affected modules: `src/ui`, `src/app`, and `src/compositor`. -- Likely affected files: `src/ui/imgui_layer.cpp`, `src/ui/imgui_layer.hpp`, - `src/app/application.cpp`, `src/compositor/compositor_present.cpp`, - `src/compositor/compositor_xdg.cpp`, and `src/compositor/compositor_xwayland.cpp`. -- Impacted OpenSpec specs: `openspec/specs/app-window/spec.md` and - `openspec/specs/compositor-capture/spec.md`. -- No dependency, packaging, or external API changes are expected. - -## Risks - -- The compositor metric path could accidentally count non-game updates if active-surface ownership - is not defined precisely. -- The cleanup could leave hidden dead code behind if the old timing path has indirect consumers. -- The UI can become more honest but less stable if timing windows are not defined consistently. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `pixi run test -p asan` -- Environment-sensitive checks: - - `pixi run test -p test` when the local runtime supports the compositor/UI path -- Manual fallback: - - allowed only for validating the visible performance panel behavior when automated UI coverage is - unavailable - - record prerequisites, observations, and proof location -- Mandatory checks with no fallback: - - build and static-analysis gates above -- Pass criteria: - - the Application performance UI exposes only `Game FPS` and `Compositor Latency` - - the legacy labels and their dead code path are absent diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/app-window/spec.md b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/app-window/spec.md deleted file mode 100644 index 7939862c..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/app-window/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -## ADDED Requirements - -### Requirement: Application Performance Panel Reports Gamer-Facing Metrics - -The Application performance panel SHALL report `Game FPS` and `Compositor Latency` instead of the -legacy `Render` and `Source` FPS metrics. - -The panel SHALL display compositor-provided `Game FPS` and `Compositor Latency` values. - -#### Scenario: Performance panel shows replacement metrics -- **WHEN** the Application performance panel is rendered -- **THEN** it SHALL display `Game FPS` and `Compositor Latency` -- **AND** it SHALL NOT display `Render` FPS or `Source` FPS - -#### Scenario: Legacy performance plots are removed -- **WHEN** the Application performance panel is rendered after this change -- **THEN** it SHALL NOT render the legacy frame-history plots associated with `Render` and `Source` - FPS - -#### Scenario: Game FPS follows active captured game surface only -- **GIVEN** a game surface is the current capture target -- **WHEN** the performance panel reports `Game FPS` -- **THEN** the reported value SHALL come from the compositor-provided metric snapshot for that - capture target only diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/compositor-capture/spec.md b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/compositor-capture/spec.md deleted file mode 100644 index 4e18ca9a..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/specs/compositor-capture/spec.md +++ /dev/null @@ -1,26 +0,0 @@ -## ADDED Requirements - -### Requirement: Compositor Capture Publishes Gameplay Metrics - -The compositor capture path SHALL publish the timing data required for the Application performance -panel to report `Game FPS` and `Compositor Latency`. - -`Game FPS` SHALL be derived from presents or commits for the currently captured game surface only. -`Compositor Latency` SHALL be derived from the interval between an eligible active-surface commit -and the corresponding compositor capture publication. - -#### Scenario: Active surface commit updates Game FPS source -- **GIVEN** a game surface is the current capture target -- **WHEN** that surface produces an eligible commit for capture -- **THEN** the compositor capture path SHALL update the `Game FPS` metric source from that event - -#### Scenario: Non-target surface does not change Game FPS source -- **GIVEN** a different surface is not the current capture target -- **WHEN** that non-target surface commits -- **THEN** the compositor capture path SHALL NOT count that event toward `Game FPS` - -#### Scenario: Commit-to-capture latency is published -- **GIVEN** an eligible active-surface commit produces a captured frame -- **WHEN** the compositor publishes the captured frame for viewer consumption -- **THEN** the compositor capture path SHALL publish `Compositor Latency` for that commit as the - elapsed commit-to-capture interval diff --git a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/tasks.md b/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/tasks.md deleted file mode 100644 index e1ed9899..00000000 --- a/openspec/changes/archive/2026-03-08-ui-replace-performance-metrics/tasks.md +++ /dev/null @@ -1,19 +0,0 @@ -## 1. Compositor metric source - -- [x] 1.1 Add compositor-owned metric state that tracks active-surface gameplay cadence and commit-to-capture timing. -- [x] 1.2 Update the active-surface commit path in the XDG and XWayland compositor hooks so only the current capture target contributes to `Game FPS`. -- [x] 1.3 Publish `Compositor Latency` from the commit event through captured-frame publication without introducing a legacy timing fallback path. -- [x] 1.4 Introduce or update a compositor-to-application runtime metrics snapshot contract outside `src/ui` so the UI consumes already-computed values without depending on compositor internals. - -## 2. App and UI integration - -- [x] 2.1 Thread the compositor metric snapshot through the application runtime to the Application performance panel. -- [x] 2.2 Replace the `Render` / `Source` FPS UI with `Game FPS` and `Compositor Latency` in the Application performance panel. -- [x] 2.3 Remove the legacy performance plots, frame-history buffers, source-frame notify hook, and any now-unused code tied only to the retired metrics. - -## 3. Contract and verification - -- [x] 3.1 Verify the implementation still matches the `app-window` and `compositor-capture` delta specs with no legacy labels or hidden fallback metric path left behind. -- [x] 3.2 Run `pixi run build -p debug` and address any compile or contract drift issues. -- [x] 3.3 Run `pixi run build -p asan`, `pixi run test -p asan`, and `pixi run build -p quality`. -- [x] 3.4 Run `pixi run test -p test` when the compositor/UI runtime is available; otherwise perform a manual visible-panel check, record prerequisites and observations, and attach proof location for the fallback. diff --git a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/.openspec.yaml b/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/.openspec.yaml deleted file mode 100644 index 4b423f3a..00000000 --- a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-08 diff --git a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/design.md b/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/design.md deleted file mode 100644 index d29e9307..00000000 --- a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/design.md +++ /dev/null @@ -1,283 +0,0 @@ -## Context - -The starting point for this change had `src/render/backend/vulkan_backend.cpp` owning Vulkan -instance/device bring-up, swapchain and headless target lifetime, DMA-BUF import and explicit sync -ingress, filter-chain runtime ownership, async preset rebuild, and final render/present -orchestration behind one public `VulkanBackend` facade. - -This refactor is brownfield and behavior-preserving. Existing runtime contracts remain anchored by: - -- `docs/project_policies.md` -- `openspec/specs/render-pipeline/spec.md` -- `openspec/specs/headless-mode/spec.md` -- `openspec/specs/goggles-filter-chain/spec.md` -- `tests/render/test_filter_boundary_contracts.cpp` -- `docs/dmabuf_sharing.md` - -`docs/project_policies.md` remains authoritative for Vulkan ownership and lifetime rules. The stale -`vk::Unique*` wording in `openspec/specs/render-pipeline/spec.md` is not part of this refactor -contract and MUST be reconciled before apply depends on that living-spec language for ownership -semantics. - -The chosen design reduces edit surface without changing `Application` integration, -headless/windowed execution, DMA-BUF explicit sync semantics, or filter-chain stage policy and -async reload behavior. - -## Goals / Non-Goals - -**Goals:** - -- Preserve `VulkanBackend` as the public integration facade. -- Extract internal seams that match real backend behaviors instead of resource buckets. -- Keep ownership, frame-boundary sequencing, and shutdown order auditable. -- Keep headless, windowed, DMA-BUF, and filter-chain behavior stable through a dependency-ordered - migration plan. -- Make `/goggles-apply` deterministic from OpenSpec artifacts alone. - -**Non-Goals:** - -- Adding a new renderer abstraction or replacing Vulkan-specific contracts. -- Changing the public backend API, `Application` wiring, or filter-chain wrapper boundary. -- Reworking compositor protocols, capture-layer contracts, or shader semantics. -- Changing stage order, explicit sync rules, or moving render-path concurrency off - `goggles::util::JobSystem`. - -## Decisions - -### 1) Keep `VulkanBackend` as a thin public facade - -Decision: - -- `VulkanBackend` remains the only public class integrated by app code. -- `vulkan_backend.hpp` remains the public declaration surface. -- The facade keeps public entrypoints, cross-subsystem orchestration, and destructive transitions - such as startup composition, `render()`, `recreate_swapchain()`, `readback_to_png()`, and - `shutdown()`. - -Rationale: - -- Preserves application call sites and minimizes externally visible change risk. -- Keeps cross-seam sequencing in one auditable place. - -Alternatives considered: - -- Separate public windowed/headless backend classes: rejected because it changes app integration and - duplicates shared lifecycle rules. -- A public generic renderer interface: rejected as out of scope and contrary to concrete Vulkan - contracts. - -### 2) Extract four behavior-oriented subsystem seams - -Decision: - -- Use four internal subsystem owners: - - `VulkanContext` for instance/device/queue/surface bring-up and stable capability facts - - `RenderOutput` for windowed/headless render targets and frame retirement - - `ExternalFrameImporter` for imported image lifetime and temporary wait-semaphore ownership - - `FilterChainController` for backend-side boundary-facing filter coordination, reload requests, - policy inputs, current preset path state, and temporary GPU-drain-safe retirement - -Rationale: - -- These seams already exist in the current code as distinct behaviors with different ownership and - sequencing rules. -- The split keeps subsystem contracts concrete and Vulkan-specific. - -Alternatives considered: - -- Resource-bucket split (device resources vs frame resources vs filter resources): rejected because - it preserves coupling across behavior boundaries. -- Expanding backend ownership across the existing filter boundary: rejected because living specs keep - long-lived filter runtime ownership, shader runtime ownership/creation, and preset texture loading - inside `goggles-filter-chain`. - -### 3) Enforce one-way dependency direction and single authority per mutable state domain - -Decision: - -- `VulkanBackend` coordinates cross-subsystem transitions. -- `VulkanContext` provides stable Vulkan handles/capability facts to the other subsystems. -- `RenderOutput`, `ExternalFrameImporter`, and `FilterChainController` MAY depend on - `VulkanContext`, but they MUST NOT depend on each other. -- Long-lived filter runtime ownership remains in `goggles-filter-chain`; backend-side - `FilterChainController` owns only boundary-facing sequencing state and temporary host-side - retirement required for GPU-drain-safe teardown. -- Target extent/format authority lives in `RenderOutput`. -- Imported image lifetime and temporary GPU-wait ownership live in `ExternalFrameImporter`. -- Stage-policy input state, current preset path state, and temporary retirement bookkeeping live in - `FilterChainController`. -- `VulkanBackend` MUST avoid mirrored cached state when an authoritative subsystem owner already - exists. - -Rationale: - -- Prevents hidden back-calls and authority confusion during the split. -- Keeps shutdown and rebuild logic auditable. - -Alternatives considered: - -- Letting `RenderOutput` trigger filter rebuilds or `FilterChainController` infer output policy: - rejected because it recreates cross-owner coupling. - -### 4) Use frame-boundary subsystem contracts instead of many small mutators - -Decision: - -- `RenderOutput` exposes one bounded begin/submit lifecycle for per-frame output work plus one - explicit rebuild path. -- `ExternalFrameImporter` exposes one import operation that returns the imported-source state needed - for the current frame and owns retirement of replaced resources. -- `FilterChainController` exposes one frame-boundary coordination path for reload requests, - boundary-facing runtime handoff, and host-side retirement bookkeeping plus explicit load/rebuild - request entrypoints. -- `render()` remains the only place that sees the current frame target, imported source, and filter - runtime together. - -Rationale: - -- Reduces state-machine leakage back into the facade. -- Preserves hot-path clarity and keeps mode-specific policy local to the owning subsystem. - -Alternatives considered: - -- Many narrow getters/setters on each subsystem: rejected because it preserves distributed mutable - state and increases orchestration complexity. - -### 5) Preserve the boundary-owned `VulkanContext` contract for host<->filter initialization - -Decision: - -- Backend-owned `VulkanContext` implementation state MUST be adapted into the boundary-owned - host<->filter `VulkanContext` contract required by `goggles-filter-chain`. -- Filter-boundary code MUST NOT include or depend on backend-only context headers. - -Rationale: - -- Keeps dependency direction compatible with the existing filter-boundary living spec. -- Prevents backend extraction from pulling backend internals into the standalone filter boundary. - -Alternatives considered: - -- Sharing backend-only `VulkanContext` headers directly with the filter boundary: rejected by the - boundary-safe contract requirements. - -### 6) Extraction order remains dependency-first and compile-safe - -Decision: - -- Apply work starts with narrow internal headers/types and any required `src/render/backend/CMakeLists.txt` - updates to keep intermediate states buildable. -- Extraction order is fixed: - 1. declaration seams - 2. `VulkanContext` - 3. `RenderOutput` - 4. `ExternalFrameImporter` - 5. `FilterChainController` - 6. final `VulkanBackend` cleanup - -Rationale: - -- Moves lower-risk, lower-coupling seams first. -- Defers the most behavior-sensitive runtime owner (`FilterChainController`) until the output and - import seams are stable. - -Alternatives considered: - -- Extracting filter-chain control first: rejected because it depends on output format/extent and the - current render orchestration shape. - -### 7) Shutdown and verification contracts are part of the design, not apply-time improvisation - -Decision: - -- Shutdown order remains explicit: wait pending async rebuild, clear pending-ready state, idle the - device, destroy filter runtimes, destroy imported image state, destroy output resources, then - destroy context-owned device/surface/debug/instance state. -- Verification contract: - - Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` - - Environment-agnostic automated checks: - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `grep -R --line-number "VulkanBackend::create_headless\|readback_to_png\|reload_shader_preset" src/render/backend tests/render openspec/changes/vulkan-backend-refactor-subsystems` - - `grep -R --line-number "\bthrow\b" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number "util::JobSystem\|reload_shader_preset" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number '#include "external_frame_importer.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/render_output.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/external_frame_importer.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "external_frame_importer.hpp"' src/render/backend/filter_chain_controller.*` - - `grep -R --line-number '#include "vulkan_context.hpp"' src/render/chain src/render/shader src/render/texture --include="*.cpp" --include="*.hpp"` - - Environment-sensitive checks: - - `ctest --preset test -R "^headless_smoke$" --output-on-failure` when the local runtime supports headless Vulkan readback coverage - - Manual fallback: - - allowed only for `headless_smoke` - - record prerequisites, observations, and proof location - - Mandatory checks with no fallback: - - `pixi run build -p quality` - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `ctest --preset test -R "^goggles_headless_integration$" --output-on-failure` - - Pass criteria: - - all baseline and mandatory commands succeed - - named backend module-layout and lifetime tests succeed - - grep evidence shows the declared backend entrypoints still exist and remain traceable - - grep evidence shows no new expected-failure exception paths in backend scope - - async preset reload remains traceable to `goggles::util::JobSystem` - - forbidden direct include edges among non-context backend subsystems are absent - - filter-boundary code continues to consume a boundary-owned `VulkanContext` contract rather than backend-only context headers - - environment-sensitive checks preserve headless readback and DMA-BUF/import behavior when the runtime supports them - -Rationale: - -- The highest-risk failure modes here are behavioral regressions during extraction, not API novelty. -- The change needs a deterministic contract for `/goggles-apply`. - -Alternatives considered: - -- Defining verification ad hoc during apply: rejected because it makes behavior preservation less - auditable. - -## Risks / Trade-offs - -- [Risk] `RenderOutput` absorbs too much policy -> Mitigation: keep it limited to target lifetime, - retirement, pacing, and one rebuild path. -- [Risk] `VulkanBackend::render()` stays too large -> Mitigation: confine it to coordination and use - private facade helpers without reintroducing subsystem back-calls. -- [Risk] authority duplication survives the split -> Mitigation: explicitly name the single owner for - target, imported-source, and runtime state in specs/tasks. -- [Risk] async reload or deferred-destroy behavior regresses -> Mitigation: keep those behaviors - owned by `FilterChainController` and require explicit verification. -- [Trade-off] more files and internal headers increase local indirection -> Mitigation: keep the - dependency graph acyclic and the extraction order narrow. - -## Migration Plan - -1. Add only the narrow internal declarations and build-graph changes needed to make multi-file - extraction compile-safe. -2. Extract `VulkanContext` and move bring-up/state facts without altering runtime flow. -3. Extract `RenderOutput`, first stabilizing swapchain mode and then headless mode under the same - seam. -4. Extract `ExternalFrameImporter` so imported image state and temporary wait-semaphore ownership are - no longer duplicated across submit paths. -5. Preserve or introduce the boundary-owned host<->filter `VulkanContext` adapter before backend - filter-coordination extraction depends on it. -6. Extract `FilterChainController` with existing async rebuild requests, staged swap handoff, and - temporary host-side retirement semantics intact while leaving long-lived runtime ownership in - `goggles-filter-chain`. -7. Add focused module-layout and lifetime audit tests/tags before final cleanup depends on them. -8. Shrink `VulkanBackend` to facade orchestration and remove mirrored state. -9. Run the verification contract and stop immediately if the work requires contract or behavior - divergence. - -Rollback strategy: - -- Revert by phase boundary, preserving the last compile-safe state. -- Do not ship a partial extraction that changes public API or dependency direction. -- If a phase uncovers behavior divergence or shutdown/async lifetime ordering drift that cannot be - resolved inside the declared contract, stop and update proposal/design/spec/tasks before resuming - apply. diff --git a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/proposal.md b/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/proposal.md deleted file mode 100644 index 7c17a05e..00000000 --- a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/proposal.md +++ /dev/null @@ -1,161 +0,0 @@ -## Why - -This change refactors a `VulkanBackend` implementation that had concentrated Vulkan bring-up, -output-target lifetime, DMA-BUF import, explicit sync, filter-chain hosting, and async preset -reload inside one public facade. That shape raised the risk of ownership drift, shutdown mistakes, -and behavior regressions whenever the render backend changed. - -The resulting responsibility-oriented split keeps the existing public API and runtime behavior stable -while making lifecycle authority, dependency direction, and verification scope explicit enough for a -fresh `/goggles-apply` run. - -## Problem - -- The pre-refactor `src/render/backend/vulkan_backend.cpp` mixed four distinct backend behaviors - behind one stateful implementation surface. -- Backend ownership and sequencing rules were spread across windowed, headless, DMA-BUF, and - filter-chain paths, making low-risk edits harder. -- Async preset reload, explicit sync import, swapchain recreation, and headless readback all depend - on auditable shutdown and frame-boundary ordering that needed an explicit refactor contract. -- Future backend work still needs a stable extraction order and verification contract so behavior - preservation does not depend on undocumented repository context. - -## Scope - -- Keep `src/render/backend/vulkan_backend.hpp` as the public `VulkanBackend` facade. -- Split internal backend responsibilities into four concrete subsystems: - - `VulkanContext` - - `RenderOutput` - - `ExternalFrameImporter` - - `FilterChainController` -- Make ownership and coordination boundaries explicit for windowed, headless, filter-chain, and - DMA-BUF paths. -- Define an implementation order and verification contract that preserves existing behavior during - extraction. - -## Non-goals - -- Changing the public `VulkanBackend` API or `Application` call sites. -- Introducing a generic renderer framework, render graph, or cross-API abstraction layer. -- Splitting the backend into separate public windowed and headless backend classes. -- Changing DMA-BUF explicit sync semantics, filter stage ordering, or async preset reload policy. -- Changing the existing `goggles-filter-chain` ownership boundary beyond the living-spec contracts it - already owns. - -## What Changes - -- The change defines a backend module-layout contract that preserves `VulkanBackend` as the public - facade while requiring responsibility-oriented internal seams. -- The subsystem authority rules keep `VulkanBackend` as the cross-subsystem coordinator, keep each - subsystem responsible for its own resources, and keep non-context subsystems from depending on one - another. -- Long-lived filter runtime ownership, shader runtime ownership/creation, shader processing, and - preset texture loading remain inside `goggles-filter-chain`, with backend-side filter coordination - limited to boundary-facing sequencing, reload requests, policy inputs, and temporary GPU-drain-safe - retirement. -- A boundary-owned host<->filter `VulkanContext` contract keeps filter-boundary code independent of - backend-only context headers. -- The extraction order starts with declaration seams and proceeds through `VulkanContext`, - `RenderOutput`, `ExternalFrameImporter`, `FilterChainController`, then final facade cleanup. -- Behavior-preservation requirements cover headless/windowed output, DMA-BUF import and explicit - sync, filter-chain stage and async-reload behavior, and shutdown ordering. -- The verification contract combines preset-driven build/test gates with targeted backend checks for - the highest-risk behavior seams. - -## Capabilities - -### New Capabilities -- `vulkan-backend-module-layout`: Internal backend refactor contract for preserving the - `VulkanBackend` facade while extracting behavior-oriented subsystems with explicit ownership, - sequencing, and verification rules. - -### Modified Capabilities -- None. - -## Risks - -- `RenderOutput` could become a new mixed-responsibility owner if swapchain, headless, pacing, and - rebuild policy are not kept behind one bounded seam. -- `VulkanBackend::render()` could remain a god method if frame-boundary contracts are not explicit. -- Extraction could duplicate authoritative state across facade and subsystem owners, especially for - target extent/format, imported image lifetime, and pending filter runtime state. -- Verification could miss a behavior regression if DMA-BUF import, async swap, or headless paths do - not keep dedicated checks. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `grep -R --line-number "VulkanBackend::create_headless\|readback_to_png\|reload_shader_preset" src/render/backend tests/render openspec/changes/vulkan-backend-refactor-subsystems` - - `grep -R --line-number "\bthrow\b" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number "util::JobSystem\|reload_shader_preset" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number '#include "external_frame_importer.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/render_output.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/external_frame_importer.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "external_frame_importer.hpp"' src/render/backend/filter_chain_controller.*` - - `grep -R --line-number '#include "vulkan_context.hpp"' src/render/chain src/render/shader src/render/texture --include="*.cpp" --include="*.hpp"` -- Environment-sensitive checks: - - `ctest --preset test -R "^headless_smoke$" --output-on-failure` when the local runtime supports headless Vulkan readback coverage -- Manual fallback: - - allowed only for `headless_smoke` - - record runtime prerequisites, observations, and proof location for the manual run -- Mandatory checks with no fallback: - - `pixi run build -p quality` - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `ctest --preset test -R "^goggles_headless_integration$" --output-on-failure` -- Pass criteria: - - preset build and test commands exit successfully - - named backend module-layout and lifetime tests exit successfully - - targeted grep output continues to show the expected backend entrypoints and proposal traceability - - grep output shows no new expected-failure exception paths in backend scope - - async preset reload remains traceable to `goggles::util::JobSystem`-backed behavior - - forbidden direct include edges among `RenderOutput`, `ExternalFrameImporter`, and - `FilterChainController` are absent - - filter-boundary code continues to use a boundary-owned `VulkanContext` contract instead of - backend-only context headers - - `goggles_headless_integration` preserves DMA-BUF import and explicit sync behavior - - `headless_smoke` preserves headless readback behavior when that environment-sensitive check is available - -## Divergence Handling - -- If implementation needs to change the `VulkanBackend` public API, `Application` call sites, - subsystem dependency direction, DMA-BUF explicit sync semantics, headless behavior, filter - stage-order semantics, or shutdown/async lifetime ordering, `/goggles-apply` MUST stop and - reconcile proposal, design, specs, and tasks before implementation continues. -- If extraction reveals hidden coupling, the default remediation is to add narrow internal seams or - adapters that preserve the declared subsystem boundaries without changing external behavior. - -## Impact - -- **Code modules/files**: `src/render/backend/vulkan_backend.hpp`, - `src/render/backend/vulkan_backend.cpp`, new internal backend subsystem files under - `src/render/backend/`, `src/render/backend/CMakeLists.txt`, and backend-touching tests under - `tests/render/`. -- **Runtime behavior guarded**: windowed swapchain flow, headless offscreen flow, - DMA-BUF/imported-image lifecycle, explicit sync submission wiring, async filter reload, and - `Application` integration points. -- **Existing specs relied on but not modified**: `openspec/specs/render-pipeline/spec.md`, - `openspec/specs/headless-mode/spec.md`, `openspec/specs/goggles-filter-chain/spec.md`, and - `openspec/specs/object-lifecycle/spec.md`. -- **Vulkan ownership authority for this change**: `docs/project_policies.md` governs Vulkan lifetime - and ownership rules for this refactor. The stale `vk::Unique*` wording in - `openspec/specs/render-pipeline/spec.md` is excluded from this change contract until it is - reconciled with policy before apply depends on it. -- **OpenSpec artifacts introduced by this change**: - - `openspec/changes/vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md` - - future living-spec sync target: `openspec/specs/vulkan-backend-module-layout/spec.md` -- **Policy-sensitive areas**: - - Vulkan ownership and destruction ordering MUST stay explicit. - - Expected runtime failures MUST remain `Result`-based. - - Render-path async work MUST remain on `util::JobSystem`. - - DMA-BUF explicit sync behavior MUST remain intact. diff --git a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md b/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md deleted file mode 100644 index 960c3860..00000000 --- a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md +++ /dev/null @@ -1,199 +0,0 @@ -## ADDED Requirements - -### Requirement: VulkanBackend Facade Remains Stable - -The backend refactor SHALL preserve `VulkanBackend` as the public integration facade for app-side -render backend usage. - -The refactor SHALL: - -- keep `src/render/backend/vulkan_backend.hpp` as the public API declaration surface -- preserve the existing public backend methods and `Application` integration points unless the - change artifacts are updated first -- keep `src/render/backend/vulkan_backend.cpp` limited to public entrypoints and high-level - orchestration after the split - -#### Scenario: Public facade preserved after extraction -- **GIVEN** the backend refactor is complete -- **WHEN** app-side integration is inspected -- **THEN** `Application` still integrates through `VulkanBackend` -- **AND** the public backend declaration surface remains `src/render/backend/vulkan_backend.hpp` - -### Requirement: Behavior-Oriented Backend Subsystems - -The backend implementation SHALL organize internal code into behavior-oriented subsystems so future -edits can remain local to one backend concern. - -The split SHALL provide subsystem boundaries that match these responsibilities: - -- `VulkanContext` for instance, device, queue, surface, validation, and stable capability facts -- `RenderOutput` for swapchain/headless target lifetime and frame retirement -- `ExternalFrameImporter` for DMA-BUF import and temporary explicit-sync wait ownership -- `FilterChainController` for backend-side boundary-facing filter coordination, reload requests, - policy inputs, current preset path state, and temporary GPU-drain-safe retirement - -Long-lived filter runtime ownership, shader runtime ownership/creation, shader processing, and -preset texture loading SHALL remain inside `goggles-filter-chain`. - -The implementation SHALL NOT introduce a generic `misc`, `helpers`, or `utils` dumping-ground module -for backend extraction. - -#### Scenario: Localized edit surface for output behavior -- **GIVEN** a future change only affects swapchain recreation, headless target lifetime, or frame - retirement behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `RenderOutput` -- **AND** unrelated importer and filter-controller logic does not need to remain in the same - translation unit - -#### Scenario: Localized edit surface for import behavior -- **GIVEN** a future change only affects imported image lifetime or explicit-sync import behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `ExternalFrameImporter` -- **AND** output-target and filter-runtime logic does not need to remain in the same translation unit - -### Requirement: Filter Boundary Ownership Remains Intact - -The backend refactor SHALL keep long-lived filter runtime ownership inside `goggles-filter-chain` -while using backend-side coordination only for boundary-facing sequencing and temporary retirement. - -#### Scenario: Runtime ownership stays in the filter boundary -- **GIVEN** the renderer initializes or reloads filter processing after the backend split -- **WHEN** runtime ownership is inspected -- **THEN** chain orchestration, shader runtime ownership/creation, shader processing, and preset - texture loading remain owned within `goggles-filter-chain` -- **AND** backend-side `FilterChainController` consumes boundary-facing contracts instead of owning - those long-lived runtime internals - -#### Scenario: Host-side retirement remains bounded -- **GIVEN** a filter runtime handoff replaces an active runtime after the backend split -- **WHEN** the previous runtime is retired -- **THEN** any host-side retirement ownership is temporary and limited to GPU-drain-safe destruction -- **AND** active runtime ownership remains in the filter boundary - -### Requirement: Subsystem Authority and Dependency Direction Remain Explicit - -The backend refactor SHALL preserve one explicit authority for each mutable backend state domain and -SHALL keep subsystem dependency direction acyclic. - -The split SHALL enforce these authority rules: - -- `VulkanBackend` coordinates cross-subsystem transitions -- `RenderOutput` is the authority for target extent, target format, and frame-retirement state -- `ExternalFrameImporter` is the authority for imported-image lifetime and temporary wait objects -- `FilterChainController` is the authority for backend-side reload request state, stage-policy input - state, current preset path state, and temporary host-side retirement bookkeeping - -Allowed internal dependency edges are: - -- `RenderOutput -> VulkanContext` -- `ExternalFrameImporter -> VulkanContext` -- `FilterChainController -> boundary-owned host<->filter VulkanContext contract` - -Forbidden internal dependency edges are: - -- `RenderOutput -/-> ExternalFrameImporter` -- `RenderOutput -/-> FilterChainController` -- `ExternalFrameImporter -/-> FilterChainController` - -#### Scenario: Single owner remains inspectable after extraction -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** target state, imported-source state, and filter-runtime state ownership are inspected -- **THEN** each mutable state domain still resolves to one named subsystem owner -- **AND** `VulkanBackend` does not duplicate that authoritative state without an explicitly documented reason - -#### Scenario: Cross-subsystem calls stay facade-owned -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** a frame is rendered or the swapchain is recreated -- **THEN** cross-boundary sequencing is coordinated by `VulkanBackend` -- **AND** non-context subsystems do not call each other directly to trigger rebuild or lifetime transitions - -### Requirement: Boundary-Owned VulkanContext Contract Remains Intact - -The backend refactor SHALL preserve a boundary-owned host<->filter `VulkanContext` contract so the -filter boundary does not consume backend-only context headers. - -#### Scenario: Filter boundary uses a boundary-owned context contract -- **GIVEN** host/backend code initializes `goggles-filter-chain` after the backend split -- **WHEN** include and initialization dependencies are inspected -- **THEN** the filter boundary consumes a boundary-owned host<->filter `VulkanContext` contract or adapter -- **AND** backend-only `VulkanContext` headers are not included directly from filter-boundary code - -### Requirement: Extraction Contract Is Explicit for Apply - -The change artifacts SHALL define a compile-safe extraction order and verification plan that minimize -behavior drift during apply. - -The change artifacts SHALL specify: - -- an initial declaration-seam step that adds only the narrow internal headers and types needed for a - buildable multi-file split -- updating `src/render/backend/CMakeLists.txt` as backend translation units land -- extracting `VulkanContext` before other subsystem owners -- extracting `RenderOutput` before `ExternalFrameImporter` and `FilterChainController` -- extracting `ExternalFrameImporter` before final facade cleanup so imported-source ownership is explicit -- preserving or introducing the boundary-owned host<->filter `VulkanContext` contract before - `FilterChainController` depends on it -- extracting `FilterChainController` after output/import seams are stable -- a verification contract that names baseline preset gates, environment-sensitive checks, fallback - policy, mandatory no-fallback checks, forbidden dependency-edge audits, and named module-layout - plus lifetime tests - -#### Scenario: Migration order is explicit from artifacts alone -- **GIVEN** implementation starts from the repository artifacts alone -- **WHEN** the artifacts are read before editing code -- **THEN** the OpenSpec artifacts specify the required extraction order -- **AND** the implementation does not depend on undocumented external context to determine safe sequencing - -### Requirement: Backend Behavior Is Preserved Across the Split - -The backend refactor SHALL preserve existing backend behavior while changing only the internal module -layout. - -The preserved behavior SHALL include: - -- windowed swapchain rendering and present flow -- headless surfaceless rendering and PNG readback behavior -- DMA-BUF plane-layout import and explicit-sync submission wiring -- filter-chain stage-order invariants and async preset reload behavior -- Result-based error propagation and no expected-failure exceptions in backend refactor scope -- `goggles::util::JobSystem`-owned async preset rebuild behavior -- filter-boundary ownership of long-lived runtime objects and boundary-owned initialization contracts -- swapchain recreation behavior, including output-format and filter-runtime rebuild coordination -- unchanged `Application` backend integration points - -#### Scenario: Headless and windowed behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** windowed and headless render flows are exercised through the defined verification plan -- **THEN** swapchain render/present behavior and headless offscreen/readback behavior remain unchanged - -#### Scenario: DMA-BUF and filter-chain behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** imported-frame, explicit-sync, and filter-boundary behavior are exercised through the defined verification plan -- **THEN** DMA-BUF import semantics, temporary wait handling, stage ordering, and async preset reload behavior remain unchanged - -#### Scenario: Error flow and async rebuild behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** backend failure paths and preset reload behavior are exercised through the defined verification plan -- **THEN** expected runtime failures still propagate through `Result`-style APIs without expected-failure exceptions -- **AND** async preset rebuild behavior remains owned by `goggles::util::JobSystem` - -### Requirement: Shutdown and Async Lifetime Ordering Remain Safe - -The backend refactor SHALL preserve auditable shutdown ordering across async filter work, imported -resources, output resources, and Vulkan context state. - -The shutdown contract SHALL require this order: - -- wait for pending async filter rebuild work and clear pending-ready state -- idle the device before subsystem teardown proceeds -- destroy filter-controller active, pending, and deferred-retire runtime state before output/context teardown -- destroy imported-image state before output/context teardown completes -- destroy output resources before destroying context-owned device, surface, debug messenger, and instance state -- treat any shutdown or async lifetime ordering drift as a proposal/spec/design reconciliation stop condition - -#### Scenario: Device-rooted resources tear down in safe order -- **GIVEN** the backend implementation is shutting down after the subsystem split -- **WHEN** teardown order is inspected or exercised through verification -- **THEN** async runtime work is resolved before subsystem destruction proceeds -- **AND** device-rooted child resources are destroyed before the owning Vulkan context state is destroyed diff --git a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/tasks.md b/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/tasks.md deleted file mode 100644 index eb50b2bc..00000000 --- a/openspec/changes/archive/2026-03-08-vulkan-backend-refactor-subsystems/tasks.md +++ /dev/null @@ -1,90 +0,0 @@ -## 1. Declaration Seams and Buildability - -- [x] 1.1 Add the narrow internal headers/types needed for a compile-safe backend split under `src/render/backend/` without changing the public `VulkanBackend` API in `src/render/backend/vulkan_backend.hpp`. -- [x] 1.2 Update `src/render/backend/CMakeLists.txt` as new backend translation units land so each extraction phase remains buildable. -- [x] 1.3 Add or update focused backend tests and scaffolding in `tests/render/` only where needed to keep the extraction verifiable by phase. -- [x] 1.4 Add or update focused audit coverage in `tests/render/` tagged for `"[vulkan-backend-module-layout]"` and `"[vulkan-backend-lifetime]"` so authority boundaries, forbidden dependency edges, and teardown ordering have named verification surfaces. - -## 2. Extract `VulkanContext` - -- [x] 2.1 Move instance, debug-messenger, optional surface, physical-device selection, logical-device creation, queue selection, and stable capability facts into `VulkanContext`. -- [x] 2.2 Keep `VulkanContext` as a stable root owner with `vk::` handles, explicit destruction, and `Result`-based factory/error flow. -- [x] 2.3 Preserve existing validation-layer behavior, headless device-selection behavior, and app-side Vulkan policy constraints during the move. -- [x] 2.4 Preserve or introduce a boundary-owned host<->filter `VulkanContext` contract or adapter so `goggles-filter-chain` does not consume backend-only context headers. - -## 3. Extract `RenderOutput` - -- [x] 3.1 Move swapchain target lifetime, frame-in-flight state, acquire/submit/present, pacing, and resize bookkeeping behind `RenderOutput`. -- [x] 3.2 Move headless offscreen target allocation, headless submission, and `readback_to_png` support behind the same output seam without creating a second public backend class. -- [x] 3.3 Keep target extent/format and frame-retirement authority in `RenderOutput`, and keep rebuild coordination triggered only by `VulkanBackend`. - -## 4. Extract `ExternalFrameImporter` - -- [x] 4.1 Move DMA-BUF plane-layout import, imported-image lifetime, and imported view/extent ownership into `ExternalFrameImporter`. -- [x] 4.2 Move temporary explicit-sync wait-object creation and retirement into `ExternalFrameImporter` so submit paths do not duplicate that ownership. -- [x] 4.3 Preserve existing DMA-BUF explicit sync semantics from compositor handoff through frame submission. - -## 5. Extract `FilterChainController` - -- [x] 5.1 Move backend-side reload request state, preset path state, policy inputs, boundary-facing control orchestration, and temporary GPU-drain-safe retirement bookkeeping into `FilterChainController` while leaving long-lived runtime ownership in `goggles-filter-chain`. -- [x] 5.2 Keep `goggles::util::JobSystem` as the only async rebuild request mechanism and preserve staged frame-boundary swap behavior through boundary-facing filter contracts. -- [x] 5.3 Keep filter controls, stage policy inputs, and prechain-resolution coordination in `FilterChainController` without moving shader runtime ownership/creation, preset texture loading, or output-policy decisions out of their existing owners. - -## 6. Shrink `VulkanBackend` to Facade Coordination - -- [x] 6.1 Rewrite `create()`, `create_headless()`, `render()`, `recreate_swapchain()`, `readback_to_png()`, and `shutdown()` to compose the four subsystem owners while keeping public behavior unchanged. -- [x] 6.2 Remove mirrored cached state from `VulkanBackend` when an authoritative subsystem owner already exists. -- [x] 6.3 Preserve `Application` integration points and keep cross-subsystem sequencing owned by `VulkanBackend` rather than by direct subsystem-to-subsystem calls. - -## 7. Verification Contract - -- [x] 7.1 Run baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- [x] 7.2 Run environment-agnostic automated checks: - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `grep -R --line-number "VulkanBackend::create_headless\|readback_to_png\|reload_shader_preset" src/render/backend tests/render openspec/changes/vulkan-backend-refactor-subsystems` - - `grep -R --line-number "\bthrow\b" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number "util::JobSystem\|reload_shader_preset" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` - - `grep -R --line-number '#include "external_frame_importer.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/render_output.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/external_frame_importer.*` - - `grep -R --line-number '#include "render_output.hpp"\|#include "external_frame_importer.hpp"' src/render/backend/filter_chain_controller.*` - - `grep -R --line-number '#include "vulkan_context.hpp"' src/render/chain src/render/shader src/render/texture --include="*.cpp" --include="*.hpp"` -- [x] 7.3 Run environment-sensitive checks when local runtime support exists: - - `ctest --preset test -R "^headless_smoke$" --output-on-failure` -- [x] 7.4 Allow manual fallback only for `headless_smoke`; record prerequisites, observations, and proof location if that fallback is used. -- [x] 7.5 Treat these checks as mandatory with no fallback: - - `pixi run build -p quality` - - `pixi run test -p asan` - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` - - `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"` - - `ctest --preset test -R "^goggles_headless_integration$" --output-on-failure` - -## 8. Spec and Divergence Control - -- [x] 8.1 Keep implementation aligned with `openspec/changes/vulkan-backend-refactor-subsystems/specs/vulkan-backend-module-layout/spec.md`, `design.md`, and the referenced living specs for render-pipeline, headless-mode, filter-chain, and object-lifecycle behavior. -- [x] 8.2 If implementation requires public API drift, dependency-direction drift, DMA-BUF explicit-sync drift, headless/windowed behavior drift, filter stage-order drift, or shutdown/async lifetime ordering drift, stop apply work and reconcile proposal, design, specs, and tasks before continuing. -- [x] 8.3 Record phase-by-phase verification evidence so each extracted seam has an auditable build/test result before the next phase starts. -- [x] 8.4 Reconcile the stale `vk::Unique*` ownership wording in `openspec/specs/render-pipeline/spec.md` or carry an explicit spec delta before implementation relies on that living-spec language for backend ownership semantics. - -## 9. Requirement Traceability - -- [x] 9.1 Keep this mapping updated during apply so each requirement and major preserved behavior has at least one implementation task and one verification command. - -| Requirement (spec.md) | Task IDs | Verification commands | -| --- | --- | --- | -| VulkanBackend Facade Remains Stable | 1.1, 6.1, 6.3 | `pixi run build -p debug`; `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` | -| Behavior-Oriented Backend Subsystems | 2.1, 3.1, 4.1, 5.1 | `pixi run build -p debug`; `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"` | -| Filter Boundary Ownership Remains Intact | 2.4, 5.1, 5.3, 8.1 | `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"`; `grep -R --line-number '#include "vulkan_context.hpp"' src/render/chain src/render/shader src/render/texture --include="*.cpp" --include="*.hpp"` | -| Subsystem Authority and Dependency Direction Remain Explicit | 3.3, 4.2, 5.3, 6.2, 6.3, 7.2 | `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"`; `grep -R --line-number '#include "external_frame_importer.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/render_output.*`; `grep -R --line-number '#include "render_output.hpp"\|#include "filter_chain_controller.hpp"' src/render/backend/external_frame_importer.*`; `grep -R --line-number '#include "render_output.hpp"\|#include "external_frame_importer.hpp"' src/render/backend/filter_chain_controller.*` | -| Boundary-Owned VulkanContext Contract Remains Intact | 2.4, 5.1, 7.2, 8.1 | `build/test/tests/goggles_tests "[vulkan-backend-module-layout]"`; `grep -R --line-number '#include "vulkan_context.hpp"' src/render/chain src/render/shader src/render/texture --include="*.cpp" --include="*.hpp"` | -| Extraction Contract Is Explicit for Apply | 1.2, 7.1, 7.2, 8.3 | `pixi run build -p debug`; `pixi run test -p asan` | -| Backend Behavior Is Preserved Across the Split | 3.2, 4.3, 5.2, 6.1, 7.2, 7.3 | `pixi run test -p asan`; `ctest --preset test -R "^(headless_smoke|goggles_headless_integration)$" --output-on-failure` | -| Shutdown and Async Lifetime Ordering Remain Safe | 1.4, 2.2, 5.1, 6.1, 7.2, 7.5, 8.2 | `build/test/tests/goggles_tests "[vulkan-backend-lifetime]"`; `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` | -| Preserved Result-Based Error Flow | 2.2, 6.1, 7.2, 8.1 | `pixi run build -p quality`; `grep -R --line-number "\bthrow\b" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` | -| Preserved `goggles::util::JobSystem` Async Reload Behavior | 5.1, 5.2, 6.1, 7.2 | `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure`; `grep -R --line-number "util::JobSystem\|reload_shader_preset" src/render/backend tests/render --include="*.cpp" --include="*.hpp"` | diff --git a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/.openspec.yaml b/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/.openspec.yaml deleted file mode 100644 index 5cb9e8f6..00000000 --- a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-09 diff --git a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/design.md b/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/design.md deleted file mode 100644 index 6ef4a492..00000000 --- a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/design.md +++ /dev/null @@ -1,55 +0,0 @@ -## Context - -`DownsamplePass` already serves as the single prechain downsampling implementation and exposes a discrete `filter_type` runtime parameter for area and gaussian behavior. The current pass always binds a linear sampler, so adding true nearest-neighbor support requires an explicit design choice for how the runtime selects sampling behavior without breaking existing values, prechain control ordering, or same-frame parameter updates. - -## Goals / Non-Goals - -**Goals:** -- Extend the existing prechain `filter_type` path to support nearest-neighbor as a third runtime mode. -- Preserve current numeric/value compatibility for area and gaussian. -- Keep runtime switching pipeline-free so the next rendered frame reflects the selected mode. -- Keep the change local to `DownsamplePass`, the existing prechain control surfaces, and targeted tests/specs. - -**Non-Goals:** -- Introducing a new prechain control identifier or a separate filter-mode subsystem. -- Changing RetroArch pass sampler semantics outside the prechain downsample path. -- Redesigning the shader-controls UI beyond what is required to expose the new discrete mode. -- Changing prechain stage ordering, semantic binding, or broader filter-chain ownership boundaries. - -## Decisions - -- Decision: Keep `filter_type` as the sole prechain downsample selector and extend its discrete range from `0..1` to `0..2`. - - Rationale: this preserves the existing control ID, prechain descriptor flow, and persisted area/gaussian meanings while making nearest-neighbor additive. - - Alternative considered: add a separate `filter_mode` parameter. Rejected because it creates an unnecessary second control path and complicates persistence/migration. - -- Decision: Preserve numeric compatibility by keeping `0 = area`, `1 = gaussian`, and introducing `2 = nearest-neighbor`. - - Rationale: existing persisted state and any prechain control snapshots continue to load without reinterpretation. - - Alternative considered: remap defaults or reorder values. Rejected because it would create avoidable compatibility risk. - -- Decision: Implement nearest-neighbor with an explicit nearest-sampling runtime path while leaving area and gaussian on the current linear-sampling path. - - Rationale: true nearest-neighbor behavior cannot rely on the current always-linear sampler path. The pass should switch sampling behavior internally without creating a new pipeline family. - - Alternative considered: approximate nearest-neighbor in the shader while keeping only the linear sampler. Rejected because it risks non-exact sampling semantics and unnecessary shader complexity. - -- Decision: Keep verification focused on render-pipeline behavior, control metadata compatibility, and targeted filter-chain tests. - - Rationale: this change is localized to prechain downsampling and should not broaden into unrelated UI or API redesign work. - -## Risks / Trade-offs - -- Control surfaces or tests may implicitly assume `filter_type.max_value == 1`. - - Mitigation: update descriptor expectations and extend targeted prechain control tests. -- Nearest-neighbor intentionally increases aliasing relative to area filtering. - - Mitigation: document the behavior in the render-pipeline spec as a distinct selectable mode rather than treating it as a regression. -- Adding a second sampler path increases pass-state complexity slightly. - - Mitigation: keep sampler ownership inside `DownsamplePass` and avoid widening the change into shared pipeline abstractions. -- If nearest-neighbor support reveals any need to change async preset reload ordering, shutdown sequencing, or filter-boundary ownership, the current design is incomplete and those concerns require a separate artifact revision before code changes continue. - -## Migration Plan - -1. Update proposal/spec artifacts so the new filter-mode contract is self-contained. -2. Implement `DownsamplePass` runtime selection changes and shader behavior for nearest-neighbor. -3. Update control/persistence surfaces so the new mode round-trips without changing current values. -4. Run targeted unit coverage, then the full ASAN + quality verification contract. - -## Open Questions - -- None. The remaining work is implementation and verification, not product-scope discovery. diff --git a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/proposal.md b/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/proposal.md deleted file mode 100644 index c3a18793..00000000 --- a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/proposal.md +++ /dev/null @@ -1,99 +0,0 @@ -# Change: Add Nearest-Neighbor Prechain Downsampling - -## Problem - -The prechain DownsamplePass currently supports only area and gaussian filtering, so users cannot choose nearest-neighbor sampling when they want the sharpest possible downsampled output. - -## Why - -Nearest-neighbor is a standard filter option for image scaling and shader workflows, especially when users want crisp pixel edges instead of area-averaged or gaussian-smoothed results. Adding it now completes the existing prechain filter selector without introducing a parallel control path. - -## Scope - -- Extend the existing prechain `filter_type` selector to include a nearest-neighbor mode. -- Preserve current area and gaussian behavior exactly, including existing persisted values. -- Keep the change inside the existing prechain/downsample architecture and runtime control flow. -- Update the render-pipeline OpenSpec contract and verification tasks for the new mode. - -## Non-goals - -- Adding a separate prechain control or alternate sampling subsystem. -- Changing prechain/effect/postchain ordering or shader semantic contracts. -- Remapping existing area or gaussian values, or changing the default filter behavior. -- Broad UI redesign, preset-format expansion beyond the new mode, or unrelated filter-chain refactors. - -## What Changes - -### Runtime filter-mode expansion - -- Extend `DownsamplePass` filter selection from two modes to three modes using the existing `filter_type` parameter. -- Add nearest-neighbor sampling behavior to the internal downsampling shader path. -- Keep runtime switching pipeline-free so control changes apply to subsequent frames without rebuild. - -### Control and persistence compatibility - -- Preserve the current prechain control surface and descriptor contract for `filter_type`. -- Keep existing area/gaussian persisted values stable and introduce nearest-neighbor as a new opt-in value. -- Ensure control snapshots and runtime parameter surfaces continue to expose deterministic prechain-first ordering. - -### Contract updates - -- Modify `render-pipeline` requirements for filter-type selection and downsample-pass behavior to cover nearest-neighbor semantics and the expanded discrete range. -- Keep implementation readiness explicit around automated verification for runtime behavior, compatibility, and prechain control coverage. - -## Capabilities - -### New Capabilities - -- None. - -### Modified Capabilities - -- `render-pipeline`: extend downsample filter selection and prechain downsample behavior to support nearest-neighbor as a backward-compatible runtime mode. - -## Impact - -- **Affected specs:** `render-pipeline` -- **Affected code (expected):** - - `src/render/chain/downsample_pass.cpp` - - `src/render/chain/downsample_pass.hpp` - - `src/render/chain/filter_chain.cpp` - - `shaders/internal/downsample.frag.slang` - - `tests/render/test_filter_controls.cpp` - - targeted render/filter-chain tests for parameter parsing or snapshot coverage - -## Policy-sensitive impacts - -- **Error handling:** invalid or unsupported filter values MUST continue to be clamped or rejected through existing control contracts without silent behavior drift. -- **Logging:** no new cascading logs for ordinary runtime filter switching. -- **Threading:** runtime mode changes stay inside the existing single-threaded render/filter path; no new ad hoc threads. -- **Vulkan/runtime ownership:** nearest-neighbor support MUST reuse existing DownsamplePass ownership, descriptor, and pipeline lifetimes. - -## Risks - -| Risk | Severity | Likelihood | Mitigation | -|------|----------|------------|------------| -| Existing persisted values change meaning | HIGH | LOW | Keep area = 0 and gaussian = 1 semantics unchanged and add nearest-neighbor as a new explicit value | -| Control metadata or UI handling assumes max = 1 | MEDIUM | MEDIUM | Update control descriptor expectations and cover prechain control enumeration in tests | -| Shader/runtime branch adds aliasing or bypasses same-resolution passthrough behavior unexpectedly | MEDIUM | LOW | Extend spec scenarios and targeted verification for nearest-neighbor plus identity passthrough | -| Boundary/API consumers regress because control snapshots no longer behave deterministically | MEDIUM | LOW | Keep existing prechain control ordering contract and verify snapshot/control coverage | - -## Verification Contract - -- **Baseline gates:** - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- **Environment-agnostic automated checks:** - - `ctest --preset test -R "goggles_unit_tests" --output-on-failure` - - `pixi run test -p asan` -- **Environment-sensitive checks:** - - None expected for this proposal; nearest-neighbor support is confined to render/filter-chain logic and shader behavior covered by automated checks. -- **Manual fallback:** - - Not allowed for the baseline build/static-analysis gates or ASAN test pass. -- **Mandatory checks with no fallback:** - - `pixi run build -p quality` - - `pixi run test -p asan` -- **Pass criteria:** - - Build gates complete without warnings-as-errors or preset failures. - - Targeted render/filter-chain tests confirm nearest-neighbor is exposed, selectable, and backward-compatible with area/gaussian semantics. diff --git a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/specs/render-pipeline/spec.md deleted file mode 100644 index 8185814b..00000000 --- a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/specs/render-pipeline/spec.md +++ /dev/null @@ -1,87 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Downsample Filter Type Selection - -The DownsamplePass SHALL support runtime selection of downsampling filter algorithm via the shader parameter interface. - -#### Scenario: Area filter (default) - -- **GIVEN** DownsamplePass with `filter_type = 0` -- **WHEN** downsampling is performed -- **THEN** weighted box filter SHALL be used -- **AND** each source pixel SHALL be weighted by coverage overlap - -#### Scenario: Gaussian filter - -- **GIVEN** DownsamplePass with `filter_type = 1` -- **WHEN** downsampling is performed -- **THEN** Gaussian-weighted bilinear sampling SHALL be used -- **AND** 4 bilinear taps SHALL approximate a Gaussian kernel -- **AND** effective sampling SHALL cover 16 source texels - -#### Scenario: Nearest-neighbor filter - -- **GIVEN** DownsamplePass with `filter_type = 2` -- **WHEN** downsampling is performed -- **THEN** nearest-neighbor sampling SHALL be used -- **AND** each output pixel SHALL sample a single nearest source texel without area or gaussian weighting - -#### Scenario: Filter type exposed as parameter - -- **GIVEN** a DownsamplePass instance -- **WHEN** `get_shader_parameters()` is called -- **THEN** a parameter named `filter_type` SHALL be returned -- **AND** min SHALL be 0, max SHALL be 2, step SHALL be 1 - -#### Scenario: Filter type runtime change - -- **GIVEN** DownsamplePass is actively rendering -- **WHEN** `set_shader_parameter("filter_type", 2.0)` is called -- **THEN** the next frame SHALL use nearest-neighbor filter -- **AND** no pipeline rebuild SHALL occur - -#### Scenario: Legacy filter values remain stable - -- **GIVEN** persisted runtime state or configuration stores `filter_type = 0` or `filter_type = 1` -- **WHEN** that state is loaded by a build that supports nearest-neighbor downsampling -- **THEN** `0` SHALL continue to mean area filtering -- **AND** `1` SHALL continue to mean gaussian filtering - -#### Scenario: Persisted nearest-neighbor value remains explicit - -- **GIVEN** persisted runtime state or configuration stores `filter_type = 2` -- **WHEN** that state is loaded by a build that supports nearest-neighbor downsampling -- **THEN** `2` SHALL select nearest-neighbor filtering -- **AND** the loaded runtime state SHALL NOT reinterpret `2` as area or gaussian filtering - -### Requirement: Downsample Pass - -The internal pass library SHALL include a configurable downsampling pass that can be added to the pre-chain. - -#### Scenario: Area filter downsampling - -- **GIVEN** source image at `1920x1080` and target resolution `640x480` -- **WHEN** downsample pass executes with `filter_type = 0` -- **THEN** each output pixel SHALL be computed as a weighted average of covered source pixels -- **AND** the result SHALL exhibit minimal aliasing compared to point sampling - -#### Scenario: Nearest-neighbor downsampling - -- **GIVEN** source image at `1920x1080` and target resolution `640x480` -- **WHEN** downsample pass executes with `filter_type = 2` -- **THEN** each output pixel SHALL be produced from nearest-neighbor sampling of the source image -- **AND** the result SHALL preserve sharp pixel edges instead of area-averaged smoothing - -#### Scenario: Downsample added to pre-chain when configured - -- **GIVEN** source resolution is configured via `--app-width` and/or `--app-height` -- **WHEN** `FilterChain` is created -- **THEN** a `DownsamplePass` SHALL be added to `m_prechain_passes` -- **AND** a framebuffer sized to target resolution SHALL be added to `m_prechain_framebuffers` - -#### Scenario: Identity passthrough at same resolution - -- **GIVEN** source and target resolution are identical -- **WHEN** downsample pass executes with any supported `filter_type` -- **THEN** output SHALL exactly match input -- **AND** no blurring or aliasing SHALL occur diff --git a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/tasks.md b/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/tasks.md deleted file mode 100644 index 7a42324f..00000000 --- a/openspec/changes/archive/2026-03-09-filter-chain-add-nearest-neighbor-downsampling/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -## 1. Spec and contract alignment - -- [x] 1.1 Update `openspec/specs/render-pipeline/spec.md` deltas in `openspec/changes/filter-chain-add-nearest-neighbor-downsampling/specs/render-pipeline/spec.md` so `filter_type` explicitly covers area, gaussian, and nearest-neighbor with backward-compatible value semantics. -- [x] 1.2 Keep proposal/design/tasks self-contained with no workflow provenance references. - -## 2. Downsample runtime implementation - -- [x] 2.1 Update `src/render/chain/downsample_pass.hpp` and `src/render/chain/downsample_pass.cpp` to extend `filter_type` metadata/range and preserve `0 = area`, `1 = gaussian`, `2 = nearest-neighbor`. -- [x] 2.2 Implement nearest-neighbor sampling in `shaders/internal/downsample.frag.slang` and any required DownsamplePass sampler-selection path so nearest uses true nearest sampling without changing area/gaussian behavior. -- [x] 2.3 Keep runtime switching pipeline-free so the next rendered frame reflects the selected mode without rebuilds or stage-order changes. - -## 3. Control and compatibility coverage - -- [x] 3.1 Update prechain control handling in `src/render/chain/filter_chain.cpp` and any related runtime surfaces so the expanded `filter_type` value range remains deterministic and backward-compatible. -- [x] 3.2 Add or update targeted tests under `tests/render/` to cover nearest-neighbor control exposure, runtime selection behavior, persisted `filter_type = 2` round-tripping, and compatibility for existing area/gaussian values. - -## 4. Verification - -- [x] 4.1 Run `pixi run build -p debug`. -- [x] 4.2 Run `ctest --preset test -R "goggles_unit_tests" --output-on-failure`. -- [x] 4.3 Run `pixi run build -p asan`. -- [x] 4.4 Run `pixi run test -p asan`. -- [x] 4.5 Run `pixi run build -p quality`. diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/.openspec.yaml b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/.openspec.yaml deleted file mode 100644 index 5cb9e8f6..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-09 diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/design.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/design.md deleted file mode 100644 index a2f4c9b6..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/design.md +++ /dev/null @@ -1,145 +0,0 @@ -## Context - -Goggles already has a viewer-side pacing path: `render.target_fps` flows through config/CLI into the -render backend, which uses `VK_KHR_present_wait` when available and a CPU sleep fallback when it is -not. That contract does not currently bound the nested compositor's callback cadence, so target apps -inside the wlroots/XWayland compositor can still run far faster than the selected target even while -the viewer presents at the desired rate. - -This change crosses `src/compositor`, `src/render/backend`, `src/app`, `src/ui`, and `src/util`. -The design MUST preserve one pacing target, keep viewer pacing reuse explicit, and avoid inventing a -second independent policy surface that can drift across the workflow. - -## Goals / Non-Goals - -**Goals:** -- Make one global target FPS govern the full `app -> compositor -> viewer` present workflow. -- Keep compositor pacing mandatory so client callback cadence no longer ignores the selected target. -- Reuse the existing viewer present-wait and CPU-throttle subsystem instead of duplicating viewer - pacing logic. -- Surface runtime pacing controls in the Application window without breaking existing config/CLI - target-FPS semantics. -- Preserve uncapped mode as `target_fps = 0`. -- Keep v1 verification minimal and observable: +/-3 FPS of target over 10 seconds on Wayland and X11 - hosts. - -**Non-Goals:** -- Separate app, compositor, and viewer FPS targets in v1. -- A viewer-only fix that leaves compositor callback pacing uncapped. -- Persisting runtime ImGui changes back into the config file. -- A broader render/compositor architecture rewrite. -- A second v1 acceptance metric for jitter beyond the FPS tolerance contract. - -## Decisions - -### Decision: Keep one boundary-owned effective pacing target - -The application SHALL resolve one effective pacing target for the current session and SHALL push that -value through one runtime update path into both the public `CompositorServer` pacing seam and viewer -pacing state. - -Rationale: -- The proposal and delta specs define one global target as the v1 pacing contract. -- A single boundary-owned value routed through the application/compositor seam prevents the - compositor and viewer from drifting after runtime UI - changes. - -Alternatives considered: -- Independent stage-specific targets: rejected because it expands scope and creates ambiguous user - behavior. -- Viewer-owned target with compositor heuristics: rejected because the compositor must be a - first-class pacing participant. - -### Decision: Reuse the existing viewer pacing subsystem as the viewer half of the contract - -The render backend SHALL continue to use the existing `RenderOutput` present-wait and CPU-throttle -logic for the viewer side of the pacing contract. The new work SHALL feed the shared target into that -existing mechanism rather than replace it. - -Rationale: -- The current viewer pacing path already encodes the supported Vulkan extension and fallback rules. -- Reuse reduces maintenance burden and keeps the authoritative render-pipeline policy in one place. - -Alternatives considered: -- Rewrite viewer pacing together with compositor pacing: rejected because it expands risk without - solving a new requirement. -- Add a second wrapper-specific pacing layer above `RenderOutput`: rejected because it duplicates the - existing policy surface. - -### Decision: Pace compositor callback publication with compositor-owned scheduler state - -The compositor SHALL stop relying on immediate `frame_done` issuance as the sole pacing mechanism for -the active capture target. Instead, it SHALL use compositor-owned pacing state to decide when the next -callback/publication is eligible while preserving uncapped behavior when `target_fps = 0`. - -Rationale: -- Immediate callback issuance is the root of the current runaway nested-compositor behavior. -- The compositor event loop is the correct ownership boundary for capture callback cadence. - -Alternatives considered: -- Pace only the exported DMA-BUF publication while leaving callback cadence uncapped: rejected because - the target app can still free-run. -- Pace every surface uniformly: rejected because the current requirements focus on the active capture - target and bounded first-version scope. - -### Decision: Make runtime pacing controls session-scoped UI state that updates both halves together - -The Application window SHALL expose runtime pacing controls that initialize from the resolved -config/CLI target and update the active session target without rewriting config on disk. A runtime UI -change SHALL update compositor and viewer pacing together through one application-owned callback path. - -Rationale: -- The request explicitly requires ImGui configuration support. -- Session-scoped runtime control avoids introducing config-write policy in this change while still - making pacing debuggable live. - -Alternatives considered: -- Config/CLI only: rejected because it leaves no runtime control surface. -- Persist UI edits back to TOML immediately: rejected because it adds unrelated config-write behavior. - -### Decision: Keep host-aware acceptance explicit but backend timing-source-agnostic - -The contract SHALL explicitly require acceptance on both Wayland and X11 hosts, but the pacing policy -shall remain grounded in Goggles-owned timing state rather than depending on a host-compositor- -specific feedback API. - -Rationale: -- The proposal and delta specs require the same acceptance contract on both Wayland and X11 hosts. -- Goggles can control its own pacing state more deterministically than host-specific signal surfaces. - -Alternatives considered: -- Wayland-only first version: rejected because the proposal and delta specs require one acceptance - contract across both Wayland and X11 hosts. -- Separate host-specific pacing contracts: rejected because one v1 contract is simpler and more - testable. - -## Risks / Trade-offs - -- [Compositor responsiveness drift] -> bound compositor pacing to the active capture path only and - keep uncapped mode explicit. -- [Target-value drift between UI, compositor, and viewer] -> use one application-owned runtime update - path and keep one effective target value. -- [Host-specific acceptance flakiness] -> make Wayland and X11 validation explicit in tasks and keep - the numeric rule simple for v1. -- [Scope creep into persistence or multi-target policy] -> keep runtime UI changes session-scoped and - defer per-stage targets. - -## Migration Plan - -1. Define the OpenSpec contract changes for render pacing, compositor participation, and Application - window runtime controls. -2. Add shared runtime pacing state and update plumbing from config/CLI/UI through the Application - boundary. -3. Introduce compositor-owned callback pacing for the active capture target while preserving uncapped - mode. -4. Reuse existing viewer pacing logic for the viewer half of the shared target. -5. Verify the shared target and runtime controls on both Wayland and X11 hosts. - -Rollback strategy: -- Revert the change as one unit if shared pacing state or compositor callback pacing proves unstable; - do not keep split viewer/compositor behavior hidden behind the same target-FPS contract. - -## Open Questions - -- None. Remaining implementation details are bounded to code-path mapping and verification, not - product-definition ambiguity. diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/implementation-context.json b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/implementation-context.json deleted file mode 100644 index 8330cf46..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/implementation-context.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "schema_version": 1, - "change_id": "present-workflow-add-frame-pacing", - "artifact_digest": "a85ab29a13ea4d58fcb008f26dccf938ab10bcb41b1ed0e4d172b84c6a2e9496", - "authoritative_contract_paths": [ - "openspec/changes/present-workflow-add-frame-pacing/proposal.md", - "openspec/changes/present-workflow-add-frame-pacing/design.md", - "openspec/changes/present-workflow-add-frame-pacing/tasks.md", - "openspec/changes/present-workflow-add-frame-pacing/specs/render-pipeline/spec.md", - "openspec/changes/present-workflow-add-frame-pacing/specs/compositor-capture/spec.md", - "openspec/changes/present-workflow-add-frame-pacing/specs/app-window/spec.md" - ], - "readiness": { - "ambiguity_closed": true, - "implementation_ready": true - }, - "cross_cutting": { - "locked_constraints": [ - "One global pacing target governs app, compositor, and viewer for v1.", - "Compositor pacing is mandatory; viewer-only pacing is out of scope.", - "Viewer present pacing MUST reuse existing present-wait and CPU-throttle behavior where possible.", - "`target_fps = 0` remains the uncapped contract.", - "Runtime Application-window changes are session-scoped and MUST update compositor and viewer together.", - "Acceptance scope includes both Wayland-hosted and X11-hosted runs." - ], - "divergence_triggers": [ - "Any proposal to add per-stage targets or stage-local defaults during apply.", - "Any implementation that leaves compositor callback cadence driven solely by immediate commit-triggered `frame_done` for the active capture target.", - "Any viewer pacing rewrite that bypasses `RenderOutput` present-wait / fallback throttling instead of reusing it.", - "Any Application-window change that rewrites config-on-disk or splits compositor and viewer runtime updates.", - "Any validation plan that drops Wayland or X11 host scope without a proposal revision." - ], - "verification_contract": { - "baseline_gates": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality" - ], - "environment_agnostic_checks": [ - "pixi run test -p asan" - ], - "environment_sensitive_checks": [ - "pixi run test -p test", - "Wayland-host pacing validation", - "X11-host pacing validation" - ], - "manual_fallback_allowed_for": [ - "Visible pacing verification on Wayland/X11 hosts when automated coverage is unavailable", - "Application-window runtime pacing control validation when automated UI coverage is unavailable" - ], - "mandatory_no_fallback": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality" - ] - } - }, - "task_groups": [ - { - "group_id": "1", - "contract_refs": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:1", - "openspec/changes/present-workflow-add-frame-pacing/design.md:35", - "openspec/changes/present-workflow-add-frame-pacing/design.md:83", - "openspec/changes/present-workflow-add-frame-pacing/specs/render-pipeline/spec.md:3", - "openspec/changes/present-workflow-add-frame-pacing/specs/app-window/spec.md:3" - ], - "candidate_paths": [ - "src/util/config.hpp", - "src/util/config.cpp", - "src/app/cli.hpp", - "src/app/cli.cpp", - "src/app/main.cpp", - "src/app/application.hpp", - "src/app/application.cpp", - "src/compositor/compositor_server.hpp", - "src/compositor/compositor_server.cpp", - "src/render/backend/vulkan_backend.hpp", - "src/render/backend/vulkan_backend.cpp", - "src/render/backend/render_output.hpp" - ], - "candidate_symbols": [ - "goggles::app::CliOptions::target_fps", - "goggles::app::Application::init_vulkan_backend", - "goggles::app::Application::init_compositor_server", - "goggles::app::Application::init_compositor_server_headless", - "goggles::input::CompositorServer::create", - "goggles::render::RenderSettings::target_fps", - "goggles::render::VulkanBackend::update_target_fps", - "goggles::render::backend_internal::RenderOutput::set_target_fps" - ], - "first_reads": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:1-8", - "src/util/config.cpp:77-95", - "src/app/main.cpp:506-513", - "src/app/application.hpp:73-80", - "src/app/application.cpp:95-115", - "src/app/application.cpp:177-209", - "src/compositor/compositor_server.hpp:60-113", - "src/render/backend/vulkan_backend.hpp:21-28", - "src/render/backend/vulkan_backend.cpp:74-78" - ], - "suggested_checks": [ - "ctest --preset test -R \"config\" --output-on-failure", - "pixi run build -p debug" - ], - "risks": [ - "Config/CLI and runtime update plumbing can drift if compositor and viewer are not updated through one application-owned path.", - "Changing the startup target contract without preserving `target_fps = 0` risks uncapped-mode regressions." - ], - "confidence": 0.83 - }, - { - "group_id": "2", - "contract_refs": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:10", - "openspec/changes/present-workflow-add-frame-pacing/design.md:67", - "openspec/changes/present-workflow-add-frame-pacing/specs/compositor-capture/spec.md:3" - ], - "candidate_paths": [ - "src/compositor/compositor_state.hpp", - "src/compositor/compositor_present.cpp", - "src/compositor/compositor_input.cpp", - "src/compositor/compositor_xdg.cpp", - "src/compositor/compositor_xwayland.cpp", - "src/compositor/compositor_layer_shell.cpp", - "src/compositor/compositor_focus.cpp", - "src/compositor/compositor_core.cpp" - ], - "candidate_symbols": [ - "goggles::input::CompositorState::handle_xdg_surface_commit", - "goggles::input::CompositorState::handle_xdg_popup_commit", - "goggles::input::CompositorState::handle_xwayland_surface_commit", - "goggles::input::CompositorState::handle_layer_surface_commit", - "goggles::input::CompositorState::request_present_reset", - "goggles::input::CompositorState::refresh_presented_frame" - ], - "first_reads": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:10-17", - "src/compositor/compositor_state.hpp:120-179", - "src/compositor/compositor_present.cpp:237-274", - "src/compositor/compositor_xdg.cpp:170-205", - "src/compositor/compositor_xdg.cpp:269-285", - "src/compositor/compositor_xwayland.cpp:343-364", - "src/compositor/compositor_layer_shell.cpp:168-219", - "src/compositor/compositor_input.cpp:335-341" - ], - "suggested_checks": [ - "pixi run test -p test", - "Manual Wayland-host and X11-host pacing validation per task 4.4" - ], - "risks": [ - "Replacing immediate `frame_done` behavior can stall targets if pacing eligibility and present-reset wakeups are not coordinated.", - "Host-sensitive behavior may diverge between Wayland and X11 if callback pacing is tied to the wrong compositor event path." - ], - "confidence": 0.74 - }, - { - "group_id": "3", - "contract_refs": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:19", - "openspec/changes/present-workflow-add-frame-pacing/design.md:51", - "openspec/changes/present-workflow-add-frame-pacing/design.md:83", - "openspec/changes/present-workflow-add-frame-pacing/specs/render-pipeline/spec.md:3", - "openspec/changes/present-workflow-add-frame-pacing/specs/app-window/spec.md:22" - ], - "candidate_paths": [ - "src/render/backend/render_output.hpp", - "src/render/backend/render_output.cpp", - "src/render/backend/vulkan_backend.cpp", - "src/ui/imgui_layer.hpp", - "src/ui/imgui_layer.cpp", - "src/app/application.cpp" - ], - "candidate_symbols": [ - "goggles::render::backend_internal::RenderOutput::set_target_fps", - "goggles::render::VulkanBackend::update_target_fps", - "goggles::ui::ImGuiLayer::draw_app_management", - "goggles::ui::ImGuiLayer::set_runtime_metrics", - "goggles::app::Application::init_imgui_layer" - ], - "first_reads": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:19-26", - "src/render/backend/render_output.hpp:51-54", - "src/render/backend/render_output.cpp:300-345", - "src/render/backend/render_output.cpp:907-915", - "src/ui/imgui_layer.hpp:152-172", - "src/ui/imgui_layer.cpp:755-836", - "src/app/application.cpp:145-196" - ], - "suggested_checks": [ - "pixi run build -p debug", - "ctest --preset test -R \"vulkan-backend\" --output-on-failure" - ], - "risks": [ - "ImGui controls can display a value that is not actually applied if the runtime callback path updates only one subsystem.", - "Viewer fallback pacing may retain stale target state if only compositor updates are wired at runtime." - ], - "confidence": 0.79 - }, - { - "group_id": "4", - "contract_refs": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:28", - "openspec/changes/present-workflow-add-frame-pacing/proposal.md:97", - "openspec/changes/present-workflow-add-frame-pacing/specs/render-pipeline/spec.md:34", - "openspec/changes/present-workflow-add-frame-pacing/specs/compositor-capture/spec.md:23", - "openspec/changes/present-workflow-add-frame-pacing/specs/app-window/spec.md:30" - ], - "candidate_paths": [ - "tests/util/test_config.cpp", - "tests/app/test_cli.cpp", - "tests/app/test_headless_child_exit.cpp", - "tests/input/auto_input_forwarding_x11.cpp", - "tests/input/auto_input_forwarding_wayland.cpp", - "tests/render/test_vulkan_backend_subsystem_contracts.cpp", - "tests/CMakeLists.txt", - "pixi.toml", - "CMakePresets.json" - ], - "candidate_symbols": [ - "TEST_CASE(\"load_config validates target_fps values\", \"[config]\")", - "TEST_CASE(\"load_config handles valid target_fps range\", \"[config]\")", - "TEST_CASE(\"parse_cli: headless mode parses all flags\", \"[cli]\")", - "TEST_CASE(\"parse_cli: headless mode requires --frames\", \"[cli]\")", - "TEST_CASE(\"Vulkan backend seam declarations stay compile-safe\", \"[vulkan-backend-module-layout]\")", - "TEST_CASE(\"Vulkan backend teardown audit hooks stay aligned with shutdown order\", \"[vulkan-backend-lifetime]\")" - ], - "first_reads": [ - "openspec/changes/present-workflow-add-frame-pacing/tasks.md:28-38", - "tests/util/test_config.cpp:77-179", - "tests/app/test_cli.cpp:1-220", - "tests/app/test_headless_child_exit.cpp:1-220", - "tests/input/auto_input_forwarding_x11.cpp:1-260", - "tests/input/auto_input_forwarding_wayland.cpp:1-260", - "tests/render/test_vulkan_backend_subsystem_contracts.cpp:66-245", - "CMakePresets.json:1-200", - "pixi.toml:1-220" - ], - "suggested_checks": [ - "ctest --preset test -R \"test_cli|headless_child_exit\" --output-on-failure", - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run test -p asan", - "pixi run build -p quality", - "pixi run test -p test" - ], - "risks": [ - "Automated coverage may not exercise both Wayland-hosted and X11-hosted pacing paths, forcing a documented manual fallback.", - "New tests that depend on host runtime details can become flaky if they are not bounded to config/runtime-plumbing assertions." - ], - "confidence": 0.82 - } - ] -} diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/manual-host-validation-fallback.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/manual-host-validation-fallback.md deleted file mode 100644 index 7904f30a..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/manual-host-validation-fallback.md +++ /dev/null @@ -1,9 +0,0 @@ -# Dual-Host Pacing Validation Fallback - -- fallback_name: `dual-host-pacing-validation-fallback` -- local_runtime_proof: `printenv DISPLAY WAYLAND_DISPLAY XDG_SESSION_TYPE` returned `DISPLAY=:0`, `WAYLAND_DISPLAY=wayland-0`, and `XDG_SESSION_TYPE=wayland`. This confirms a Wayland-hosted session with XWayland available, not a true pair of separate Wayland-host and X11-host runtimes for the required dual-host acceptance pass. - -| Target | Host type | Observed FPS window | Proof location | -| --- | --- | --- | --- | -| Active capture target driven from the Application window pacing controls | Wayland host | Unobserved in this apply run; a manual host run is still required to record capped and uncapped FPS behavior over the required observation window. | `openspec/changes/present-workflow-add-frame-pacing/manual-host-validation-fallback.md` | -| Active capture target driven from the Application window pacing controls | X11 host | Unobserved in this apply run; `DISPLAY=:0` came from XWayland inside a Wayland-hosted session, so a separate X11-host session is still required for true X11-host acceptance. | `openspec/changes/present-workflow-add-frame-pacing/manual-host-validation-fallback.md` | diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/proposal.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/proposal.md deleted file mode 100644 index b4813423..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/proposal.md +++ /dev/null @@ -1,124 +0,0 @@ -## Why - -`pixi run start -- vkcube` can currently drive the nested compositor and viewer at extremely high -frame rates because the compositor-side present path does not pace client callbacks while the viewer -only paces its own final presentation. That wastes power, makes the end-to-end cadence depend on the -host compositor, and breaks the expectation that one target FPS governs the whole Goggles session. - -## Problem - -- `render.target_fps` currently paces the viewer backend, but it does not govern compositor callback - cadence for captured clients. -- The nested compositor can still issue `frame_done` as fast as commits arrive, so the target app may - free-run even when the viewer is paced. -- The Application window exposes performance metrics but no runtime control for the pacing target, - leaving config and CLI as the only control surfaces. -- Wayland-hosted and X11-hosted sessions need one consistent pacing contract for this first version. - -## Scope - -- Extend pacing from the current viewer-only behavior into the full `app -> compositor -> viewer` - present workflow. -- Keep one global pacing target for v1 instead of stage-specific targets. -- Reuse the existing viewer present-wait and CPU-throttle subsystem where possible rather than - introducing a second independent pacing policy. -- Add Application-window runtime controls for pacing that stay aligned with the existing - config/CLI-driven `render.target_fps` contract. -- Define acceptance around FPS tolerance only for v1: +/-3 FPS over 10 seconds on both Wayland and - X11 hosts. - -## What Changes - -- Update render-pipeline requirements so `render.target_fps` becomes the authoritative pacing target - for the full present workflow, not only the viewer's final swapchain present. -- Update compositor-capture requirements so the compositor participates in pacing the capture and - client callback flow before frames reach the viewer. -- Update Application-window requirements so runtime pacing controls are exposed alongside the - existing management/performance surfaces and follow the existing target-FPS contract. -- Preserve `target_fps = 0` as the uncapped mode and keep the existing viewer fallback behavior - explicit when present-wait-style pacing is unavailable. - -## Capabilities - -### New Capabilities -- None. - -### Modified Capabilities -- `render-pipeline`: `render.target_fps` changes from viewer-final-present pacing to a global - end-to-end pacing contract that still reuses the current present-wait/fallback subsystem. -- `compositor-capture`: the compositor capture path changes from immediate callback-driven flow to a - paced capture/publication path that participates in the global target FPS contract. -- `app-window`: the Application window changes from metrics-only pacing visibility to runtime pacing - control plus explicit precedence with existing config/CLI target-FPS settings. - -## Non-goals - -- Introduce separate app-side, compositor-side, or viewer-side FPS targets in v1. -- Accept a viewer-only solution that leaves the nested compositor uncapped. -- Add a second numeric jitter gate in v1 beyond the FPS tolerance rule. -- Redesign unrelated shader, surface-management, or performance-panel behavior. -- Change packaging, dependency, or distribution workflows. - -## Impact - -- Affected modules: `src/compositor`, `src/render/backend`, `src/app`, `src/ui`, and `src/util`. -- Likely affected files: - - `src/compositor/compositor_server.hpp` - - `src/compositor/compositor_server.cpp` - - `src/compositor/compositor_xdg.cpp` - - `src/compositor/compositor_xwayland.cpp` - - `src/compositor/compositor_layer_shell.cpp` - - `src/compositor/compositor_input.cpp` - - `src/compositor/compositor_present.cpp` - - `src/compositor/compositor_state.hpp` - - `src/render/backend/render_output.cpp` - - `src/render/backend/render_output.hpp` - - `src/render/backend/vulkan_backend.cpp` - - `src/app/application.cpp` - - `src/app/application.hpp` - - `src/app/main.cpp` - - `src/ui/imgui_layer.cpp` - - `src/ui/imgui_layer.hpp` - - `src/util/config.hpp` - - `src/util/config.cpp` -- Impacted OpenSpec specs: - - `openspec/specs/render-pipeline/spec.md` - - `openspec/specs/compositor-capture/spec.md` - - `openspec/specs/app-window/spec.md` -- No expected packaging or dependency changes. - -## Risks - -- Compositor pacing can drift from viewer pacing if the shared target is not routed through one - policy boundary. -- Host-specific behavior can diverge between Wayland and X11 if callback pacing semantics are not - made explicit in the contract. -- Runtime controls can drift from config/CLI semantics if the precedence chain is not defined. -- Injecting pacing into the compositor event loop can regress responsiveness if the contract does not - bound what is paced vs what remains immediate. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `pixi run test -p asan` -- Environment-sensitive checks: - - `pixi run test -p test` when local compositor/runtime support is available - - targeted host validation for Wayland and X11 pacing behavior when the local runtime supports both -- Manual fallback: - - allowed only for visible pacing verification on Wayland/X11 hosts and Application-window runtime - control behavior when automated coverage is unavailable - - record target FPS, host type, observed FPS window, and proof location -- Mandatory checks with no fallback: - - the baseline build/static-analysis gates above -- Pass criteria: - - one global pacing target governs the compositor and viewer present workflow - - `target_fps = 0` remains uncapped - - Wayland-hosted and X11-hosted sessions stay within +/-3 FPS of the selected target over 10 - seconds - - the Application window exposes runtime pacing control without breaking the existing config/CLI - contract diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/app-window/spec.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/app-window/spec.md deleted file mode 100644 index 8bcc38e4..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/app-window/spec.md +++ /dev/null @@ -1,45 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Target FPS CLI Override - -The application SHALL allow overriding the effective global pacing target FPS from the command line. - -#### Scenario: Override target fps via CLI -- **GIVEN** the application is started with `--target-fps 120` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `120` -- **AND** the override SHALL take precedence over the config file -- **AND** the effective global pacing target for the current session SHALL be `120` - -#### Scenario: Disable frame cap via CLI -- **GIVEN** the application is started with `--target-fps 0` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `0` -- **AND** the effective global pacing target SHALL be uncapped - -## ADDED Requirements - -### Requirement: Application Window Runtime Frame Pacing Controls - -The Application window SHALL expose runtime controls for the effective global pacing target used by -the current Goggles session. - -The controls SHALL initialize from the resolved `render.target_fps` value and SHALL update the active -session pacing target without requiring restart. - -#### Scenario: Runtime controls reflect startup pacing target -- **GIVEN** the application window is rendered after config and CLI resolution -- **WHEN** the runtime pacing controls are shown -- **THEN** they SHALL reflect the current effective `render.target_fps` value for the session - -#### Scenario: Runtime controls update active pacing target -- **GIVEN** the Application window runtime pacing controls are visible -- **WHEN** the user selects a new non-zero target FPS -- **THEN** the effective global pacing target SHALL update for the current session -- **AND** the compositor and viewer pacing paths SHALL observe the same updated target - -#### Scenario: Runtime controls allow uncapped mode -- **GIVEN** the Application window runtime pacing controls are visible -- **WHEN** the user selects uncapped pacing -- **THEN** the effective global pacing target SHALL become `0` -- **AND** the session SHALL switch to uncapped pacing without restart diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/compositor-capture/spec.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/compositor-capture/spec.md deleted file mode 100644 index 7d55fcdc..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/compositor-capture/spec.md +++ /dev/null @@ -1,27 +0,0 @@ -## ADDED Requirements - -### Requirement: Compositor Capture Participates in Global Frame Pacing - -The compositor capture path SHALL participate in the same effective target FPS contract that drives -viewer presentation for the current Goggles session. - -For the active capture target, the compositor SHALL pace callback/publication flow so the nested -target application is not driven solely by immediate commit-triggered `frame_done` issuance. - -#### Scenario: Active capture target follows global pacing target -- **GIVEN** an active capture target and a non-zero effective target FPS -- **WHEN** the compositor is issuing callbacks and publishing captured frames for that target -- **THEN** the compositor SHALL apply pacing for that target using the effective global target FPS -- **AND** the target SHALL NOT be driven solely by immediate commit-triggered callback issuance - -#### Scenario: Uncapped mode bypasses compositor pacing delays -- **GIVEN** the effective global target FPS is `0` -- **WHEN** the compositor is issuing callbacks and publishing frames for the active target -- **THEN** the compositor SHALL bypass target-interval pacing delays -- **AND** the workflow SHALL remain explicitly uncapped - -#### Scenario: Host acceptance scope covers Wayland and X11 -- **GIVEN** Goggles is running on either a Wayland host or an X11 host -- **WHEN** the active capture target participates in the paced compositor path -- **THEN** the compositor pacing contract SHALL apply in both host environments -- **AND** acceptance SHALL use the same target-FPS rule in both environments diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/render-pipeline/spec.md deleted file mode 100644 index 2f76af6c..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/specs/render-pipeline/spec.md +++ /dev/null @@ -1,38 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Present Wait Frame Pacing - -The render backend SHALL use `VK_KHR_present_wait` when supported to pace viewer presentation and -avoid uncapped mailbox behavior on high-end GPUs. - -`render.target_fps` SHALL be treated as the effective global pacing target for the current Goggles -session. - -The viewer backend SHALL reuse the existing present-wait and CPU-throttle fallback behavior as the -viewer half of that global pacing contract. - -#### Scenario: Present wait enabled -- **GIVEN** the physical device supports `VK_KHR_present_wait` -- **WHEN** the swapchain is created -- **THEN** the device SHALL enable the extension -- **AND** the present mode SHALL be `FIFO` -- **AND** the backend SHALL use present wait to pace viewer presentation to `render.target_fps` - -#### Scenario: Uncapped target fps -- **GIVEN** `render.target_fps` is set to `0` -- **WHEN** present wait is available -- **THEN** the backend SHALL skip waiting for a target interval -- **AND** viewer presentation SHALL proceed as fast as `FIFO` allows - -#### Scenario: Present wait unsupported -- **GIVEN** `VK_KHR_present_wait` is not supported -- **WHEN** the swapchain is created -- **THEN** the backend SHALL prefer `MAILBOX` present mode -- **AND** it SHALL apply CPU-side frame capping when `render.target_fps` is non-zero -- **AND** it SHALL fall back to `FIFO` if `MAILBOX` is unavailable - -#### Scenario: Target fps changes via config or runtime control -- **GIVEN** a Goggles session has an effective `render.target_fps` -- **WHEN** configuration, CLI startup, or Application-window runtime controls change that target -- **THEN** the backend SHALL update viewer pacing to the new target without requiring restart -- **AND** `render.target_fps = 0` SHALL continue to mean uncapped pacing diff --git a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/tasks.md b/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/tasks.md deleted file mode 100644 index 7caa67bd..00000000 --- a/openspec/changes/archive/2026-03-09-present-workflow-add-frame-pacing/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -## 1. Shared pacing target and runtime plumbing - -- [x] 1.1 Extend the runtime pacing state so one effective `render.target_fps` value can be read and - updated through the application boundary without splitting compositor and viewer policy. -- [x] 1.2 Thread the effective pacing target from config/CLI startup state into both - `input::CompositorServer` and `render::VulkanBackend`, preserving `target_fps = 0` as uncapped. -- [x] 1.3 Add one runtime update path in `src/app/application.cpp` that applies pacing changes to the - compositor and viewer together. - -## 2. Compositor capture pacing - -- [x] 2.1 Add compositor-owned pacing state for the active capture target in `src/compositor/` so - callback/publication flow no longer depends only on immediate commit-triggered `frame_done`. -- [x] 2.2 Update the XDG, XWayland, and layer-shell capture paths to use the compositor pacing policy - while keeping uncapped mode explicit. -- [x] 2.3 Verify the compositor pacing path preserves active-target behavior, present reset behavior, - and compatibility across Wayland-hosted and X11-hosted sessions. - -## 3. Viewer reuse and Application window controls - -- [x] 3.1 Reuse the existing `src/render/backend/render_output.cpp` present-wait / throttle logic as - the viewer half of the shared pacing contract. -- [x] 3.2 Add Application-window runtime pacing controls in `src/ui/imgui_layer.*` that reflect the - current effective target and support uncapped mode. -- [x] 3.3 Wire Application-window pacing changes through `src/app/application.cpp` so compositor and - viewer updates are applied together without restart. - -## 4. Verification - -- [x] 4.1 Verify the implementation matches the `render-pipeline`, `compositor-capture`, and - `app-window` delta specs and preserves the one-global-target contract. -- [x] 4.2 Add or update targeted automated coverage for pacing configuration and runtime update - plumbing where repository test surfaces allow bounded coverage. -- [x] 4.3 Run `pixi run build -p debug`, `pixi run build -p asan`, `pixi run test -p asan`, and - `pixi run build -p quality`. -- [x] 4.4 Run host-sensitive pacing validation for Wayland and X11 when the local runtime supports - both; otherwise record a named manual fallback with target, host type, observed FPS window, and - proof location. diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/.openspec.yaml b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/.openspec.yaml deleted file mode 100644 index f34a3859..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-10 diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/design.md b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/design.md deleted file mode 100644 index 28740121..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/design.md +++ /dev/null @@ -1,100 +0,0 @@ -## Context - -The current CI static-analysis path executes Semgrep and the quality build in one serialized lane. -That composition keeps both checks authoritative but elongates pull-request feedback by making the -slowest lane a strict serial chain. - -The requested outcome is to reduce both total workflow duration and static-analysis path duration -while preserving equivalent PR-gated safety and keeping `pixi run ci` as the local reproduction -surface. - -This design changes workflow composition only; it does not alter runtime product behavior. - -## Goals / Non-Goals - -**Goals:** -- Partition static-analysis checks into independently executable lanes so CI can parallelize them. -- Keep Semgrep and quality build as blocking PR checks with equivalent merge protection. -- Preserve deterministic local reproduction through `pixi run ci`. -- Keep CI contracts explicit in OpenSpec artifacts and implementation-context mapping. - -**Non-Goals:** -- Changing what Semgrep checks for or weakening quality-build semantics. -- Moving required checks outside PR-triggered gating. -- Reworking unrelated CI lanes (`format`, `build-test`) beyond integration touch points. -- Defining a strict numeric SLA for job durations. - -## Decisions - -### Decision: Split static-analysis into two canonical lanes - -The CI task backend SHALL expose separate static-analysis lanes for: -- Semgrep + fixture verification -- quality-build execution - -Rationale: -- The current serial lane composes independent checks that can be run concurrently. -- Lane decomposition keeps command ownership in one repository-controlled script while enabling CI - job-level scheduling improvements. - -Alternatives considered: -- Keep one static-analysis lane and tune compiler flags only: rejected because it cannot unlock - workflow-level parallelism. -- Move Semgrep to non-blocking informational checks: rejected because it violates the required - blocking-gate contract. - -### Decision: Parallelize PR job graph while preserving equivalent required coverage - -The GitHub Actions workflow SHALL run Semgrep and quality-build static-analysis lanes in separate -PR jobs that are both required before merge. - -Rationale: -- Parallel jobs reduce critical path time without dropping checks. -- Separate job visibility improves diagnosis when one static-analysis surface regresses. - -Alternatives considered: -- Keep one PR job with background subprocess fan-out: rejected because job-level result handling and - observability are weaker than explicit parallel jobs. -- Shift one check to post-merge/nightly: rejected because it reduces PR-time safety. - -### Decision: Preserve a combined local lane as deterministic reproduction contract - -`pixi run ci --lane static-analysis` SHALL remain a combined local entrypoint that runs both -static-analysis surfaces in a deterministic repository-defined order, while exposing split lanes for -targeted reproduction. - -Rationale: -- Contributors rely on one canonical local command that reflects CI intent. -- Split lanes are still needed for focused debugging and CI wiring. - -Alternatives considered: -- Remove combined lane and require two manual commands locally: rejected because it increases drift - risk and operator burden. - -## Risks / Trade-offs - -- [Coverage drift during workflow refactor] -> enforce both split jobs as required PR checks and keep - spec scenarios explicit. -- [Local/CI contract mismatch] -> keep split and combined behavior codified in one task backend and - one spec delta. -- [Marginal speed gain despite decomposition] -> retain timing-stage instrumentation and compare - baseline vs updated run data. -- [Cache invalidation from job separation] -> preserve explicit ccache setup and restore keys for - each split job. - -## Migration Plan - -1. Update CI OpenSpec requirement deltas to codify split-lane + equivalent-gate behavior. -2. Extend `scripts/task/ci.sh` lane model to provide separate Semgrep and quality lanes while - keeping combined static-analysis behavior. -3. Update `pixi.toml`/workflow command surfaces so local and CI use canonical lane entrypoints. -4. Reshape `.github/workflows/ci.yml` static-analysis graph into parallel required jobs. -5. Verify baseline gates, lane commands, and CI timing/coverage outcomes. - -Rollback strategy: -- Revert lane split and workflow graph changes together to restore prior serialized behavior if - coverage or determinism contracts break. - -## Open Questions - -- None. diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/implementation-context.json b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/implementation-context.json deleted file mode 100644 index da4bdd50..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/implementation-context.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "schema_version": 1, - "change_id": "ci-accelerate-static-analysis", - "artifact_digest": "fd63c311e599ed24685097345a7023671c1cf906bdc79767324c1cfa01dc4496", - "authoritative_contract_paths": [ - "openspec/changes/ci-accelerate-static-analysis/proposal.md", - "openspec/changes/ci-accelerate-static-analysis/design.md", - "openspec/changes/ci-accelerate-static-analysis/tasks.md", - "openspec/changes/ci-accelerate-static-analysis/specs/ci/spec.md" - ], - "readiness": { - "ambiguity_closed": true, - "implementation_ready": true - }, - "cross_cutting": { - "locked_constraints": [ - "Semgrep and quality-build checks remain blocking PR gates.", - "Static-analysis optimization preserves equivalent PR coverage and does not move required checks to non-PR triggers.", - "Local reproduction remains repository-owned via pixi run ci lane entrypoints.", - "Build and static analysis orchestration remains Pixi/CMake preset aligned." - ], - "divergence_triggers": [ - "Any workflow change that makes either Semgrep or quality-build non-blocking in PR.", - "Any lane refactor that removes or changes deterministic behavior of pixi run ci --lane static-analysis.", - "Any proposal to move required checks to post-merge, nightly, or scheduled-only execution.", - "Any implementation that updates CI composition without corresponding spec/task contract alignment." - ], - "verification_contract": { - "baseline_gates": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality" - ], - "environment_agnostic_checks": [ - "pixi run ci --lane static-analysis", - "pixi run ci --lane build-test" - ], - "environment_sensitive_checks": [ - "Inspect GitHub Actions PR run for split static-analysis jobs and equivalent required coverage" - ], - "manual_fallback": { - "allowed_only_for": [ - "Comparing hosted CI timing deltas when run-to-run variance prevents strict local timing equivalence" - ], - "required_record": [ - "run links", - "commit SHA", - "job durations" - ] - }, - "mandatory_no_fallback": [ - "Semgrep PR gate remains blocking", - "quality-build PR gate remains blocking" - ], - "pass_criteria": [ - "Overall workflow wall-clock improves for equivalent PR coverage.", - "Static-analysis PR path duration improves for equivalent PR coverage.", - "Local pixi run ci static-analysis entrypoint remains deterministic and repository-owned.", - "Split static-analysis jobs preserve equivalent required PR checks." - ] - } - }, - "task_groups": [ - { - "group_id": "1", - "contract_refs": [ - "openspec/changes/ci-accelerate-static-analysis/tasks.md#1-static-analysis-lane-decomposition", - "openspec/changes/ci-accelerate-static-analysis/specs/ci/spec.md#Requirement:-Static-Analysis-Coverage-Supports-Parallel-PR-Execution" - ], - "candidate_paths": [ - "scripts/task/ci.sh" - ], - "candidate_symbols": [ - "run_static_analysis_lane", - "run_timed_stage", - "container_lane_command", - "case \"$LANE\"" - ], - "first_reads": [ - "scripts/task/ci.sh:163", - "scripts/task/ci.sh:251", - "scripts/task/ci.sh:431", - "scripts/task/ci.sh:503", - "scripts/task/ci.sh:574" - ], - "suggested_checks": [ - "pixi run ci --lane static-analysis", - "pixi run ci --lane static-analysis --runner container --cache-mode warm" - ], - "risks": [ - "Lane split can unintentionally change failure propagation semantics.", - "Container and host lane dispatch may drift if lane names diverge." - ], - "confidence": 0.89 - }, - { - "group_id": "2", - "contract_refs": [ - "openspec/changes/ci-accelerate-static-analysis/tasks.md#2-ci-workflow-graph-reshaping", - "openspec/changes/ci-accelerate-static-analysis/specs/ci/spec.md#Requirement:-Repo-Controlled-Semgrep-Gate" - ], - "candidate_paths": [ - ".github/workflows/ci.yml" - ], - "candidate_symbols": [ - "jobs.static-analysis-semgrep", - "jobs.static-analysis-quality", - "jobs.build-and-test", - "jobs.format-check" - ], - "first_reads": [ - ".github/workflows/ci.yml:21", - ".github/workflows/ci.yml:77" - ], - "suggested_checks": [ - "gh run view --repo goggles-dev/Goggles --json jobs,status,conclusion" - ], - "risks": [ - "PR protection can become weaker if one split job is not marked required.", - "Job split can reduce cache hit rate if ccache keys are not aligned." - ], - "confidence": 0.86 - }, - { - "group_id": "3", - "contract_refs": [ - "openspec/changes/ci-accelerate-static-analysis/tasks.md#3-local-command-surface-alignment", - "openspec/changes/ci-accelerate-static-analysis/specs/ci/spec.md#Requirement:-Static-Analysis-Coverage-Supports-Parallel-PR-Execution" - ], - "candidate_paths": [ - "pixi.toml", - "scripts/task/help.sh", - "scripts/task/ci.sh" - ], - "candidate_symbols": [ - "tasks.ci", - "tasks.semgrep", - "usage()" - ], - "first_reads": [ - "pixi.toml:41", - "pixi.toml:90", - "scripts/task/help.sh:10", - "scripts/task/ci.sh:166" - ], - "suggested_checks": [ - "pixi run help", - "pixi run ci --lane static-analysis", - "pixi run semgrep" - ], - "risks": [ - "Command help text may fall out of sync with implemented lane options.", - "Local reproduction claim can drift if split lane names are undocumented." - ], - "confidence": 0.82 - }, - { - "group_id": "4", - "contract_refs": [ - "openspec/changes/ci-accelerate-static-analysis/tasks.md#4-verification", - "openspec/changes/ci-accelerate-static-analysis/proposal.md#Validation-Plan" - ], - "candidate_paths": [ - "scripts/task/ci.sh", - ".github/workflows/ci.yml", - "pixi.toml" - ], - "candidate_symbols": [ - "print_stage_summary", - "run_static_analysis_lane", - "jobs" - ], - "first_reads": [ - "scripts/task/ci.sh:124", - "scripts/task/ci.sh:603", - ".github/workflows/ci.yml:109" - ], - "suggested_checks": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality", - "pixi run ci --lane static-analysis", - "pixi run ci --lane build-test" - ], - "risks": [ - "Hosted CI timing variance can mask improvements if only one run is compared.", - "Verification can pass locally while PR required-check configuration regresses remotely." - ], - "confidence": 0.84 - } - ] -} diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/proposal.md b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/proposal.md deleted file mode 100644 index b99168cb..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/proposal.md +++ /dev/null @@ -1,102 +0,0 @@ -## Why - -The current CI static-analysis coverage runs Semgrep and the quality build in one serialized lane, -which dominates workflow wall-clock time and slows pull-request feedback. - -This change restructures CI static-analysis execution to preserve equivalent PR-gated safety while -reducing end-to-end latency and retaining reproducible local entrypoints. - -## Problem - -- The `Static Analysis` PR check is the longest CI job and extends total workflow completion time. -- Semgrep and quality build checks are currently composed as one lane, preventing job-level - parallelism. -- Local reproduction relies on `pixi run ci`, so any CI reshaping must preserve deterministic - repository-controlled command surfaces. - -## Scope - -- Restructure static-analysis execution so Semgrep and quality-build checks can run as independent - PR-gated jobs. -- Preserve equivalent PR coverage: both checks remain required before merge. -- Keep local reproduction aligned through `pixi run ci` lane entrypoints. -- Update OpenSpec CI requirements, design rationale, and implementation tasks to reflect the new - contract. - -## What Changes - -- Split static-analysis execution into two independently runnable lanes: Semgrep-focused and - quality-build-focused. -- Update GitHub Actions CI job graph so those lanes run in parallel as separate required PR checks. -- Keep a combined static-analysis local entrypoint that executes both checks for deterministic local - reproduction. -- Preserve existing policy and quality semantics: Semgrep remains blocking and quality build remains - blocking. - -## Capabilities - -### New Capabilities -- None. - -### Modified Capabilities -- `ci`: static-analysis requirements change from serialized lane composition to parallelizable, - independently runnable lanes with equivalent PR-gated coverage and preserved local reproduction - semantics. - -## Non-goals - -- Reducing PR safety coverage by moving required checks to non-PR triggers. -- Replacing Semgrep or quality build with weaker substitutes. -- Broad CI redesign outside static-analysis composition and its local/CI entrypoints. -- Defining a hard SLA runtime target; this remains best-effort optimization. - -## Impact - -- Affected modules/files: - - `.github/workflows/ci.yml` - - `scripts/task/ci.sh` - - `pixi.toml` - - optional CI helper docs if command surfaces change -- Impacted OpenSpec specs: - - `openspec/specs/ci/spec.md` -- No product runtime, Vulkan, shader, packaging, or API behavior changes are expected. - -## Policy-sensitive impacts - -- Build/test orchestration MUST continue using Pixi task entrypoints and CMake/CTest preset-based - commands. -- Static-analysis checks remain blocking gates in PR. -- Repository-controlled determinism MUST be preserved for local and CI execution. - -## Risks - -| Risk | Severity | Likelihood | Mitigation | -|------|----------|------------|------------| -| PR coverage accidentally weakens during job split | HIGH | MEDIUM | Require both Semgrep and quality jobs as blocking PR checks | -| Local reproduction drifts from CI behavior | MEDIUM | MEDIUM | Keep `pixi run ci` as canonical entrypoint with explicit lane mapping | -| Cache behavior changes offset expected speedup | MEDIUM | MEDIUM | Keep ccache configuration explicit and measure stage timing before/after | -| Workflow complexity grows with limited net gain | MEDIUM | LOW | Bound scope to static-analysis decomposition only and keep deterministic lanes | - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `pixi run ci --lane static-analysis` - - `pixi run ci --lane build-test` -- Environment-sensitive checks: - - CI run inspection of PR job graph and durations in GitHub Actions -- Manual fallback: - - allowed only for comparing CI job timing deltas when hosted-run variance prevents strict local - equivalence - - record run links, commit SHA, and observed job durations -- Mandatory checks with no fallback: - - Semgrep and quality-build PR gates remain blocking -- Pass criteria: - - overall CI workflow wall-clock improves against the baseline run for equivalent coverage - - static-analysis PR path duration improves against the baseline run - - local `pixi run ci` entrypoints remain deterministic and reproduce lane behavior - - required PR coverage remains equivalent after job reshaping diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/specs/ci/spec.md b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/specs/ci/spec.md deleted file mode 100644 index c8e0654c..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/specs/ci/spec.md +++ /dev/null @@ -1,69 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Repo-Controlled Semgrep Gate - -The CI system SHALL run a repo-controlled Semgrep scan as a blocking step in the static-analysis -workflow. - -The CI system SHALL allow the Semgrep blocking step to run in a dedicated PR-gated job that MAY run -in parallel with other static-analysis checks, provided equivalent required PR coverage is preserved. - -#### Scenario: Static-analysis job runs checked-in Semgrep rules -- **GIVEN** the repository defines a Semgrep configuration and local ruleset -- **WHEN** the static-analysis workflow runs in CI -- **THEN** it SHALL execute Semgrep from repository-checked-in configuration -- **AND** it SHALL fail the job when Semgrep reports a blocking finding - -#### Scenario: Local and CI Semgrep use repository-defined entrypoints -- **GIVEN** the repository exposes repository-owned Semgrep entrypoints for local and CI execution -- **WHEN** contributors run the local command and CI runs the static-analysis job graph -- **THEN** both flows SHALL evaluate the same checked-in ruleset -- **AND** both flows SHALL remain deterministic from repository state - -#### Scenario: Semgrep complements the existing quality gate -- **GIVEN** the static-analysis workflow requires both Semgrep and `pixi run build -p quality` -- **WHEN** Semgrep is executed as part of the static-analysis PR checks -- **THEN** the workflow SHALL retain the existing quality build gate -- **AND** Semgrep SHALL complement rather than replace that gate - -#### Scenario: Semgrep scope stays limited to approved policy bans -- **GIVEN** the repository policy identifies both tool-enforceable and review-only rules -- **WHEN** the CI Semgrep gate evaluates the repository -- **THEN** it SHALL cover only the approved high-signal policy bans selected for Semgrep -- **AND** it SHALL NOT duplicate formatting, naming, include-order, lockfile/preset, or - runtime-validation checks that are owned by other tools - -## ADDED Requirements - -### Requirement: Static Analysis Coverage Supports Parallel PR Execution - -The CI static-analysis coverage SHALL be partitioned into independently executable Semgrep and -quality-build surfaces that can run as separate required PR checks. - -The repository SHALL preserve a combined static-analysis entrypoint for deterministic local -reproduction while exposing split entrypoints for targeted execution. - -#### Scenario: PR checks run split static-analysis surfaces -- **GIVEN** pull-request CI executes static-analysis coverage -- **WHEN** the workflow schedules static-analysis jobs -- **THEN** it SHALL run Semgrep and quality-build static-analysis surfaces as separate jobs or lanes -- **AND** both surfaces SHALL remain required merge gates - -#### Scenario: Combined local static-analysis entrypoint remains available -- **GIVEN** a contributor runs the canonical local static-analysis command -- **WHEN** the command executes -- **THEN** it SHALL run both Semgrep and quality-build static-analysis surfaces from repository-owned - lane definitions -- **AND** it SHALL preserve deterministic behavior from repository state - -#### Scenario: Split local entrypoints support targeted reproduction -- **GIVEN** a contributor needs to isolate one static-analysis surface -- **WHEN** the contributor invokes the split Semgrep or quality-build lane directly -- **THEN** each lane SHALL execute only its owned static-analysis surface -- **AND** each lane SHALL preserve the same policy and blocking semantics used by CI - -#### Scenario: Static-analysis optimization keeps equivalent PR coverage -- **GIVEN** the static-analysis composition is changed for throughput -- **WHEN** CI protection rules evaluate required checks -- **THEN** PR coverage SHALL remain equivalent to running both Semgrep and quality-build checks -- **AND** required checks SHALL NOT be moved to non-PR triggers to satisfy this requirement diff --git a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/tasks.md b/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/tasks.md deleted file mode 100644 index db1ba194..00000000 --- a/openspec/changes/archive/2026-03-10-ci-accelerate-static-analysis/tasks.md +++ /dev/null @@ -1,34 +0,0 @@ -## 1. Static-analysis lane decomposition - -- [x] 1.1 Extend `scripts/task/ci.sh` to expose split static-analysis lanes for Semgrep and - quality-build execution while preserving the combined `static-analysis` lane. -- [x] 1.2 Keep stage timing output for each split lane so bottlenecks remain observable before and - after the workflow reshape. -- [x] 1.3 Verify split lane failure semantics remain blocking for their owned checks. - -## 2. CI workflow graph reshaping - -- [x] 2.1 Update `.github/workflows/ci.yml` to run Semgrep and quality static-analysis lanes as - separate PR jobs that can execute in parallel. -- [x] 2.2 Keep both split static-analysis jobs configured as required merge gates to preserve - equivalent PR coverage. -- [x] 2.3 Preserve explicit ccache/Pixi setup per split job so job separation does not remove - deterministic toolchain and cache behavior. - -## 3. Local command surface alignment - -- [x] 3.1 Update `pixi.toml` task surfaces (and any directly related CI helper docs) so local - `pixi run ci` lane usage maps clearly to split and combined static-analysis behavior. -- [x] 3.2 Confirm canonical local reproduction remains available with `pixi run ci --lane - static-analysis`. -- [x] 3.3 Confirm targeted local reproduction works for each split static-analysis lane. - -## 4. Verification - -- [x] 4.1 Run `pixi run ci --lane static-analysis` and confirm both static-analysis surfaces execute - with blocking semantics. -- [x] 4.2 Run `pixi run ci --lane build-test` to ensure unrelated CI lane behavior is not regressed. -- [x] 4.3 Run baseline build/static gates: `pixi run build -p debug`, `pixi run build -p asan`, and - `pixi run build -p quality`. -- [x] 4.4 Validate CI run output shows both split static-analysis PR checks, equivalent required - coverage, and improved total/static-analysis timing versus the baseline run when measured. diff --git a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/.openspec.yaml b/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/.openspec.yaml deleted file mode 100644 index 0b4defe0..00000000 --- a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-01 diff --git a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/proposal.md b/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/proposal.md deleted file mode 100644 index 2e7b3d19..00000000 --- a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/proposal.md +++ /dev/null @@ -1,81 +0,0 @@ -# Change: Remove Bundled GPL Cursor Theme Assets - -## Problem - -Goggles currently depends on bundled cursor theme files under `assets/cursor` for compositor software cursor rendering, and those assets are shipped in AppImage payloads. This keeps GPL-licensed cursor-theme content in the distributed artifact and creates avoidable license friction for project-level licensing decisions. - -## Why - -We need cursor rendering that is license-clean, behavior-stable, and independent of repository-shipped theme packs. Eliminating bundled GPL cursor assets reduces compliance risk while preserving current input-forwarding and software-cursor UX. - -## Scope - -- Replace hard dependency on bundled `assets/cursor` with a runtime cursor source chain. -- Preserve current compositor cursor behavior (visibility, hotspot alignment, pointer-lock hiding). -- Remove cursor-theme packaging from shipped artifacts. -- Update OpenSpec contracts for input-forwarding and packaging behavior. - -## Non-goals - -- Reworking pointer forwarding semantics, lock/confine behavior, or overlay visibility rules. -- Introducing new cursor customization UI. -- Broad compositor architecture refactors outside cursor-image sourcing and packaging paths. -- Changing shader/resource packaging beyond cursor-theme removal. - -## What Changes - -### Runtime cursor source strategy - -Implement a deterministic cursor source order for compositor software cursor imagery: - -1. Runtime-provided cursor image + hotspot from active session cursor state (when available). -2. System cursor lookup by standard cursor name (no bundled theme path required). -3. Built-in generated fallback cursor image + hotspot to guarantee startup behavior. - -### Packaging and resource path updates - -- Stop requiring `assets/cursor` in runtime resource discovery. -- Stop shipping `assets/cursor` in AppImage staging. -- Keep other packaged assets behavior unchanged. - -### Contract updates - -- Modify `input-forwarding` spec to require runtime/system/fallback cursor sourcing instead of bundled theme assets. -- Add packaging requirement that distributed artifacts do not include bundled cursor-theme assets. - -## Impact - -- **Affected specs:** `input-forwarding`, `packaging` -- **Affected code (expected):** - - `src/compositor/compositor_server.cpp` - - `src/compositor/compositor_server.hpp` - - `src/app/application.cpp` - - `scripts/task/appimage_stage.sh` - - optional: `assets/cursor/*` removal and related docs updates - -## Policy-sensitive impacts - -- **Error handling:** Cursor source selection and fallbacks MUST return/propagate structured failures via `Result` and avoid silent fallback failures. -- **Logging:** Cursor-source transitions and hard failures SHOULD be logged once with actionable context (source selected, fallback reason). -- **Threading:** No new direct `std::thread`/`std::jthread` usage in render/pipeline paths. -- **Ownership/lifetime:** Cursor pixel buffers/textures MUST retain explicit ownership and cleanup parity with current compositor texture lifecycle. - -## Risks - -| Risk | Severity | Likelihood | Mitigation | -|------|----------|------------|------------| -| Runtime cursor source unavailable in some environments | HIGH | MEDIUM | Enforce built-in generated fallback and startup-path tests | -| Hotspot mismatch causes perceived cursor offset | MEDIUM | MEDIUM | Add hotspot alignment checks in targeted cursor rendering tests | -| Packaging regression removes required non-cursor assets | MEDIUM | LOW | Add AppImage staging assertions scoped to cursor paths only | -| Behavior drift during pointer lock or overlay visibility | MEDIUM | LOW | Re-run existing input-forwarding and software-cursor regression checks | - -## Validation Plan - -1. `pixi run build -p test` succeeds. -2. `pixi run test -p test` passes relevant input-forwarding/compositor tests. -3. AppImage staging output does not contain `usr/share/goggles/assets/cursor`. -4. Manual runtime checks confirm: - - software cursor renders with correct hotspot when unlocked, - - software cursor hides during pointer lock, - - software cursor hides when overlay is visible, - - fallback cursor appears when system cursor lookup is unavailable. diff --git a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/input-forwarding/spec.md b/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/input-forwarding/spec.md deleted file mode 100644 index 20d26861..00000000 --- a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/input-forwarding/spec.md +++ /dev/null @@ -1,39 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Compositor Software Cursor - -The system SHALL render a software cursor inside compositor-presented frames for the focused -surface. - -The compositor server SHALL: -- Track cursor position in surface-local coordinates. -- Render the cursor overlay into the compositor-presented frame buffer. -- Hide the software cursor when pointer lock is active or when input forwarding is suspended. -- Source cursor imagery via runtime cursor providers without requiring bundled cursor theme assets. -- Use a deterministic fallback chain: runtime cursor image when available, then system cursor lookup, - then a built-in generated cursor image. -- Preserve hotspot-correct placement for all cursor sources. - -#### Scenario: Cursor visible for compositor surface -- **GIVEN** the compositor is presenting a surface frame -- **AND** the focused surface does not hold a pointer lock -- **WHEN** pointer forwarding is active -- **THEN** a software cursor is rendered into the presented frame -- **AND** the cursor position matches the compositor cursor coordinates used for pointer events - -#### Scenario: Cursor hidden during pointer lock -- **GIVEN** a focused surface activates `zwp_pointer_constraints_v1.lock_pointer` -- **WHEN** pointer lock is active -- **THEN** the software cursor is hidden -- **AND** only relative pointer events continue to be delivered - -#### Scenario: Runtime cursor source unavailable -- **GIVEN** no runtime cursor image is available from the active session cursor source -- **WHEN** the compositor needs a cursor image for rendering -- **THEN** the compositor attempts system cursor lookup -- **AND** if system lookup is unavailable it renders the built-in generated fallback cursor - -#### Scenario: Cursor hidden while UI overlay is visible -- **GIVEN** the viewer UI overlay is visible -- **WHEN** pointer events are suspended for forwarded clients -- **THEN** the compositor software cursor is hidden diff --git a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/packaging/spec.md b/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/packaging/spec.md deleted file mode 100644 index 015bf0f3..00000000 --- a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/specs/packaging/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -## ADDED Requirements - -### Requirement: Cursor Theme Asset Exclusion - -The packaging workflow SHALL NOT ship bundled cursor-theme assets as part of Goggles distribution -artifacts. - -The packaging workflow SHALL: -- Exclude `assets/cursor` from AppImage payload content. -- Keep existing required packaged assets (configuration templates and shader assets) intact. -- Preserve software cursor runtime behavior through runtime/system/fallback cursor sourcing. - -#### Scenario: AppImage payload excludes bundled cursor theme assets -- **GIVEN** AppImage staging is executed for a release build -- **WHEN** the staged payload tree is inspected -- **THEN** `usr/share/goggles/assets/cursor` is absent -- **AND** other expected asset roots remain present - -#### Scenario: Viewer still starts with software cursor after packaging change -- **GIVEN** a packaged Goggles runtime without bundled cursor theme assets -- **WHEN** the viewer starts and presents a forwarded surface -- **THEN** software cursor rendering remains available through runtime/system/fallback cursor sourcing diff --git a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/tasks.md b/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/tasks.md deleted file mode 100644 index 74d9a8d1..00000000 --- a/openspec/changes/archive/2026-03-10-remove-gpl-cursor-theme-assets/tasks.md +++ /dev/null @@ -1,26 +0,0 @@ -## 1. Runtime Cursor Source Refactor - -- [x] 1.1 Replace bundled-theme loading in `src/compositor/compositor_server.cpp` with runtime/system/fallback cursor source selection. -- [x] 1.2 Remove hard requirement for `XCURSOR_PATH=/assets` in `src/app/application.cpp` while preserving cursor size and visibility semantics. -- [x] 1.3 Add deterministic fallback cursor generation path (image + hotspot) so compositor startup does not depend on external theme files. -- [x] 1.4 Keep pointer-lock and overlay visibility behavior unchanged in compositor cursor rendering paths. - -## 2. Packaging and Asset Surface Updates - -- [x] 2.1 Update `scripts/task/appimage_stage.sh` so AppImage payload excludes bundled cursor theme assets. -- [x] 2.2 Remove or relocate `assets/cursor/*` from tracked runtime payload inputs according to final implementation plan. -- [x] 2.3 Verify packaged runtime still resolves required non-cursor assets (config/shaders) after cursor asset removal. - -## 3. Spec and Documentation Updates - -- [x] 3.1 Add `input-forwarding` spec delta to replace bundled cursor-theme requirement with runtime/system/fallback sourcing contract. -- [x] 3.2 Add `packaging` spec delta requiring cursor-theme assets to be excluded from distributed artifacts. -- [x] 3.3 Update user-facing docs/config comments that currently imply bundled cursor theme dependency. - -## 4. Verification - -- [x] 4.1 Build verification: `pixi run build -p test`. -- [x] 4.2 Test verification: `pixi run test -p test`. -- [x] 4.3 Packaging verification: stage AppImage and assert `usr/share/goggles/assets/cursor` is absent. -- [ ] 4.4 Runtime verification (manual): confirm software cursor render/hide behavior in unlocked, pointer-locked, and overlay-visible states. -- [ ] 4.5 Fallback verification (manual): simulate unavailable system cursor source and confirm generated fallback cursor renders with correct hotspot. diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/design.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/design.md deleted file mode 100644 index 5e825c11..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/design.md +++ /dev/null @@ -1,593 +0,0 @@ -# Design: Filter Chain Diagnostics and Debugging System - -## Technical Approach - -The diagnostics system introduces a layered observability architecture over the existing filter chain runtime without modifying the core rendering data path. The strategy is to instrument existing boundaries — preset loading, shader compilation, pass recording, texture binding, and temporal operations — rather than restructure them. - -The core pattern is a **sink-agnostic event bus** that decouples diagnostic observation from consumption. Every instrumentation point emits a structured `DiagnosticEvent` into a `DiagnosticSession`. The session fans out events to registered sink adapters (log, test-harness, UI, capture). A policy object governs which events are promoted, suppressed, or trigger capture. - -This maps directly to the proposal's five-layer architecture: -- **Layer A (Policy & Session)**: `DiagnosticSession` + `DiagnosticPolicy` in `src/util/diagnostics/` -- **Layer B (Authoring Analysis)**: Instrumentation in `RetroArchPreprocessor`, `ShaderRuntime`, `ChainBuilder` -- **Layer C (Runtime Validation)**: Instrumentation in `ChainExecutor::record()`, `ChainExecutor::bind_pass_textures()`, `ChainResources::install()` -- **Layer D (Capture & Inspection)**: New `CaptureManager` integrated with `ChainExecutor` via readback staging buffers -- **Layer E (Quality Validation)**: Extensions to `tests/visual/image_compare.hpp` and new golden harness infrastructure - -The activation model uses the existing preprocessor-gated pattern from Tracy (`TRACY_ENABLE` → `GOGGLES_DIAGNOSTICS_FORENSIC`) for Tier 2. Tier 0 and Tier 1 are runtime-toggled through `DiagnosticPolicy`. - -## Architecture Decisions - -### Decision: Place Diagnostic Core in `src/util/diagnostics/` - -**Choice**: New `src/util/diagnostics/` module for event model, session, policy, sinks, and ledgers. -**Alternatives considered**: (a) Scatter diagnostic types across existing modules; (b) Create a top-level `src/diagnostics/` peer to `src/render/`. -**Rationale**: Diagnostics are cross-cutting infrastructure consumed by `render/chain/`, `render/shader/`, `render/backend/`, and `tests/`. The `util/` namespace already holds logging, profiling, error handling, and config — all infrastructure peers. A dedicated subdirectory keeps the diagnostic surface organized without creating a new top-level module. Option (a) would fragment the event model; option (b) would break the established convention that infrastructure lives under `util/`. - -### Decision: Single `DiagnosticEvent` Type With Variant Evidence Payload - -**Choice**: One `DiagnosticEvent` struct with severity, category, localization key, session reference, and a `std::variant`-based evidence payload. -**Alternatives considered**: (a) Event class hierarchy with virtual methods; (b) Completely untyped `std::any` payload. -**Rationale**: A single concrete type avoids virtual dispatch overhead on the hot path (Tier 0 checks run every frame). `std::variant` provides compile-time type safety for evidence payloads while keeping the event type uniform for sink dispatch. A class hierarchy would bloat the vtable and complicate sink implementations. `std::any` would lose type safety at consumption sites. - -### Decision: Sink Adapter as Single-Method Interface - -**Choice**: `DiagnosticSink` abstract base with one pure virtual method: `void receive(const DiagnosticEvent& event)`. -**Alternatives considered**: (a) Callback-based (`std::function`); (b) Multi-method interface with per-category receivers. -**Rationale**: A single-method interface is the narrowest possible contract. It makes sink implementations trivial (log sink: format and write; test sink: push to vector). The codebase uses abstract bases with factory functions returning `ResultPtr` — this follows that pattern. Callbacks would obscure ownership and make sink lifecycle management harder. Per-category methods would couple the interface to the category enum. - -### Decision: DiagnosticSession Owns Ledgers, Not Sinks - -**Choice**: The `DiagnosticSession` owns the degradation ledger, binding ledger, semantic ledger, and chain manifest as in-memory structures. Sinks receive events but do not own state. -**Alternatives considered**: Sinks accumulate their own ledger state. -**Rationale**: Ledgers must be queryable by the boundary API (`goggles_chain_*` functions) without knowing which sinks are registered. Centralizing state in the session makes the C API straightforward: `goggles_chain_diagnostics_*` functions read from the session. Sinks are output-only channels. - -### Decision: GPU Timestamp Queries via Dedicated Query Pool - -**Choice**: Allocate a `vk::QueryPool` with `vk::QueryType::eTimestamp` sized to `2 * (max_passes + 2)` per frame-in-flight. Reset and write queries during command recording. Read back results after fence signal using `vk::Device::getQueryPoolResults` with `vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait`. -**Alternatives considered**: (a) Use Tracy's GPU timing infrastructure directly; (b) Use `VK_EXT_host_query_reset` for simpler reset. -**Rationale**: Tracy GPU zones are compile-time gated and require Tracy to be enabled. Tier 1 GPU timestamps must work independently of Tracy. A dedicated query pool gives full control over readback timing and avoids coupling to Tracy's instrumentation model. `VK_EXT_host_query_reset` is not universally available; `vkCmdResetQueryPool` inside the command buffer is the safe fallback. The query pool is allocated once and reused, so the overhead is allocation-time only. - -### Decision: Readback Staging Uses Existing Framebuffer Allocation Pattern - -**Choice**: Readback staging buffers use the same `vk::Buffer` + `vk::DeviceMemory` allocation pattern as the existing `FilterPass::create_ubo_buffer()`, with `vk::BufferUsageFlagBits::eTransferDst` and host-visible + host-coherent memory. -**Alternatives considered**: (a) Introduce a VMA-based allocator; (b) Use persistent mapped buffers. -**Rationale**: The codebase does not use VMA — every allocation goes through `vk::Device::allocateMemory` with manual memory type selection via `find_memory_type`. Introducing VMA would be a separate, larger refactor. The existing pattern is proven and consistent. Persistent mapping is fine for staging buffers and already used for UBO buffers. - -### Decision: Tier 2 Compile-Time Gate Follows Tracy's Pattern - -**Choice**: `GOGGLES_DIAGNOSTICS_FORENSIC` preprocessor define enables Tier 2 instrumentation. When undefined, Tier 2 code paths are compiled out entirely via `#ifdef` guards and no-op macros. -**Alternatives considered**: Runtime `if constexpr` with a template parameter; always-compiled with runtime disable. -**Rationale**: The codebase already uses `TRACY_ENABLE` for the same purpose (see `src/util/profiling.hpp`). Developers expect this pattern. `if constexpr` would require template parameterization across the recording hot path, which would be invasive. Always-compiled code would violate the zero-overhead requirement for Tier 2. - -### Decision: Boundary API Extensions Use the Existing `goggles_chain_` C API Pattern - -**Choice**: New diagnostic functions follow the same pattern: `goggles_chain_diagnostics_session_create(...)`, `goggles_chain_diagnostics_sink_register(...)`, etc. All return `goggles_chain_status_t`. New struct types use `struct_size` versioning. -**Alternatives considered**: A separate `goggles_diagnostics_` function prefix with its own opaque handle. -**Rationale**: The diagnostic session is scoped to a chain runtime instance. Separating the API would require passing both a chain handle and a diagnostics handle, complicating the host contract. The existing C API already has `goggles_chain_error_last_info_get` as a diagnostic precedent. Extending the existing prefix maintains a single namespace for host code. - -### Decision: TOML Configuration Under `[diagnostics]` Section - -**Choice**: Add a `[diagnostics]` section to the shipped template at `config/goggles.template.toml`, have first-run bootstrap copy it to `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` when the runtime user config is missing, and extend `goggles::Config` with a `Diagnostics` member. -**Alternatives considered**: Separate diagnostics config file; environment variables only. -**Rationale**: The application already loads all configuration from a single TOML file via `goggles::load_config()`. Adding a section follows the established pattern. Environment variables are appropriate for CI overrides but should not be the primary configuration mechanism. A separate file would fragment configuration. - -## Data Flow - -### Diagnostic Event Flow - -``` - Instrumentation Points DiagnosticSession Sinks - ===================== ================== ===== - - RetroArchPreprocessor ─┐ - ShaderRuntime ─────────┤ - ChainBuilder ──────────┤ ┌──────────────────┐ - ChainExecutor ─────────┼── emit() ──→│ DiagnosticSession │──→ LogSink (spdlog) - ChainResources ────────┤ │ │──→ TestHarnessSink - CaptureManager ────────┘ │ ┌─ policy ──────┐│──→ TracySink (Tier 2) - │ │ strict/compat ││──→ ImGuiSink (future) - │ │ capture mode ││──→ CaptureSink (future) - │ │ tier level ││ - │ └────────────────┘│ - │ ┌─ ledgers ─────┐│ - │ │ degradation ││ - │ │ binding ││ - │ │ semantic ││ - │ │ execution tl ││ - │ │ chain manifest ││ - │ └────────────────┘│ - └──────────────────────┘ - │ - ▼ - Boundary C API - goggles_chain_diagnostics_* -``` - -### Per-Frame Recording with Diagnostics - -``` - ChainRuntime::record() - │ - ├─ ChainExecutor::record() - │ │ - │ ├─ [Tier 1] reset GPU timestamp query pool - │ │ - │ ├─ ensure_prechain_passes() - │ │ └─ emit: allocation event - │ │ - │ ├─ record_prechain() - │ │ ├─ [Tier 1] write timestamp (start) - │ │ ├─ pass->record(cmd, ctx) - │ │ └─ [Tier 1] write timestamp (end) - │ │ - │ ├─ for each effect pass: - │ │ ├─ set_source_size / set_output_size / ... (semantic injection) - │ │ │ └─ [Tier 0] emit: semantic assignment event - │ │ │ - │ │ ├─ bind_pass_textures() - │ │ │ ├─ [Tier 0] emit: binding plan event per pass - │ │ │ └─ [Tier 0] on fallback: emit degradation event - │ │ │ - │ │ ├─ [Tier 1] write timestamp (start) - │ │ ├─ [debug labels] vkCmdBeginDebugUtilsLabelEXT - │ │ ├─ pass->record(cmd, ctx) - │ │ ├─ [debug labels] vkCmdEndDebugUtilsLabelEXT - │ │ ├─ [Tier 1] write timestamp (end) - │ │ │ - │ │ └─ [Tier 1+] optional intermediate readback copy - │ │ - │ ├─ record_postchain() - │ │ - │ ├─ frame_history.push() - │ │ └─ [Tier 0] emit: history push event - │ │ - │ └─ copy_feedback_framebuffers() - │ └─ [Tier 0] emit: feedback copy event - │ - └─ [Tier 1] async readback: GPU timestamps + captured images -``` - -### Authoring Analysis Flow (Preset Load) - -``` - ChainRuntime::load_preset() - │ - ├─ PresetParser::load() - │ └─ emit: preset parse event (normalized structure, redirect graph) - │ - ├─ For each pass: - │ ├─ RetroArchPreprocessor::preprocess() - │ │ ├─ resolve_includes() - │ │ │ └─ emit: include graph event + source provenance - │ │ ├─ extract_parameters() - │ │ └─ split_by_stage() - │ │ - │ ├─ ShaderRuntime::compile_retroarch_shader() - │ │ └─ emit: compile report event (per-stage, timing, cache-hit) - │ │ - │ └─ merge_reflection() - │ └─ emit: reflection report event (merged contract) - │ - ├─ ChainBuilder::build() - │ ├─ validate graph topology - │ ├─ validate temporal requirements - │ └─ emit: chain manifest event - │ - ├─ [Strict mode] Reflection conformance gate - │ └─ emit: conformance verdict event - │ - └─ ChainResources::install() - ├─ emit: installation event (pass count, generation id) - └─ update session identity hashes -``` - -## File Changes - -| File | Action | Description | -|------|--------|-------------| -| `src/util/diagnostics/diagnostic_event.hpp` | Create | Diagnostic event model: `Severity`, `Category`, `LocalizationKey`, `DiagnosticEvent`, evidence payload variants | -| `src/util/diagnostics/diagnostic_session.hpp` | Create | `DiagnosticSession`: event emission, sink registry, ledger ownership, session identity | -| `src/util/diagnostics/diagnostic_session.cpp` | Create | Session implementation: fan-out to sinks, ledger population, policy evaluation | -| `src/util/diagnostics/diagnostic_policy.hpp` | Create | `DiagnosticPolicy`: strict/compat mode, tier level, capture mode, severity promotion rules | -| `src/util/diagnostics/diagnostic_sink.hpp` | Create | `DiagnosticSink` abstract interface | -| `src/util/diagnostics/log_sink.hpp` | Create | `LogSink`: formats events and routes to spdlog with dedicated logger name | -| `src/util/diagnostics/log_sink.cpp` | Create | LogSink implementation | -| `src/util/diagnostics/test_harness_sink.hpp` | Create | `TestHarnessSink`: collects events in queryable vector, supports filter-by-category/severity | -| `src/util/diagnostics/test_harness_sink.cpp` | Create | TestHarnessSink implementation | -| `src/util/diagnostics/degradation_ledger.hpp` | Create | `DegradationLedger`: records fallback substitutions, unresolved semantics, reflection loss | -| `src/util/diagnostics/binding_ledger.hpp` | Create | `BindingLedger`: per-pass, per-frame resolved resource table | -| `src/util/diagnostics/semantic_ledger.hpp` | Create | `SemanticAssignmentLedger`: per-pass semantic destination classification and values | -| `src/util/diagnostics/execution_timeline.hpp` | Create | `ExecutionTimeline`: ordered event list with optional GPU/CPU timestamps | -| `src/util/diagnostics/chain_manifest.hpp` | Create | `ChainManifest`: normalized preset description (passes, aliases, textures, temporal) | -| `src/util/diagnostics/source_provenance.hpp` | Create | `SourceProvenanceMap`: expanded-line to original-file-and-line mapping | -| `src/util/diagnostics/compile_report.hpp` | Create | `CompileReport`: per-stage success, diagnostics, timing, cache status | -| `src/util/diagnostics/session_identity.hpp` | Create | `SessionIdentity`: content hashes, generation id, environment fingerprint | -| `src/util/diagnostics/gpu_timestamp_pool.hpp` | Create | `GpuTimestampPool`: Vulkan query pool wrapper for per-pass GPU timing | -| `src/util/diagnostics/gpu_timestamp_pool.cpp` | Create | GpuTimestampPool implementation | -| `src/util/diagnostics/CMakeLists.txt` | Create | Build target for diagnostics module | -| `src/util/config.hpp` | Modify | Add `Config::Diagnostics` struct with mode, tier, strict toggle, capture depth, retention | -| `src/util/config.cpp` | Modify | Parse `[diagnostics]` TOML section | -| `src/render/chain/chain_runtime.hpp` | Modify | Add `std::unique_ptr m_diagnostic_session` member; add diagnostic session lifecycle methods | -| `src/render/chain/chain_runtime.cpp` | Modify | Create `DiagnosticSession` during `ChainRuntime::create()`; wire session into executor and builder | -| `src/render/chain/chain_executor.hpp` | Modify | Accept optional `DiagnosticSession*` in `record()` signature | -| `src/render/chain/chain_executor.cpp` | Modify | Add Tier 0 event emission in `bind_pass_textures()` for binding plan, fallback detection, semantic population; Tier 1 GPU timestamp writes; debug label insertion | -| `src/render/chain/chain_builder.hpp` | Modify | Accept optional `DiagnosticSession*` in `build()` factory | -| `src/render/chain/chain_builder.cpp` | Modify | Emit chain manifest, conformance gate events; pass session to shader compilation | -| `src/render/chain/chain_resources.hpp` | Modify | Add generation id (`uint64_t m_generation_id`); accept optional `DiagnosticSession*` in `install()` | -| `src/render/chain/chain_resources.cpp` | Modify | Emit installation event; increment generation id on install | -| `src/render/shader/retroarch_preprocessor.hpp` | Modify | Add optional `SourceProvenanceMap*` output parameter to `preprocess()` and `preprocess_source()` | -| `src/render/shader/retroarch_preprocessor.cpp` | Modify | Build provenance map during `resolve_includes()` and compatibility rewrites | -| `src/render/shader/shader_runtime.hpp` | Modify | Add optional `CompileReport*` output parameter to `compile_retroarch_shader()` | -| `src/render/shader/shader_runtime.cpp` | Modify | Populate compile report with per-stage timing, diagnostics, cache-hit state | -| `src/render/chain/api/c/goggles_filter_chain.h` | Modify | Add `goggles_chain_diagnostics_*` function declarations, `GogglesChainDiagnosticsCreateInfo`, `GogglesChainDiagnosticsEventCallback` types | -| `src/render/chain/api/c/goggles_filter_chain.cpp` | Modify | Implement C API diagnostic functions delegating to `ChainRuntime` | -| `src/render/chain/api/cpp/goggles_filter_chain.hpp` | Modify | Add C++ diagnostic session wrapper methods | -| `src/render/chain/api/cpp/goggles_filter_chain.cpp` | Modify | Implement C++ diagnostic methods | -| `config/goggles.template.toml` | Modify | Add `[diagnostics]` section with defaults; first-run bootstrap copies the template to `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` when needed | -| `tests/render/test_diagnostic_event_model.cpp` | Create | Unit tests for event model, severity ordering, policy promotion | -| `tests/render/test_diagnostic_session.cpp` | Create | Unit tests for session lifecycle, multi-sink fan-out, ledger population | -| `tests/render/test_diagnostic_sinks.cpp` | Create | Unit tests for log sink formatting, test-harness sink collection and filtering | -| `tests/render/test_authoring_validation.cpp` | Create | Tests for static validation: compile report emission, reflection conformance, provenance | -| `tests/render/test_binding_ledger.cpp` | Create | Tests for binding plan recording, fallback detection, producer chain tracking | -| `tests/visual/test_intermediate_golden.cpp` | Create | Tests for per-pass intermediate golden comparisons | -| `tests/visual/test_temporal_golden.cpp` | Create | Tests for multi-frame temporal sequence golden comparisons | -| `tests/visual/test_diff_heatmap.cpp` | Create | Tests for diff heatmap generation | -| `tests/visual/image_compare.hpp` | Modify | Add `structural_similarity` field to `CompareResult`; add region-of-interest overload | -| `tests/visual/image_compare.cpp` | Modify | Implement structural similarity metric; implement ROI comparison | - -## Interfaces / Contracts - -### Core Event Model (`src/util/diagnostics/diagnostic_event.hpp`) - -```cpp -namespace goggles::diagnostics { - -enum class Severity : uint8_t { debug, info, warning, error }; - -enum class Category : uint8_t { authoring, runtime, quality, capture }; - -struct LocalizationKey { - static constexpr uint32_t CHAIN_LEVEL = UINT32_MAX; - uint32_t pass_ordinal = CHAIN_LEVEL; - std::string_view stage; // e.g., "compile", "bind", "record", "preset_parse" - std::string_view resource; // e.g., texture name, semantic name, or empty -}; - -struct SessionIdentity { - std::string preset_hash; - std::string expanded_source_hash; - std::string compiled_contract_hash; - uint64_t generation_id = 0; - uint32_t frame_start = 0; - uint32_t frame_end = 0; - std::string capture_mode; // "minimal", "standard", "investigate", "forensic" - std::string environment_fingerprint; -}; - -// Evidence payload variants — each category defines its own evidence types -struct BindingEvidence { /* resource id, fallback status, extent, producer pass */ }; -struct SemanticEvidence { /* member name, classification, value, offset */ }; -struct CompileEvidence { /* stage, success, messages, timing_us, cache_hit */ }; -struct ReflectionEvidence { /* stage, resource summary, merge conflicts */ }; -struct ProvenanceEvidence { /* original file, original line, rewrite applied */ }; -struct CaptureEvidence { /* image data reference, pass ordinal, frame index */ }; - -using EvidencePayload = std::variant< - std::monostate, - BindingEvidence, - SemanticEvidence, - CompileEvidence, - ReflectionEvidence, - ProvenanceEvidence, - CaptureEvidence ->; - -struct DiagnosticEvent { - Severity severity; - Severity original_severity; // before policy promotion - Category category; - LocalizationKey localization; - uint32_t frame_index = 0; - uint64_t timestamp_ns = 0; - std::string message; - EvidencePayload evidence; - // Session identity is carried by the session, not duplicated per event -}; - -} // namespace goggles::diagnostics -``` - -### Sink Interface (`src/util/diagnostics/diagnostic_sink.hpp`) - -```cpp -namespace goggles::diagnostics { - -class DiagnosticSink { -public: - virtual ~DiagnosticSink() = default; - virtual void receive(const DiagnosticEvent& event) = 0; -}; - -} // namespace goggles::diagnostics -``` - -### Diagnostic Session (`src/util/diagnostics/diagnostic_session.hpp`) - -```cpp -namespace goggles::diagnostics { - -using SinkId = uint32_t; - -class DiagnosticSession { -public: - [[nodiscard]] static auto create(DiagnosticPolicy policy) -> std::unique_ptr; - - void emit(DiagnosticEvent event); - - auto register_sink(std::unique_ptr sink) -> SinkId; - void unregister_sink(SinkId id); - - [[nodiscard]] auto policy() const -> const DiagnosticPolicy&; - void set_policy(DiagnosticPolicy policy); - - [[nodiscard]] auto identity() const -> const SessionIdentity&; - void update_identity(SessionIdentity identity); - - // Ledger access for boundary API - [[nodiscard]] auto degradation_ledger() const -> const DegradationLedger&; - [[nodiscard]] auto binding_ledger() const -> const BindingLedger&; - [[nodiscard]] auto semantic_ledger() const -> const SemanticAssignmentLedger&; - [[nodiscard]] auto execution_timeline() const -> const ExecutionTimeline&; - [[nodiscard]] auto chain_manifest() const -> const ChainManifest*; - [[nodiscard]] auto authoring_verdict() const -> std::optional; - - // Aggregate counts for quick queries - [[nodiscard]] auto event_count(Severity severity) const -> uint32_t; - [[nodiscard]] auto event_count(Category category) const -> uint32_t; - - void begin_frame(uint32_t frame_index); - void end_frame(); - void reset(); - -private: - DiagnosticPolicy m_policy; - SessionIdentity m_identity; - std::vector>> m_sinks; - SinkId m_next_sink_id = 0; - - DegradationLedger m_degradation_ledger; - BindingLedger m_binding_ledger; - SemanticAssignmentLedger m_semantic_ledger; - ExecutionTimeline m_timeline; - std::unique_ptr m_manifest; - std::optional m_verdict; - - std::array m_severity_counts{}; - std::array m_category_counts{}; -}; - -} // namespace goggles::diagnostics -``` - -### Diagnostic Policy (`src/util/diagnostics/diagnostic_policy.hpp`) - -```cpp -namespace goggles::diagnostics { - -enum class PolicyMode : uint8_t { compatibility, strict }; -enum class CaptureMode : uint8_t { minimal, standard, investigate, forensic }; -enum class ActivationTier : uint8_t { tier0, tier1, tier2 }; - -struct DiagnosticPolicy { - PolicyMode mode = PolicyMode::compatibility; - CaptureMode capture_mode = CaptureMode::standard; - ActivationTier tier = ActivationTier::tier0; - uint32_t capture_frame_limit = 1; - uint64_t retention_bytes = 256 * 1024 * 1024; // 256 MB - bool promote_fallback_to_error = false; // derived from mode == strict - bool reflection_loss_is_fatal = false; // derived from mode == strict -}; - -} // namespace goggles::diagnostics -``` - -### Boundary API Extension (`src/render/chain/api/c/goggles_filter_chain.h`) - -```c -// New status code -#define GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE ((goggles_chain_status_t)10u) - -// New diagnostic reporting modes -#define GOGGLES_CHAIN_DIAG_MODE_MINIMAL ((uint32_t)0u) -#define GOGGLES_CHAIN_DIAG_MODE_STANDARD ((uint32_t)1u) -#define GOGGLES_CHAIN_DIAG_MODE_INVESTIGATE ((uint32_t)2u) -#define GOGGLES_CHAIN_DIAG_MODE_FORENSIC ((uint32_t)3u) - -// New policy modes -#define GOGGLES_CHAIN_DIAG_POLICY_COMPATIBILITY ((uint32_t)0u) -#define GOGGLES_CHAIN_DIAG_POLICY_STRICT ((uint32_t)1u) - -struct GogglesChainDiagnosticsCreateInfo { - uint32_t struct_size; - uint32_t reporting_mode; - uint32_t policy_mode; - uint32_t activation_tier; // 0, 1, or 2 -}; - -struct GogglesChainDiagnosticsSummary { - uint32_t struct_size; - uint32_t reporting_mode; - uint32_t policy_mode; - uint32_t error_count; - uint32_t warning_count; - uint32_t info_count; -}; - -// Callback type for sink adapter via C boundary -typedef void(GOGGLES_CHAIN_CALL* goggles_chain_diagnostic_event_cb)( - uint32_t severity, uint32_t category, uint32_t pass_ordinal, - const char* message_utf8, void* user_data); - -// Session lifecycle -GOGGLES_CHAIN_API goggles_chain_status_t GOGGLES_CHAIN_CALL -goggles_chain_diagnostics_session_create( - goggles_chain_t* chain, - const GogglesChainDiagnosticsCreateInfo* create_info); - -GOGGLES_CHAIN_API goggles_chain_status_t GOGGLES_CHAIN_CALL -goggles_chain_diagnostics_session_destroy(goggles_chain_t* chain); - -// Sink registration -GOGGLES_CHAIN_API goggles_chain_status_t GOGGLES_CHAIN_CALL -goggles_chain_diagnostics_sink_register( - goggles_chain_t* chain, - goggles_chain_diagnostic_event_cb callback, - void* user_data, - uint32_t* out_sink_id); - -GOGGLES_CHAIN_API goggles_chain_status_t GOGGLES_CHAIN_CALL -goggles_chain_diagnostics_sink_unregister( - goggles_chain_t* chain, - uint32_t sink_id); - -// State queries -GOGGLES_CHAIN_API goggles_chain_status_t GOGGLES_CHAIN_CALL -goggles_chain_diagnostics_summary_get( - const goggles_chain_t* chain, - GogglesChainDiagnosticsSummary* out_summary); -``` - -### GPU Timestamp Pool (`src/util/diagnostics/gpu_timestamp_pool.hpp`) - -```cpp -namespace goggles::diagnostics { - -class GpuTimestampPool { -public: - [[nodiscard]] static auto create(vk::Device device, vk::PhysicalDevice physical_device, - uint32_t max_passes, uint32_t frames_in_flight) - -> Result>; - - void reset_frame(vk::CommandBuffer cmd, uint32_t frame_index); - void write_timestamp(vk::CommandBuffer cmd, uint32_t frame_index, - uint32_t pass_ordinal, bool is_start); - [[nodiscard]] auto read_results(uint32_t frame_index) - -> Result>>; // pass_ordinal -> duration_us - - [[nodiscard]] auto is_available() const -> bool; - -private: - vk::Device m_device; - vk::QueryPool m_pool; - float m_timestamp_period = 0.0f; - uint32_t m_queries_per_frame = 0; - uint32_t m_frames_in_flight = 0; - bool m_available = false; -}; - -} // namespace goggles::diagnostics -``` - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|-------------|----------| -| Unit | Event model severity ordering, policy promotion rules, localization key construction | Pure C++ unit tests in `tests/render/test_diagnostic_event_model.cpp` with Catch2 | -| Unit | DiagnosticSession: multi-sink fan-out, sink failure isolation, event counting, ledger population | Unit tests with mock sinks; verify event delivery order and counts | -| Unit | TestHarnessSink: collection, filter by severity/category, emission order preservation | Direct sink instantiation and assertion on collected events | -| Unit | LogSink: verify formatted output routes to spdlog dedicated logger | Capture spdlog output and verify format | -| Unit | DegradationLedger: entry creation, query by pass ordinal, frame ordering | Populate ledger with known entries, query and verify | -| Unit | BindingLedger: resolved/substituted/unresolved classification, extent recording | Populate and query binding entries per pass | -| Unit | SemanticAssignmentLedger: destination classification, value recording, alias resolution | Populate and query semantic entries | -| Unit | ChainManifest: deterministic generation from PresetConfig, temporal requirement extraction | Compare manifests from same preset, verify byte-identical | -| Unit | SourceProvenanceMap: line mapping through includes, rewrite tracking | Preprocess known shader sources, verify provenance entries | -| Unit | CompileReport: per-stage recording, cache-hit tracking, timing | Compile known shaders, verify report fields | -| Unit | DiagnosticPolicy: strict mode severity promotion, tier gating | Create policies, verify event transformation | -| Unit | GpuTimestampPool: creation with/without timestamp support, availability detection | Test with mock device properties | -| Integration | Authoring validation end-to-end: load preset, verify compile report + reflection report + conformance | Use existing test preset corpus; verify diagnostic events via test-harness sink | -| Integration | Runtime validation: load preset, record frame, verify binding ledger + degradation ledger | Headless recording with test-harness sink; assert on ledger contents | -| Integration | Boundary API: create session, register sink, load preset, record, query summary | C API tests extending existing `test_filter_chain_c_api_contracts.cpp` | -| Integration | GPU timestamps: record frame with Tier 1, verify timeline has per-pass timing | Headless integration with real Vulkan device | -| E2E | Intermediate golden workflow: capture per-pass outputs, compare against baselines | Extend existing visual regression suite | -| E2E | Temporal golden workflow: multi-frame capture, verify history/feedback progression | New temporal golden test presets | -| E2E | Authoring corpus: invalid presets produce expected diagnostic verdicts | Batch test with intentionally invalid presets | - -## Migration / Rollout - -### Phase 1: Contract Visibility (Foundation) - -**Deliverables:** -- `src/util/diagnostics/` module with event model, session, policy, sinks (log + test-harness) -- Chain manifest generation in `ChainBuilder` -- Source provenance tracking in `RetroArchPreprocessor` -- Compile and reflection reports in `ShaderRuntime` -- Reflection conformance gate (strict mode: reject; compat mode: degrade + record) -- Parameter and semantic conformance validation -- Authoring verdict generation -- `[diagnostics]` config section -- Unit tests for all new types -- Authoring validation integration tests - -**Integration points:** `ChainBuilder::build()` gains optional `DiagnosticSession*` parameter. `RetroArchPreprocessor::preprocess()` gains optional `SourceProvenanceMap*` output. `ShaderRuntime::compile_retroarch_shader()` gains optional `CompileReport*` output. All existing call sites pass `nullptr` by default — zero behavioral change without a diagnostic session. - -**Revertible:** Remove `src/util/diagnostics/` and revert parameter additions to factory functions. - -### Phase 2: Runtime Truth - -**Deliverables:** -- Binding ledger population in `ChainExecutor::bind_pass_textures()` -- Degradation ledger population (fallback detection at bind time) -- Semantic assignment ledger in semantic injection path -- Execution timeline with CPU timestamps -- Generation-aware rebuild/swap reporting in `ChainResources::install()` -- Boundary C API diagnostic session lifecycle + sink registration + summary query -- Diagnostic session member on `ChainRuntime` -- Runtime validation integration tests - -**Integration points:** `ChainExecutor::record()` gains optional `DiagnosticSession*`. `ChainResources::install()` increments generation id and emits installation event. `goggles_filter_chain.h` gets new function declarations. - -**Revertible:** Revert `ChainExecutor` and `ChainResources` instrumentation; remove C API additions. - -### Phase 3: Image and Temporal Evidence - -**Deliverables:** -- `GpuTimestampPool` for per-pass GPU timing -- Vulkan debug label insertion at pass boundaries (`VK_EXT_debug_utils`) -- Intermediate output readback via staging buffers (on-demand capture) -- History and feedback surface capture -- Capture control through boundary API -- Diff heatmap generation in `tests/visual/` -- Structural similarity metric in image comparison library -- Region-of-interest comparison -- Intermediate golden workflow - -**Integration points:** `GpuTimestampPool` allocated in `ChainRuntime::create()` when Tier 1 active. Readback staging allocated per-capture-request. Debug labels conditional on extension availability. - -**Revertible:** Remove GPU query pool, staging buffers, debug labels; revert image comparison extensions. - -### Phase 4: Regression Hardening - -**Deliverables:** -- Authoring validation corpus (intentionally invalid presets) -- Semantic-probe presets (size, frame counter, parameter isolation) -- Temporal golden baselines for history and feedback -- Per-pass intermediate golden baselines -- Earliest-divergence localization in visual regression -- Tiered CI/release workflow configuration -- Forensic capture (Tier 2, compile-time gated) - -**Revertible:** Remove test corpus additions and golden baselines; revert Tier 2 compile flag. - -### Migration Notes - -- No data migration required. The diagnostics system adds new artifacts alongside existing behavior. -- No feature flags beyond the `[diagnostics]` TOML config and `GOGGLES_DIAGNOSTICS_FORENSIC` compile flag. -- Existing tests are unaffected: all diagnostic parameters default to `nullptr` or inactive state. -- Strict mode is opt-in. CI workflows can enable it progressively once the authoring corpus is clean. - -## Open Questions - -- [ ] Should the `DiagnosticEvent` use `std::string` or `std::string_view` for the message field? String views are cheaper but require the underlying storage to outlive the event. If events are consumed synchronously by sinks before the frame ends, `string_view` is safe. If events are buffered for async serialization, `std::string` is needed. **Recommendation:** Use `std::string` for safety; optimize to `string_view` with arena allocation in Phase 3 if profiling shows allocation pressure. -- [ ] Should intermediate readback (Phase 3) use a ring of staging buffers or allocate per-capture? A ring avoids repeated allocation but consumes persistent memory. **Recommendation:** Start with per-capture allocation; add ring pooling if capture frequency warrants it. -- [ ] What is the maximum number of passes we need to support for GPU timestamp queries? The current `max_passes` is implicitly unbounded. **Recommendation:** Cap at 64 passes for initial query pool sizing; emit a diagnostic warning if a preset exceeds this. -- [ ] Should the `ChainManifest` be serializable to JSON for external tooling? **Recommendation:** Yes, add JSON serialization in Phase 2 alongside the boundary API, using the project's existing serializer pattern if one exists or a lightweight approach. diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/interview.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/interview.md deleted file mode 100644 index 7a4a6a50..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/interview.md +++ /dev/null @@ -1,139 +0,0 @@ -# SDD Interview Spec: Filter Chain Diagnostics System - -## Metadata - -- Interview ID: fcd-2026-0310-001 -- Change Name: filter-chain-diagnostics -- Rounds: 4 -- Final Ambiguity Score: 19.5% -- Type: brownfield -- Generated: 2026-03-10 -- Threshold: 0.2 -- Status: PASSED - -## Clarity Breakdown - -| Dimension | Score | Weight | Weighted | -| ------------------ | ----- | ------ | ------------- | -| Goal Clarity | 0.90 | 0.35 | 0.315 | -| Constraint Clarity | 0.70 | 0.25 | 0.175 | -| Success Criteria | 0.75 | 0.25 | 0.188 | -| Context Clarity | 0.85 | 0.15 | 0.128 | -| **Total Clarity** | | | **0.805** | -| **Ambiguity** | | | **0.195** | - -## Goal - -Design a comprehensive diagnostics and debugging system for the multi-pass filter chain architecture. The system must validate three classes of failures: (1) syntax/authoring errors in shader source and presets, (2) runtime/binding/execution errors during GPU pipeline operation, (3) output-quality/semantic-correctness errors in rendered results. The design document must be complete and evidence-driven, covering all three failure classes comprehensively. Implementation is expected to be phased. - -## Constraints - -- **Activation model:** Layered — compile-time gate (like Tracy) for heavy instrumentation with zero overhead when disabled, plus lightweight always-on validation for cheap checks that runs every frame. -- **Integration model:** Adapter pattern — sink-agnostic core diagnostic engine that emits to abstract sinks, with adapters routing to existing infrastructure (spdlog, Tracy, ImGui, headless capture) and future backends. -- **Scope:** Full design covering all three failure classes. Implementation can be phased, but the design must be complete. -- **Output format:** Markdown report written to project root. -- **Abstraction level:** The report must describe everything in terms of mechanisms, responsibilities, data flow, execution stages, and observability boundaries — not specific code symbols. - -## Non-Goals - -- Direct implementation of the diagnostics system (this is a design/exploration task) -- Modifying existing code paths during this change -- Building a standalone shader IDE or external tool - -## Acceptance Criteria - -- [ ] Report reconstructs the engine's current model from code evidence (pipeline structure, shader flow, parameter resolution, semantic injection, resource routing, execution model, output composition) -- [ ] Design validates syntax/authoring errors: observable state, instrumentation points, invariants, structured artifacts, failure localization, developer evidence -- [ ] Design validates runtime/binding/execution errors: same coverage as above -- [ ] Design validates output-quality/semantic-correctness errors: same coverage as above -- [ ] Design includes layered diagnostics architecture with compile-time and runtime tiers -- [ ] Design includes adapter-pattern sink-agnostic core with concrete adapter specifications -- [ ] Design includes full reporting/capture modes -- [ ] Design includes static validation, runtime validation, and quality-validation workflows -- [ ] Design includes intermediate-output inspection strategy -- [ ] Design includes parameter/semantic conformance strategy -- [ ] Design includes automated regression and golden-output strategy -- [ ] Design includes operational workflow for investigating failures -- [ ] Design addresses all four key scenarios: incorrect rendering, pipeline regression, compilation failure, performance bottleneck -- [ ] Design includes scalability, performance, and developer-experience tradeoffs -- [ ] Design includes risks, blind spots, and mitigation strategies - -## Assumptions Exposed & Resolved - -| Assumption | Challenge | Resolution | -| --- | --- | --- | -| Diagnostics should be always-on | Would always-on overhead be acceptable? | Layered: compile-time gate for heavy, always-on for cheap | -| Should extend existing infra | Build on existing vs. self-contained? | Adapter pattern: sink-agnostic core with adapters | -| Runtime diagnostics alone sufficient | Would just runtime errors be valuable enough? | Full design required, all three classes, phased implementation | -| Single primary scenario | Which scenario defines the acceptance bar? | All four scenarios are equally critical | - -## Technical Context - -### Existing Diagnostic Infrastructure (Mature) -- **Error handling:** Result/ResultPtr with ErrorCode enum, GOGGLES_TRY/GOGGLES_MUST macros, source location capture -- **Logging:** spdlog with configurable levels, console+file sinks, [VVL] prefix for Vulkan validation -- **Vulkan validation:** VK_LAYER_KHRONOS_validation integrated via debug messenger, severity-based routing -- **Profiling:** Tracy macros (GOGGLES_PROFILE_FUNCTION/SCOPE/TAG/VALUE), zero-overhead when disabled -- **Sanitizers:** ASAN/UBSAN in CMake presets -- **Runtime metrics:** FPS + latency with 120-frame rolling history, ImGui visualization -- **ImGui overlay:** Shader controls, performance dashboard, real-time parameter tuning -- **Frame capture:** Headless mode with PNG export via stb_image_write - -### Existing Diagnostic Gaps -- Shader compilation error details not extracted/formatted for display -- No intermediate texture inspection (per-pass output viewing) -- No per-pass GPU timing breakdown -- No structured diagnostic artifact emission -- No SPIR-V dump/disassembly on demand -- No GPU memory tracking or pressure warnings -- No automated regression/golden-output framework -- No command buffer debug labels - -### Pipeline Architecture -- **Stages:** Pre-chain (downsample) → Main chain (N filter passes) → Post-chain (output blit) -- **Shader flow:** .slangp preset parsing → .slang preprocessing (stage split, parameter extraction) → Slang compilation → SPIR-V + reflection -- **Semantic injection:** Per-frame UBO + push constant updates (source/output/original sizes, frame count, viewport, MVP) -- **Resource routing:** Framebuffers per pass, feedback buffers, frame history (circular 7-deep), texture registry, alias-based texture references -- **Execution:** Single command buffer recording with layout transitions between passes - -## Ontology (Key Entities) - -| Entity | Fields | Relationships | -| --- | --- | --- | -| DiagnosticEvent | severity, category, pass_index, stage, message, evidence | emitted_by Pass/Pipeline, routed_to Sink | -| DiagnosticSink | type, enabled, filter_policy | receives DiagnosticEvent | -| PassDiagnostics | pass_index, input_extent, output_extent, timing, texture_bindings | belongs_to FilterPass | -| ValidationRule | target_class, check_fn, severity, description | validates Pass/Pipeline/Shader | -| CaptureFrame | frame_index, pass_outputs[], parameter_state, semantic_state | captured_during Execution | -| RegressionBaseline | preset_name, golden_outputs[], tolerance | compared_against CaptureFrame | - -## Interview Transcript - -
-Full Q&A (4 rounds) - -### Round 1 - -**Q:** Should the diagnostics system be always-on (runtime overhead every frame), opt-in at runtime (config/CLI toggle), opt-in at compile-time (like Tracy's zero-overhead pattern), or a standalone offline tool? -**A:** Layered: compile-time + runtime — Zero-overhead compile gate for heavy instrumentation, plus lightweight always-on validation that costs near-zero. -**Ambiguity:** 59% → 41% (Goal: 0.70, Constraints: 0.50, Criteria: 0.40, Context: 0.80) - -### Round 2 - -**Q:** What is the primary scenario where this diagnostics system must prove its value? -**A:** All of them — new shader renders incorrectly, regression after pipeline change, shader compilation/load failure, and performance bottleneck isolation are all equally critical. -**Ambiguity:** 41% → 34% (Goal: 0.75, Constraints: 0.50, Criteria: 0.60, Context: 0.85) - -### Round 3 - -**Q:** Should this system compose the existing diagnostic infrastructure (spdlog, Tracy, ImGui, headless capture) or be a self-contained parallel system? -**A:** Adapter pattern (sink-agnostic) — Core diagnostic engine is independent and can emit to any sink, with adapters that route to spdlog, Tracy, ImGui. -**Ambiguity:** 34% → 28% (Goal: 0.80, Constraints: 0.65, Criteria: 0.60, Context: 0.85) - -### Round 4 (Contrarian) - -**Q:** What if you only built runtime/binding diagnostics and deferred static validation and golden-output regression? Would the system still justify its complexity? -**A:** Full design, phased implementation — The design document must cover all three failure classes comprehensively. Implementation can be phased, but the design must be complete. -**Ambiguity:** 28% → 19.5% (Goal: 0.90, Constraints: 0.70, Criteria: 0.75, Context: 0.85) - -
diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/proposal.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/proposal.md deleted file mode 100644 index c9a9b9fb..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/proposal.md +++ /dev/null @@ -1,124 +0,0 @@ -# Proposal: Filter Chain Diagnostics and Debugging System - -## Intent - -The filter chain's multi-pass pipeline is powerful but opaque when things go wrong. Today, a shader that compiles but renders incorrectly gives developers almost no structured evidence to localize the problem to a specific pass, binding, semantic, or resource. Silent fallback substitution (replacing missing textures with the source image) masks binding bugs. Reflection loss after successful compilation degrades silently into confusing runtime behavior. Intermediate pass outputs are invisible — only the final composited output can be inspected. - -This change designs a comprehensive diagnostics system that makes silent degradation impossible, localizes failures to a specific pass/stage/resource/semantic, and produces structured evidence for both developer triage and automated regression. The design covers three failure classes: syntax/authoring errors, runtime/binding/execution errors, and output-quality/semantic-correctness errors. - -**Why now:** The filter chain boundary has stabilized (standalone `goggles-filter-chain` target, C API, boundary-safe contracts). The existing observability primitives (spdlog, Tracy, Vulkan validation, headless capture, image comparison) provide building blocks but are not connected into a cohesive diagnostic system. - -## Scope - -### In Scope - -- **Layered diagnostics architecture** with three tiers: always-on lightweight validation (Tier 0), runtime opt-in capture and tracing (Tier 1, config-toggled), compile-time gated forensic instrumentation (Tier 2, zero-overhead when disabled) -- **Sink-agnostic diagnostic core** using adapter pattern: core emits structured events to abstract sinks; adapters route to spdlog, Tracy, ImGui, headless capture, and a test-harness collector -- **Authoring analysis layer** (static validation): preset normalization, include/redirect graph building, source provenance mapping, compile report with source-mapped diagnostics, reflection conformance gate, parameter/semantic conformance validation -- **Runtime validation layer**: binding ledger (per-pass resolved resource table with fallback status), semantic assignment ledger, degradation ledger, execution timeline with per-pass timing, generation-aware rebuild/swap validation -- **Capture and inspection layer**: intermediate pass output readback (on-demand and on-failure), history/feedback surface capture, semantic/parameter snapshot, metadata sidecars, reproducibility envelope -- **Quality validation layer**: golden comparison harness for per-pass intermediate outputs (not just final output), semantic-probe presets for contract testing, temporal-consistency validation, diff heatmap generation -- **Four reporting modes**: Minimal (verdict + error counts), Standard (manifest + coverage tables + one-frame trace), Investigate (intermediate captures + detailed ledgers), Forensic (full temporal capture + complete artifact bundle) -- **Operational workflows** for four key scenarios: incorrect rendering, pipeline regression, compilation failure, performance bottleneck isolation -- **Phased rollout plan** with four implementation phases - -### Out of Scope - -- Direct implementation of the diagnostics system (this change produces the design document and proposal only) -- Modifying existing rendering code paths -- Building a standalone external tool or shader IDE -- Diagnostics for the compositor/capture pipeline (Wayland surface management, DMA-BUF import/export) -- GPU memory pressure monitoring (deferred to a future change) - -## Approach - -The design report (`filter-chain-diagnostics-design.md`) reconstructs the engine model from code evidence, then specifies a five-layer diagnostics architecture: - -1. **Layer A: Policy & Session Control** — defines capture depth, retention policy, strict vs. compatibility mode, session identity with content hashes -2. **Layer B: Authoring Analysis** — static validation before any GPU execution; produces chain manifest, source provenance, compile/reflection reports, authoring verdict -3. **Layer C: Runtime Validation** — validates bindings, semantics, temporal resources, and fallback substitutions during live frame recording; produces binding/semantic/degradation ledgers -4. **Layer D: Capture & Inspection** — optional deep evidence: intermediate images, semantic snapshots, push constant dumps, reproducibility envelopes -5. **Layer E: Quality Validation** — golden comparisons, semantic-probe presets, temporal-consistency checks, diff heatmaps, localization from output deltas to upstream passes - -The core is sink-agnostic: a structured event model with severity, category, pass localization, and evidence payload. Adapters route events to existing infrastructure (spdlog for logging, Tracy for timing, ImGui for live inspection, headless capture for regression). A test-harness sink enables automated assertion on diagnostic events. - -The activation model is layered: Tier 0 always-on checks cost < 1% frame time. Tier 1 runtime opt-in (GPU timestamp queries, readback) costs 5–15%. Tier 2 compile-time gated forensic capture follows Tracy's zero-overhead pattern. - -Implementation is designed for four phases: (1) contract visibility, (2) runtime truth, (3) image/temporal evidence, (4) regression hardening. - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `src/render/chain/` | Modified | Instrumentation points for binding ledger, semantic ledger, pass-level events, intermediate capture hooks | -| `src/render/shader/` | Modified | Compile report emission, source provenance mapping, reflection conformance gate | -| `src/render/backend/` | Modified | GPU timestamp query infrastructure, debug label insertion, readback staging | -| `src/render/texture/` | Modified | Texture lifecycle tracking, capture readback support | -| `src/util/` | New + Modified | Diagnostic event model, sink interface, adapter registry, diagnostic configuration | -| `src/ui/` | Modified | Diagnostic inspection panels (intermediate texture gallery, binding table, semantic table) | -| `tests/render/` | New + Modified | Diagnostic event assertion tests, authoring validation tests, conformance test harness | -| `tests/visual/` | Modified | Extended golden baseline infrastructure for per-pass intermediate outputs | -| `config/goggles.template.toml` | Modified | New `[diagnostics]` section in the shipped template; first-run bootstrap materializes it into `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` when the runtime user config is missing | - -## Impacted OpenSpec Specs - -| Spec | Impact | -|------|--------| -| `render-pipeline` | Modified — new requirements for diagnostic instrumentation points, compile reports, reflection conformance | -| `goggles-filter-chain` | Modified — boundary API extensions for diagnostic event emission and capture control | -| `shader-testing` | Modified — extended to include structured compile reports, authoring validation corpus | -| `visual-regression` | Modified — extended to support per-pass intermediate golden baselines, not just final output | -| `profiling` | Modified — per-pass GPU timestamp queries complement Tracy CPU profiling | -| `diagnostics` (new) | New spec domain covering the diagnostic event model, sink contracts, and validation workflows | - -## Policy-Sensitive Impacts - -- **Error handling:** Diagnostic events are NOT errors in the `Result` sense. They are structured observations. The existing error propagation path remains unchanged. Diagnostics operate alongside, not instead of, error handling. -- **Logging:** Diagnostic log sink routes through spdlog but uses a dedicated logger name to avoid flooding the main application log. Diagnostic events carry structured metadata beyond what spdlog formats natively. -- **Vulkan API:** GPU timestamp queries and debug labels use `vk::` APIs exclusively. Readback staging uses existing `vk::Buffer` allocation patterns. -- **Threading:** Diagnostic event emission in the render path is single-threaded (same as render recording). Async serialization to disk uses the existing job system. -- **Lifetime/ownership:** Diagnostic sinks are owned by the diagnostic session, which is scoped to the application lifetime. Capture buffers follow the same RAII patterns as existing framebuffers. - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| Adapter pattern adds complexity without enough sink diversity to justify it | Medium | Start with 2 sinks (log + test-harness). Adapter interface is narrow (one method). Add UI/capture sinks in later phases. | -| GPU readback stalls for intermediate capture degrade frame timing | Medium | Use async readback with fence-based staging. Tier 1 captures are opt-in and can be scoped to specific passes/frames. | -| Diagnostic overhead masks real performance issues during profiling | Low | Tier 0 checks are < 1% overhead. Tier 1 timing measurements exclude diagnostic operations from their own measurements. | -| Golden baselines for intermediate outputs are GPU-vendor dependent | High | Use scenario-specific tolerances with per-device profiles. Keep a tightly controlled deterministic smoke subset. Use perceptual metrics alongside exact diffs. | -| Forensic capture disk volume becomes unmanageable | Medium | Tiered capture modes with configurable retention. Hash unchanged outputs to skip re-capture. Hard disk space limit with rotation. | -| Silent fallback removal in strict mode breaks existing preset loading | Low | Strict mode is opt-in (CI default). Compatibility mode preserves current behavior but records degradation events. Migration path documented. | - -## Rollback Plan - -This change produces a design document only — no code is modified. Rollback is trivial: - -- Remove `filter-chain-diagnostics-design.md` from project root -- Remove `openspec/changes/filter-chain-diagnostics/` directory -- No code changes to revert - -For future implementation phases, each phase is independently revertible: -- Phase 1 (contract visibility): revert new diagnostic source files and test additions -- Phase 2 (runtime truth): revert instrumentation points in chain/shader modules -- Phase 3 (capture): revert readback infrastructure and capture hooks -- Phase 4 (regression): revert golden baseline extensions and new test presets - -## Dependencies - -- Existing `goggles-filter-chain` boundary API (stable, already shipped) -- Existing spdlog, Tracy, ImGui infrastructure (mature, no changes needed to core libraries) -- Existing headless mode and image comparison library (stable foundation for quality validation) -- Existing Catch2 test infrastructure (harness for diagnostic event assertions) - -## Success Criteria - -- [ ] Design report reconstructs the engine's current model from code evidence covering all 7 subsystems (pipeline structure, shader flow, parameter resolution, semantic injection, resource routing, execution model, output composition) -- [ ] Design validates all three failure classes with observable state, instrumentation points, invariants, structured artifacts, failure localization, and evidence presentation strategies -- [ ] Design specifies a layered architecture with Tier 0 (always-on), Tier 1 (runtime opt-in), and Tier 2 (compile-time gated) activation -- [ ] Design specifies sink-agnostic core with concrete adapter specifications for at least spdlog, Tracy, ImGui, and test-harness sinks -- [ ] Design includes four reporting modes (Minimal, Standard, Investigate, Forensic) with clear scoping rules -- [ ] Design addresses all four key scenarios: incorrect rendering localization, pipeline regression detection, compilation failure diagnosis, performance bottleneck isolation -- [ ] Design includes phased rollout plan with independently implementable and revertible phases -- [ ] Design identifies risks and blind spots with concrete mitigation strategies -- [ ] Design report is written to project root as `filter-chain-diagnostics-design.md` using only mechanism/data-flow language (no code symbols) diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/diagnostics/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/diagnostics/spec.md deleted file mode 100644 index e1cd55e3..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/diagnostics/spec.md +++ /dev/null @@ -1,382 +0,0 @@ -# Diagnostics Specification - -## Purpose - -Defines the structured diagnostic event model, sink-agnostic adapter contracts, layered activation tiers, reporting modes, session identity, and validation workflows that form the filter-chain diagnostics system. This domain covers the core infrastructure that all other diagnostic capabilities (authoring analysis, runtime validation, capture, quality validation) are built upon. - -## Requirements - -### Requirement: Diagnostic Event Model - -The diagnostics system SHALL define a structured event type that carries severity, category, pass localization, and an evidence payload. Every diagnostic observation emitted by any layer of the system MUST use this event type. - -#### Scenario: Event carries required fields -- GIVEN a diagnostic observation is generated during filter-chain operation -- WHEN the observation is emitted as a diagnostic event -- THEN the event SHALL include a severity level (error, warning, info, debug) -- AND the event SHALL include a category (authoring, runtime, quality, capture) -- AND the event SHALL include a localization key identifying the pass ordinal, processing stage, and affected resource or semantic name where applicable -- AND the event SHALL include a session identity reference - -#### Scenario: Event carries optional evidence payload -- GIVEN a diagnostic event is emitted with supporting evidence -- WHEN a sink receives the event -- THEN the event MAY include a structured evidence payload containing resource identifiers, extent values, semantic values, image data references, or source location information -- AND the evidence payload format SHALL be defined by the event category - -#### Scenario: Events without pass context use chain-level localization -- GIVEN a diagnostic observation applies to the chain as a whole rather than a specific pass -- WHEN the observation is emitted -- THEN the localization key SHALL use a sentinel pass ordinal indicating chain-level scope -- AND the processing stage SHALL identify the chain-level operation (preset parsing, graph validation, policy evaluation) - -### Requirement: Severity Classification - -The diagnostics system SHALL define a closed set of severity levels with deterministic ordering and policy-driven promotion rules. - -#### Scenario: Severity levels are ordered -- GIVEN the set of diagnostic severity levels -- WHEN severity values are compared -- THEN the ordering SHALL be debug < info < warning < error -- AND no severity level outside this closed set SHALL be emittable - -#### Scenario: Policy-driven severity promotion -- GIVEN a diagnostic policy configured in strict mode -- WHEN a fallback substitution event is emitted that would normally be a warning -- THEN the diagnostics system SHALL promote the event to error severity -- AND the original severity SHALL be preserved in the event metadata for audit - -#### Scenario: Default severity in compatibility mode -- GIVEN a diagnostic policy configured in compatibility mode -- WHEN a fallback substitution event is emitted -- THEN the event SHALL retain warning severity -- AND the event SHALL be recorded in the degradation ledger - -### Requirement: Sink-Agnostic Adapter Interface - -The diagnostics system SHALL define a sink adapter interface that decouples diagnostic event production from event consumption. The interface SHALL consist of a single method that receives a diagnostic event. - -#### Scenario: Multiple sinks receive the same event -- GIVEN two or more sink adapters are registered with the diagnostic session -- WHEN a diagnostic event is emitted -- THEN each registered sink adapter SHALL receive the event -- AND sink delivery order SHALL be deterministic within a session - -#### Scenario: Sink failure does not block event emission -- GIVEN a sink adapter encounters an error during event processing -- WHEN the adapter returns from the event delivery call -- THEN the diagnostic session SHALL NOT halt event emission to other sinks -- AND the diagnostic session SHALL record the sink failure as a diagnostic event itself - -#### Scenario: No sinks registered -- GIVEN a diagnostic session is created with no sink adapters registered -- WHEN diagnostic events are emitted -- THEN events SHALL be silently discarded -- AND no runtime error SHALL occur - -### Requirement: Concrete Sink Adapters - -The diagnostics system SHALL provide at minimum two concrete sink adapters: a logging sink and a test-harness sink. - -#### Scenario: Logging sink routes to spdlog -- GIVEN a logging sink adapter is registered -- WHEN a diagnostic event is received -- THEN the adapter SHALL format the event and route it to spdlog using a dedicated logger name distinct from the main application logger -- AND the spdlog severity SHALL correspond to the diagnostic event severity - -#### Scenario: Test-harness sink collects events for assertion -- GIVEN a test-harness sink adapter is registered in a test context -- WHEN diagnostic events are emitted during a test run -- THEN the adapter SHALL collect all events in an ordered, queryable container -- AND tests SHALL be able to assert on event count, severity, category, and localization fields - -#### Scenario: Test-harness sink supports filtering -- GIVEN a test-harness sink with collected events -- WHEN a test queries events by category and severity -- THEN the sink SHALL return only events matching the specified filter criteria -- AND the returned events SHALL preserve emission order - -### Requirement: Diagnostic Session Identity - -Every diagnostic session SHALL carry an identity that uniquely identifies the validation context and is attached to every emitted event and every produced artifact. - -#### Scenario: Session identity contains content hashes -- GIVEN a diagnostic session is created for a loaded preset -- WHEN the session identity is constructed -- THEN the identity SHALL include the preset revision hash, the expanded-source hash, and the compiled-contract hash - -#### Scenario: Session identity contains runtime context -- GIVEN a diagnostic session is active during frame recording -- WHEN the session identity is inspected -- THEN the identity SHALL include the runtime generation identifier, the frame range, the capture mode, and an environment fingerprint - -#### Scenario: Session identity is stable across repeated runs -- GIVEN the same preset with the same source content and the same environment -- WHEN two diagnostic sessions are created -- THEN the content hashes in both session identities SHALL be identical -- AND the runtime generation identifiers MAY differ - -### Requirement: Layered Activation Tiers - -The diagnostics system SHALL support three activation tiers that control the cost and depth of diagnostic instrumentation. - -#### Scenario: Tier 0 baseline checks for an active session -- GIVEN a diagnostic session is active with the default diagnostics tier -- WHEN filter-chain frames are recorded -- THEN Tier 0 checks (binding coverage, semantic coverage, degradation detection) SHALL execute every frame for that session -- AND Tier 0 overhead SHALL be less than 1% of frame time - -#### Scenario: No diagnostic session keeps runtime behavior unchanged -- GIVEN a filter-chain runtime has no active diagnostic session -- WHEN filter-chain frames are recorded -- THEN the runtime SHALL preserve existing rendering behavior without creating diagnostic artifacts -- AND diagnostics-only ledgers and sink delivery SHALL remain inactive until a session is created - -#### Scenario: Tier 1 runtime opt-in -- GIVEN a build with Tier 1 diagnostics enabled via configuration -- WHEN filter-chain frames are recorded -- THEN additional diagnostic collection (GPU timestamp queries, selected readback, execution timeline) SHALL be active -- AND Tier 1 overhead SHALL be between 5% and 15% of frame time - -#### Scenario: Tier 2 compile-time gated forensic capture -- GIVEN a build compiled without the forensic instrumentation compile flag -- WHEN the binary is inspected -- THEN no Tier 2 instrumentation symbols or code paths SHALL be present -- AND no Tier 2 overhead SHALL exist - -#### Scenario: Tier 2 enabled at compile time -- GIVEN a build compiled with the forensic instrumentation compile flag enabled -- WHEN forensic capture is triggered -- THEN full intermediate readback, complete event timeline, and artifact bundle generation SHALL be available - -#### Scenario: Tier 2 macros are compiled out by default -- GIVEN a build where `GOGGLES_DIAGNOSTICS_FORENSIC` is not defined -- WHEN forensic helper macros are included from `src/util/diagnostics/forensic.hpp` -- THEN `GOGGLES_DIAG_FORENSIC_SCOPE(name)` and `GOGGLES_DIAG_FORENSIC_CAPTURE(session, pass, cmd)` SHALL expand to no-op expressions -- AND no Tier 2 capture code SHALL be required by downstream translation units - -### Requirement: Diagnostic Policy Configuration - -The diagnostics system SHALL support a policy configuration that controls strict versus compatibility mode, capture depth, retention limits, and per-degradation severity rules. - -#### Scenario: Strict mode forbids silent fallback -- GIVEN diagnostic policy is set to strict mode -- WHEN a texture binding resolves via fallback substitution -- THEN the diagnostics system SHALL emit an error-severity event -- AND the fallback SHALL be forbidden (the pass SHALL NOT execute with the substituted resource) - -#### Scenario: Compatibility mode records fallback -- GIVEN diagnostic policy is set to compatibility mode -- WHEN a texture binding resolves via fallback substitution -- THEN the diagnostics system SHALL emit a warning-severity event -- AND the fallback SHALL proceed and the pass SHALL execute with the substituted resource -- AND the substitution SHALL be recorded in the degradation ledger - -#### Scenario: Configuration via TOML -- GIVEN a Goggles configuration file with a `[diagnostics]` section -- WHEN the application initializes -- THEN the diagnostics system SHALL read capture mode, strict/compatibility toggle, and retention policy from the configuration -- AND missing configuration keys SHALL fall back to documented defaults - -#### Scenario: Concrete policy shape matches runtime config -- GIVEN the runtime diagnostics policy is created from Goggles config and boundary API inputs -- WHEN the active policy is inspected -- THEN it SHALL consist of `mode`, `capture_mode`, `tier`, `capture_frame_limit`, `retention_bytes`, `promote_fallback_to_error`, and `reflection_loss_is_fatal` -- AND the TOML-backed configuration surface SHALL map `mode`, `strict`, `tier`, `capture_frame_limit`, and `retention_bytes` onto that policy - -### Requirement: Reporting Modes - -The diagnostics system SHALL support four reporting modes that control the breadth and depth of diagnostic output. - -#### Scenario: Minimal mode output -- GIVEN reporting mode is set to Minimal -- WHEN a diagnostic report is generated -- THEN the report SHALL include the final verdict, compact failure summary, degraded-state markers, error counts by category, and session metadata -- AND the report SHALL NOT include intermediate image captures except final output on failure - -#### Scenario: Standard mode output -- GIVEN reporting mode is set to Standard -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Minimal plus normalized chain manifest, compile and reflection summaries, pass graph summary, binding coverage table, semantic coverage table, and one-frame execution trace - -#### Scenario: Investigate mode output -- GIVEN reporting mode is set to Investigate -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Standard plus selected intermediate outputs, uniform and push-constant dumps for selected passes, detailed source provenance, expanded degradation ledger, and history and feedback snapshots for selected frames - -#### Scenario: Forensic mode output -- GIVEN reporting mode is set to Forensic -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Investigate plus all pass outputs for selected frame ranges, full temporal capture of history and feedback resources, full event timeline, and complete artifact bundle with hashes and environment fingerprint - -### Requirement: Degradation Ledger - -The diagnostics system SHALL maintain a degradation ledger that records every instance of fallback substitution, unresolved semantic binding, missing reflection data, or policy downgrade during a diagnostic session. - -#### Scenario: Fallback substitution is ledgered -- GIVEN a texture binding falls back to the source image during frame recording -- WHEN the binding is resolved -- THEN the degradation ledger SHALL record the pass ordinal, the expected resource identity, the substituted resource identity, and the frame index - -#### Scenario: Unresolved semantic is ledgered -- GIVEN a semantic destination member has no matching semantic assignment -- WHEN semantic values are populated for a pass -- THEN the degradation ledger SHALL record the pass ordinal, the unresolved member name, and the destination offset - -#### Scenario: Ledger is queryable by pass -- GIVEN a degradation ledger with multiple entries across different passes -- WHEN the ledger is queried for a specific pass ordinal -- THEN only degradation entries for that pass SHALL be returned -- AND entries SHALL be ordered by frame index - -### Requirement: Chain Manifest - -The diagnostics system SHALL produce a chain manifest that describes the normalized structure of a loaded preset including pass order, scaling configuration, texture declarations, alias declarations, and temporal requirements. - -#### Scenario: Manifest reflects loaded preset -- GIVEN a preset has been successfully loaded and compiled -- WHEN the chain manifest is generated -- THEN the manifest SHALL list every pass with its ordinal, shader source path, scale type, scale factor, format override, and wrap mode -- AND the manifest SHALL list every declared alias with its target pass -- AND the manifest SHALL list every declared preset texture with its path - -#### Scenario: Manifest includes temporal requirements -- GIVEN a preset uses history or feedback resources -- WHEN the chain manifest is generated -- THEN the manifest SHALL include the inferred history depth -- AND the manifest SHALL list every feedback-producing pass with its consumer passes - -#### Scenario: Manifest is deterministic -- GIVEN the same preset is loaded twice in identical environments -- WHEN chain manifests are generated for both loads -- THEN both manifests SHALL be byte-identical - -### Requirement: Binding Ledger - -The diagnostics system SHALL produce a per-pass, per-frame binding ledger that records the resolved resource table including source identity, fallback status, resource extents, and producer relationship for every texture binding. - -#### Scenario: All bindings are accounted for -- GIVEN an effect pass with reflected texture bindings -- WHEN the binding ledger is populated during frame recording -- THEN every reflected texture binding SHALL have an entry in the ledger -- AND each entry SHALL classify the binding as resolved, substituted (fallback), or unresolved - -#### Scenario: Binding ledger records producer chain -- GIVEN a pass samples the output of an earlier pass via alias -- WHEN the binding ledger entry is created -- THEN the entry SHALL record the producing pass ordinal and the alias name used for resolution - -#### Scenario: Binding ledger records extents -- GIVEN a texture binding is resolved to a concrete resource -- WHEN the binding ledger entry is created -- THEN the entry SHALL record the width, height, and format of the bound resource - -### Requirement: Semantic Assignment Ledger - -The diagnostics system SHALL produce a per-pass semantic assignment ledger that records resolved semantic values and their destination offsets in uniform buffers and push constants. - -#### Scenario: All semantic destinations are classified -- GIVEN a pass with reflected uniform buffer and push constant members -- WHEN the semantic assignment ledger is populated -- THEN every destination member SHALL be classified as parameter, semantic, static, or unresolved - -#### Scenario: Semantic values are recorded -- GIVEN a pass receives semantic injection for source size, output size, and frame count -- WHEN the semantic assignment ledger is populated -- THEN the ledger SHALL record the concrete values written for each semantic destination - -#### Scenario: Alias-size destinations are resolved -- GIVEN a pass has destination members with size suffixes that resolve via the alias-size table -- WHEN the semantic assignment ledger is populated -- THEN the ledger SHALL record the resolved producer pass and the concrete size values - -### Requirement: Execution Timeline - -The diagnostics system SHALL produce an ordered execution timeline that records allocation, recording, history push, feedback copy, and final composition events with timestamps when Tier 1 or higher is active. - -#### Scenario: Timeline records pass-level events -- GIVEN Tier 1 diagnostics are active -- WHEN effect passes are recorded for a frame -- THEN the execution timeline SHALL include a start and end event for each pass with timing data - -#### Scenario: GPU timing is populated on recycled frame slots -- GIVEN Tier 1 diagnostics are active with GPU timestamps available -- WHEN a frame slot is reused after the previous submission has completed -- THEN pass end events for that slot SHALL be annotated with GPU duration in microseconds -- AND the first use of a slot MAY omit GPU duration until results become available - -#### Scenario: Timeline records temporal operations -- GIVEN Tier 1 diagnostics are active and the preset uses history and feedback -- WHEN a frame completes recording -- THEN the execution timeline SHALL include history push events and feedback copy events with their respective timing data - -#### Scenario: Timeline ordering matches execution order -- GIVEN an execution timeline for a single frame -- WHEN the timeline events are enumerated -- THEN events SHALL appear in the order they were executed -- AND pass events SHALL appear in pass-ordinal order - -### Requirement: Authoring Verdict - -The diagnostics system SHALL produce an authoring verdict after static validation that summarizes whether the preset is ready for execution, with categorized findings. - -#### Scenario: Clean preset produces passing verdict -- GIVEN a preset where all passes compile, all reflections succeed, and all contracts validate -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "pass" -- AND the finding count for errors SHALL be zero - -#### Scenario: Compilation failure produces failing verdict -- GIVEN a preset where one pass fails to compile -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "fail" -- AND the findings SHALL include at least one error-severity entry identifying the failing pass ordinal and compilation stage - -#### Scenario: Reflection loss produces conditional verdict -- GIVEN a preset where compilation succeeds but reflection fails for one pass in compatibility mode -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "degraded" -- AND the findings SHALL include a warning identifying the pass ordinal and reflection stage -- AND the verdict SHALL note that strict mode would have produced a "fail" verdict - -### Requirement: Source Provenance Map - -The diagnostics system SHALL produce a source provenance map that links expanded and rewritten shader text back to original file paths and line numbers. - -#### Scenario: Include expansion is tracked -- GIVEN a shader source that uses #include directives -- WHEN the source provenance map is generated after preprocessing -- THEN every line in the expanded source SHALL map back to a file path and line number in the original source tree - -#### Scenario: Compatibility rewrites are tracked -- GIVEN a shader source that undergoes compatibility text rewrites during preprocessing -- WHEN the source provenance map is generated -- THEN rewritten regions SHALL map back to the original source location before rewriting -- AND the provenance entry SHALL indicate that a rewrite was applied - -#### Scenario: Provenance enables error localization -- GIVEN a compilation error at a specific line in the expanded source -- WHEN the source provenance map is consulted -- THEN the original file path and line number SHALL be recoverable -- AND the diagnostic event for the compilation error SHALL include the original source location - -### Requirement: Compile and Reflection Reports - -The diagnostics system SHALL produce per-pass compile reports and reflection reports as structured artifacts. - -#### Scenario: Compile report records per-stage results -- GIVEN shader compilation has been attempted for a pass -- WHEN the compile report is generated -- THEN the report SHALL include per-stage (vertex, fragment) compilation success or failure, diagnostic messages, timing, and cache-hit status - -#### Scenario: Reflection report records merged contract -- GIVEN shader reflection has been performed for a pass -- WHEN the reflection report is generated -- THEN the report SHALL include per-stage reflected resources (uniform buffer members, push constant members, texture bindings, vertex inputs) -- AND the report SHALL include the merged pass contract with any binding collisions or type mismatches noted - -#### Scenario: Reflection absence is explicitly reported -- GIVEN a pass where compilation succeeds but reflection produces an empty contract -- WHEN the reflection report is generated -- THEN the report SHALL explicitly flag reflection absence -- AND the severity SHALL be error in strict mode and warning in compatibility mode diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/goggles-filter-chain/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/goggles-filter-chain/spec.md deleted file mode 100644 index e9025b0e..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,115 +0,0 @@ -# Delta for goggles-filter-chain - -## ADDED Requirements - -### Requirement: Diagnostic Session Lifecycle Through Boundary API - -The filter-chain boundary SHALL expose diagnostic session creation, query, and teardown through the boundary-facing contract so that host code can control diagnostic depth without accessing chain internals. - -#### Scenario: Host creates diagnostic session with policy -- GIVEN a READY filter-chain runtime instance -- WHEN the host requests diagnostic session creation with a specified reporting mode and strict/compatibility policy -- THEN the boundary SHALL create a diagnostic session scoped to the runtime's lifetime -- AND the session SHALL apply the specified policy to all subsequent diagnostic emission - -#### Scenario: Host queries diagnostic session state -- GIVEN an active diagnostic session on a filter-chain runtime -- WHEN the host queries session state through the boundary API -- THEN the boundary SHALL return the current reporting mode, policy mode, and aggregate event counts by severity - -#### Scenario: No diagnostic session is a valid state -- GIVEN a READY filter-chain runtime with no diagnostic session created -- WHEN the runtime records frames -- THEN the runtime SHALL operate without diagnostics-specific artifacts or sink delivery -- AND host-visible diagnostic ledgers and summaries SHALL remain unavailable until a session is created - -### Requirement: Sink Registration Through Boundary API - -The filter-chain boundary SHALL allow host code to register and unregister diagnostic sink adapters without exposing concrete sink implementation types. - -#### Scenario: Host registers a sink adapter -- GIVEN a diagnostic session exists on a filter-chain runtime -- WHEN the host registers a sink adapter through the boundary API -- THEN subsequent diagnostic events SHALL be delivered to the registered sink -- AND the registration SHALL return an identifier for later unregistration - -#### Scenario: Host unregisters a sink adapter -- GIVEN a sink adapter is registered with a diagnostic session -- WHEN the host unregisters the sink using its identifier -- THEN subsequent diagnostic events SHALL NOT be delivered to the unregistered sink -- AND events already in flight SHALL complete delivery - -#### Scenario: Multiple sinks from different hosts -- GIVEN a diagnostic session with two registered sink adapters -- WHEN a diagnostic event is emitted -- THEN both sinks SHALL receive the event -- AND delivery order SHALL be deterministic (registration order) - -### Requirement: Diagnostic Event Retrieval for External Consumers - -The filter-chain boundary SHALL expose lightweight diagnostic session summary and callback delivery primitives without requiring external consumers to access runtime internals. - -#### Scenario: Host retrieves diagnostic summary counts -- GIVEN a diagnostic session is active and events have been emitted -- WHEN the host calls `goggles_chain_diagnostics_summary_get(...)` -- THEN the boundary SHALL return the active reporting mode, policy mode, and aggregate error, warning, and info counts -- AND the call SHALL return `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` when no session exists - -#### Scenario: Host receives events through callback sink registration -- GIVEN a diagnostic session exists on a runtime -- WHEN the host registers `goggles_chain_diagnostic_event_cb` through `goggles_chain_diagnostics_sink_register(...)` -- THEN subsequent emitted events SHALL be forwarded to that callback with severity, category, pass ordinal, message text, and user data -- AND the registration SHALL produce a sink identifier that can be passed to `goggles_chain_diagnostics_sink_unregister(...)` - -#### Scenario: Retrieval before preset load -- GIVEN a runtime in CREATED state with no preset loaded -- WHEN the host requests diagnostic artifacts through the boundary API -- THEN the boundary SHALL return a not-initialized status -- AND no partial or stale artifacts SHALL be returned - -### Requirement: Capture Control Through Boundary API - -The filter-chain boundary SHALL reserve future space for capture control, while the current implemented boundary surface stops at session lifecycle, callback sink registration, and summary retrieval. - -#### Scenario: Current boundary does not yet expose capture requests -- GIVEN a host is using the current C boundary header -- WHEN it inspects the exported diagnostics functions -- THEN the implemented diagnostics surface SHALL consist of `goggles_chain_diagnostics_session_create(...)`, `goggles_chain_diagnostics_session_destroy(...)`, `goggles_chain_diagnostics_sink_register(...)`, `goggles_chain_diagnostics_sink_unregister(...)`, and `goggles_chain_diagnostics_summary_get(...)` -- AND no pass-range or frame-range capture request API SHALL yet be present - - - -### Requirement: Boundary Diagnostic Event Emission Does Not Break Frame Recording Contract - -Diagnostic event emission through the boundary SHALL NOT violate the existing frame recording performance contract. - -#### Scenario: Tier 0 events during record -- GIVEN the filter-chain is recording commands for a frame -- WHEN Tier 0 diagnostic events are emitted -- THEN no heap allocation, file I/O, shader compilation, or blocking wait SHALL occur in the event emission path - -#### Scenario: Tier 1 events with GPU timestamps -- GIVEN Tier 1 diagnostics are active during frame recording -- WHEN GPU timestamp queries are inserted for pass-level timing -- THEN timestamp query commands SHALL be recorded into the same command buffer -- AND timestamp readback SHALL occur asynchronously after frame submission, not during recording - -## MODIFIED Requirements - -### Requirement: Error Model and Diagnostics Contract - -All fallible APIs MUST return `goggles_chain_status_t` and MUST NOT surface exceptions as part of the public contract. `goggles_chain_status_to_string(...)` MUST return a stable static string and unknown status values MUST map to `"UNKNOWN_STATUS"`. Structured diagnostics MUST be queryable through `goggles_chain_error_last_info_get(...)` and, when a diagnostic session is active, through the diagnostic session's event and artifact retrieval APIs. - -(Previously: Optional structured diagnostics were queried only through `goggles_chain_error_last_info_get(...)` with `GOGGLES_CHAIN_STATUS_NOT_SUPPORTED` when unsupported. The diagnostic session now provides a richer, always-available diagnostic pathway.) - -#### Scenario: Diagnostic session supplements last-error -- GIVEN a diagnostic session is active and an API call fails -- WHEN the host queries diagnostics -- THEN `goggles_chain_error_last_info_get(...)` SHALL still return the last error info -- AND the diagnostic session SHALL contain a corresponding diagnostic event with richer context including localization and evidence payload - -#### Scenario: Diagnostics-not-active status has a stable code -- GIVEN a READY runtime with no diagnostic session created -- WHEN the host calls a diagnostics-session-only function such as sink registration or summary retrieval -- THEN the boundary SHALL return `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` -- AND `goggles_chain_status_to_string(...)` SHALL map that code to `"DIAGNOSTICS_NOT_ACTIVE"` diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/profiling/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/profiling/spec.md deleted file mode 100644 index 91224e15..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/profiling/spec.md +++ /dev/null @@ -1,68 +0,0 @@ -# Delta for profiling - -## ADDED Requirements - -### Requirement: Per-Pass GPU Timestamp Queries - -The profiling system SHALL support per-pass GPU timestamp queries that measure actual GPU execution time for each filter pass, complementing existing Tracy CPU profiling. - -#### Scenario: GPU timestamps recorded per effect pass -- GIVEN Tier 1 diagnostics are active and the GPU supports timestamp queries -- WHEN effect passes are recorded for a frame -- THEN a GPU timestamp query SHALL be written before and after each pass's draw commands -- AND the timestamp pair SHALL be associated with the pass ordinal - -#### Scenario: GPU timestamps for pre-processing and final composition -- GIVEN Tier 1 diagnostics are active -- WHEN the pre-processing region and final composition region are recorded -- THEN GPU timestamp queries SHALL bracket these regions as well -- AND timestamps SHALL be distinguishable from effect-pass timestamps by region identifier - -#### Scenario: GPU timestamp readback is asynchronous -- GIVEN GPU timestamp queries have been written during frame recording -- WHEN the frame submission completes -- THEN timestamp results SHALL be read back asynchronously (after fence signal) -- AND readback SHALL NOT stall the next frame's recording - -#### Scenario: GPU timestamps unavailable -- GIVEN the physical device reports `timestampPeriod` of zero or does not support timestamp queries -- WHEN Tier 1 diagnostics are requested -- THEN GPU timestamp collection SHALL be silently disabled -- AND a diagnostic info-severity event SHALL note that GPU timestamps are unavailable -- AND other Tier 1 diagnostics SHALL continue to function - -### Requirement: GPU Timing Integration with Execution Timeline - -Per-pass GPU timestamp results SHALL be integrated into the diagnostic execution timeline to provide a unified view of CPU and GPU pass durations. - -#### Scenario: Execution timeline includes GPU timing -- GIVEN Tier 1 diagnostics are active and GPU timestamps are available -- WHEN the execution timeline is generated for a frame -- THEN each pass event in the timeline SHALL include both CPU-side timing (from Tracy or wall clock) and GPU-side timing (from timestamp queries) - -#### Scenario: GPU timing identifies bottleneck pass -- GIVEN a frame with multiple effect passes and GPU timestamps -- WHEN the execution timeline is analyzed -- THEN the pass with the longest GPU duration SHALL be identifiable from the timeline data -- AND the timeline SHALL support sorting or ranking passes by GPU duration - -### Requirement: Profiling Debug Labels - -The profiling system SHALL insert Vulkan debug labels at pass boundaries so that GPU profiling tools and validation layers can associate work with specific passes. - -#### Scenario: Debug labels inserted per pass -- GIVEN debug label support is available (VK_EXT_debug_utils) -- WHEN effect passes are recorded -- THEN a debug label SHALL be inserted before each pass's draw commands identifying the pass by ordinal and shader name -- AND the label SHALL be ended after the pass's draw commands - -#### Scenario: Debug labels for temporal operations -- GIVEN debug label support is available -- WHEN history push and feedback copy operations are recorded -- THEN debug labels SHALL bracket these operations with descriptive names - -#### Scenario: Debug labels disabled without extension -- GIVEN the Vulkan instance or device does not support VK_EXT_debug_utils -- WHEN pass recording occurs -- THEN no debug label insertion SHALL be attempted -- AND no runtime error SHALL occur diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/render-pipeline/spec.md deleted file mode 100644 index da9deab9..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/render-pipeline/spec.md +++ /dev/null @@ -1,117 +0,0 @@ -# Delta for render-pipeline - -## ADDED Requirements - -### Requirement: Reflection Conformance Gate - -The render pipeline SHALL enforce a reflection conformance gate after shader compilation that validates the merged pass contract before pass resources are created. - -#### Scenario: Strict mode rejects empty reflection -- GIVEN diagnostic policy is set to strict mode -- WHEN a shader pass compiles successfully but reflection produces an empty contract -- THEN the pipeline SHALL reject the pass and emit an error-severity diagnostic event -- AND the pass SHALL NOT be installed into the compiled chain - -#### Scenario: Compatibility mode degrades on empty reflection -- GIVEN diagnostic policy is set to compatibility mode -- WHEN a shader pass compiles successfully but reflection produces an empty contract -- THEN the pipeline SHALL install the pass with a degraded marker -- AND a warning-severity diagnostic event SHALL be emitted -- AND the degradation SHALL be recorded in the degradation ledger - -#### Scenario: Binding collision detection -- GIVEN a merged reflection contract for a pass -- WHEN two reflected resources claim the same binding slot with different types or layouts -- THEN the conformance gate SHALL reject the pass in strict mode -- AND the conformance gate SHALL emit a diagnostic event identifying both conflicting resources - -### Requirement: Diagnostic Instrumentation Points in Shader Flow - -The render pipeline SHALL emit diagnostic events at each stage of the shader processing flow to support authoring analysis. - -#### Scenario: Preset parsing emits diagnostic event -- GIVEN a preset file is being loaded -- WHEN preset parsing completes (success or failure) -- THEN the pipeline SHALL emit a diagnostic event with category "authoring" containing the normalized preset structure and any parse errors - -#### Scenario: Include expansion emits diagnostic event -- GIVEN shader source undergoes include expansion -- WHEN expansion completes for a pass -- THEN the pipeline SHALL emit a diagnostic event recording the include graph depth, cycle detection result, and expansion success or failure - -#### Scenario: Stage compilation emits diagnostic event -- GIVEN vertex and fragment stages are compiled for a pass -- WHEN compilation completes for each stage -- THEN the pipeline SHALL emit a diagnostic event per stage containing compilation success, diagnostic messages, timing, and cache-hit status - -#### Scenario: Reflection emits diagnostic event -- GIVEN compiled stages undergo reflection -- WHEN reflection completes for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the reflected resource summary and any merge conflicts - -#### Scenario: Diagnostic-aware pipeline entry points use explicit optional outputs -- GIVEN diagnostic-aware authoring analysis is enabled during preset load -- WHEN the render pipeline APIs are invoked -- THEN `ChainBuilder::build(...)` SHALL accept an optional `diagnostics::DiagnosticSession* session = nullptr` -- AND `RetroArchPreprocessor::preprocess(const std::filesystem::path& shader_path, diagnostics::SourceProvenanceMap* provenance = nullptr)` SHALL expose optional provenance capture -- AND `ShaderRuntime::compile_retroarch_shader(const std::string& vertex_source, const std::string& fragment_source, const std::string& module_name, diagnostics::CompileReport* report = nullptr)` SHALL expose optional compile-report output - -### Requirement: Source Provenance Tracking in Preprocessing - -The render pipeline's shader preprocessing stage SHALL maintain a source provenance map that tracks the origin of every line through include expansion and compatibility rewrites. - -#### Scenario: Provenance survives include expansion -- GIVEN a shader with nested includes -- WHEN preprocessing completes -- THEN every line in the expanded output SHALL have a provenance entry mapping to an original file path and line number - -#### Scenario: Provenance survives compatibility rewrites -- GIVEN a shader that undergoes compatibility text rewrites -- WHEN preprocessing completes -- THEN rewritten lines SHALL retain provenance to the original source location -- AND the provenance entry SHALL indicate that a rewrite transformation was applied - -### Requirement: Pass-Level Runtime Diagnostic Events - -The render pipeline SHALL emit diagnostic events during per-pass frame recording to support runtime validation. - -#### Scenario: Binding plan event per pass -- GIVEN effect passes are being recorded for a frame -- WHEN texture bindings are rebuilt for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the resolved binding plan with resource identities, fallback status, and extents - -#### Scenario: Semantic population event per pass -- GIVEN effect passes are being recorded for a frame -- WHEN semantic values are populated for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the semantic assignment summary with destination classifications - -#### Scenario: Fallback substitution event -- GIVEN a non-source texture is missing at binding time during frame recording -- WHEN the engine substitutes the source image as a fallback -- THEN the pipeline SHALL emit a diagnostic event with the expected resource identity, the substituted resource identity, and the pass ordinal - -## MODIFIED Requirements - -### Requirement: Shader Runtime Compilation - -The render shader subsystem SHALL compile Slang shaders to SPIR-V at runtime using the Slang API, supporting both HLSL-style native shaders and GLSL-style RetroArch shaders. Compilation SHALL produce structured compile report artifacts consumable by the diagnostics system. - -(Previously: Compilation produced SPIR-V and cached results but did not emit structured diagnostic artifacts.) - -#### Scenario: Compilation emits structured compile report -- GIVEN a shader is compiled (cache miss) -- WHEN compilation completes -- THEN the shader runtime SHALL produce a structured compile report containing per-stage success, diagnostic messages with source locations, compilation timing, and cache state -- AND the report SHALL be emittable as a diagnostic event - -#### Scenario: Cache hit records provenance -- GIVEN a cached `.spv` file exists with matching source hash -- WHEN the shader is requested and loaded from cache -- THEN the compile report SHALL indicate cache-hit status -- AND the report SHALL preserve the source hash for session identity construction - -#### Scenario: Runtime signatures match implemented diagnostics hooks -- GIVEN diagnostics-aware compilation is requested for a RetroArch pass -- WHEN the runtime compiles that pass -- THEN the compile entry point SHALL accept separate preprocessed vertex and fragment source strings plus a module name -- AND compile-report emission SHALL remain optional so existing non-diagnostic call sites can pass `nullptr` diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/shader-testing/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/shader-testing/spec.md deleted file mode 100644 index 8026f92b..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/shader-testing/spec.md +++ /dev/null @@ -1,82 +0,0 @@ -# Delta for shader-testing - -## ADDED Requirements - -### Requirement: Structured Compile Report in Batch Testing - -The batch shader testing tool SHALL produce structured compile reports per preset that include per-stage compilation diagnostics with source-mapped locations, reflection summaries, and authoring verdicts. - -#### Scenario: Compile report includes source-mapped diagnostics -- GIVEN a preset with a shader that produces compilation warnings or errors -- WHEN the batch test processes the preset -- THEN the per-preset result SHALL include a compile report with diagnostic messages mapped to original source file paths and line numbers via the source provenance map - -#### Scenario: Compile report includes reflection summary -- GIVEN a preset with shaders that compile successfully -- WHEN the batch test processes the preset -- THEN the per-preset result SHALL include a reflection summary listing reflected resources (uniform buffer members, push constant members, texture bindings) per stage per pass - -#### Scenario: Compile report includes authoring verdict -- GIVEN a preset has been processed through parsing, compilation, and reflection -- WHEN the batch test generates the per-preset result -- THEN the result SHALL include an authoring verdict (pass, degraded, or fail) based on the diagnostics system's static validation - -### Requirement: Authoring Validation Corpus - -The shader testing infrastructure SHALL maintain categorized test presets that exercise authoring validation rules, including intentionally invalid presets. - -#### Scenario: Invalid preset corpus for parse failures -- GIVEN a set of intentionally malformed preset files -- WHEN batch testing runs the authoring validation corpus -- THEN each malformed preset SHALL produce a "fail" authoring verdict -- AND the diagnostic findings SHALL identify the specific parse failure - -#### Scenario: Invalid shader corpus for compile failures -- GIVEN a set of presets referencing intentionally invalid shader sources -- WHEN batch testing runs the authoring validation corpus -- THEN each preset SHALL produce a "fail" authoring verdict with compilation-stage errors -- AND error messages SHALL include source-mapped locations - -#### Scenario: Reflection-loss corpus -- GIVEN a set of presets where compilation succeeds but reflection produces incomplete contracts -- WHEN batch testing runs the authoring validation corpus in strict mode -- THEN each preset SHALL produce a "fail" authoring verdict -- AND the findings SHALL identify the specific passes with reflection loss - -#### Scenario: Valid presets in corpus still pass -- GIVEN the existing set of valid RetroArch presets -- WHEN batch testing runs the authoring validation corpus -- THEN all previously passing presets SHALL continue to produce "pass" authoring verdicts - -### Requirement: Semantic and Parameter Conformance Reporting in Batch Testing - -The batch shader testing tool SHALL produce parameter and semantic conformance reports that identify unresolved overrides, duplicate parameter names, and unresolved semantic destinations. - -#### Scenario: Unused override detection -- GIVEN a preset with numeric overrides that do not match any reflected parameter destination -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each unused override with the override name and the preset source location - -#### Scenario: Duplicate parameter name detection -- GIVEN a preset where multiple passes declare parameters with the same name -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each duplicate parameter name with the pass ordinals involved - -#### Scenario: Unresolved semantic destination detection -- GIVEN a pass with reflected uniform buffer members that do not match any known semantic family -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each unresolved destination with the member name, pass ordinal, and suggested near-match if one exists - -## MODIFIED Requirements - -### Requirement: Machine-readable results - -The batch test tool SHALL output results in JSON format containing per-preset status, summary statistics, structured compile reports, reflection summaries, authoring verdicts, and conformance findings. - -(Previously: JSON output included per-preset path, parse_ok, compile_ok, error, and summary counts only.) - -#### Scenario: JSON output format with diagnostics -- GIVEN batch tests have completed -- WHEN results are written to build/shader_test_results.json -- THEN each preset entry SHALL include: path, parse_ok, compile_ok, error (if any), authoring_verdict, compile_report (per-stage diagnostics), reflection_summary, and conformance_findings -- AND summary SHALL include: total, passed, failed, skipped, degraded counts diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/visual-regression/spec.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/visual-regression/spec.md deleted file mode 100644 index 870b5357..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/specs/visual-regression/spec.md +++ /dev/null @@ -1,138 +0,0 @@ -# Delta for visual-regression - -## ADDED Requirements - -### Requirement: Per-Pass Intermediate Output Golden Baselines - -The visual regression system SHALL support golden image baselines for selected intermediate pass outputs, not only the final composited output. - -#### Scenario: Intermediate golden for a specific pass -- GIVEN a golden reference image exists for pass ordinal 2 of a specific preset -- WHEN the headless pipeline captures intermediate output for pass 2 and compares it to the golden -- THEN the comparison SHALL use the same tolerance and metric infrastructure as final-output comparisons -- AND the comparison result SHALL identify the pass ordinal in its report - -#### Scenario: Multiple intermediate goldens per preset -- GIVEN golden reference images exist for passes 0, 2, and 4 of a multi-pass preset -- WHEN the visual regression suite runs for that preset -- THEN each intermediate golden SHALL be compared independently -- AND failures SHALL be reported per pass with pass ordinal in the failure message - -#### Scenario: Intermediate golden update workflow -- GIVEN the golden update mechanism (`pixi run update-golden`) -- WHEN intermediate golden generation is requested for a preset -- THEN the tool SHALL capture and store intermediate outputs for the specified pass ordinals -- AND intermediate goldens SHALL follow the naming convention `{preset_name}_pass{ordinal}.png` - -#### Scenario: Missing intermediate golden is a skip, not a failure -- GIVEN no intermediate golden reference exists for a pass -- WHEN visual regression attempts to compare intermediate output for that pass -- THEN the test SHALL emit a Catch2 SKIP (not FAIL) -- AND the skip message SHALL direct the user to generate the intermediate golden - -### Requirement: Earliest-Divergence Localization - -The visual regression system SHALL support locating the earliest intermediate pass whose output diverges from its golden baseline, enabling failure localization to a specific pass rather than only the final output. - -#### Scenario: Divergence localized to earliest failing pass -- GIVEN intermediate goldens exist for passes 0 through 4 -- WHEN pass 2 intermediate output diverges from its golden but passes 0 and 1 match -- THEN the regression report SHALL identify pass 2 as the earliest divergent pass -- AND the report SHALL note that passes 3 and 4 are downstream of the divergence - -#### Scenario: No intermediate goldens available falls back to final-only -- GIVEN no intermediate goldens exist for a preset -- WHEN the final output diverges from its golden -- THEN the regression report SHALL report the final output failure -- AND the report SHALL note that intermediate golden baselines are unavailable for pass-level localization - -### Requirement: Temporal Sequence Golden Baselines - -The visual regression system SHALL support golden baselines for multi-frame temporal sequences, validating that history and feedback surfaces produce expected results across frames. - -#### Scenario: Multi-frame golden sequence -- GIVEN golden reference images exist for frames 1, 3, and 5 of a temporal preset -- WHEN the headless pipeline captures outputs at those frame indices and compares to goldens -- THEN each frame comparison SHALL be independent -- AND failures SHALL report the frame index alongside the comparison metrics - -#### Scenario: Feedback surface golden validation -- GIVEN a preset that uses feedback routing and golden references exist for the feedback consumer pass at frames 2 and 4 -- WHEN the visual regression suite captures intermediate outputs at those frames -- THEN the comparison SHALL validate that the feedback surface correctly carries the prior frame's pass output - -#### Scenario: Temporal golden update workflow -- GIVEN the golden update mechanism is invoked with frame range specification -- WHEN temporal golden generation is requested -- THEN the tool SHALL capture outputs at the specified frame indices -- AND temporal goldens SHALL follow the naming convention `{preset_name}_frame{index}.png` for final outputs and `{preset_name}_pass{ordinal}_frame{index}.png` for intermediates - -### Requirement: Semantic-Probe Presets for Contract Testing - -The visual regression system SHALL support synthetic semantic-probe presets that test specific contract behaviors such as size semantic correctness, frame counter progression, and parameter isolation. - -#### Scenario: Size semantic probe -- GIVEN a synthetic preset designed to encode source size and output size into pixel values -- WHEN the headless pipeline runs the probe at known source and viewport sizes -- THEN the captured output SHALL encode the expected size values -- AND the regression suite SHALL validate the encoded values against expected values rather than using pixel-diff comparison - -#### Scenario: Frame counter probe -- GIVEN a synthetic preset designed to encode the frame count modulo a known value into pixel intensity -- WHEN the headless pipeline captures multiple frames -- THEN each frame's output SHALL encode the expected frame count value -- AND temporal progression SHALL be verified across the captured sequence - -#### Scenario: Parameter isolation probe -- GIVEN a synthetic preset where a single parameter controls a measurable visual property -- WHEN the parameter is set to two distinct values and both outputs are captured -- THEN only the expected visual property SHALL differ between the two captures -- AND the regression suite SHALL report any unexpected pixel differences outside the expected region - -### Requirement: Diff Heatmap Generation - -The visual regression system SHALL support generating diff heatmaps that visually highlight regions of divergence between actual and golden images. - -#### Scenario: Heatmap generated on comparison failure -- GIVEN a golden comparison that fails -- WHEN heatmap generation is enabled -- THEN a heatmap image SHALL be produced that maps per-pixel error magnitude to a color gradient -- AND the heatmap SHALL use a perceptually uniform color scale from no-error to maximum-error - -#### Scenario: Heatmap for intermediate pass failures -- GIVEN an intermediate pass golden comparison that fails -- WHEN heatmap generation is enabled -- THEN the heatmap SHALL be generated for the intermediate pass output -- AND the heatmap file SHALL include the pass ordinal in its filename - -## MODIFIED Requirements - -### Requirement: Image comparison library - -The system SHALL provide a C++ image comparison library at `tests/visual/image_compare.hpp` that compares two PNG images with configurable per-channel tolerance and supports additional perceptual quality metrics. - -(Previously: The library supported per-channel tolerance comparison and basic diff image generation. It now also supports structural similarity metrics and region-of-interest comparison.) - -#### Scenario: Structural similarity metric -- GIVEN two PNG files of the same dimensions -- WHEN `compare_images()` is called with structural similarity enabled -- THEN `CompareResult` SHALL include a `structural_similarity` field with a value between 0.0 and 1.0 -- AND the structural similarity metric SHALL complement per-channel tolerance for perceptual quality assessment - -#### Scenario: Region-of-interest comparison -- GIVEN two PNG files and a specified rectangular region of interest -- WHEN `compare_images()` is called with the region specification -- THEN comparison metrics SHALL be computed only within the specified region -- AND `CompareResult.failing_pixels` SHALL count only pixels within the region that exceed tolerance - -### Requirement: Golden image update workflow - -The system SHALL provide a reproducible mechanism to regenerate golden reference images including intermediate pass goldens and temporal sequence goldens. - -(Previously: The update workflow captured only final bypass and zfast goldens. It now supports intermediate and temporal golden generation.) - -#### Scenario: update-golden captures intermediate goldens -- GIVEN the project is built and intermediate golden generation is configured -- WHEN `pixi run update-golden` is executed with intermediate pass specification -- THEN golden images for the specified intermediate passes SHALL be captured and stored -- AND intermediate goldens SHALL be stored alongside final goldens with pass-ordinal naming diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/state.yaml b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/state.yaml deleted file mode 100644 index 6aa77287..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/state.yaml +++ /dev/null @@ -1,30 +0,0 @@ -phase: verified -apply_progress: - phase1: complete - phase2: complete - phase3: complete - phase4: complete - completed_tasks: 81 - total_tasks: 81 -verify_result: pass_with_warnings -interview_state: - interview_id: "fcd-2026-0310-001" - active: false - rounds_completed: 4 - current_ambiguity: 0.195 - threshold: 0.2 - project_type: brownfield - challenge_modes_used: ["contrarian"] -artifacts: - interview: true - proposal: true - specs: true - design: true - tasks: true -spec_domains: - - diagnostics (new) - - render-pipeline (delta) - - goggles-filter-chain (delta) - - shader-testing (delta) - - visual-regression (delta) - - profiling (delta) diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/tasks.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/tasks.md deleted file mode 100644 index ffe9357e..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/tasks.md +++ /dev/null @@ -1,340 +0,0 @@ -# Tasks: Filter Chain Diagnostics and Debugging System - -## Phase 1: Foundation — Core Diagnostic Types, Event Model, Session, and Sinks - -### 1.0 Build Infrastructure - -- [x] 1.0.1 Create `src/util/diagnostics/CMakeLists.txt` defining a `goggles_diagnostics` OBJECT library. Add source files incrementally as they are created. Link against `goggles_util` and `Vulkan::Vulkan`. Apply `goggles_enable_clang_tidy`, `goggles_enable_sanitizers`, and `goggles_enable_profiling`. - - **Verify:** `pixi run build -p debug` succeeds with the empty library target. - -- [x] 1.0.2 Wire `goggles_diagnostics` into the build graph by adding `add_subdirectory(diagnostics)` to `src/util/CMakeLists.txt`, and linking `goggles_diagnostics` into `goggles_render_chain_obj` in `src/render/chain/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` succeeds; `goggles_diagnostics` objects appear in the link. - -### 1.1 Diagnostic Event Model - -- [x] 1.1.1 Create `src/util/diagnostics/diagnostic_event.hpp` defining `goggles::diagnostics::Severity` (debug, info, warning, error), `Category` (authoring, runtime, quality, capture), `LocalizationKey` (pass_ordinal with `CHAIN_LEVEL` sentinel, stage `string_view`, resource `string_view`), evidence payload variants (`BindingEvidence`, `SemanticEvidence`, `CompileEvidence`, `ReflectionEvidence`, `ProvenanceEvidence`, `CaptureEvidence`), `EvidencePayload` as `std::variant`, and `DiagnosticEvent` struct per the design interfaces section. - - **Verify:** `pixi run build -p debug` compiles. Header is include-only (no .cpp needed). - -- [x] 1.1.2 Create `tests/render/test_diagnostic_event_model.cpp` with Catch2 tests: severity ordering (debug < info < warning < error), `LocalizationKey::CHAIN_LEVEL` sentinel value correctness, `DiagnosticEvent` construction with each evidence variant, `std::monostate` default evidence. Add to `tests/CMakeLists.txt` under `goggles_tests` sources. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes diagnostic event model tests. - -### 1.2 Diagnostic Policy - -- [x] 1.2.1 Create `src/util/diagnostics/diagnostic_policy.hpp` defining `PolicyMode` (compatibility, strict), `CaptureMode` (minimal, standard, investigate, forensic), `ActivationTier` (tier0, tier1, tier2), and `DiagnosticPolicy` struct with defaults per design. Include severity promotion logic: `promote_fallback_to_error` derived from `mode == strict`, `reflection_loss_is_fatal` derived from `mode == strict`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.2.2 Add policy unit tests to `tests/render/test_diagnostic_event_model.cpp` (or a new `tests/render/test_diagnostic_policy.cpp`): strict mode sets `promote_fallback_to_error = true`; compatibility mode keeps it `false`; tier values are ordered; default policy is compatibility/standard/tier0. Add new file to `tests/CMakeLists.txt` if separate. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes policy tests. - -### 1.3 Sink Interface and Concrete Sinks - -- [x] 1.3.1 Create `src/util/diagnostics/diagnostic_sink.hpp` defining abstract `DiagnosticSink` with single pure virtual `void receive(const DiagnosticEvent& event)` and virtual destructor. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.3.2 Create `src/util/diagnostics/log_sink.hpp` and `src/util/diagnostics/log_sink.cpp` implementing `LogSink` that formats `DiagnosticEvent` fields and routes to spdlog via a dedicated logger name `"diagnostics"`. Map `Severity` to spdlog levels. Add `log_sink.cpp` to `src/util/diagnostics/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles with `LogSink`. - -- [x] 1.3.3 Create `src/util/diagnostics/test_harness_sink.hpp` and `src/util/diagnostics/test_harness_sink.cpp` implementing `TestHarnessSink` that collects events in a `std::vector`, supports `events_by_category(Category)`, `events_by_severity(Severity)`, `event_count()`, and `clear()`. Add `test_harness_sink.cpp` to `src/util/diagnostics/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles with `TestHarnessSink`. - -- [x] 1.3.4 Create `tests/render/test_diagnostic_sinks.cpp` with Catch2 tests: `TestHarnessSink` collects events in emission order; filter by category returns only matching events; filter by severity returns only matching events; `LogSink` does not throw on receive (verify via test-harness that spdlog logger is invoked). Add to `tests/CMakeLists.txt`. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes sink tests. - -### 1.4 Session Identity - -- [x] 1.4.1 Create `src/util/diagnostics/session_identity.hpp` defining `SessionIdentity` struct: `preset_hash`, `expanded_source_hash`, `compiled_contract_hash` (all `std::string`), `generation_id` (`uint64_t`), `frame_start`/`frame_end` (`uint32_t`), `capture_mode` (`std::string`), `environment_fingerprint` (`std::string`). - - **Verify:** `pixi run build -p debug` compiles. - -### 1.5 Ledgers - -- [x] 1.5.1 Create `src/util/diagnostics/degradation_ledger.hpp` defining `DegradationLedger` with `DegradationEntry` (pass ordinal, expected resource, substituted resource, frame index, degradation type enum). Provide `record(...)`, `entries_for_pass(uint32_t)`, `all_entries()`, `clear()`. Header-only or minimal .cpp. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.5.2 Create `src/util/diagnostics/binding_ledger.hpp` defining `BindingLedger` with `BindingEntry` (pass ordinal, binding slot, status enum: resolved/substituted/unresolved, resource identity, extent w/h/format, producer pass ordinal, alias name). Provide `record(...)`, `entries_for_pass(uint32_t)`, `all_entries()`, `clear()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.5.3 Create `src/util/diagnostics/semantic_ledger.hpp` defining `SemanticAssignmentLedger` with `SemanticEntry` (pass ordinal, member name, classification enum: parameter/semantic/static/unresolved, value as `std::variant>`, offset). Provide `record(...)`, `entries_for_pass(uint32_t)`, `all_entries()`, `clear()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.5.4 Create `src/util/diagnostics/execution_timeline.hpp` defining `ExecutionTimeline` with `TimelineEvent` (event type enum: pass_start/pass_end/history_push/feedback_copy/allocation, pass ordinal, cpu_timestamp_ns, gpu_duration_us optional). Provide `record(...)`, `events()`, `events_for_pass(uint32_t)`, `clear()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.5.5 Create `tests/render/test_binding_ledger.cpp` with Catch2 tests: record entries across multiple passes; query by pass ordinal returns only that pass; status classification is correct; producer chain is recorded; extents are recorded. Add to `tests/CMakeLists.txt`. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes ledger tests. - -### 1.6 Chain Manifest - -- [x] 1.6.1 Create `src/util/diagnostics/chain_manifest.hpp` defining `ChainManifest` with per-pass entries (ordinal, shader path, scale type, scale factor, format override, wrap mode), alias list, texture declarations, temporal requirements (history depth, feedback producers/consumers). Provide factory `static auto from_preset(const PresetConfig&) -> ChainManifest`. - - **Verify:** `pixi run build -p debug` compiles. - -### 1.7 Diagnostic Session - -- [x] 1.7.1 Create `src/util/diagnostics/diagnostic_session.hpp` defining `DiagnosticSession` class per the design interface: `create(DiagnosticPolicy)`, `emit(DiagnosticEvent)`, `register_sink(unique_ptr)` returning `SinkId`, `unregister_sink(SinkId)`, policy get/set, identity get/update, ledger accessors (`degradation_ledger()`, `binding_ledger()`, `semantic_ledger()`, `execution_timeline()`, `chain_manifest()`, `authoring_verdict()`), `event_count(Severity)`, `event_count(Category)`, `begin_frame(uint32_t)`, `end_frame()`, `reset()`. - - **Verify:** `pixi run build -p debug` compiles (header only at this point). - -- [x] 1.7.2 Create `src/util/diagnostics/diagnostic_session.cpp` implementing session: fan-out to registered sinks on `emit()`, populate ledgers from event category/evidence, apply policy severity promotion before fan-out, track severity/category counts, handle sink failure isolation (catch exceptions from sink `receive()` and emit a meta-event). Add to `src/util/diagnostics/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.7.3 Create `tests/render/test_diagnostic_session.cpp` with Catch2 tests: session with no sinks silently discards events; session with one sink delivers events; session with two sinks delivers to both in registration order; severity promotion in strict mode changes event severity but preserves `original_severity`; event counting by severity and category is correct; `begin_frame`/`end_frame` updates frame tracking; `reset()` clears all ledgers and counts. Add to `tests/CMakeLists.txt`. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes session tests. - -### 1.8 TOML Configuration - -- [x] 1.8.1 Add `Config::Diagnostics` struct to `src/util/config.hpp` with fields: `std::string mode` (default `"standard"`), `bool strict` (default `false`), `uint32_t tier` (default `0`), `uint32_t capture_frame_limit` (default `1`), `uint64_t retention_bytes` (default `256 * 1024 * 1024`). Add `Diagnostics diagnostics` member to `Config`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.8.2 Extend `load_config()` in `src/util/config.cpp` to parse the `[diagnostics]` TOML section. Missing keys fall back to defaults. Invalid values produce `Result` errors. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 1.8.3 Add `[diagnostics]` section to `config/goggles.template.toml` with commented-out defaults: `# mode = "standard"`, `# strict = false`, `# tier = 0`; first-run bootstrap materializes `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml` from the template when the runtime user config is missing. - - **Verify:** Existing config load tests still pass: `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure`. - -- [x] 1.8.4 Add test cases to `tests/util/test_config.cpp` for: config with `[diagnostics]` section parses correctly; config without `[diagnostics]` uses defaults; invalid tier value returns error. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes config tests. - -### 1.9 Phase 1 Gate - -- [x] 1.9.1 Run full CI-parity gate: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. All pass with zero new warnings. - - **Verify:** All three commands succeed. - - -## Phase 2: Authoring Analysis — Compile Reports, Provenance, Conformance - -### 2.1 Source Provenance - -- [x] 2.1.1 Create `src/util/diagnostics/source_provenance.hpp` defining `SourceProvenanceMap` with `ProvenanceEntry` (original file path, original line number, rewrite flag, rewrite description). Provide `record(uint32_t expanded_line, ProvenanceEntry)`, `lookup(uint32_t expanded_line) -> const ProvenanceEntry*`, `size()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 2.1.2 Add optional `SourceProvenanceMap*` output parameter to `RetroArchPreprocessor::preprocess()` and `preprocess_source()` in `src/render/shader/retroarch_preprocessor.hpp`. Default to `nullptr` for backward compatibility. - - **Verify:** `pixi run build -p debug` compiles; existing preprocessor tests pass unchanged. - -- [x] 2.1.3 Implement provenance tracking in `RetroArchPreprocessor::resolve_includes()` in `src/render/shader/retroarch_preprocessor.cpp`: when provenance map pointer is non-null, record expanded-line to original-file-and-line entries during include expansion. Track compatibility rewrites similarly. - - **Verify:** `pixi run build -p debug` compiles; existing preprocessor tests pass. - -- [x] 2.1.4 Add provenance-specific tests to `tests/render/test_retroarch_preprocessor.cpp` (or new `tests/render/test_source_provenance.cpp`): preprocess a shader with `#include`, verify provenance maps expanded lines to original files; preprocess a shader with compatibility rewrites, verify rewrite flag is set on affected entries. Add new file to `tests/CMakeLists.txt` if separate. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes provenance tests. - -### 2.2 Compile Reports - -- [x] 2.2.1 Create `src/util/diagnostics/compile_report.hpp` defining `CompileReport` with `StageReport` entries (stage enum: vertex/fragment, success bool, diagnostic messages vector with source-mapped locations, timing_us, cache_hit bool). Provide `add_stage(StageReport)`, `stages()`, `all_succeeded()`, `total_timing_us()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 2.2.2 Add optional `CompileReport*` output parameter to `ShaderRuntime::compile_retroarch_shader()` in `src/render/shader/shader_runtime.hpp`. Default to `nullptr`. - - **Verify:** `pixi run build -p debug` compiles; existing shader tests pass unchanged. - -- [x] 2.2.3 Populate compile report in `ShaderRuntime::compile_retroarch_shader()` in `src/render/shader/shader_runtime.cpp`: record per-stage compilation success, diagnostic messages, timing (use `std::chrono::steady_clock`), and cache-hit status. Only populate when report pointer is non-null. - - **Verify:** `pixi run build -p debug` compiles; existing shader tests pass. - -- [x] 2.2.4 Add compile report tests to `tests/render/test_shader_runtime.cpp` (or new `tests/render/test_compile_report.cpp`): compile a valid shader with `CompileReport*`, verify `all_succeeded() == true` and two stage entries; compile an invalid shader, verify failure stage is recorded with diagnostic messages. Add new file to `tests/CMakeLists.txt` if separate. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes compile report tests. - -### 2.3 Authoring Verdict - -- [x] 2.3.1 Define `AuthoringVerdict` in `src/util/diagnostics/diagnostic_event.hpp` (or a new `authoring_verdict.hpp`): verdict enum (pass, degraded, fail), findings vector (severity + localization + message), convenience factory `from_compile_reports(...)`. - - **Verify:** `pixi run build -p debug` compiles. - -### 2.4 Chain Manifest Generation in ChainBuilder - -- [x] 2.4.1 Add optional `DiagnosticSession*` parameter to `ChainBuilder::build()` in `src/render/chain/chain_builder.hpp`. Default to `nullptr`. - - **Verify:** `pixi run build -p debug` compiles; existing call sites unchanged. - -- [x] 2.4.2 Implement chain manifest generation in `ChainBuilder::build()` in `src/render/chain/chain_builder.cpp`: when session pointer is non-null, build `ChainManifest` from the `PresetConfig`, emit it as a diagnostic event with category `authoring` and stage `"manifest"`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 2.4.3 Wire provenance and compile reports into `ChainBuilder::build()`: pass `SourceProvenanceMap*` to preprocessor and `CompileReport*` to shader runtime during per-pass compilation. Emit compile report events and provenance events through the diagnostic session. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 2.4.4 Implement reflection conformance gate in `ChainBuilder::build()`: after shader compilation, if session is non-null and policy is strict, reject passes with empty reflection contracts (emit error-severity event, return error). In compatibility mode, mark pass as degraded and emit warning-severity event. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 2.4.5 Generate authoring verdict in `ChainBuilder::build()`: after all passes are compiled and validated, produce `AuthoringVerdict` from accumulated compile reports and conformance results. Store in the diagnostic session. - - **Verify:** `pixi run build -p debug` compiles. - -### 2.5 Authoring Integration Tests - -- [x] 2.5.1 Create `tests/render/test_authoring_validation.cpp` with Catch2 tests exercising the full authoring analysis path: load a valid preset with a diagnostic session using `TestHarnessSink`, verify chain manifest event is emitted, compile report events are emitted per pass, authoring verdict is "pass". Add to `tests/CMakeLists.txt`. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes authoring validation tests. - -- [x] 2.5.2 Add authoring validation tests for error cases in `tests/render/test_authoring_validation.cpp`: preset with intentionally broken shader path produces "fail" verdict with error-severity events; test reflection conformance gate in strict mode rejects degraded pass. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes error-case tests. - -### 2.6 Phase 2 Gate - -- [x] 2.6.1 Run full CI-parity gate: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. All pass with zero new warnings. - - **Verify:** All three commands succeed. - - -## Phase 3: Runtime Validation and Capture — Binding Ledger, GPU Timestamps, Intermediate Capture - -### 3.1 Diagnostic Session in ChainRuntime - -- [x] 3.1.1 Add `std::unique_ptr m_diagnostic_session` member to `ChainRuntime` in `src/render/chain/chain_runtime.hpp`. Add `create_diagnostic_session(diagnostics::DiagnosticPolicy)` and `diagnostic_session()` accessor methods. - - **Verify:** `pixi run build -p debug` compiles; no behavioral change without session creation. - -- [x] 3.1.2 Implement session creation in `ChainRuntime::create_diagnostic_session()` in `src/render/chain/chain_runtime.cpp`. Wire session into `ChainBuilder::build()` during `load_preset()`. Register default `LogSink` when session is created. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.2 Runtime Event Emission in ChainExecutor - -- [x] 3.2.1 Add optional `diagnostics::DiagnosticSession*` parameter to `ChainExecutor::record()` in `src/render/chain/chain_executor.hpp`. Default to `nullptr`. - - **Verify:** `pixi run build -p debug` compiles; existing call sites unchanged. - -- [x] 3.2.2 Implement Tier 0 binding plan event emission in `ChainExecutor::bind_pass_textures()` in `src/render/chain/chain_executor.cpp`: when session is non-null, emit a diagnostic event per pass with category `runtime`, stage `"bind"`, containing `BindingEvidence` for each resolved texture binding (resource identity, fallback status, extent, producer pass). - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.2.3 Implement Tier 0 fallback detection in `ChainExecutor::bind_pass_textures()`: when a texture binding resolves via fallback substitution, emit a degradation event (warning in compatibility mode, error in strict mode). Record in the session's degradation ledger. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.2.4 Implement Tier 0 semantic assignment event emission in the semantic injection path of `ChainExecutor::record()`: when session is non-null, emit a diagnostic event per pass with category `runtime`, stage `"semantic"`, containing `SemanticEvidence`. Populate the session's semantic ledger. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.2.5 Implement execution timeline recording in `ChainExecutor::record()`: when session is non-null, record `TimelineEvent` entries for pass start/end, history push, feedback copy via `begin_frame()`/`end_frame()` lifecycle. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.3 Generation-Aware Install in ChainResources - -- [x] 3.3.1 Add `uint64_t m_generation_id = 0` member to `ChainResources` in `src/render/chain/chain_resources.hpp`. Add optional `diagnostics::DiagnosticSession*` parameter to `install()`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.3.2 Implement generation tracking in `ChainResources::install()` in `src/render/chain/chain_resources.cpp`: increment `m_generation_id` on each install. When session is non-null, emit an installation event with pass count, generation id, and session identity update. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.4 GPU Timestamp Pool - -- [x] 3.4.1 Create `src/util/diagnostics/gpu_timestamp_pool.hpp` and `src/util/diagnostics/gpu_timestamp_pool.cpp` implementing `GpuTimestampPool` per design interface: `create(device, physical_device, max_passes, frames_in_flight)` returning `Result`, `reset_frame(cmd, frame_index)`, `write_timestamp(cmd, frame_index, pass_ordinal, is_start)`, `read_results(frame_index)` returning per-pass durations, `is_available()`. Handle unavailable timestamps gracefully (timestampPeriod == 0). Add to `src/util/diagnostics/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.4.2 Integrate GPU timestamp queries into `ChainExecutor::record()`: when session is non-null and Tier >= 1, call `reset_frame()` at start, `write_timestamp()` before/after each pass recording. After frame completion (async, not during recording), call `read_results()` and populate execution timeline with GPU durations. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.5 Vulkan Debug Labels - -- [x] 3.5.1 Add debug label insertion helpers to `ChainExecutor` (or a utility in `src/util/diagnostics/`): `begin_debug_label(cmd, name, color)` and `end_debug_label(cmd)` using `vk::CommandBuffer::beginDebugUtilsLabelEXT` / `endDebugUtilsLabelEXT`. No-op when `VK_EXT_debug_utils` is not available (check via dispatch table). - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.5.2 Insert debug labels in `ChainExecutor::record()` around each pass recording, history push, and feedback copy operations. Label includes pass ordinal and shader name. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.6 Boundary API Extensions - -- [x] 3.6.1 Add new status code `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` (value 10) to `src/render/chain/api/c/goggles_filter_chain.h`. Add diagnostic mode/policy `#define` constants, `GogglesChainDiagnosticsCreateInfo`, `GogglesChainDiagnosticsSummary`, and `goggles_chain_diagnostic_event_cb` callback type per design. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.6.2 Add C API function declarations to `src/render/chain/api/c/goggles_filter_chain.h`: `goggles_chain_diagnostics_session_create`, `goggles_chain_diagnostics_session_destroy`, `goggles_chain_diagnostics_sink_register`, `goggles_chain_diagnostics_sink_unregister`, `goggles_chain_diagnostics_summary_get`. Add corresponding `typedef` aliases. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.6.3 Implement C API diagnostic functions in `src/render/chain/api/c/goggles_filter_chain.cpp`: delegate to `ChainRuntime` diagnostic session methods. Create a `CallbackSink` adapter wrapping `goggles_chain_diagnostic_event_cb` for sink registration. Return `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` when no session exists. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.6.4 Add C++ diagnostic wrapper methods to `src/render/chain/api/cpp/goggles_filter_chain.hpp` and implement in `src/render/chain/api/cpp/goggles_filter_chain.cpp`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 3.6.5 Update `goggles_chain_status_to_string()` in `src/render/chain/api/c/goggles_filter_chain.cpp` to handle the new `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` code. - - **Verify:** `pixi run build -p debug` compiles. - -### 3.7 Runtime Integration Tests - -- [x] 3.7.1 Add boundary API diagnostic tests to `tests/render/test_filter_chain_c_api_contracts.cpp`: create session with policy, register callback sink, load preset (which triggers authoring events), record frame (which triggers runtime events), query summary, verify counts. Verify `DIAGNOSTICS_NOT_ACTIVE` returned when no session exists. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes C API diagnostic tests. - -- [x] 3.7.2 Create `tests/render/test_runtime_diagnostics.cpp` with Catch2 integration tests: load a preset with diagnostic session and `TestHarnessSink`, record one frame, verify binding ledger has entries for each pass, semantic ledger has entries, execution timeline has pass start/end events. Add to `tests/CMakeLists.txt`. - - **Verify:** `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` passes runtime diagnostics tests. - -### 3.8 Phase 3 Gate - -- [x] 3.8.1 Run full CI-parity gate: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. All pass with zero new warnings. - - **Verify:** All three commands succeed. - - -## Phase 4: Integration, Quality, and Regression Hardening - -### 4.1 Image Comparison Extensions - -- [x] 4.1.1 Add `structural_similarity` field (double, 0.0-1.0) to `CompareResult` in `tests/visual/image_compare.hpp`. Add boolean parameter `compute_ssim` to `compare_images()` (default `false`) for backward compatibility. - - **Verify:** `pixi run build -p debug` compiles; existing image compare tests pass unchanged. - -- [x] 4.1.2 Implement structural similarity (SSIM) computation in `tests/visual/image_compare.cpp`: when `compute_ssim` is true, compute SSIM over luminance channel and populate `CompareResult::structural_similarity`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 4.1.3 Add region-of-interest overload to `compare_images()` in `tests/visual/image_compare.hpp` and `tests/visual/image_compare.cpp`: accept a `Rect` (x, y, width, height) parameter, compute metrics only within the region. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 4.1.4 Add SSIM and ROI tests to `tests/visual/test_image_compare.cpp`: verify SSIM of identical images is 1.0; verify SSIM of completely different images is < 0.5; verify ROI comparison only counts pixels within the region. - - **Verify:** `ctest --preset test -R "^image_compare_unit_tests$" --output-on-failure` passes. - -### 4.2 Diff Heatmap Generation - -- [x] 4.2.1 Add `generate_diff_heatmap(const Image& actual, const Image& reference, const std::filesystem::path& output)` function declaration to `tests/visual/image_compare.hpp`. - - **Verify:** `pixi run build -p debug` compiles (declaration only). - -- [x] 4.2.2 Implement `generate_diff_heatmap()` in `tests/visual/image_compare.cpp`: compute per-pixel absolute difference, map to a perceptually uniform color gradient (blue=0 through red=max), write as PNG. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 4.2.3 Add heatmap test to `tests/visual/test_image_compare.cpp`: generate heatmap from two known images, verify output file exists and has expected dimensions. - - **Verify:** `ctest --preset test -R "^image_compare_unit_tests$" --output-on-failure` passes. - -### 4.3 Intermediate Golden Baselines (Visual Regression Infrastructure) - -- [x] 4.3.1 Create `tests/visual/test_intermediate_golden.cpp` with test infrastructure: helper function to run headless pipeline with diagnostic session capturing intermediate pass outputs at specified ordinals. Use `TestHarnessSink` to collect capture events. Register test with `tests/visual/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles. - -- [x] 4.3.2 Add intermediate golden comparison logic in `tests/visual/test_intermediate_golden.cpp`: compare captured intermediate outputs against golden baselines using `{preset_name}_pass{ordinal}.png` naming. Missing goldens emit Catch2 SKIP. Failures report pass ordinal. - - **Verify:** `pixi run build -p debug` compiles. - -### 4.4 Temporal Golden Baselines - -- [x] 4.4.1 Create `tests/visual/test_temporal_golden.cpp` with test infrastructure: multi-frame capture with golden comparison at specified frame indices. Use `{preset_name}_frame{index}.png` naming for final outputs and `{preset_name}_pass{ordinal}_frame{index}.png` for intermediates. Register with `tests/visual/CMakeLists.txt`. - - **Verify:** `pixi run build -p debug` compiles. - -### 4.5 Forensic Capture Compile-Time Gate - -- [x] 4.5.1 Add `GOGGLES_DIAGNOSTICS_FORENSIC` preprocessor define support: add CMake option `GOGGLES_DIAGNOSTICS_FORENSIC` (default OFF) in the diagnostics CMakeLists.txt. When enabled, set compile definition on `goggles_diagnostics` and dependent targets. Follow Tracy pattern from `src/util/profiling.hpp`. - - **Verify:** `pixi run build -p debug` compiles with and without the flag. - -- [x] 4.5.2 Create `src/util/diagnostics/forensic.hpp` with Tier 2 macros: `GOGGLES_DIAG_FORENSIC_SCOPE(name)`, `GOGGLES_DIAG_FORENSIC_CAPTURE(session, pass, cmd)`. When `GOGGLES_DIAGNOSTICS_FORENSIC` is not defined, all macros expand to `(void)0`. - - **Verify:** `pixi run build -p debug` compiles. - -### 4.6 Spec Updates and Documentation - -- [x] 4.6.1 Update `openspec/changes/filter-chain-diagnostics/specs/diagnostics/spec.md` with any requirement adjustments discovered during implementation (e.g., concrete method signatures, edge cases found). - - **Verify:** Spec remains valid and all scenarios still match implementation. - -- [x] 4.6.2 Update `openspec/changes/filter-chain-diagnostics/specs/render-pipeline/spec.md` to reflect actual parameter signatures added to `ChainBuilder::build()`, `RetroArchPreprocessor::preprocess()`, and `ShaderRuntime::compile_retroarch_shader()`. - - **Verify:** Spec requirements match implemented signatures. - -- [x] 4.6.3 Update `openspec/changes/filter-chain-diagnostics/specs/goggles-filter-chain/spec.md` to reflect the actual boundary API function signatures and status codes. - - **Verify:** Spec requirements match C API header. - -### 4.7 Final Gate - -- [x] 4.7.1 Run full CI-parity gate: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. All pass with zero new warnings. - - **Verify:** All three commands succeed. - -- [x] 4.7.2 Run `pixi run semgrep` and verify no new policy violations from diagnostic code. - - **Verify:** Semgrep passes cleanly. - -- [x] 4.7.3 Run `pixi run format` and verify no formatting changes needed in new files. - - **Verify:** `pixi run format` reports no changes. - ---- - -## Summary - -| Phase | Tasks | Focus | -|-------|-------|-------| -| Phase 1 | 25 | Foundation: event model, policy, sinks, session, ledgers, manifest, config | -| Phase 2 | 17 | Authoring analysis: provenance, compile reports, conformance gate, verdict | -| Phase 3 | 21 | Runtime: executor instrumentation, GPU timestamps, debug labels, boundary API | -| Phase 4 | 18 | Integration: image metrics, heatmaps, golden infrastructure, forensic gate, specs | -| **Total** | **81** | | - -## Implementation Order - -Phases are strictly sequential: Phase 2 depends on types and session from Phase 1; Phase 3 depends on authoring pipeline from Phase 2; Phase 4 depends on runtime infrastructure from Phase 3. Within each phase, tasks are ordered by dependency (infrastructure before consumers, types before tests). Each phase ends with a CI-parity gate to catch regressions early. - -## Key Risks - -- **GPU timestamp availability**: `GpuTimestampPool` must gracefully degrade when `timestampPeriod == 0`. Task 3.4.1 explicitly handles this. -- **Backward compatibility**: All new parameters default to `nullptr`; existing call sites are unmodified. Each phase is independently revertible. -- **Test isolation**: Diagnostic integration tests (3.7.x) require headless Vulkan which may not be available in CI. Mark GPU-dependent tests with appropriate labels and CI disable guards. -- **SSIM implementation complexity**: Task 4.1.2 implements a simplified luminance-only SSIM. Full multi-channel SSIM can be deferred. diff --git a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/verify-report.md b/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/verify-report.md deleted file mode 100644 index 57dce440..00000000 --- a/openspec/changes/archive/2026-03-11-filter-chain-diagnostics/verify-report.md +++ /dev/null @@ -1,229 +0,0 @@ -# Verification Report - -**Change**: filter-chain-diagnostics -**Date**: 2026-03-11 -**Artifact store mode**: engram + openspec (hybrid) - ---- - -## Completeness - -| Metric | Value | -|--------|-------| -| Tasks total | 81 | -| Tasks complete | 81 | -| Tasks incomplete | 0 | - -All 81 tasks across 4 phases are marked `[x]` in `tasks.md`. - -**WARNING**: `state.yaml` is stale — reports `phase: apply`, 34/66 tasks, phases 3-4 pending. Actual implementation has all 81 tasks complete. Must be updated before archive. - ---- - -## Build & Tests Execution - -**Build (ASAN)**: ✅ Passed -``` -pixi run build -p asan → ninja: no work to do. -``` - -**Build (Quality / clang-tidy as errors)**: ✅ Passed -``` -pixi run build -p quality → 37/37 targets built, 0 errors -``` - -**Semgrep**: ✅ Passed -``` -pixi run semgrep → 0 findings, 8 rules on 123 files -Self-test: 11/11 expected positives matched -``` - -**Format**: ✅ Passed -``` -pixi run -e lint ci-format → "Code is properly formatted" -``` - -**Unit Tests (ASAN preset)**: ✅ 208 cases — 206 passed, 2 skipped, 0 failed -``` -ctest --preset asan → 10/10 suites passed, 30632 assertions -``` -The 2 skipped tests are pre-existing (MBZ/zfast preset fixtures absent), unrelated to diagnostics. - -**Visual Tests (test preset)**: ✅ 14/15 passed -``` -GOGGLES_INCLUDE_VISUAL_TESTS=1 ctest --preset test → 14/15 passed - -Diagnostics-related (all passed): - test_intermediate_golden ✅ 0.58s - test_temporal_golden ✅ 0.56s - test_semantic_probes ✅ 1.66s - image_compare_unit_tests ✅ 0.00s - test_shader_basic ✅ 5.98s - -Pre-existing failure (NOT related to this change): - test_aspect_ratio ❌ right bar pixel assertion failure -``` - -**Coverage**: ➖ Not configured - ---- - -## Spec Compliance Matrix - -### diagnostics/spec.md — Core Infrastructure (36 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Diagnostic Event Model | Event carries required fields | `test_diagnostic_event_model > Severity ordering` + `DiagnosticEvent construction with each evidence variant` | ✅ COMPLIANT | -| Diagnostic Event Model | Event carries optional evidence payload | `test_diagnostic_event_model > DiagnosticEvent construction with each evidence variant` | ✅ COMPLIANT | -| Diagnostic Event Model | Chain-level localization | `test_diagnostic_event_model > LocalizationKey CHAIN_LEVEL sentinel` | ✅ COMPLIANT | -| Severity Model | Severity levels ordered | `test_diagnostic_event_model > Severity ordering` | ✅ COMPLIANT | -| Severity Model | Category independent of severity | `test_diagnostic_event_model > DiagnosticEvent construction with each evidence variant` | ✅ COMPLIANT | -| Sink-Agnostic Adapter | Single-method sink receives events | `test_diagnostic_sinks > TestHarnessSink collects events in emission order` | ✅ COMPLIANT | -| Sink-Agnostic Adapter | Sink failure isolation | `test_diagnostic_session > Session self-reports sink failures` | ✅ COMPLIANT | -| Sink-Agnostic Adapter | Multi-sink delivery | `test_diagnostic_session > Session with two sinks delivers to both` | ✅ COMPLIANT | -| Layered Activation | Tier 0 always active | `DiagnosticPolicy defaults are compatibility mode` + `ChainRuntime emits runtime diagnostics ledgers` | ✅ COMPLIANT | -| Layered Activation | Tier gating | `test_diagnostic_event_model > ActivationTier ordering` | ✅ COMPLIANT | -| Diagnostic Policy | Default compat mode | `DiagnosticPolicy defaults are compatibility mode` | ✅ COMPLIANT | -| Diagnostic Policy | Strict mode promotes severity | `Strict policy sets promotion flags` + `Session severity promotion in strict mode` | ✅ COMPLIANT | -| Session Lifecycle | Creation | `Session with no sinks silently discards events` | ✅ COMPLIANT | -| Session Lifecycle | Reset clears state | `Session reset clears all state` | ✅ COMPLIANT | -| Session Lifecycle | Frame tracking | `Session begin_frame/end_frame tracking` | ✅ COMPLIANT | -| Session Lifecycle | Sink unregistration | `Session unregister_sink removes sink` | ✅ COMPLIANT | -| Session Identity | Identity attached | `Session attaches identity to emitted events` | ✅ COMPLIANT | -| Reporting Modes | Minimal | `Minimal reporting keeps only compact verdict data` | ✅ COMPLIANT | -| Reporting Modes | Standard | `Standard reporting includes manifest coverage and trace` | ✅ COMPLIANT | -| Reporting Modes | Investigate | `Investigate reporting adds captures provenance and degradation details` | ✅ COMPLIANT | -| Reporting Modes | Forensic | `Forensic reporting includes full event timeline and artifact marker` | ✅ COMPLIANT | -| Degradation Ledger | Per-pass recording | `DegradationLedger records per-pass degradations in frame order` | ✅ COMPLIANT | -| Binding Ledger | Record and query | `BindingLedger records and queries entries` | ✅ COMPLIANT | -| Binding Ledger | Status classification | `BindingLedger status classification` | ✅ COMPLIANT | -| Binding Ledger | Extent recording | `BindingLedger records extents` | ✅ COMPLIANT | -| Binding Ledger | Clear | `BindingLedger clear` | ✅ COMPLIANT | -| Semantic Ledger | Scalar and vector semantics | `SemanticAssignmentLedger records scalar and vector semantics` | ✅ COMPLIANT | -| Chain Manifest | Deterministic order | `ChainManifest preserves deterministic insertion order` | ✅ COMPLIANT | -| Execution Timeline | Events populated | `ChainRuntime emits runtime diagnostics ledgers` (verifies non-empty timeline with typed events) | ✅ COMPLIANT | -| Authoring Verdict | Pass for clean | `Authoring verdict pass for clean session` | ✅ COMPLIANT | -| Authoring Verdict | Degraded for reflection loss | `Authoring verdict degraded for empty reflection in compat mode` | ✅ COMPLIANT | -| Authoring Verdict | Fail for compile error | `Authoring verdict fail for compile error` | ✅ COMPLIANT | -| Source Provenance | Map records entries | `SourceProvenanceMap records and retrieves entries` | ✅ COMPLIANT | -| Source Provenance | Rewrite tracking | `RetroArchPreprocessor provenance tracks compatibility rewrites` | ✅ COMPLIANT | -| Source Provenance | Include expansion | `RetroArchPreprocessor provenance tracking with includes` | ✅ COMPLIANT | -| Source Provenance | No-provenance still works | `RetroArchPreprocessor without provenance still works` | ✅ COMPLIANT | -| Compile Report | Per-stage success | `CompileReport tracks successful stages` | ✅ COMPLIANT | -| Compile Report | Per-stage failure | `CompileReport tracks failed stages` | ✅ COMPLIANT | -| Compile Report | Cache hits | `CompileReport tracks cache hits` | ✅ COMPLIANT | -| Event Counting | Severity and category | `Session event counting by severity and category` | ✅ COMPLIANT | - -### goggles-filter-chain/spec.md — Boundary API (4 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Diagnostic Session Lifecycle | Create/destroy via C API | `Filter chain C API diagnostics lifecycle and summary` | ✅ COMPLIANT | -| Sink Registration | Register/unregister | `Filter chain C API diagnostics lifecycle and summary` | ✅ COMPLIANT | -| Summary Query | Event counts | `Filter chain C API diagnostics lifecycle and summary` | ✅ COMPLIANT | -| Error Model | DIAGNOSTICS_NOT_ACTIVE | `Filter chain C API diagnostics lifecycle and summary` | ✅ COMPLIANT | - -### render-pipeline/spec.md — Shader Instrumentation (4 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Reflection Conformance | Strict rejects empty | `Strict mode reflection conformance gate rejects empty reflection` | ✅ COMPLIANT | -| Reflection Conformance | Compat degrades | `Authoring verdict degraded for empty reflection in compat mode` | ✅ COMPLIANT | -| Diagnostic Instrumentation | Compile events + provenance | `Authoring events track provenance` + `Chain manifest stored in session` | ✅ COMPLIANT | -| Optional Parameters | Backward compat nullptr | Build succeeds; all existing callers pass nullptr | ✅ COMPLIANT | - -### shader-testing/spec.md — Structured Reports (3 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Compile Reports | Source-mapped diagnostics | `Shader batch report preserves source-mapped compile diagnostics` | ✅ COMPLIANT | -| Authoring Corpus | Categories maintained | `Shader batch report validates maintained authoring corpus categories` | ✅ COMPLIANT | -| Batch Output | JSON written | `Shader batch report writes diagnostic JSON` | ✅ COMPLIANT | - -### visual-regression/spec.md — Golden Infrastructure (9 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Intermediate Goldens | Per-pass output comparison | `test_intermediate_golden` (CTest visual, PASSED) | ✅ COMPLIANT | -| Temporal Goldens | Multi-frame golden comparison | `test_temporal_golden` (CTest visual, PASSED) | ✅ COMPLIANT | -| Semantic Probes | Size/frame counter/parameter probes | `test_semantic_probes` (CTest visual, PASSED) | ✅ COMPLIANT | -| SSIM | Identical images → 1.0 | `structural similarity for identical images is 1.0` | ✅ COMPLIANT | -| SSIM | Different images → low | `structural similarity for opposite images is low` | ✅ COMPLIANT | -| ROI Comparison | Pixels within region only | `roi comparison only counts pixels inside the region` | ✅ COMPLIANT | -| Diff Heatmap | Correct dimensions | `diff heatmap written with expected dimensions` | ✅ COMPLIANT | -| Earliest-Divergence | First failing pass | `earliest divergence localization reports first failing pass` | ✅ COMPLIANT | -| Earliest-Divergence | Fallback w/o intermediates | `earliest divergence localization falls back when no intermediates exist` | ✅ COMPLIANT | - -### profiling/spec.md — GPU Timestamps and Debug Labels (5 scenarios) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| GPU Timestamps | Query pool integration | `ChainRuntime emits runtime diagnostics ledgers` (exercises full pipeline with GpuTimestampPool) | ⚠️ PARTIAL | -| GPU Timeline | Timeline contains timing events | `ChainRuntime emits runtime diagnostics ledgers` (verifies typed timeline events) | ✅ COMPLIANT | -| Debug Labels | VK_EXT_debug_utils insertion | Code at `chain_executor.cpp:114-129`; no dedicated test | ⚠️ PARTIAL | -| Async Readback | No-stall capture | `ChainRuntime captures pass outputs` (exercises readback) | ⚠️ PARTIAL | -| Graceful Degradation | Handle unavailable timestamps | `GpuTimestampPool::create()` checks `timestampPeriod`; no dedicated unavailable-path test | ⚠️ PARTIAL | - -**Compliance summary**: 57/61 scenarios COMPLIANT, 4 scenarios PARTIAL (profiling domain — integration-tested but no dedicated unit tests) - ---- - -## Correctness (Static — Structural Evidence) - -| Requirement | Status | Notes | -|------------|--------|-------| -| Event model, policy, sinks, session | ✅ Implemented | Full `src/util/diagnostics/` module | -| All ledgers + manifest | ✅ Implemented | Degradation, binding, semantic, timeline, manifest, provenance | -| Compile report | ✅ Implemented | Per-stage with cache/timing tracking | -| GpuTimestampPool | ✅ Implemented | `gpu_timestamp_pool.cpp` with create/reset/write/read/availability | -| Boundary C/C++ API | ✅ Implemented | Session lifecycle, sink registration, summary query | -| TOML `[diagnostics]` config | ✅ Implemented | Template + runtime parsing | -| Production wiring | ✅ Implemented | `FilterChainController` auto-enables from config | -| Forensic compile-time gate | ✅ Implemented | `forensic.hpp` macros | -| Debug labels | ✅ Implemented | Extension availability check at runtime | -| Visual regression extensions | ✅ Implemented | SSIM, ROI, heatmap, localization, intermediate/temporal goldens | -| Authoring corpus + batch report | ✅ Implemented | Test data + JSON output | -| Semantic probe presets | ✅ Implemented | size, frame counter, parameter isolation | - ---- - -## Coherence (Design) - -| Decision | Followed? | Notes | -|----------|-----------|-------| -| Diagnostic core in `src/util/diagnostics/` | ✅ Yes | | -| Single DiagnosticEvent with variant payload | ✅ Yes | | -| Sink as single-method interface | ✅ Yes | | -| Session owns ledgers, not sinks | ✅ Yes | | -| GPU timestamps via dedicated query pool | ✅ Yes | | -| Readback staging uses existing pattern | ✅ Yes | | -| Tier 2 compile-time gate (Tracy pattern) | ✅ Yes | | -| Boundary API uses `goggles_chain_` prefix | ✅ Yes | | -| TOML config under `[diagnostics]` | ✅ Yes | | -| File changes table fidelity | ✅ Yes | All files match; minor naming diffs (heatmap test merged into image_compare) | - ---- - -## Issues Found - -**CRITICAL** (must fix before archive): -None - -**WARNING** (should fix): -1. **Stale `state.yaml`**: Reports phase: apply, 34/66 tasks, phases 3-4 pending. Should be updated to reflect all-complete status before archive. -2. **No dedicated `GpuTimestampPool` unit tests**: Exercised via integration but lacks isolated tests for creation with unavailable timestamps, reset, write, read, and `is_available()` paths. -3. **No dedicated debug label test**: Code includes runtime extension checks but no test verifies label insertion or graceful no-op. -4. **Pre-existing `test_aspect_ratio` failure**: Unrelated to this change. - -**SUGGESTION** (nice to have): -1. Add isolated `GpuTimestampPool` unit tests (creation with/without timestamp support, read-back). -2. Add compile-time test for `GOGGLES_DIAGNOSTICS_FORENSIC` macro no-op expansion. - ---- - -## Verdict - -**PASS WITH WARNINGS** - -All 81 tasks complete. CI-parity gate fully green: ASAN build, quality build (clang-tidy as errors), semgrep (0 findings), and format all pass. 208 unit tests pass (206 + 2 pre-existing skips, 30632 assertions). All 5 diagnostics-related visual tests pass (`test_intermediate_golden`, `test_temporal_golden`, `test_semantic_probes`, `image_compare_unit_tests`, `test_shader_basic`). 57 of 61 spec scenarios have full runtime compliance evidence; 4 profiling scenarios are partially covered through integration tests. All 10 design decisions followed. Only pre-archive action needed: update stale `state.yaml`. diff --git a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md b/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md deleted file mode 100644 index d5d945a0..00000000 --- a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md +++ /dev/null @@ -1,153 +0,0 @@ -# Design: Profiling GPU Timestamp Unavailable Testability Seam - -## Technical Approach - -The change adds a diagnostics-scoped testability seam for the one profiling branch that still lacks deterministic evidence: `GPU timestamps unavailable`. The default runtime path in `src/render/chain/chain_runtime.cpp` continues to auto-detect timestamp support from the active `vk::PhysicalDevice` and create a normal `GpuTimestampPool` exactly as it does today. - -The new seam has two parts: -- a policy-level override that can request `force_unavailable` when a diagnostic session is created, and -- an explicit `GpuTimestampPool` unavailable-construction path that produces the same no-op pool state currently reached only when `timestampPeriod <= 0`. - -This keeps the change narrow: tests can deterministically force the unavailable branch without stubbing `vk::PhysicalDevice::getProperties()`, without introducing a broad Vulkan capability abstraction, and without changing user-visible profiling behavior. - -## Architecture Decisions - -### Decision: Put the override on `DiagnosticPolicy`, not on Vulkan-device wrappers - -**Choice**: Extend `goggles::diagnostics::DiagnosticPolicy` with a small defaulted enum that controls timestamp availability resolution for the session, with `auto_detect` as the production default and `force_unavailable` as the deterministic test mode. -**Alternatives considered**: (a) Stub or wrap `vk::PhysicalDevice::getProperties()`; (b) add a repo-wide Vulkan capability abstraction; (c) add a mutable test hook or global override in `ChainRuntime`. -**Rationale**: `DiagnosticPolicy` already defines profiling/session behavior at the boundary where tests create Tier 1 sessions. Adding one defaulted diagnostics field keeps the seam local to profiling setup, preserves all existing call sites, and avoids widening the render stack into a general mocking framework. - -### Decision: Add an explicit unavailable `GpuTimestampPool` factory - -**Choice**: Add a dedicated `GpuTimestampPool` construction path for the unavailable state, implemented in `src/util/diagnostics/gpu_timestamp_pool.*`, and reuse it both for forced-unavailable tests and for the existing `timestampPeriod <= 0` branch. -**Alternatives considered**: (a) Overload `create()` with raw timestamp-period injection; (b) expose mutable internal state or subclassing hooks; (c) leave the unavailable path hardware-gated. -**Rationale**: The class already models unavailable timestamps as a safe no-op pool (`is_available() == false`, reset/write calls return immediately, `read_results()` returns an empty vector). Making that state constructible directly is the smallest possible production seam and gives unit tests deterministic coverage without changing the normal available-path allocation logic. - -### Decision: Reuse the existing runtime event path instead of synthesizing test-only events - -**Choice**: Keep `ChainRuntime::sync_gpu_timestamp_pool()` as the single place that creates the pool and emits the existing runtime info event when the resulting pool is unavailable. -**Alternatives considered**: (a) Emit a synthetic unavailable event directly from tests; (b) add a second test-only runtime branch just for event emission. -**Rationale**: Verification needs evidence for the real Tier 1 runtime path, not a parallel test harness. Reusing the existing session setup path means the deterministic test still proves the same event emission and `timestamps_active()` gating used in production. - -## Data Flow - -### Default production path - -```text -DiagnosticPolicy(auto_detect) - | - v -ChainRuntime::create_diagnostic_session() - | - v -ChainRuntime::sync_gpu_timestamp_pool() - | - +--> GpuTimestampPool::create(device, physical_device, max_passes, frames) - | - +--> physical_device.getProperties().limits.timestampPeriod - +--> timestampPeriod > 0 -> allocate query pool, available = true - \--> timestampPeriod <= 0 -> create unavailable pool - | - \--> if !pool->is_available() emit runtime info event -``` - -### Deterministic unavailable test path - -```text -DiagnosticPolicy(force_unavailable) - | - v -ChainRuntime::create_diagnostic_session() - | - v -ChainRuntime::sync_gpu_timestamp_pool() - | - +--> GpuTimestampPool::create_unavailable() - | - \--> emit "GPU timestamps are unavailable on this device" - | - +--> ChainExecutor timestamps remain inactive - \--> frame recording still succeeds -``` - -### Unit-level unavailable pool coverage - -```text -test_gpu_timestamp_pool.cpp - | - +--> available-path test uses real device + normal create() - \--> unavailable-path test uses create_unavailable() - -> is_available() == false - -> reset/write calls are safe no-ops - -> read_results() returns empty samples -``` - -## File Changes - -| File | Action | Description | -|------|--------|-------------| -| `src/util/diagnostics/diagnostic_policy.hpp` | Modify | Add the defaulted timestamp-availability override enum used only by diagnostics session setup. | -| `src/util/diagnostics/gpu_timestamp_pool.hpp` | Modify | Declare the explicit unavailable factory/helper and keep the existing create API for normal detection. | -| `src/util/diagnostics/gpu_timestamp_pool.cpp` | Modify | Implement the unavailable factory and make the hardware-unavailable branch delegate to the same path. | -| `src/render/chain/chain_runtime.cpp` | Modify | Resolve the policy override in `sync_gpu_timestamp_pool()`, choose normal create vs forced unavailable, and preserve existing runtime event emission. | -| `tests/render/test_gpu_timestamp_pool.cpp` | Modify | Replace the environment-gated unavailable assertion with deterministic construction of the unavailable pool while retaining real-device coverage for the available path. | -| `tests/render/test_runtime_diagnostics.cpp` | Modify | Add deterministic Tier 1 unavailable-event coverage via the policy override and keep the current available GPU-duration/debug-label runtime evidence. | - -## Interfaces / Contracts - -```cpp -namespace goggles::diagnostics { - -enum class GpuTimestampAvailabilityMode : uint8_t { - auto_detect, - force_unavailable, -}; - -struct DiagnosticPolicy { - PolicyMode mode = PolicyMode::compatibility; - CaptureMode capture_mode = CaptureMode::standard; - ActivationTier tier = ActivationTier::tier0; - uint32_t capture_frame_limit = 1; - uint64_t retention_bytes = 256ULL * 1024 * 1024; - bool promote_fallback_to_error = false; - bool reflection_loss_is_fatal = false; - GpuTimestampAvailabilityMode gpu_timestamp_availability = - GpuTimestampAvailabilityMode::auto_detect; -}; - -class GpuTimestampPool { -public: - [[nodiscard]] static auto create(vk::Device device, vk::PhysicalDevice physical_device, - uint32_t max_passes, uint32_t frames_in_flight) - -> Result>; - - [[nodiscard]] static auto create_unavailable() -> std::unique_ptr; -}; - -} // namespace goggles::diagnostics -``` - -`ChainRuntime::sync_gpu_timestamp_pool()` resolves the pool as follows: -- `auto_detect`: call existing `GpuTimestampPool::create(...)` -- `force_unavailable`: bypass physical-device property detection and install `create_unavailable()` - -No other runtime code needs a new interface. `ChainExecutor::timestamps_active()` continues to rely on `gpu_timestamp_pool->is_available()`, so forced-unavailable sessions automatically follow the existing no-op behavior. - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|-------------|----------| -| Unit | Explicit unavailable pool semantics | In `tests/render/test_gpu_timestamp_pool.cpp`, construct `create_unavailable()` and assert `is_available() == false`, reset/write methods do not fail, and `read_results()` is empty without any hardware skip. | -| Unit | Normal available timestamp behavior | Keep the current real-device `create()` coverage for available query-pool allocation, timestamp writes, and sample ordering; still `SKIP()` only when no timestamp-capable GPU is present. | -| Integration | Tier 1 unavailable diagnostic event path | In `tests/render/test_runtime_diagnostics.cpp`, create a Tier 1 session with `gpu_timestamp_availability = force_unavailable`, register `TestHarnessSink`, record a frame, and assert the runtime info event is emitted while frame recording still succeeds. | -| Integration | Tier 1 available GPU durations and debug labels | Preserve the current runtime tests that use real hardware for `gpu_duration_us` evidence and debug-label capture/disabled-dispatch behavior. | -| Regression | Production default remains unchanged | Ensure existing callers that do not set the override still auto-detect timestamps from `vk::PhysicalDevice` and continue passing the current profiling tests. | - -## Migration / Rollout - -No migration required. The new policy field defaults to `auto_detect`, so existing production callers and API bridges retain current behavior unless a test explicitly opts into `force_unavailable`. - -## Open Questions - -- [ ] None blocking. The remaining follow-up is task refresh so implementation work can be split around the policy enum, unavailable factory, and deterministic test updates. diff --git a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md b/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md deleted file mode 100644 index a8f92b3c..00000000 --- a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md +++ /dev/null @@ -1,84 +0,0 @@ -# Proposal: Profiling GPU Timestamp and Debug Label Test Coverage - -## Intent - -The archived `filter-chain-diagnostics` change already shipped `GpuTimestampPool`, Vulkan debug labels, and GPU timing integration, but verification still fails because the profiling scenario `GPU timestamps unavailable` cannot be proven on timestamp-capable hardware with the current product/test contract. Existing tests now cover the available-timestamp path and debug-label behavior, yet the unavailable-path still depends on the active GPU environment. - -This change now expands from pure test coverage into a small, deliberate testability improvement: add a deterministic seam that lets tests force the unavailable-timestamp path without trying to stub Vulkan-Hpp physical-device property queries. The goal remains spec evidence, not new profiling behavior. - -## Scope - -### In Scope - -- Add a minimal production seam for timestamp capability injection or explicit unavailable-pool construction so tests can deterministically exercise the unavailable-timestamp branch on timestamp-capable hardware -- Keep the seam narrowly scoped to profiling capability determination and pool creation, without introducing a broader Vulkan abstraction layer -- Extend `GpuTimestampPool` tests to prove both available and forced-unavailable behavior without environment-dependent skips -- Extend Tier 1 runtime diagnostics coverage so the unavailable-timestamps diagnostic event is asserted deterministically -- Preserve the existing debug-label and GPU-duration runtime evidence already added for this change - -### Out of Scope - -- New profiling product features or behavior changes visible to end users -- Broad refactors of Vulkan capability discovery or general mocking of `vk::PhysicalDevice` queries -- Reworking the diagnostic event model, sink infrastructure, or execution timeline schema -- Tracy instrumentation tests or Tier 2 forensic capture coverage - -## Approach - -Introduce the smallest production-facing seam that can represent `timestamps unavailable` as a first-class testable condition. Preferred direction: make timestamp availability/pool construction injectable at the profiling boundary, or provide a constrained factory/helper that can deliberately construct an unavailable `GpuTimestampPool` for tests and runtime callers that need deterministic behavior. Avoid trying to intercept Vulkan-Hpp static property queries, which would widen scope and couple tests to brittle dispatch details. - -Then update the profiling tests to use that seam in two places: isolated `GpuTimestampPool` coverage for forced-unavailable behavior, and Tier 1 runtime diagnostics coverage that proves the diagnostic info event emitted when Tier 1 requests GPU timing but timestamps are unavailable. Keep the rest of the runtime path unchanged. - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `src/util/diagnostics/gpu_timestamp_pool.hpp` | Modified | Define the minimal testability seam for timestamp capability or unavailable-pool creation | -| `src/util/diagnostics/gpu_timestamp_pool.cpp` | Modified | Implement the constrained unavailable-path construction without changing normal runtime behavior | -| `src/render/chain/chain_executor.cpp` | Modified | Thread the seam through the runtime profiling path only if needed for deterministic Tier 1 unavailable coverage | -| `tests/render/test_gpu_timestamp_pool.cpp` | Modified | Replace environment-limited unavailable-path coverage with deterministic assertions | -| `tests/render/test_runtime_diagnostics.cpp` | Modified | Add deterministic Tier 1 unavailable-timestamp event coverage while preserving existing available-path/debug-label evidence | -| `tests/CMakeLists.txt` | Modified | Keep profiling test sources registered if test layout changes | - -## Impacted OpenSpec Specs - -| Spec | Impact | -|------|--------| -| `profiling` | No spec changes — the seam exists only to prove already-specified behavior deterministically | - -## Policy-Sensitive Impacts - -- **Vulkan API**: Keep using `vk::` APIs only; do not introduce raw `Vk*`, `vk::Unique*`, or `vk::raii::*` wrappers as part of the seam -- **Error handling**: Unavailable-timestamp construction must preserve existing `Result`/expected-style behavior and no-op safety guarantees -- **Architecture**: Limit the seam to profiling capability selection; avoid creating a repo-wide device-capabilities abstraction just for tests -- **Tests**: Prefer deterministic assertions over GPU-environment branching for the unavailable path; retain `SKIP()` only for truly hardware-dependent available-path evidence - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| The seam grows into a wider production abstraction than needed | Medium | Keep the contract narrowly focused on timestamp availability or unavailable-pool construction only | -| The new seam invalidates the earlier no-design-needed assumption | High | Treat this proposal update as a trigger for a focused design pass before more implementation | -| Runtime wiring changes could accidentally alter normal timestamp-capable behavior | Medium | Preserve the default production path and add tests that prove both default-available and forced-unavailable behavior | -| Existing tasks no longer accurately describe the next implementation slice | High | Refresh tasks after design confirms the exact seam shape | - -## Rollback Plan - -Remove the production seam and any related deterministic unavailable-path tests, then fall back to environment-gated coverage only. This reverts the change to its prior test-only scope. - -## Dependencies - -- Existing `GpuTimestampPool` implementation in `src/util/diagnostics/gpu_timestamp_pool.*` -- Existing Tier 1 profiling runtime tests in `tests/render/test_runtime_diagnostics.cpp` -- Existing profiling spec in `openspec/specs/profiling/spec.md` -- Prior verification report identifying the environment-limited unavailable-timestamp gap - -## Success Criteria - -- [ ] The proposal reflects the expanded scope from test-only coverage to a minimal production testability seam -- [ ] The chosen seam can deterministically represent `GPU timestamps unavailable` on timestamp-capable hardware -- [ ] `GpuTimestampPool` unavailable-path tests no longer rely on environment-limited skips for their core assertion -- [ ] Tier 1 runtime diagnostics can deterministically assert the unavailable-timestamps diagnostic event -- [ ] Normal timestamp-capable runtime behavior and existing debug-label/GPU-duration evidence remain intact -- [ ] No profiling spec delta is required because user-visible behavior is unchanged -- [ ] Follow-on design/tasks work can describe the implementation slice precisely enough to unblock apply and future verify diff --git a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml b/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml deleted file mode 100644 index 813a93f7..00000000 --- a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml +++ /dev/null @@ -1,13 +0,0 @@ -phase: archive -artifact_store: hybrid -artifacts: - proposal: true - specs: false - design: true - tasks: true - verify: true - archive: true -notes: - source_of_truth: openspec/specs/profiling/spec.md - delta_spec: none -last_updated: 2026-03-11 diff --git a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md b/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md deleted file mode 100644 index d6f6393f..00000000 --- a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md +++ /dev/null @@ -1,52 +0,0 @@ -# Tasks: Profiling GPU Timestamp Unavailable Testability Seam - -## Phase 1: Diagnostics Policy and Unavailable Pool Foundation - -- [x] 1.1 Update `src/util/diagnostics/diagnostic_policy.hpp` to add the defaulted `GpuTimestampAvailabilityMode` enum and a `gpu_timestamp_availability` field on `DiagnosticPolicy`, preserving the current production default of `auto_detect`. - - **Verify:** `pixi run build -p test` - -- [x] 1.2 Update `src/util/diagnostics/gpu_timestamp_pool.hpp` to declare an explicit unavailable construction path for `GpuTimestampPool` and document it as the shared path for forced-unavailable tests and hardware-unavailable runtime fallback. - - **Verify:** `pixi run build -p test` - -- [x] 1.3 Update `src/util/diagnostics/gpu_timestamp_pool.cpp` so the existing `timestampPeriod <= 0` branch delegates to the explicit unavailable construction path while preserving current available-path allocation and no-op unavailable semantics. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -## Phase 2: Runtime Wiring and Deterministic Profiling Coverage - -- [x] 2.1 Update `src/render/chain/chain_runtime.cpp` so `ChainRuntime::sync_gpu_timestamp_pool()` resolves `DiagnosticPolicy::gpu_timestamp_availability`, uses normal auto-detection by default, and reuses the existing unavailable-event path when `force_unavailable` is requested. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -- [x] 2.2 Update `tests/render/test_gpu_timestamp_pool.cpp` so unavailable-path coverage constructs the explicit unavailable pool directly and asserts deterministic no-op behavior (`is_available() == false`, safe reset/write calls, empty readback) without hardware-based `SKIP()` on the core unavailable assertion. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -- [x] 2.3 Update `tests/render/test_runtime_diagnostics.cpp` so Tier 1 runtime diagnostics create a session with `gpu_timestamp_availability = force_unavailable`, record a frame through the real runtime path, and assert the info-severity `"GPU timestamps are unavailable on this device"` event while frame recording still succeeds. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -- [x] 2.4 Keep the existing available-path profiling assertions in `tests/render/test_runtime_diagnostics.cpp` intact by proving the default `auto_detect` path still carries GPU-duration/debug-label evidence unchanged when timestamps are available. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -## Phase 3: Registration and Goggles Regression Gates - -- [x] 3.1 Update `tests/CMakeLists.txt` only if required to keep `tests/render/test_gpu_timestamp_pool.cpp` and `tests/render/test_runtime_diagnostics.cpp` registered correctly after the diagnostics-policy seam changes. - - **Verify:** `pixi run build -p test` - -- [x] 3.2 Run targeted profiling coverage with `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` to confirm the deterministic unavailable-path evidence replaces the prior environment-limited skip and that the profiling test binary still passes on the active Vulkan setup. - - **Verify:** `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - -- [x] 3.3 Run the Goggles CI-parity gate `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` to prove the seam stays narrow, preserves normal runtime behavior, and remains policy-clean. - - **Verify:** `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` - ---- - -## Summary - -| Phase | Tasks | Focus | -|-------|-------|-------| -| Phase 1 | 3 | Diagnostics policy override and explicit unavailable pool construction | -| Phase 2 | 4 | Runtime wiring plus deterministic unavailable-path profiling evidence | -| Phase 3 | 3 | Test registration and Goggles regression gates | -| **Total** | **10** | | - -## Implementation Order - -Start with the diagnostics-scoped seam in `src/util/diagnostics/diagnostic_policy.hpp` and `src/util/diagnostics/gpu_timestamp_pool.*` so the unavailable state becomes a first-class construct before touching runtime wiring. Then update `src/render/chain/chain_runtime.cpp` and the profiling tests to exercise the real unavailable-event path deterministically, and finish with registration/build verification plus the standard ASAN and quality gates. diff --git a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md b/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md deleted file mode 100644 index 3c7b6523..00000000 --- a/openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md +++ /dev/null @@ -1,90 +0,0 @@ -## Verification Report - -**Change**: profiling-gpu-timestamp-test-coverage -**Version**: living `openspec/specs/profiling/spec.md` (no delta spec) -**Mode**: hybrid - ---- - -### Completeness -| Metric | Value | -|--------|-------| -| Tasks total | 10 | -| Tasks complete | 10 | -| Tasks incomplete | 0 | - -Tasks were validated from Engram artifact `sdd/profiling-gpu-timestamp-test-coverage/tasks` (#116). - ---- - -### Build & Tests Execution - -**Build**: ✅ Passed -- `pixi run build -p debug` -- `pixi run build -p asan` -- `pixi run build -p quality` - -**Tests**: ✅ Passed -- `ASAN_OPTIONS=detect_leaks=0 ./goggles_tests --list-tests "[profiling]"` -> 7 profiling-tagged cases enumerated -- `ASAN_OPTIONS=detect_leaks=0 ./goggles_tests "[profiling]"` -> 7 profiling cases passed, 110 assertions, 0 failures -- `pixi run -- ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` -> `214` passed, `0` failed, `2` skipped, CTest `1/1` passed -- `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` -> ASAN CTest `10/10` passed, quality build passed - -**Coverage**: ➖ Not configured (`openspec/config.yaml` has no `rules.verify.coverage_threshold`) - ---- - -### Spec Compliance Matrix - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Per-Pass GPU Timestamp Queries | GPU timestamps recorded per effect pass | `tests/render/test_gpu_timestamp_pool.cpp` > `GpuTimestampPool records available timestamp regions`; `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime Tier 1 diagnostics expose GPU timing evidence` | ✅ COMPLIANT | -| Per-Pass GPU Timestamp Queries | GPU timestamps for pre-processing and final composition | `tests/render/test_gpu_timestamp_pool.cpp` > `GpuTimestampPool records available timestamp regions`; `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime Tier 1 diagnostics expose GPU timing evidence` | ✅ COMPLIANT | -| Per-Pass GPU Timestamp Queries | GPU timestamp readback is asynchronous | `tests/render/test_gpu_timestamp_pool.cpp` > `GpuTimestampPool readback stays non-blocking while the next frame records` | ✅ COMPLIANT | -| Per-Pass GPU Timestamp Queries | GPU timestamps unavailable | `tests/render/test_gpu_timestamp_pool.cpp` > `GpuTimestampPool degrades cleanly when timestamps are unavailable`; `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime reports unavailable GPU timestamps deterministically` | ✅ COMPLIANT | -| GPU Timing Integration with Execution Timeline | Execution timeline includes GPU timing | `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime Tier 1 diagnostics expose GPU timing evidence` | ✅ COMPLIANT | -| GPU Timing Integration with Execution Timeline | GPU timing identifies bottleneck pass | `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime Tier 1 diagnostics expose GPU timing evidence` | ✅ COMPLIANT | -| Profiling Debug Labels | Debug labels inserted per pass | `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime emits profiling debug labels when dispatch is available` | ✅ COMPLIANT | -| Profiling Debug Labels | Debug labels for temporal operations | `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime emits profiling debug labels when dispatch is available` | ✅ COMPLIANT | -| Profiling Debug Labels | Debug labels disabled without extension | `tests/render/test_runtime_diagnostics.cpp` > `ChainRuntime emits profiling debug labels when dispatch is available`; `tests/render/test_runtime_diagnostics.cpp` > `ScopedDebugLabel skips incomplete debug-utils dispatch` | ✅ COMPLIANT | - -**Compliance summary**: 9/9 scenarios compliant - ---- - -### Correctness (Static - Structural Evidence) -| Requirement | Status | Notes | -|------------|--------|-------| -| Per-Pass GPU Timestamp Queries | ✅ Implemented | `src/util/diagnostics/gpu_timestamp_pool.cpp` exposes `create_unavailable()` and preserves the normal property-query path, `src/render/chain/chain_runtime.cpp` routes Tier 1 setup through `DiagnosticPolicy::gpu_timestamp_availability`, and `src/render/chain/chain_executor.cpp` continues to flush/read timestamps from the real record path. | -| GPU Timing Integration with Execution Timeline | ✅ Implemented | `src/render/chain/chain_executor.cpp` annotates timeline end events from timestamp samples, and the runtime profiling test proves pass, prechain, and final-composition GPU durations are attached and rankable. | -| Profiling Debug Labels | ✅ Implemented | `src/render/chain/debug_label_scope.hpp` centralizes begin/end dispatch availability checks, `src/render/chain/chain_executor.cpp` uses that helper around pass/history/feedback labels, and the runtime profiling test captures the expected labels while the helper test proves incomplete dispatch yields zero begin/end calls. | - ---- - -### Coherence (Design / Proposal) -| Decision | Followed? | Notes | -|----------|-----------|-------| -| Put the timestamp availability override on `DiagnosticPolicy` | ✅ Yes | `src/util/diagnostics/diagnostic_policy.hpp` adds the defaulted `GpuTimestampAvailabilityMode` seam exactly where design placed it. | -| Add an explicit unavailable `GpuTimestampPool` factory | ✅ Yes | `src/util/diagnostics/gpu_timestamp_pool.hpp` and `src/util/diagnostics/gpu_timestamp_pool.cpp` provide `create_unavailable()` and reuse it for the hardware-unavailable branch. | -| Reuse the existing runtime event path in `ChainRuntime::sync_gpu_timestamp_pool()` | ✅ Yes | `src/render/chain/chain_runtime.cpp` selects auto-detect vs forced-unavailable and emits the existing info event without a parallel runtime-only test hook. | -| File changes stay within the narrow proposal intent | ⚠️ Slightly extended | The implementation also factors debug-label dispatch handling into `src/render/chain/debug_label_scope.hpp` and updates `src/render/chain/chain_executor.cpp` so the disabled-extension behavior can be proven directly. This is consistent with the proposal goal of preserving profiling debug-label evidence, but it goes beyond the original file table. | - ---- - -### Issues Found - -**CRITICAL** (must fix before archive): -- None. - -**WARNING** (should fix): -- The implementation introduces a small additional production helper (`src/render/chain/debug_label_scope.hpp`) beyond the proposal/design file tables; the behavior is validated and remains narrow, but the artifact documentation was not updated to mention that helper. - -**SUGGESTION** (nice to have): -- If archive review wants the artifact set to mirror implementation exactly, refresh the proposal/design file-change tables to mention `src/render/chain/debug_label_scope.hpp` and the corresponding `src/render/chain/chain_executor.cpp` extraction. - ---- - -### Verdict -PASS WITH WARNINGS - -All 10 tasks are complete, all 9 profiling spec scenarios now have passed runtime evidence, and the decisive Goggles build/test lanes passed; the only remaining concern is a small documented-vs-implemented file-scope drift around the debug-label helper extraction. diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/design.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/design.md deleted file mode 100644 index cfef7c42..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/design.md +++ /dev/null @@ -1,289 +0,0 @@ -# Design: Extract Filter Chain Standalone Project - -> **Scope of current work:** This design describes the full extraction target. The current change -> implements Phase 1–2 monorepo groundwork (see `tasks.md`). Phases 3–4 of this design are future -> work that will be carried out in the standalone repository once created. - -## Technical Approach - -Goggles already treats the filter chain as a boundary-owned dependency with a preserved post-retarget -contract. This change does not rework that boundary. It extracts the existing library-owned slice -into a standalone CMake project that owns its own repository layout, support code, assets, package -metadata, and verification. - -The extracted project keeps the same functional split: - -- Goggles remains responsible for swapchain lifecycle, import, synchronization, submission, and - presentation. -- The standalone library owns chain, shader, texture, diagnostics, asset resolution, and public C/C++ - boundary behavior. -- Goggles consumes the extracted library through the installed package surface. - -Target standalone repository layout: - -```text -filter-chain/ -|- CMakeLists.txt -|- cmake/ -|- include/ -|- src/ -|- tests/ -`- assets/ -``` - -Within that layout: - -- `include/` contains the installed C header, C++ wrapper header, and installed public support - headers. -- `src/` contains chain, shader, texture, diagnostics, and private support implementation. -- `tests/` contains installed-surface contract tests and downstream consumer validation. -- `assets/` contains the packaged shaders, presets, fixtures, and related data required by runtime - behavior and contract verification. -- `cmake/` contains dependency discovery helpers and package config/export templates. - -## Architecture Decisions - -### Decision: Start from the current consumer boundary and extract only the standalone project concerns - -**Choice**: Treat the public boundary, post-retarget behavior, and Goggles backend/controller -consumption model as fixed baseline. - -**Alternatives considered**: Re-open the Goggles consumer boundary during extraction. - -**Rationale**: Re-scoping those concerns here would blur the extraction goal and duplicate boundary -cleanup that is already in place. - -### Decision: Extract the current reusable runtime slice into a standalone project root - -**Choice**: Move the reusable implementation currently centered in `src/render/chain/`, -`src/render/shader/`, and `src/render/texture/` into a standalone project rooted at `include/`, -`src/`, `tests/`, `assets/`, and `cmake/`. - -**Alternatives considered**: Keep publishing from Goggles-owned build fragments; keep a nested -subtree such as `standalone/` as the long-term repository layout. - -**Rationale**: The change goal is an independent project, not a better in-repo slice. The extracted -repository root must already reflect the final owned layout. - -### Decision: Move required support and diagnostics code under library ownership - -**Choice**: Any support contract required by installed public headers or extracted implementation is -owned by the standalone project under `include/` or `src/`. - -**Alternatives considered**: Keep depending on Goggles `src/util/*`; split support into a separate -shared utility package. - -**Rationale**: Standalone extraction is incomplete until the library can build and verify without -Goggles-private support ownership. - -### Decision: Package and verify the library through installed public surfaces - -**Choice**: The standalone project installs headers, libraries, assets, and CMake package metadata, -then validates the installed package with contract tests and downstream consumer builds. - -**Alternatives considered**: Accept source-tree tests as sufficient. - -**Rationale**: Extraction success depends on the installed package contract, not on in-repo source -access. - -### Decision: Ship static and shared variants only - -**Choice**: The standalone package publishes supported `STATIC` and `SHARED` variants and does not -define a supported `MODULE` surface. - -**Alternatives considered**: Single-variant export; `MODULE` plugin packaging. - -**Rationale**: The supported linkage surface is explicit and downstream verification must cover both -forms. - -### Decision: Goggles integrates as an external package first - -**Choice**: Goggles resolves the library through `find_package(...)` as the normal dependency path. -`add_subdirectory(...)` remains optional only for explicit side-by-side development. - -**Alternatives considered**: Keep source-based Goggles integration as the default acceptance path. - -**Rationale**: The extracted library is only complete when Goggles consumes it like any other -downstream package consumer. - -### Decision: Monorepo shared-variant presets are transitional only - -**Choice**: The root `.shared` hidden preset and `test-shared` presets in Goggles `CMakePresets.json` -exist only as transitional host-integration coverage. They verify that Goggles builds and tests when -the in-repo `goggles-filter-chain` target is built as `SHARED`. They are not package/distribution -validation and must not be treated as such. - -**Alternatives considered**: Remove them immediately during Phase 1 cleanup; keep them permanently as -a Goggles-owned shared-linkage check. - -**Rationale**: Removing them now would leave no way to catch shared-build regressions during the -monorepo groundwork phases. Keeping them permanently would duplicate the standalone project's own -static/shared package validation and blur the ownership boundary. The correct lifecycle is: -1. Retain during Phases 1–4 as a lightweight host-integration guard. -2. Remove in Phase 5 once the standalone project owns real static/shared package validation. -3. If Goggles still needs a shared-linkage host check after extraction, it should consume the - installed shared package through `find_package(...)`, not rebuild the library in-tree. - -Do not add additional root shared-variant presets beyond the current `test-shared`. - -## Data Flow - -### Standalone build and install flow - -```text -project root - |- include/ - |- src/ - |- tests/ - |- assets/ - `- cmake/ - -cmake configure - -> build static library - -> build shared library - -> install headers + libraries + assets + package metadata - -> run installed-surface contract tests - -> run downstream consumer package validation -``` - -### Runtime ownership after extraction - -```text -Goggles backend - -> FilterChainController - -> installed C++ wrapper - -> installed C ABI - -> chain + shader + texture + diagnostics runtime -``` - -### Asset resolution after extraction - -```text -installed consumer or installed contract test - -> library asset lookup contract - -> packaged asset root - -> presets / shaders / fixtures opened from library-owned assets -``` - -## File Changes - -| File | Action | Description | -|------|--------|-------------| -| `CMakeLists.txt` | Create | Define the standalone top-level project, targets, install rules, export rules, and test enablement. | -| `cmake/FilterChainDependencies.cmake` | Create | Resolve third-party dependencies without Goggles-specific wrapper assumptions. | -| `cmake/GogglesFilterChainConfig.cmake.in` | Create | Provide package config metadata for installed consumers. | -| `include/goggles_filter_chain.h` | Create | Install the public C ABI header at the standalone project root. | -| `include/goggles_filter_chain.hpp` | Create | Install the public C++ wrapper header at the standalone project root. | -| `include/goggles/filter_chain/*.hpp` | Create | Install public support and diagnostics-facing headers owned by the standalone project. | -| `src/chain/*` | Create | Host chain runtime, parser, pass execution, frame history, and ABI adapter implementation. | -| `src/shader/*` | Create | Host shader runtime, preprocessing, reflection, and related implementation. | -| `src/texture/*` | Create | Host texture-loading implementation. | -| `src/diagnostics/*` | Create | Host library-owned diagnostics runtime support required by the contract. | -| `src/support/*` | Create | Host private support modules required by extracted implementation. | -| `assets/*` | Create | Host packaged shaders, presets, fixtures, and related runtime or verification data. | -| `tests/contract/*` | Create | Verify the installed public surface and preserved retarget contract. | -| `tests/consumer/*` | Create | Verify downstream `find_package(...)` consumption for static and shared variants. | -| Goggles CMake integration | Modify | Make installed package consumption the primary Goggles dependency path. | -| `src/render/CMakeLists.txt` | Modify | Stop owning the library package surface inside Goggles and link the external package target. | -| `tests/CMakeLists.txt` | Modify | Keep Goggles host verification only after reusable contract acceptance moves to the standalone project. | - -## Interfaces / Contracts - -### Public surface retained from the current baseline - -The extracted project keeps the same public contract already established in the current tree: - -- `goggles_filter_chain.h` -- `goggles_filter_chain.hpp` -- `goggles/filter_chain/error.hpp` -- `goggles/filter_chain/result.hpp` -- `goggles/filter_chain/filter_controls.hpp` -- `goggles/filter_chain/scale_mode.hpp` -- `goggles/filter_chain/vulkan_context.hpp` - -Diagnostics public headers (`goggles/filter_chain/diagnostics/*.hpp`) are planned for Phase 4 and do -not exist in the current monorepo tree. Diagnostics types are currently defined only in the C API -header and C++ wrapper. - -The change is about ownership and packaging of that contract, not contract redesign. - -### Support ownership rules - -1. Installed public headers MUST not depend on Goggles-private headers. -2. Extracted implementation MUST not require Goggles-private support ownership. -3. Support types needed by the public contract belong under installed library ownership. -4. Support types needed only by implementation belong under uninstalled private library ownership. - -During the monorepo groundwork phase, public headers under `goggles/filter_chain/` define support -types inline using shared `#ifndef` guards that match the monorepo `util/` counterparts. This -prevents ODR violations while both copies coexist. The shared-guard approach is transitional and -will be eliminated when the standalone project owns the sole copy of each type. - -### Export model - -Installed consumer contract: - -```cmake -find_package(GogglesFilterChain CONFIG REQUIRED) - -target_link_libraries(consumer PRIVATE GogglesFilterChain::goggles-filter-chain) -``` - -Export rules: - -- `GogglesFilterChain::goggles-filter-chain` is always available. -- `GogglesFilterChain::goggles-filter-chain-static` and - `GogglesFilterChain::goggles-filter-chain-shared` are exported for explicit validation. -- Package metadata uses standard dependency discovery and imported targets. -- Package metadata does not require Goggles source paths, Pixi wrappers, or Conda-specific - environment variables. - -### Goggles integration contract after extraction - -- Installed package consumption is the normal and release-relevant path. -- Local source wiring may remain available for explicit side-by-side development. -- Goggles backend code stays on the installed public boundary and does not regain direct access to - chain internals. - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|--------------|----------| -| Unit | Parser, preprocessor, reflection, control helpers, support utilities | Run library-owned tests from the standalone project. | -| Contract | C ABI, C++ wrapper, post-retarget behavior, diagnostics summaries | Compile and run against installed headers, installed libraries, and installed assets. | -| Consumer | Package usability | Build out-of-tree `find_package(...)` consumers for static and shared variants. | -| Asset | Packaged shader, preset, and fixture lookup | Verify runtime behavior without cwd or Goggles-repository assumptions. | -| Goggles integration | Host-owned behavior with external package consumption | Keep Goggles tests focused on controller/backend ownership and package integration. | - -## Migration / Rollout - -> **Relationship to `tasks.md` phases:** The phases below describe the standalone extraction -> lifecycle. `tasks.md` Phase 1–2 (monorepo groundwork) prepares the boundary for these phases. -> `tasks.md` Phase 3+ maps to the phases below. - -### Phase 1: Create the standalone project root - -- Create the project-root `CMakeLists.txt`, `cmake/`, `include/`, `src/`, `tests/`, and `assets/` - layout. -- Move the reusable implementation and public headers into that owned structure. - -### Phase 2: Move support and assets under library ownership - -- Replace remaining Goggles-private support dependencies with library-owned support modules. -- Move diagnostics support, fixtures, presets, and shader assets under library ownership. - -### Phase 3: Build, install, and export the package - -- Publish static and shared libraries. -- Install headers, package metadata, and assets together. -- Export package targets for downstream discovery. - -### Phase 4: Verify installed surfaces and switch Goggles to package-first consumption - -- Run installed-surface contract tests and downstream consumer validation. -- Update Goggles to consume the installed package through `find_package(...)` as the normal path. -- Keep Goggles verification focused on host-owned behavior and preserved post-retarget semantics. - -## Open Questions - -- [ ] None. diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/proposal.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/proposal.md deleted file mode 100644 index 9fe3166d..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/proposal.md +++ /dev/null @@ -1,126 +0,0 @@ -## Why - -Goggles already treats the filter chain as a boundary-owned dependency with explicit post-retarget -behavior. The remaining work is to extract the library into its own project with its own source -layout, support code, assets, build, install, export, and verification surface. - -## Problem - -- The filter-chain code still depends on Goggles repository layout, Goggles-owned support modules, - and Goggles-owned test assets. -- The current package boundary is not yet proven as an independent install/export contract owned by - the library project itself. -- Goggles cannot yet treat the filter chain as a normal external package dependency with - `find_package(...)` as the expected integration path. - -## Scope - -- Extract `goggles-filter-chain` into a standalone CMake project rooted at `include/`, `src/`, - `tests/`, `assets/`, and `cmake/`. -- Move required support code, diagnostics support, fixtures, and assets under library ownership. -- Ship an installable and exportable package that supports `STATIC` and `SHARED` outputs without a - `MODULE` variant. -- Verify the installed public surface directly and make Goggles consume the library as an external - dependency through `find_package(...)` first. -- Preserve the current post-retarget public contract. - -## What Changes - -- Define the extracted project as the owner of the public headers, private support modules, - diagnostics support, test fixtures, and packaged assets required to build and verify the library - outside Goggles. -- Require a standalone build/install/export flow that works from the extracted project root without - Goggles wrappers or Goggles source-tree assumptions. -- Require installed-surface verification and downstream consumer checks against the exported package. -- Require Goggles to resolve the library as an external package dependency through - `find_package(...)`, with `add_subdirectory(...)` kept only as an explicit local-development - option. - -## Capabilities - -### New Capabilities -- `filter-chain-standalone-project`: defines the independent repository layout, support ownership, - build/install/export contract, installed-surface verification, and downstream package consumption - for the extracted filter-chain project. -- `filter-chain-assets-package`: defines the library-owned asset package used by installed contract - verification and downstream consumers. - -### Modified Capabilities -- `goggles-filter-chain`: tighten the target contract from a reusable in-repo target to a true - standalone project package with paired `STATIC` and `SHARED` outputs. -- `build-system`: require an independent CMake build/install/export workflow plus - `find_package(...)`-first Goggles consumption. -- `filter-chain-c-api`: require the installed C ABI surface to remain consumable outside the Goggles - repository. -- `filter-chain-cpp-wrapper`: require the installed C++ wrapper surface to remain consumable outside - the Goggles repository. - -## Non-goals - -- Do not revisit or extend the already-established Goggles consumer boundary. -- Do not use Goggles-only builds or tests as the acceptance proof for extraction. -- Do not change the post-retarget public contract. -- Do not introduce or require a `MODULE` library variant. - -## Exact Success Criteria - -> **Note:** These criteria describe the fully extracted end state. The current monorepo change -> implements extraction groundwork (Phase 1–2 in `tasks.md`). Criteria below will be demonstrated -> incrementally as later phases complete. - -- [ ] The extracted project builds from its own repository root with layout rooted at `include/`, - `src/`, `tests/`, `assets/`, and `cmake/`, without requiring the Goggles repository. -- [ ] The extracted project installs and exports a consumable CMake package without Goggles-specific, - Pixi-specific, or Conda-specific assumptions. -- [ ] The extracted project publishes supported `STATIC` and `SHARED` outputs and does not require - or expose a `MODULE` target. -- [ ] Public headers and library-owned internal code do not depend on Goggles-private `src/util/*` - headers or Goggles source-tree include layout. -- [ ] Required support code, diagnostics support, fixtures, shaders, presets, and related assets are - owned by the extracted project. -- [ ] Contract and consumer verification run against the installed public surface and installed asset - package. -- [ ] Goggles consumes the extracted package through `find_package(...)` as the normal dependency - path. -- [ ] The installed C and C++ public surfaces preserve the current post-retarget contract. - -## Impact - -- Affected modules: extracted library code under `src/render/chain`, `src/render/shader`, - `src/render/texture`, required support currently under `src/util`, Goggles dependency wiring under - `cmake/` and `src/render/`, and reusable filter-chain tests and fixtures now under `tests/render/`. -- Likely affected files: extracted project `CMakeLists.txt`, package config/export files, project-root - `include/`, `src/`, `tests/`, `assets/`, and `cmake/` content, plus Goggles render/test wiring - that switches to external package consumption. -- Impacted OpenSpec specs: `openspec/specs/goggles-filter-chain/spec.md`, - `openspec/specs/build-system/spec.md`, `openspec/specs/filter-chain-c-api/spec.md`, and - `openspec/specs/filter-chain-cpp-wrapper/spec.md`. -- Policy-sensitive areas: package/export correctness, ownership of support code and diagnostics, - installed-surface verification quality, asset provenance, and maintaining one-way dependency - direction between Goggles host code and the extracted library. - -## Risks - -- Hidden support and asset dependencies can surface only once the library builds outside Goggles. -- Installed-surface verification can expose assumptions currently masked by source-tree builds. -- Static and shared packaging can diverge if compile definitions, transitive dependencies, or asset - lookup are not kept aligned. -- Goggles can drift back toward source coupling if external package consumption is not kept primary. - -## Validation Plan - -Verification contract: -- Standalone project proof: - - configure, build, test, install, and export the extracted project from a clean checkout using - its own documented CMake workflow - - verify installed-surface contract tests and downstream consumer checks against the installed - package - - verify both `STATIC` and `SHARED` package consumption paths and confirm no `MODULE` target is - part of the supported contract -- Goggles consumer proof: - - configure and build Goggles against the installed package through `find_package(...)` - - run Goggles host-side verification needed to confirm the preserved post-retarget boundary -- Pass criteria: - - every Exact Success Criteria item is demonstrated with repository-local evidence - - the extracted project owns the support and asset surface it requires - - Goggles consumes the package externally without redefining the public contract diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/build-system/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/build-system/spec.md deleted file mode 100644 index 6884efdc..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/build-system/spec.md +++ /dev/null @@ -1,58 +0,0 @@ -# Delta for build-system - -## ADDED Requirements - -### Requirement: CMake-First Standalone Filter Project Workflow - -The build system SHALL define the extracted filter runtime as a CMake-first standalone project with -repository layout rooted at `include/`, `src/`, `tests/`, `assets/`, and `cmake/`. The documented -configure, build, test, install, and export workflow SHALL be runnable from a clean checkout without -requiring Goggles-specific wrappers. - -#### Scenario: Clean checkout uses standalone CMake entry points -- GIVEN a clean checkout of the extracted filter-chain project -- WHEN a maintainer follows the documented standalone workflow -- THEN configure, build, test, and install steps SHALL execute through project-owned CMake entry points -- AND the workflow SHALL NOT require Pixi task wrappers, Goggles preset files, or Conda-specific environment assumptions - -#### Scenario: Separate consumer validates exported package -- GIVEN the standalone project has been installed to a prefix -- WHEN a separate CMake consumer project resolves that install tree -- THEN package discovery, target resolution, and public-header inclusion SHALL succeed through the exported package contract -- AND validation SHALL occur without adding the library sources back into the consumer source tree - -### Requirement: Goggles External Dependency Primary Path - -Goggles SHALL consume the extracted filter runtime through `find_package(...)` as the primary -integration path. `add_subdirectory(...)` MAY remain available only as an explicit local-development -convenience and SHALL NOT be the required or default downstream integration contract. - -#### Scenario: Goggles normal integration uses package discovery -- GIVEN Goggles is configured against an installed standalone filter-chain package -- WHEN render targets resolve the filter runtime dependency -- THEN Goggles SHALL obtain the dependency through `find_package(...)` -- AND normal integration guidance SHALL treat package discovery as the primary supported path - -#### Scenario: Local development subdirectory path stays optional -- GIVEN a developer is iterating on Goggles and a local checkout of the extracted library together -- WHEN the developer opts into a local-development source-based workflow -- THEN `add_subdirectory(...)` MAY be used to wire that checkout for development convenience -- AND Goggles release acceptance SHALL NOT depend on `add_subdirectory(...)` being the primary consumer path - -### Requirement: Paired Static and Shared Package Outputs - -The standalone build and export workflow SHALL publish supported `STATIC` and `SHARED` library -outputs for the filter runtime. The exported package contract SHALL validate both output forms and -SHALL NOT require a `MODULE` target. - -#### Scenario: Package exports static and shared variants -- GIVEN the standalone project is built for distribution -- WHEN install and export artifacts are inspected -- THEN supported package artifacts SHALL include both `STATIC` and `SHARED` library outputs -- AND consumers SHALL not be required to build or load a `MODULE` target to use the package - -#### Scenario: Downstream validation covers both supported output forms -- GIVEN downstream consumer validation is run against the installed standalone package -- WHEN maintainers verify supported linkage modes -- THEN the validation evidence SHALL cover consumption of both `STATIC` and `SHARED` outputs -- AND success criteria SHALL not treat a `MODULE` build as an acceptable substitute for either supported output form diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-assets-package/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-assets-package/spec.md deleted file mode 100644 index 622de170..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-assets-package/spec.md +++ /dev/null @@ -1,44 +0,0 @@ -# filter-chain-assets-package Specification - -## Purpose - -Define the standalone filter-chain asset package contract used by installed verification and -downstream consumers. - -## Requirements - -### Requirement: Library-Owned Asset Package - -The standalone filter-chain project SHALL publish a library-owned asset package that contains the -fixtures, presets, shader assets, and related data needed for installed contract verification and -documented downstream consumption. Those assets SHALL be owned and versioned by the standalone -project rather than by the Goggles repository. - -#### Scenario: Installed project exposes standalone-owned assets -- GIVEN the standalone filter-chain project has been installed or staged for distribution -- WHEN maintainers inspect the installed asset content used for contract verification -- THEN required presets, shaders, and related fixtures SHALL be present as standalone project-owned assets -- AND those verification assets SHALL NOT be sourced from Goggles-owned directories - -### Requirement: Asset Resolution Is Package-Oriented - -Consumers and installed verification flows SHALL resolve standalone filter-chain assets through the -standalone project's documented package-oriented asset location rules. Asset lookup SHALL NOT depend -on Goggles checkout-relative paths or the caller's current working directory. - -#### Scenario: Installed tests resolve assets without repository context -- GIVEN installed contract tests or sample consumers run outside the standalone project source tree -- WHEN they load packaged presets or shader assets -- THEN asset resolution SHALL succeed through the standalone package's documented asset location contract -- AND success SHALL NOT depend on current working directory or Goggles repository-relative paths - -### Requirement: Assets Support Public-Surface Validation - -The standalone asset package SHALL provide the minimum reusable content needed to verify the public -surface from installed `STATIC` and `SHARED` distributions. - -#### Scenario: Public-surface validation reuses the same owned assets -- GIVEN maintainers validate both installed `STATIC` and installed `SHARED` package consumption paths -- WHEN contract verification is executed against each distribution form -- THEN both validation flows SHALL use the standalone library-owned asset package -- AND neither validation flow SHALL require a separate Goggles-owned fixture source diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-c-api/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-c-api/spec.md deleted file mode 100644 index 67072b90..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-c-api/spec.md +++ /dev/null @@ -1,41 +0,0 @@ -# Delta for filter-chain-c-api - -## ADDED Requirements - -### Requirement: Installed C ABI Consumer Contract - -The filter-chain C API public surface MUST remain consumable from the installed standalone package -without Goggles repository context. The installed C header and exported package metadata MUST be -sufficient for external C consumers to compile, link, and use the ABI without Goggles-private -headers, source-tree-relative include roots, or Goggles-owned fixtures. - -#### Scenario: External C consumer uses installed header only -- GIVEN an external C consumer points include and link settings at the installed standalone package -- WHEN it compiles against the installed filter-chain C header -- THEN the consumer SHALL obtain all required public declarations from the installed package surface -- AND compilation SHALL NOT require Goggles source-tree headers or Goggles-private `src/util/*` dependencies - -#### Scenario: Installed ABI validation uses library-owned fixtures -- GIVEN C ABI contract verification runs against the installed standalone package -- WHEN those checks exercise preset loading, controls, or retarget behavior -- THEN required fixtures and assets SHALL come from the standalone library-owned asset package -- AND the verification SHALL NOT depend on Goggles-owned test data paths - -### Requirement: Post-Retarget Output Contract Persists Outside Goggles Tree - -The installed standalone C ABI MUST preserve the existing post-retarget public contract. A -successful format-only retarget for an unchanged preset MUST preserve active preset identity, -control state, and other source-independent preset-derived runtime state, while explicit preset load -or reload remains the full rebuild path. - -#### Scenario: Installed consumer retarget preserves preset-derived state -- GIVEN an external consumer uses the installed C ABI with a runtime in READY state -- WHEN it requests output-target retargeting for a format-only change -- THEN the runtime SHALL preserve active preset identity and existing control state on success -- AND the consumer SHALL NOT need to reload the preset merely to adopt the new output target - -#### Scenario: Installed consumer explicit reload remains distinct -- GIVEN an external consumer uses the installed C ABI with an active preset -- WHEN it explicitly loads or reloads a preset -- THEN that request SHALL remain the full preset/runtime rebuild path -- AND the installed ABI contract SHALL keep that path distinct from format-only retarget behavior diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-cpp-wrapper/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-cpp-wrapper/spec.md deleted file mode 100644 index 5e0ecfb1..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/filter-chain-cpp-wrapper/spec.md +++ /dev/null @@ -1,40 +0,0 @@ -# Delta for filter-chain-cpp-wrapper - -## ADDED Requirements - -### Requirement: Installed Wrapper Consumer Contract - -The filter-chain C++ wrapper SHALL remain consumable from the installed standalone package using -only the installed C ABI, boundary-owned wrapper/support headers, and required third-party headers. -Wrapper consumers SHALL NOT need Goggles-private `src/util/*` headers, Goggles backend types, or -Goggles source-tree-relative include roots. - -#### Scenario: External C++ consumer includes installed wrapper -- GIVEN an external C++ consumer resolves the installed standalone filter-chain package -- WHEN it includes the installed wrapper header and compiles typed wrapper calls -- THEN the wrapper surface SHALL be complete using the installed package headers and required third-party dependencies -- AND the consumer SHALL NOT need Goggles-private support headers or Goggles backend implementation types - -#### Scenario: Installed wrapper validation uses standalone-owned support -- GIVEN wrapper contract verification is run against the installed standalone package -- WHEN wrapper tests build and execute outside the Goggles repository -- THEN required wrapper-support contracts SHALL come from the standalone project itself -- AND verification SHALL NOT assume Goggles checkout-relative include layout - -### Requirement: Wrapper Retarget Contract Survives Standalone Packaging - -The C++ wrapper SHALL preserve the existing post-retarget public contract when consumed from the -installed standalone package. Successful format-only retarget SHALL preserve active preset identity -and source-independent runtime state, while explicit preset load or reload remains the rebuild path. - -#### Scenario: Installed wrapper retarget preserves runtime state -- GIVEN backend or downstream C++ code holds a wrapper-owned runtime from the installed package -- WHEN it requests output-target retargeting for a format-only change -- THEN the wrapper contract SHALL preserve active preset identity and existing control state on success -- AND the callsite SHALL NOT need to express the change as preset reload - -#### Scenario: Installed wrapper explicit reload remains rebuild behavior -- GIVEN downstream C++ code uses the installed wrapper with an active preset -- WHEN it explicitly loads or reloads a preset -- THEN the wrapper SHALL treat that request as full preset/runtime rebuild behavior -- AND the installed wrapper contract SHALL keep that path distinct from output-format retargeting diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/goggles-filter-chain/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/goggles-filter-chain/spec.md deleted file mode 100644 index 70a0d0ef..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,68 +0,0 @@ -# Delta for goggles-filter-chain - -## MODIFIED Requirements - -### Requirement: Standalone Filter Library Target - -The extracted filter runtime SHALL be an independently buildable, installable, and exportable -standalone CMake project that publishes the downstream target contract `goggles-filter-chain`. -The standalone project SHALL use repository layout rooted at `include/`, `src/`, `tests/`, -`assets/`, and `cmake/`. Release acceptance SHALL require `STATIC` and `SHARED` library outputs, -and SHALL NOT require or expose a `MODULE` library variant as part of the package contract. - -#### Scenario: Standalone checkout builds without Goggles repository -- GIVEN a clean checkout of the extracted filter-chain project -- WHEN the documented CMake workflow configures and builds the project -- THEN the project SHALL build without requiring the Goggles source tree, Pixi wrappers, or Conda-specific paths -- AND the project layout consumed by that workflow SHALL be rooted at `include/`, `src/`, `tests/`, `assets/`, and `cmake/` - -#### Scenario: Installed package preserves stable target identity -- GIVEN the standalone project has been installed and exported -- WHEN a downstream consumer resolves the package through CMake package discovery -- THEN the consumer SHALL obtain the filter runtime through the target contract `goggles-filter-chain` -- AND consuming the installed package SHALL NOT require downstream target renaming or source-tree include assumptions - -#### Scenario: Distribution excludes module-only success criteria -- GIVEN the standalone project is prepared for release validation -- WHEN library artifacts and exported targets are inspected -- THEN `STATIC` and `SHARED` outputs SHALL both be available as supported deliverables -- AND no `MODULE` library variant SHALL be required for success or documented as part of the supported package surface - -## ADDED Requirements - -### Requirement: Library-Owned Support Boundary - -The extracted library SHALL own every public contract and every library-private support contract -required to build and verify itself outside the Goggles repository. Public headers and library-owned -internal code SHALL NOT depend on Goggles-private `src/util/*` headers, Goggles application config -types, or Goggles source-tree include-layout assumptions. - -#### Scenario: Public surface excludes Goggles-private support headers -- GIVEN an external consumer compiles against the standalone library public headers -- WHEN header dependencies are audited from the install include root -- THEN the public surface SHALL depend only on boundary-owned declarations and allowed third-party headers -- AND no public header SHALL require Goggles-private `src/util/*` includes or Goggles app-private types - -#### Scenario: Library-owned internals build without Goggles-private support -- GIVEN the standalone project builds its library sources from its own `src/` tree -- WHEN library-owned internal sources are compiled outside the Goggles repository -- THEN required support code SHALL be provided by the standalone project itself -- AND compilation SHALL NOT depend on Goggles-private helper ownership or Goggles checkout-relative include paths - -### Requirement: Installed Public-Surface Verification Boundary - -Reusable contract verification for the extracted library SHALL run against the installed public -surface using library-owned fixtures and assets. Goggles host integration coverage MAY verify host -wiring, but SHALL NOT replace installed-surface proof for the standalone library contract. - -#### Scenario: Installed contract tests stay boundary-only -- GIVEN the standalone library has been installed to a test prefix -- WHEN contract tests compile and run against the installed headers, libraries, and package metadata -- THEN those tests SHALL validate boundary behavior without including Goggles-private source headers -- AND passing Goggles in-tree tests alone SHALL NOT satisfy standalone contract verification - -#### Scenario: Library-owned fixtures and assets back verification -- GIVEN reusable contract tests exercise presets, shaders, or related runtime assets -- WHEN those tests run against the installed public surface -- THEN required fixtures and assets SHALL come from the library-owned project content -- AND the tests SHALL NOT depend on Goggles-owned fixture directories or shader asset paths diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/render-pipeline/spec.md deleted file mode 100644 index 9f817abc..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/specs/render-pipeline/spec.md +++ /dev/null @@ -1,23 +0,0 @@ -# Delta for render-pipeline - -## MODIFIED Requirements - -### Requirement: Swapchain Format Matching - -The render backend SHALL match swapchain output color space to the current source image -color-space classification to preserve pixel values. When the source classification changes without -an explicit preset change request, the pipeline SHALL recreate only backend-owned swapchain and -presentation resources, SHALL retarget the filter runtime through the installed public boundary, -and SHALL preserve source-independent preset-derived state instead of forcing a full preset reload. - -#### Scenario: External package retarget keeps host ownership split -- GIVEN Goggles consumes the filter runtime as an external standalone package -- WHEN the source image classification changes to require a different output format -- THEN Goggles SHALL recreate only host-owned swapchain and presentation resources -- AND the external filter runtime SHALL be retargeted through its public boundary rather than by reloading the preset - -#### Scenario: External consumption preserves preset-derived state -- GIVEN Goggles is linked against the standalone package and a preset is already active -- WHEN a format-only retarget succeeds -- THEN the active preset selection, control layout, and parameter overrides SHALL remain unchanged -- AND source-independent preset-derived runtime work SHALL remain available after the transition diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/tasks.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/tasks.md deleted file mode 100644 index b9c9054a..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/tasks.md +++ /dev/null @@ -1,111 +0,0 @@ -# Tasks: Extract Filter Chain Standalone Project - -## Phase 1: Keep only extraction groundwork in the monorepo - -- [x] 1.1 Remove smoke-matrix presets, tasks, and other package-rehearsal wiring that only served - fake intermediate packaging modes. - **Note:** The root `.shared` hidden preset and `test-shared` configure/build/test presets in - `CMakePresets.json` are transitional host-integration coverage only. They verify that Goggles - still builds and tests when the in-repo `goggles-filter-chain` target is built as SHARED. - These presets must be removed once the standalone project owns real static/shared package - validation (Phase 5). Do not add additional root shared-variant presets. -- [x] 1.2 Establish in-repo public include tree (`src/render/chain/include/goggles/filter_chain/`) - mirroring the standalone project's future public surface. Migrate internal headers - (`error.hpp`, `result.hpp`, `vulkan_context.hpp`, `filter_controls.hpp`, `scale_mode.hpp`, - diagnostics shims) into this tree and rewrite consumers to use canonical include paths. - **Status:** All 5 public headers are self-contained. `filter_controls.hpp` and - `vulkan_context.hpp` define types inline. `error.hpp`, `result.hpp`, and `scale_mode.hpp` - define types with shared `#ifndef` guards matching `util/` counterparts for ODR safety. - The 8 diagnostics forwarding shims were removed; chain sources include - `` directly. -- [x] 1.3 Decouple `goggles-filter-chain` library target from `goggles_util` public linkage. - Extract `goggles_util_logging_obj` OBJECT library, add `spdlog::spdlog` as direct private - dependency, move `Vulkan::Vulkan` to PUBLIC, tighten OBJECT library visibility - (PUBLIC -> PRIVATE) in chain, shader, and texture CMakeLists, and add - `GogglesFilterChain::goggles-filter-chain` ALIAS for future `find_package()` consumption. -- [x] 1.4 Add `FilterChainController::record()` encapsulation so backend code delegates recording - through the controller facade instead of reaching through to the raw runtime. -- [x] 1.5 Add retarget-preserves-state contract test exercising the C++ wrapper API with a live - Vulkan runtime, verifying that `retarget_output()` preserves stage policy, prechain - resolution, and control values. -- [x] 1.6 Improve C API header documentation: add `@note` annotations about host-owned handles, - retarget semantics, and swapchain responsibilities. - -## Phase 2: Finish boundary-owned public support in the current tree - -- [x] 2.1 Replace remaining public-header dependence on Goggles-private support and diagnostics - headers with library-owned headers under the future standalone `include/` surface. - **Done:** Public headers `error.hpp`, `result.hpp`, and `scale_mode.hpp` under - `goggles/filter_chain/` are now self-contained with inline type definitions. Shared - `#ifndef` guards with `util/error.hpp` and `util/scale_mode.hpp` prevent ODR violations - in the monorepo. `${CMAKE_SOURCE_DIR}/src` moved from PUBLIC to PRIVATE on - `goggles-filter-chain` so downstream consumers no longer see the full repo source tree. - The C API implementation now uses canonical `` include paths. - Boundary contract tests validate all 5 public headers are util-free. -- [x] 2.2 Move the remaining reusable runtime support now living under Goggles `src/util/` into a - library-owned layout that can be copied to a standalone `src/` tree without host coupling. - **Note:** Phase 2.1 made public headers self-contained via shared include guards. The - shared-guard approach is transitional — it keeps both copies of the types in sync via - identical definitions. The extraction phase will remove the util copies entirely. - **Done:** Internal headers `vulkan_result.hpp`, `shader/slang_reflect.hpp`, and - `texture/texture_loader.hpp` now include the library-owned `` - instead of ``. No `.hpp` file in the chain/shader/texture subtrees includes - non-diagnostics `util/` headers. Remaining `util/logging.hpp`, `util/profiling.hpp`, and - `util/serializer.hpp` includes from `.cpp` implementation files are provided by the PRIVATE - `goggles_util` link and will transfer to standalone ownership in Phase 3. -- [x] 2.3 Trim or convert any remaining compatibility forwarders that are no longer needed once the - boundary-owned headers fully cover the public contract. - **Done:** Removed the transitional forwarder headers `src/render/chain/vulkan_context.hpp` and - `src/render/chain/filter_controls.hpp`. All internal chain consumers, test files, and host UI - now include through canonical `` paths. The `goggles_ui` target was - given a direct `goggles-filter-chain` link to provide the public include directory. Boundary - contract tests updated to remove forwarder-existence assertions. - -## Phase 3: Create the standalone repository skeleton - -- [ ] 3.1 Create the standalone project root with `CMakeLists.txt`, `cmake/`, `include/`, `src/`, - `tests/`, and `assets/` as the owned layout. -- [ ] 3.2 Move the reusable implementation from `src/render/chain/`, `src/render/shader/`, and - `src/render/texture/` into standalone-owned modules while preserving the current host/library - responsibility split. -- [ ] 3.3 Move reusable contract tests into standalone-owned `tests/contract/` and keep Goggles - tests focused on host integration only. - -## Phase 4: Move assets and diagnostics under library ownership - -- [ ] 4.1 Create the library-owned asset package under `assets/` for presets, shaders, fixtures, - and related verification data required by runtime and tests. -- [ ] 4.2 Make runtime asset lookup and contract tests resolve library-owned assets without Goggles - checkout-relative assumptions. -- [ ] 4.3 Move diagnostics support required by the public surface under standalone-owned include/src - paths instead of Goggles utility ownership. - **Note:** The 8 diagnostics forwarding shims (`goggles/filter_chain/diagnostics/*.hpp`) were - removed in Phase 1 cleanup as they added no value. Chain sources already include - `` directly. This task now focuses on moving the diagnostics - implementation itself under standalone ownership. - -## Phase 5: Build, export, and consume the real package - -- [ ] 5.1 Publish supported `STATIC` and `SHARED` library targets from the standalone project and do - not define a supported `MODULE` variant. -- [ ] 5.2 Install headers, libraries, assets, and CMake package metadata, then export - `GogglesFilterChain::goggles-filter-chain` plus explicit static/shared targets. - **Note:** Install rules were intentionally removed from the monorepo chain target during - Phase 1 cleanup. This task implements proper install/export from the standalone project - with config-file packages and namespaced export sets. -- [ ] 5.3 Add downstream consumer validation that uses `find_package(GogglesFilterChain CONFIG - REQUIRED)` for both static and shared linkage. -- [ ] 5.4 Update Goggles so installed package consumption through `find_package(...)` is the normal - dependency path, with any source-based path kept only as an explicit development option. -- [ ] 5.5 Remove the transitional root `.shared` and `test-shared` presets from Goggles - `CMakePresets.json`. Once the standalone project owns static/shared package validation, - Goggles no longer needs its own shared-variant build presets. If Goggles still needs a - host-integration shared-linkage check, it should consume the installed shared package - through `find_package(...)`, not rebuild the library in-tree as SHARED. - -## Phase 6: End-to-end verification - -- [ ] 6.1 Prove the standalone project can configure, build, test, install, and run consumer - validation from a clean checkout. -- [ ] 6.2 Run Goggles build plus focused host-side verification against the installed package to - confirm preserved post-retarget behavior and external package consumption. diff --git a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/verify-report.md b/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/verify-report.md deleted file mode 100644 index f7d4e06f..00000000 --- a/openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/verify-report.md +++ /dev/null @@ -1,153 +0,0 @@ -# Verification Report - -**Change**: extract-filter-chain-standalone-project -**Scope**: Phase 1–2 monorepo groundwork (Phase 3–6 is future work in standalone repository) -**Date**: 2026-03-13 - ---- - -## Completeness - -| Metric | Value | -|--------|-------| -| Tasks total (in-scope Phase 1–2) | 12 | -| Tasks complete | 12 | -| Tasks incomplete | 0 | -| Tasks total (future Phase 3–6) | 12 | -| Tasks deferred | 12 | - -All in-scope tasks for Phase 1 (1.1–1.6) and Phase 2 (2.1–2.3) are complete. -Phase 3–6 tasks are designated as future work per the design document scope note. - ---- - -## Build & Tests Execution - -**Build (ASAN)**: ✅ Passed -``` -pixi run build -p asan → ninja: no work to do. (clean build, zero errors) -``` - -**Build (quality / clang-tidy as errors)**: ✅ Passed -``` -pixi run build -p quality → ninja: no work to do. (clean build, zero clang-tidy errors) -``` - -**Tests (ASAN)**: ✅ 225 test cases — 223 passed, 2 skipped, 0 failed -``` -test cases: 225 | 223 passed | 2 skipped -assertions: 30961 | 30961 passed -10/10 Test #1: goggles_unit_tests Passed 10.36 sec -100% tests passed, 0 tests failed out of 10 -``` - -**Coverage**: ➖ Not configured (`rules.verify.coverage_threshold` not set in config.yaml) - ---- - -## Spec Compliance Matrix - -### In-Scope Scenarios (Phase 1–2 Groundwork) - -| Spec | Scenario | Test(s) | Result | -|------|----------|---------|--------| -| goggles-filter-chain | Public surface excludes Goggles-private support headers | `test_filter_boundary_contracts > "Filter chain boundary control contract coverage"` + `"Filter chain wrapper boundary contract coverage"` | ✅ COMPLIANT | -| goggles-filter-chain | Library-owned internals build without Goggles-private support | `test_filter_boundary_contracts > "Filter chain wrapper boundary contract coverage"` + structural audit | ⚠️ PARTIAL | -| filter-chain-c-api | Installed consumer retarget preserves preset-derived state | `test_filter_chain_retarget > "Controller and backend retarget path stays distinct from reload"` + `"Retarget failure path stays staged and non-destructive"` | ✅ COMPLIANT | -| filter-chain-c-api | Installed consumer explicit reload remains distinct | `test_filter_boundary_contracts > "Filter chain wrapper boundary contract coverage"` + `test_filter_chain_retarget > "Pending reload swaps only after activation..."` + `"Controller and backend retarget path stays distinct from reload"` | ✅ COMPLIANT | -| filter-chain-cpp-wrapper | Installed wrapper retarget preserves runtime state | `test_filter_chain_retarget_contract > "Filter chain output retarget preserves runtime state"` + `"Retarget failure preserves runtime state for continued use"` | ✅ COMPLIANT | -| filter-chain-cpp-wrapper | Installed wrapper explicit reload remains rebuild behavior | `test_filter_chain_retarget > "Pending reload swaps only after activation..."` + `"Explicit reload failure preserves the previous runtime"` | ✅ COMPLIANT | -| render-pipeline | External package retarget keeps host ownership split | `test_filter_boundary_contracts > "Async swap and resize safety contract coverage"` + `test_filter_chain_retarget > "Controller retarget preserves active runtime..."` + `"Controller and backend retarget path stays distinct from reload"` + `"Retarget failure path stays staged and non-destructive"` | ✅ COMPLIANT | -| render-pipeline | External consumption preserves preset-derived state | `test_filter_chain_retarget_contract > "Filter chain output retarget preserves runtime state"` + `test_filter_chain_retarget > "Filter chain output retarget preserves runtime state"` | ✅ COMPLIANT | - -**In-scope compliance**: 7/8 COMPLIANT, 1/8 PARTIAL - -**PARTIAL note for "Library-owned internals build without Goggles-private support"**: Public headers -are verified self-contained (no `util/` includes). Internal `.hpp` files in chain/shader/texture use -canonical `` paths. Full proof requires the standalone project building -without the Goggles source tree, which is Phase 3+ scope. - -### Future Scenarios (Phase 3–6 — Not Yet Testable) - -| Spec | Scenario | Status | -|------|----------|--------| -| goggles-filter-chain | Standalone checkout builds without Goggles repository | 🔜 Phase 3 | -| goggles-filter-chain | Installed package preserves stable target identity | 🔜 Phase 5 | -| goggles-filter-chain | Distribution excludes module-only success criteria | 🔜 Phase 5 | -| goggles-filter-chain | Installed contract tests stay boundary-only | 🔜 Phase 3 | -| goggles-filter-chain | Library-owned fixtures and assets back verification | 🔜 Phase 4 | -| build-system | Clean checkout uses standalone CMake entry points | 🔜 Phase 3 | -| build-system | Separate consumer validates exported package | 🔜 Phase 5 | -| build-system | Goggles normal integration uses package discovery | 🔜 Phase 5 | -| build-system | Local development subdirectory path stays optional | 🔜 Phase 5 | -| build-system | Package exports static and shared variants | 🔜 Phase 5 | -| build-system | Downstream validation covers both supported output forms | 🔜 Phase 5 | -| filter-chain-c-api | External C consumer uses installed header only | 🔜 Phase 3 | -| filter-chain-c-api | Installed ABI validation uses library-owned fixtures | 🔜 Phase 4 | -| filter-chain-cpp-wrapper | External C++ consumer includes installed wrapper | 🔜 Phase 3 | -| filter-chain-cpp-wrapper | Installed wrapper validation uses standalone-owned support | 🔜 Phase 3 | -| filter-chain-assets-package | Installed project exposes standalone-owned assets | 🔜 Phase 4 | -| filter-chain-assets-package | Installed tests resolve assets without repository context | 🔜 Phase 4 | -| filter-chain-assets-package | Public-surface validation reuses the same owned assets | 🔜 Phase 4 | - ---- - -## Correctness (Static — Structural Evidence) - -| Requirement | Status | Notes | -|-------------|--------|-------| -| Public headers self-contained (no private util includes) | ✅ Implemented | All 5 headers under `include/goggles/filter_chain/` verified clean | -| C API `@note` documentation (handles, retarget, swapchain) | ✅ Implemented | Annotations present in `goggles_filter_chain.h` | -| C++ wrapper free of private util dependencies | ✅ Implemented | Includes only canonical paths and third-party headers | -| C API impl uses canonical `` paths | ✅ Implemented | All 4 public headers included via canonical paths | -| `${CMAKE_SOURCE_DIR}/src` PRIVATE on goggles-filter-chain | ✅ Implemented | PUBLIC includes limited to chain/include, api/c, api/cpp | -| GogglesFilterChain::goggles-filter-chain ALIAS | ✅ Implemented | Conditional ALIAS defined in render CMakeLists.txt | -| goggles_util_logging_obj OBJECT library | ✅ Implemented | Defined in util CMakeLists.txt, consumed via TARGET_OBJECTS | -| No PUBLIC goggles_util linkage on goggles-filter-chain | ✅ Implemented | Only Vulkan::Vulkan and nonstd::expected-lite are PUBLIC | -| spdlog::spdlog direct PRIVATE dependency | ✅ Implemented | Listed as PRIVATE link library on goggles-filter-chain | -| Internal headers use `` | ✅ Implemented | vulkan_result.hpp, slang_reflect.hpp, texture_loader.hpp migrated | -| Zero `` in src/render/ | ✅ Implemented | Grep confirms zero occurrences | -| Forwarder headers removed | ✅ Implemented | chain/vulkan_context.hpp and chain/filter_controls.hpp deleted | -| FilterChainController::record() encapsulation | ✅ Implemented | Backend delegates via controller facade at two callsites | -| Retarget-preserves-state contract test | ✅ Implemented | test_filter_chain_retarget_contract.cpp covers success + failure paths | - ---- - -## Coherence (Design) - -| Decision | Followed? | Notes | -|----------|-----------|-------| -| Start from current consumer boundary (no boundary redesign) | ✅ Yes | Public API types unchanged | -| Monorepo shared-variant presets are transitional only | ✅ Yes | Only `.shared` + `test-shared` exist, no additional shared presets added | -| FilterChainController::record() encapsulation | ✅ Yes | record() declared and two backend callsites verified | -| Retarget-preserves-state contract test via C++ wrapper | ✅ Yes | Tests exercise FilterChainRuntime with live Vulkan runtime | -| goggles_util_logging_obj OBJECT library with spdlog PRIVATE | ✅ Yes | Defined and consumed as specified | -| File changes match design table | ✅ Yes | render/CMakeLists.txt and tests/CMakeLists.txt modifications match | - ---- - -## Issues Found - -**CRITICAL** (must fix before archive): -None - -**WARNING** (should fix): -- 1 PARTIAL scenario: "Library-owned internals build without Goggles-private support" — full proof - deferred to Phase 3 standalone build. Current evidence is structural (headers migrated, canonical - paths used) but the definitive test (building outside Goggles) cannot run until the standalone - project exists. This is expected given the Phase 1–2 scope. - -**SUGGESTION** (nice to have): -None - ---- - -## Verdict - -**PASS WITH WARNINGS** - -All 12 in-scope Phase 1–2 tasks are complete. Both builds (ASAN + quality/clang-tidy) pass cleanly. -All 225 test cases pass (2 skipped, unrelated to this change). 7/8 in-scope spec scenarios are fully -compliant with behavioral test evidence; the remaining 1 is structurally verified with full behavioral -proof deferred to Phase 3 by design. All 6 design decisions were followed without deviation. The -monorepo groundwork is ready for archive; Phase 3–6 standalone extraction is future work. diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/.openspec.yaml b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/.openspec.yaml deleted file mode 100644 index 6dfce101..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-12 diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/design.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/design.md deleted file mode 100644 index 0aecd0e0..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/design.md +++ /dev/null @@ -1,166 +0,0 @@ -## Context - -The current render path already separates most preset-derived work from final presentation, but the -backend/controller lifecycle collapses source-format retargeting into full runtime recreation. -`VulkanBackend::recreate_swapchain()` and `FilterChainController::recreate_filter_chain()` currently -route format-only changes through the same path used for explicit preset reload behavior. - -This design keeps eager preset processing intact and narrows the change to the real source-format -dependency: swapchain-bound output/postchain state. - -## Goals / Non-Goals - -**Goals:** -- Preserve eager preset processing. -- Separate swapchain/output retargeting from full preset reload. -- Keep source-independent preset runtime state alive across output-format retargeting. -- Preserve explicit preset reload as the only full rebuild path. -- Preserve backend ownership of swapchain lifecycle and presentation. -- Preserve control layout, parameter overrides, stage policy, and active preset selection across - successful output retarget. - -**Non-Goals:** -- Delay whole preset processing until the first source image arrives. -- Rework shader semantics, pass ordering, or unrelated backend architecture. -- Collapse backend-owned render-output responsibilities into the chain runtime. -- Turn format retarget into a hidden full preset rebuild fallback. - -## Decisions - -### Decision: Split persistent preset state from retargetable output state - -`ChainRuntime` SHALL retain source-independent preset-derived state across output-format retargets. -Swapchain-bound state SHALL be isolated so it can be rebuilt independently. - -Persistent state includes: -- parsed/installed preset state -- effect passes and effect-stage resources -- preset texture registry -- control descriptors and control values -- stage policy and prechain configuration -- diagnostics session and other runtime-owned non-output state - -Retargetable state includes: -- active swapchain/output format binding -- output/postchain pass objects -- swapchain-bound postchain framebuffers and related present-path state - -Rationale: -- Most preset work does not depend on the final output format. -- The duplicated startup/rebuild work exists because lifecycle code reloads the full runtime instead - of retargeting only the output side. - -### Decision: Introduce a dedicated output-state helper before deeper resource splitting - -The first implementation step SHALL isolate retargetable output/postchain state behind a dedicated -helper owned by the runtime/resources layer. Splitting `ChainResources` into broader persistent vs. -retargetable sub-objects SHALL remain a follow-on refactor only if the helper boundary proves too -tight. - -Rationale: -- This gives the change a narrow seam for output-format retargeting without forcing an immediate - large-scale internal re-layout. -- It preserves current ownership and pass-graph wiring while making the swapchain-bound state - explicit. - -### Decision: Backend remains authoritative for swapchain lifecycle - -`VulkanBackend` SHALL continue to own swapchain recreation and final presentation lifecycle. -Format-only source color-space changes SHALL recreate the swapchain, then call a narrower chain -retarget API instead of a full runtime rebuild path. - -Rationale: -- The change is about separating retargeting from preset reload, not about moving presentation - ownership into the chain layer. - -### Decision: Explicit preset reload remains a full rebuild path - -User-requested preset changes or explicit reloads SHALL continue to use the existing full preset -reload behavior through controller async rebuild orchestration. - -Rationale: -- This keeps the meaning of preset reload stable. -- It prevents output-only retargeting from becoming a leaky general rebuild path. - -### Decision: Overlapping format change retargets pending runtime before swap - -If a format change lands while an explicit async reload already has a pending runtime, the -controller SHALL update the authoritative output target and retarget that pending runtime before it -becomes active. The controller SHALL NOT swap in a runtime built for the stale target and then -retarget it immediately afterward. - -Rationale: -- Swap activation should make the new runtime current in its final output configuration. -- Pre-retargeting keeps swap-complete signaling aligned with the actually active output target and - avoids an unnecessary extra retarget on the newly swapped runtime. - -### Decision: Retarget failure preserves active runtime state - -Retarget SHALL be staged as candidate output-state rebuild followed by swap-on-success. -If retarget fails, the previously active runtime state SHALL remain active and observable. - -Rationale: -- Failure isolation must match the current non-destructive reload contract. - -## State Model - -- `active runtime`: currently rendering preset/runtime state -- `pending runtime`: full rebuilt runtime for explicit preset reloads only -- `active output state`: swapchain-bound output/postchain state associated with the active runtime -- `authoritative output target`: controller/backend view of target format and extent - -Retarget path: -1. backend detects source color-space change -2. backend recreates swapchain -3. controller forwards new output target to active runtime -4. runtime builds candidate output state only -5. runtime swaps output state on success -6. previous output state is destroyed after successful replacement - -Full reload path: -1. app/backend requests preset reload -2. controller builds pending runtime asynchronously -3. controller reapplies authoritative state, including the latest output target -4. if output target changed while reload was pending, controller pre-retargets pending runtime -5. pending runtime swaps in at safe point - -## Module / File Plan - -- `src/render/chain/chain_runtime.hpp` -- `src/render/chain/chain_runtime.cpp` -- `src/render/chain/chain_resources.hpp` -- `src/render/chain/chain_resources.cpp` -- `src/render/chain/output_pass.hpp` -- `src/render/chain/output_pass.cpp` -- `src/render/chain/api/c/goggles_filter_chain.h` -- `src/render/chain/api/c/goggles_filter_chain.cpp` -- `src/render/chain/api/cpp/goggles_filter_chain.hpp` -- `src/render/chain/api/cpp/goggles_filter_chain.cpp` -- `src/render/backend/filter_chain_controller.hpp` -- `src/render/backend/filter_chain_controller.cpp` -- `src/render/backend/vulkan_backend.hpp` -- `src/render/backend/vulkan_backend.cpp` - -## Verification Strategy - -- Add boundary/API coverage for the retarget operation and its validation behavior. -- Add controller/runtime tests covering: - - format-only retarget does not trigger preset rebuild semantics - - active preset selection and control layout remain unchanged across retarget - - parameter overrides and policy remain intact across retarget - - failure leaves previous runtime active -- Update backend seam tests so format change routes through retarget, while explicit reload remains - the only full rebuild path. - -## Risks / Trade-offs - -- Hidden coupling between effect resources and output-side resources can complicate the split. -- Retarget and async reload sequencing must be explicit to avoid stale output-target state. -- Retarget after swapchain recreation cannot cheaply roll back swapchain ownership, so error - reporting and runtime-state preservation both matter. - -## Resolved Follow-Ups - -- Use a dedicated output-state helper as the first isolation step; do not start by splitting - `ChainResources` internally. -- When async reload and format retarget overlap, pre-retarget the pending runtime before swap. diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/proposal.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/proposal.md deleted file mode 100644 index 20ae6d27..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/proposal.md +++ /dev/null @@ -1,100 +0,0 @@ -## Why - -Goggles currently treats a source color-space change as a reason to rebuild the full filter runtime, -even though most preset work is independent of the final swapchain output format. In the common -startup path, this means Goggles can eagerly load a preset, then recreate the swapchain after the -first real source frame arrives, and reload the same preset again. - -This creates duplicated parse/compile/load work, repeated failure logs for the same bad preset, and -extra churn in a path that should mostly be an output retarget. - -## Problem - -- `src/render/backend/vulkan_backend.cpp` treats source-format-driven swapchain recreation as a full - filter-chain reinit path. -- `src/render/backend/filter_chain_controller.cpp` reloads the active preset again during runtime - recreation even when the preset itself did not change. -- Most preset work is source-format-independent: preset parse, include expansion, shader - compile/reflection, preset texture loading, and effect-pass setup. -- The truly source-format-dependent work is mainly swapchain/output-format selection and - swapchain-bound output/postchain state. - -## Scope - -- Separate swapchain-format retargeting from full preset reload. -- Preserve eager preset processing and keep source-independent preset-derived runtime state alive - across output-format retargeting. -- Rebuild only swapchain-bound output/postchain state when source color-space classification changes - and maps to a different output format. -- Keep explicit preset reload as the only full preset/runtime rebuild path. -- Preserve existing async reload, error propagation, and backend ownership boundaries. - -## What Changes - -- Introduce a boundary-facing retarget operation for format-only output changes. -- Isolate retargetable output-side state behind a dedicated output-state helper before attempting any - broader `ChainResources` split. -- Update backend/controller sequencing so swapchain recreation on format-only changes retargets the - active runtime instead of destroying it and reloading the preset. -- Preserve the current explicit reload path for user-requested preset changes or reloads. -- When format retarget overlaps an explicit async reload, retarget the pending runtime before swap - instead of swapping in stale output state and retargeting afterward. - -## Capabilities - -### Modified Capabilities -- `render-pipeline` -- `goggles-filter-chain` - -## Non-goals - -- Delay whole preset processing until the first real frame. -- Change shader semantic contracts, stage ordering, or unrelated filter/backend ownership. -- Redesign preset authoring or diagnostics behavior beyond the retarget-vs-reload distinction. -- Guarantee zero first-frame cost; the goal is to remove duplicated source-independent work. - -## Impact - -- Affected modules: `src/render/backend`, `src/render/chain`, `tests/render`. -- Likely affected files: `src/render/backend/vulkan_backend.cpp`, - `src/render/backend/filter_chain_controller.cpp`, `src/render/backend/filter_chain_controller.hpp`, - `src/render/chain/chain_runtime.cpp`, `src/render/chain/chain_runtime.hpp`, - `src/render/chain/chain_resources.cpp`, `src/render/chain/chain_resources.hpp`, - `src/render/chain/output_pass.cpp`, `src/render/chain/output_pass.hpp`, - `src/render/chain/api/c/goggles_filter_chain.h`, - `src/render/chain/api/c/goggles_filter_chain.cpp`, - `src/render/chain/api/cpp/goggles_filter_chain.hpp`, - `src/render/chain/api/cpp/goggles_filter_chain.cpp`, and targeted `tests/render/*` coverage. -- Impacted OpenSpec specs: `openspec/specs/render-pipeline/spec.md` and - `openspec/specs/goggles-filter-chain/spec.md`. - -## Risks - -- Splitting persistent and retargetable state can expose hidden coupling inside `ChainResources`. -- Retarget and async reload can race unless controller state clearly distinguishes full rebuild from - output-only retarget. -- Failure handling must keep the current active runtime usable when retargeting fails. -- Tests may currently assume format change implies full rebuild and will need contract updates. - -## Resolved Implementation Direction - -- Start with a dedicated output-state helper as the first isolation seam. -- On format-change / async-reload overlap, pre-retarget the pending runtime before it becomes active. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` -- Targeted contract coverage: - - render/filter tests for retarget-vs-reload behavior, preserved control state, preserved preset - selection, and failure rollback behavior -- Pass criteria: - - format-only swapchain changes do not repeat preset parse/compile/texture-load work - - explicit preset reload remains the only full rebuild path - - successful retarget preserves active preset state and control behavior - - failed retarget leaves the previous active runtime usable diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/goggles-filter-chain/spec.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/goggles-filter-chain/spec.md deleted file mode 100644 index c8170e22..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,42 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Complete Filter Runtime Ownership Boundary - -The filter boundary SHALL own filter-chain orchestration, shader runtime ownership/creation, shader -processing, and preset texture loading internals. When the host retargets output format without -changing the active preset, the boundary SHALL preserve source-independent preset-derived runtime -state across that retarget. - -#### Scenario: Source-independent preset work survives output retarget -- **GIVEN** a preset runtime has already completed parsing, shader compilation/reflection, preset - texture loading, and effect-pass setup -- **WHEN** the host requests output-format retargeting for a source color-space change -- **THEN** that source-independent preset-derived work SHALL remain available after the retarget -- **AND** the boundary SHALL expose the same effect-stage behavior after activation - -### Requirement: Host Backend Responsibility Boundary - -The host backend SHALL remain responsible for swapchain lifecycle, external image import, -synchronization, queue submission, and present. The host backend SHALL use boundary-facing retarget -behavior for swapchain/output-format changes and SHALL reserve full preset/runtime rebuild behavior -for explicit preset reload requests. - -#### Scenario: Format retarget is handed off without full preset rebuild -- **GIVEN** swapchain output format must change because the source color-space classification changed -- **WHEN** host backend recreation is triggered -- **THEN** host backend code SHALL recreate swapchain and present-path resources -- **AND** filter runtime retargeting SHALL be invoked through boundary-facing contracts without - forcing full preset rebuild behavior - -#### Scenario: Explicit preset reload still uses rebuild path -- **GIVEN** the user explicitly requests a preset reload or selects a different preset -- **WHEN** the host backend coordinates the change -- **THEN** the boundary interaction SHALL use the full preset/runtime rebuild path -- **AND** the request SHALL NOT be collapsed into output-format-only retarget behavior - -#### Scenario: Pending runtime is aligned before activation -- **GIVEN** the host backend has a pending runtime from an explicit reload -- **AND** the authoritative output target changes before that pending runtime becomes active -- **WHEN** the backend/controller prepares the pending runtime for activation -- **THEN** the boundary interaction SHALL align that pending runtime to the current output target -- **AND** activation SHALL occur only after the pending runtime matches the latest output format diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/render-pipeline/spec.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/render-pipeline/spec.md deleted file mode 100644 index a3494e8f..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/specs/render-pipeline/spec.md +++ /dev/null @@ -1,81 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Swapchain Format Matching - -The render backend SHALL match swapchain output color space to the current source image -color-space classification to preserve pixel values. When the source classification changes without - a preset change request, the pipeline SHALL retarget only swapchain-bound output resources needed -for the new output format and SHALL NOT treat the event as a full preset reload. - -#### Scenario: Source color-space change retargets output side -- **GIVEN** a preset is already active and rendering with an SRGB-matched output path -- **WHEN** the source image classification changes to UNORM -- **THEN** the swapchain SHALL be recreated with a matching UNORM output format -- **AND** the output-side runtime resources bound to swapchain presentation SHALL be retargeted for - the new format - -#### Scenario: Output retarget preserves active preset state -- **GIVEN** a source color-space change triggers output-format retargeting -- **WHEN** the retarget succeeds -- **THEN** the active preset selection SHALL remain unchanged -- **AND** existing parameter overrides and control layout SHALL remain unchanged - -### Requirement: Runtime Shader Preset Reload - -The render pipeline SHALL support rebuilding the RetroArch filter chain at runtime when the -application explicitly requests a new `.slangp` preset or explicitly reloads the current preset. -Explicit preset reload SHALL remain a full preset/runtime rebuild and SHALL remain distinct from -output-format retargeting caused by source color-space changes. - -#### Scenario: Explicit preset reload performs full rebuild -- **GIVEN** a preset is active and the application explicitly requests a preset reload -- **WHEN** the render pipeline handles the request -- **THEN** it SHALL perform full preset reload behavior -- **AND** preset parsing, include expansion, shader compilation/reflection, preset texture loading, - and effect-pass setup SHALL be re-executed before the replacement runtime becomes active - -#### Scenario: Output retarget is not an explicit preset reload -- **GIVEN** the active preset path and runtime controls have not changed -- **WHEN** only the source color-space classification changes -- **THEN** the pipeline SHALL NOT report or execute the event as an explicit preset reload -- **AND** the next frame after a successful transition SHALL continue using the same preset-derived - effect behavior - -#### Scenario: Explicit reload failure preserves previous runtime -- **GIVEN** the application explicitly requests a preset reload -- **WHEN** the requested reload fails before replacement activation -- **THEN** the previously active runtime SHALL remain active for rendering -- **AND** the failure SHALL be reported without leaving a partially activated replacement runtime - -### Requirement: Async Filter Lifecycle Safety - -The render pipeline SHALL preserve async preset reload, output-format retarget, chain swap, and -resize safety behavior after introducing the `goggles-filter-chain` boundary. - -#### Scenario: Output retarget completion is observable only after activation -- **GIVEN** an output-format retarget is performed asynchronously -- **WHEN** the retargeted runtime becomes active for rendering -- **THEN** swap-complete notification SHALL be observable only after the retargeted runtime is active -- **AND** consumers SHALL observe the retargeted output path as current state - -#### Scenario: Output retarget failure keeps prior runtime active -- **GIVEN** an output-format retarget attempt fails before activation -- **WHEN** host code checks active runtime state and swap-complete state -- **THEN** the previously active runtime SHALL remain the active rendering runtime -- **AND** no swap-complete indication SHALL be emitted for the failed retarget - -#### Scenario: Pending reload is retargeted before swap -- **GIVEN** an explicit preset reload is building a pending runtime -- **AND** the authoritative source color-space classification changes before that runtime becomes - active -- **WHEN** the pending runtime is prepared for swap -- **THEN** the pending runtime SHALL be retargeted to the latest output format before activation -- **AND** the system SHALL NOT swap in a runtime bound to stale output format and immediately retarget - it afterward - -#### Scenario: Retarget does not change eager preset processing semantics -- **GIVEN** a preset has already been processed into an active runtime -- **WHEN** a later source color-space change triggers output-format retargeting -- **THEN** the system SHALL preserve eager preset processing behavior -- **AND** it SHALL NOT defer preset parsing, compilation, or preset-texture preparation until first - use after the retarget diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/tasks.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/tasks.md deleted file mode 100644 index a67d1dac..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/tasks.md +++ /dev/null @@ -1,44 +0,0 @@ -## 1. Runtime retarget seam - -- [x] 1.1 Introduce a dedicated output-state helper in `src/render/chain/chain_resources.hpp` and - `src/render/chain/chain_resources.cpp` so swapchain-bound output/postchain state is isolated from - persistent preset-derived runtime state without starting from a broad `ChainResources` split. -- [x] 1.2 Add a runtime retarget operation in `src/render/chain/chain_runtime.hpp` and - `src/render/chain/chain_runtime.cpp` that rebuilds only output-side state. -- [x] 1.3 Extend the C and C++ boundary APIs in `src/render/chain/api/c/goggles_filter_chain.h`, - `src/render/chain/api/c/goggles_filter_chain.cpp`, - `src/render/chain/api/cpp/goggles_filter_chain.hpp`, and - `src/render/chain/api/cpp/goggles_filter_chain.cpp` with an output-retarget entrypoint. - -## 2. Controller and backend wiring - -- [x] 2.1 Update `src/render/backend/filter_chain_controller.hpp` and - `src/render/backend/filter_chain_controller.cpp` to track authoritative output-target state and - orchestrate retarget separately from full preset reload. -- [x] 2.2 Update `src/render/backend/vulkan_backend.hpp` and - `src/render/backend/vulkan_backend.cpp` so format-only swapchain recreation calls the retarget - path instead of `init_filter_chain()` when an active chain exists. -- [x] 2.3 Audit `src/app/application.cpp` trigger flow to keep format-change detection backend-owned - and avoid reintroducing direct runtime coupling. - -## 3. Preservation and failure semantics - -- [x] 3.1 Preserve active preset selection, stage policy, prechain configuration, control layout, - and parameter overrides across successful retarget. -- [x] 3.2 Keep retarget failure non-destructive: previous runtime/output state remains active and - observable on failure. -- [x] 3.3 Define and implement retarget-vs-pending-reload interaction rules so async reload and - format retarget do not drift into overlapping state machines, with pending runtimes pre-retargeted - before swap whenever the authoritative output target changes during reload. - -## 4. Verification - -- [x] 4.1 Extend `tests/render/test_filter_chain_c_api_contracts.cpp` for retarget contract - coverage. -- [x] 4.2 Add focused runtime/controller coverage in - `tests/render/test_filter_chain_retarget.cpp` for preserved state, no preset rebuild semantics, - and failure rollback. -- [x] 4.3 Update `tests/render/test_vulkan_backend_subsystem_contracts.cpp` and any relevant - boundary tests so format-only changes expect retarget behavior while explicit reload remains the - only full rebuild entry. -- [x] 4.4 Run `pixi run build -p test` and `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure`. diff --git a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/verify-report.md b/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/verify-report.md deleted file mode 100644 index 6c079bae..00000000 --- a/openspec/changes/archive/2026-03-13-retarget-output-without-preset-reload/verify-report.md +++ /dev/null @@ -1,119 +0,0 @@ -## Verification Report - -**Change**: retarget-output-without-preset-reload -**Version**: N/A - ---- - -### Completeness -| Metric | Value | -|--------|-------| -| Tasks total | 13 | -| Tasks complete | 13 | -| Tasks incomplete | 0 | - -All tasks in sections 1–4 are marked `[x]`. - ---- - -### Build & Tests Execution - -**Build (asan)**: ✅ Passed -``` -pixi run build -p asan → [2/2] Linking CXX executable tests/goggles_tests -``` - -**Build (quality / clang-tidy)**: ✅ Passed -``` -pixi run build -p quality → ninja: no work to do. -``` - -**Tests**: ✅ 221 passed / ❌ 0 failed / ⚠️ 2 skipped -``` -test cases: 223 | 221 passed | 2 skipped -assertions: 30892 | 30892 passed -100% tests passed, 0 tests failed out of 1 -``` - -Skipped tests: -- `ScopedDebugLabel skips incomplete debug-utils dispatch` — unrelated to this change (debug-label availability guard) - -**Coverage**: ➖ Not configured - ---- - -### Spec Compliance Matrix - -#### goggles-filter-chain/spec.md - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Complete Filter Runtime Ownership Boundary | Source-independent preset work survives output retarget | `test_filter_chain_retarget.cpp` > "Filter chain output retarget preserves runtime state" | ✅ COMPLIANT | -| Host Backend Responsibility Boundary | Format retarget is handed off without full preset rebuild | `test_vulkan_backend_subsystem_contracts.cpp` > "Swapchain recreation path validates retarget vs reload distinction" + `test_filter_chain_retarget.cpp` > "Controller and backend retarget path stays distinct from reload" | ✅ COMPLIANT | -| Host Backend Responsibility Boundary | Explicit preset reload still uses rebuild path | `test_filter_chain_retarget.cpp` > "Controller and backend retarget path stays distinct from reload" (verifies `reload_shader_preset` separate from `retarget_filter_chain`) | ✅ COMPLIANT | -| Host Backend Responsibility Boundary | Pending runtime is aligned before activation | `test_filter_chain_retarget.cpp` > "Pending reload swaps only after activation and preserves authoritative state" | ✅ COMPLIANT | - -#### render-pipeline/spec.md - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Swapchain Format Matching | Source color-space change retargets output side | `test_vulkan_backend_subsystem_contracts.cpp` > "Swapchain recreation path validates retarget vs reload distinction" (verifies `retarget_filter_chain` called from `recreate_swapchain`) | ✅ COMPLIANT | -| Swapchain Format Matching | Output retarget preserves active preset state | `test_filter_chain_retarget.cpp` > "Filter chain output retarget preserves runtime state" + "Controller retarget preserves active runtime without swap signaling" | ✅ COMPLIANT | -| Runtime Shader Preset Reload | Explicit preset reload performs full rebuild | `test_filter_chain_retarget.cpp` > "Controller and backend retarget path stays distinct from reload" (structural) + "Pending reload swaps only after activation…" (behavioral) | ✅ COMPLIANT | -| Runtime Shader Preset Reload | Output retarget is not an explicit preset reload | `test_filter_chain_retarget.cpp` > "Controller retarget preserves active runtime without swap signaling" (no swap-complete after retarget) + "Controller and backend retarget path stays distinct from reload" (structural separation) | ✅ COMPLIANT | -| Runtime Shader Preset Reload | Explicit reload failure preserves previous runtime | `test_filter_chain_retarget.cpp` > "Explicit reload failure preserves the previous runtime" | ✅ COMPLIANT | -| Async Filter Lifecycle Safety | Output retarget completion is observable only after activation | `test_filter_chain_retarget.cpp` > "Controller retarget preserves active runtime without swap signaling" (verifies no swap-complete emitted for retarget-only path) | ✅ COMPLIANT | -| Async Filter Lifecycle Safety | Output retarget failure keeps prior runtime active | `test_filter_chain_retarget.cpp` > "Controller retarget failure keeps the previous runtime usable" | ✅ COMPLIANT | -| Async Filter Lifecycle Safety | Pending reload is retargeted before swap | `test_filter_chain_retarget.cpp` > "Pending reload swaps only after activation and preserves authoritative state" + "Retarget failure path stays staged and non-destructive" (structural verification of pre-retarget on pending chain) | ✅ COMPLIANT | -| Async Filter Lifecycle Safety | Retarget does not change eager preset processing semantics | `test_filter_chain_retarget.cpp` > "Filter chain output retarget preserves runtime state" (preset state preserved, no deferred processing) + `test_filter_chain_c_api_contracts.cpp` > C API retarget validation (retarget preserves control values after load) | ✅ COMPLIANT | - -**Compliance summary**: 13/13 scenarios compliant - ---- - -### Correctness (Static — Structural Evidence) -| Requirement | Status | Notes | -|------------|--------|-------| -| Output-state helper isolation | ✅ Implemented | `ChainResources::OutputState` struct at `chain_resources.hpp:42`; `retarget_output()` builds candidate, swaps on success | -| Runtime retarget operation | ✅ Implemented | `ChainRuntime::retarget_output()` at `chain_runtime.cpp:311` delegates to resources layer | -| C API retarget entrypoint | ✅ Implemented | `goggles_chain_output_retarget_vk()` in C API with validation | -| C++ API retarget entrypoint | ✅ Implemented | `FilterChainRuntime::retarget_output()` at `goggles_filter_chain.cpp:271` | -| Controller output-target tracking | ✅ Implemented | `OutputTarget` struct and `authoritative_output_target` in controller | -| Controller retarget orchestration | ✅ Implemented | `FilterChainController::retarget_filter_chain()` at `filter_chain_controller.cpp:318` | -| Backend swapchain retarget routing | ✅ Implemented | `vulkan_backend.cpp:204` calls `retarget_filter_chain` during swapchain recreation | -| Pending runtime pre-retarget | ✅ Implemented | `align_runtime_output_target()` called on pending chain before swap | -| Retarget failure non-destructive | ✅ Implemented | Candidate-then-swap pattern in `ChainResources::retarget_output()` | - ---- - -### Coherence (Design) -| Decision | Followed? | Notes | -|----------|-----------|-------| -| Split persistent preset state from retargetable output state | ✅ Yes | `OutputState` isolates swapchain-bound state; preset, controls, textures, policy survive retarget | -| Introduce dedicated output-state helper before deeper resource splitting | ✅ Yes | `OutputState` is a focused helper inside `ChainResources`, not a full split | -| Backend remains authoritative for swapchain lifecycle | ✅ Yes | `VulkanBackend::recreate_swapchain()` drives swapchain then calls retarget | -| Explicit preset reload remains full rebuild path | ✅ Yes | `reload_shader_preset` builds full pending runtime; `retarget_filter_chain` only retargets output | -| Overlapping format change retargets pending runtime before swap | ✅ Yes | `align_runtime_output_target(pending_chain, ...)` called before activation | -| Retarget failure preserves active runtime state | ✅ Yes | Candidate-then-swap in resources; controller does not destroy active chain on retarget failure | - -Design file plan lists `output_pass.hpp`/`output_pass.cpp` — both exist in tree (pre-existing files, not newly introduced in this change). All other files in the plan are modified. - ---- - -### Issues Found - -**CRITICAL** (must fix before archive): -None - -**WARNING** (should fix): -None - -**SUGGESTION** (nice to have): -None - ---- - -### Verdict -**PASS** - -All 13 tasks complete. Both ASAN and quality builds pass. All 221 tests pass (2 unrelated skips). All 13 spec scenarios are covered by passing tests. Design decisions faithfully followed. Implementation is ready for archive. diff --git a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/design.md b/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/design.md deleted file mode 100644 index e54bf7e6..00000000 --- a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/design.md +++ /dev/null @@ -1,608 +0,0 @@ -# Design: Standalone Filter-Chain Extraction - -## Technical Approach - -The filter-chain library is already architecturally self-contained after Phase 1-2 groundwork: it -owns chain orchestration, shader runtime, texture loading, diagnostics, and the public C/C++ -boundary contract through a composition of 5 OBJECT libraries into a single `goggles-filter-chain` -target with a `GogglesFilterChain::goggles-filter-chain` ALIAS. What remains is structural: severing -the 13+ file dependency on `util/logging.hpp`, `util/profiling.hpp`, and `util/serializer.hpp`; -decoupling the `goggles_diagnostics` OBJECT library from `goggles_util`; creating a standalone CMake -project at `filter-chain/`; moving sources, tests, and assets under library ownership; adding -install/export rules; and switching Goggles to package-first consumption. - -The extraction is implemented as an in-repo subdirectory (`filter-chain/`) per the key decision from -the archived Phase 1-2 design. This keeps everything in a single git history for review, allows -atomic commits that move files and update consumers together, and produces a provably standalone -CMake project ready for future `git subtree split`. - -The design follows the proposal's phased strategy (Phases 3-6), with each phase producing a -verifiable intermediate state. Each phase has a concrete build gate that proves correctness before -proceeding. - -## Architecture Decisions - -### Decision: Single composite library target with internal OBJECT modules - -**Choice**: The standalone `filter-chain/CMakeLists.txt` defines 6 internal OBJECT libraries -(`fc_chain_obj`, `fc_shader_obj`, `fc_texture_obj`, `fc_diagnostics_obj`, `fc_support_obj`, -`fc_logging_obj`) composed into one `goggles-filter-chain` library target (STATIC or SHARED). -This mirrors the existing composition model in `src/render/CMakeLists.txt`. - -**Alternatives considered**: -1. *Separate installed targets per module* (e.g., `GogglesFilterChain::chain`, - `GogglesFilterChain::shader`): Exposes internal module boundaries to consumers. The public - contract is a single library; splitting targets would complicate the package surface and invite - cherry-picking internal modules. -2. *Single flat source list without OBJECT intermediaries*: Loses per-module log tags - (`GOGGLES_LOG_TAG="render.chain"` vs `"render.shader"`), per-module compile definitions, and - per-module clang-tidy configuration. - -**Rationale**: The OBJECT composition model is already proven in-tree. Per-module OBJECT libraries -preserve log tag differentiation, targeted compile definitions, and compile-time isolation without -exposing module boundaries to consumers. The composition into a single exported target matches the -existing `GogglesFilterChain::goggles-filter-chain` contract. - -### Decision: Library-owned support shims replace util/ facades - -**Choice**: Create 3 shim files under `filter-chain/src/support/`: -- `logging.hpp` + `logging.cpp`: wraps spdlog with the same `GOGGLES_LOG_*` macro contract -- `profiling.hpp`: wraps Tracy with the same `GOGGLES_PROFILE_*` macro contract (header-only) -- `serializer.hpp`: provides `BinaryWriter`/`BinaryReader` + `read_file_binary()` (header-only) - -Each shim replicates the interface contract of the corresponding `util/` header while depending -only on its own third-party dependency. The error types (`ErrorCode`, `Error`, `Result`, -`GOGGLES_TRY`, `GOGGLES_MUST`) are already library-owned under -`include/goggles/filter_chain/error.hpp` from Phase 1-2 work. - -**Alternatives considered**: -1. *Extract util/ as a shared utility package*: Overengineered -- only 3 thin facades are needed. - The filter chain does not use config, job_system, paths, or any other util/ code. -2. *Keep depending on goggles_util at build time via add_subdirectory()*: Defeats the standalone - goal. The library cannot configure without the Goggles tree. -3. *Conditional compilation switching between util/ and library-owned shims*: Unnecessary - complexity. After sources move, no TU compiles against both. - -**Rationale**: The shims are small (logging.hpp is 57 lines, profiling.hpp is 41 lines, -serializer.hpp is 153 lines). Copying these interface contracts is cheaper and safer than -maintaining a shared dependency. The `#ifndef GOGGLES_ERROR_TYPES_DEFINED` guard on the existing -library-owned `error.hpp` already handles the ODR boundary. - -### Decision: Diagnostics decoupling by severing the goggles_util link - -**Choice**: Move all 4 `.cpp` files and ~20 headers from `src/util/diagnostics/` to -`filter-chain/src/diagnostics/`. Replace the `goggles_util` PUBLIC link with direct dependencies: -`Vulkan::Vulkan` (for `gpu_timestamp_pool`), the library-owned `error.hpp` (for `Result`), and -`spdlog::spdlog` (for `log_sink`). Remove the transitive pull of `toml11`, `BS_thread_pool`, and -`Threads`. - -**Alternatives considered**: -1. *Keep diagnostics in src/util/ and link into filter-chain as an external dependency*: The - diagnostics code is used exclusively by the filter chain. Keeping it in util/ perpetuates a - misplaced ownership boundary. -2. *Create a separate goggles_diagnostics package*: Only one consumer (filter-chain) exists. A - separate package adds complexity without benefit. - -**Rationale**: Audit of all diagnostics headers confirms the `goggles_util` link exists because -`goggles_diagnostics` is built under `src/util/` and uses `${CMAKE_SOURCE_DIR}/src` include paths --- not because it depends on config, job_system, or toml11 types. The only actual coupling is: -- `gpu_timestamp_pool.hpp` includes `` -- replaced by the library-owned - `` -- `log_sink.cpp` uses spdlog -- linked directly as PRIVATE dependency -- `diagnostic_policy.hpp`, `diagnostic_session.hpp`, `diagnostic_event.hpp`, etc. are - self-contained (no goggles_util types) - -### Decision: Asset package layout with three categories - -**Choice**: Create `filter-chain/assets/` with three subdirectories: -``` -filter-chain/assets/ - shaders/test/ # Test shader fixtures (moved from shaders/retroarch/test/) - shaders/upstream/ # Curated upstream subset (crt-lottes-fast and dependencies) - diagnostics/ # Authoring corpus + semantic probes (moved from tests/util/test_data/) -``` - -Tests resolve assets via a `FILTER_CHAIN_ASSET_DIR` compile definition pointing to this root. -Installed assets go to `/share/goggles-filter-chain/assets/`. - -**Alternatives considered**: -1. *Flat asset directory*: Loses category distinction, harder to audit what comes from upstream vs - what is library-authored. -2. *Symlinks to Goggles assets during transition*: Breaks standalone build. The standalone project - must own all assets it needs. -3. *Ship the full shaders/retroarch/ mirror*: Includes ~200+ presets the library does not test. - The curated subset contains only the files referenced by moved tests. - -**Rationale**: Three categories match the three distinct asset sources identified in the -exploration. The compile definition approach (`FILTER_CHAIN_ASSET_DIR`) is consistent with the -existing `GOGGLES_SOURCE_DIR` pattern and works from both build tree and install prefix. The curated -upstream subset is intentionally minimal -- exactly the files needed by `test_zfast_integration.cpp` -(crt-lottes-fast.slangp and its dependencies). - -### Decision: Config-file CMake package export model - -**Choice**: Export via `GogglesFilterChainConfig.cmake.in` template using CMake's install(EXPORT) -and configure_package_config_file(). Three exported targets: -- `GogglesFilterChain::goggles-filter-chain` (the canonical consumer target) -- `GogglesFilterChain::goggles-filter-chain-static` (explicit STATIC validation) -- `GogglesFilterChain::goggles-filter-chain-shared` (explicit SHARED validation) - -The config file includes `FilterChainDependencies.cmake` to resolve transitive PUBLIC dependencies -(Vulkan, expected-lite) before defining imported targets. PRIVATE dependencies (spdlog, slang, -stb_image) are not forwarded. - -**Alternatives considered**: -1. *find_package() component model*: Components are for optional subsystems. The filter chain is a - single cohesive library. -2. *Export only the primary target, no explicit static/shared*: Consumer validation requires - explicit proof of both linkage modes. Without named targets, the test project cannot force a - specific variant. -3. *pkg-config only*: Does not support imported targets, transitive dependency forwarding, or - CMake-native consumption. - -**Rationale**: The config-file package is the standard CMake approach for installed library -consumption. Including `FilterChainDependencies.cmake` from the config template ensures transitive -PUBLIC deps (Vulkan, expected-lite) are resolved before target definition, which is required for -correct imported target usage. The explicit static/shared targets exist only for validation -- the -canonical consumer target adapts to whatever variant is installed. - -### Decision: Goggles transition from add_subdirectory() to find_package() - -**Choice**: Phase 3 uses `add_subdirectory(filter-chain/)` as a transitional bridge. Phase 5 -switches to `find_package(GogglesFilterChain CONFIG REQUIRED)` as the primary path. -`add_subdirectory()` remains available as an optional local-development convenience via a CMake -option (e.g., `GOGGLES_USE_BUNDLED_FILTER_CHAIN`). - -**Alternatives considered**: -1. *Jump directly to find_package() in Phase 3*: Impossible -- install/export rules are not ready - until Phase 5. Goggles would break during the intermediate phases. -2. *Remove add_subdirectory() entirely after switch*: Loses the side-by-side development - convenience for iterating on filter-chain changes within the Goggles tree. -3. *Always use add_subdirectory() and defer find_package() to a future change*: Does not prove the - installed package contract, which is the core success criterion. - -**Rationale**: The transitional bridge maintains continuous build integrity while sources are moved. -The CMake option pattern (`GOGGLES_USE_BUNDLED_FILTER_CHAIN`) is a well-established convention for -"vendored vs system" dependency selection. - -## Data Flow - -### Standalone project build, install, and consumption flow - -``` -filter-chain/ - CMakeLists.txt - cmake/ - FilterChainDependencies.cmake - GogglesFilterChainConfig.cmake.in - CompilerConfig.cmake - CodeQuality.cmake - include/ - goggles_filter_chain.h - goggles_filter_chain.hpp - goggles/filter_chain/*.hpp - src/ - chain/ (moved from src/render/chain/) - shader/ (moved from src/render/shader/) - texture/ (moved from src/render/texture/) - diagnostics/ (moved from src/util/diagnostics/) - support/ (library-owned logging, profiling, serializer shims) - tests/ - contract/ (~22 moved contract test files) - consumer/ (out-of-tree find_package() validation projects) - assets/ - shaders/test/ (moved from shaders/retroarch/test/) - shaders/upstream/ (curated crt-lottes-fast subset) - diagnostics/ (moved from tests/util/test_data/filter_chain_diagnostics/) - -cmake configure (cmake -S filter-chain/ -B build) - -> FilterChainDependencies.cmake resolves Vulkan, expected-lite, spdlog, slang, stb_image - -> Define OBJECT libraries: fc_chain_obj, fc_shader_obj, fc_texture_obj, - fc_diagnostics_obj, fc_support_obj, fc_logging_obj - -> Compose goggles-filter-chain target (STATIC or SHARED) - -> Build test executable from tests/contract/ sources - -cmake install (cmake --install build --prefix ) - -> /include/ (public headers) - -> /lib/ (libgoggles-filter-chain.a and/or .so) - -> /lib/cmake/GogglesFilterChain/ (package config + targets) - -> /share/goggles-filter-chain/assets/ (installed assets) -``` - -### Goggles consumption after switch - -``` -Goggles CMakeLists.txt - -> find_package(GogglesFilterChain CONFIG REQUIRED) - -> GogglesFilterChainConfig.cmake - -> include(FilterChainDependencies.cmake) # resolve Vulkan, expected-lite - -> include(GogglesFilterChainTargets.cmake) # define imported targets - -> target_link_libraries(goggles_render PUBLIC GogglesFilterChain::goggles-filter-chain) - -Goggles backend code - -> #include (installed C++ wrapper) - -> #include (installed public header) - -> #include (installed public header) - -> Resolved from installed package include paths, not source tree -``` - -### Include path remapping for moved sources - -``` -BEFORE (in-tree): AFTER (standalone): - #include -> #include "support/logging.hpp" - #include -> #include "support/profiling.hpp" - #include -> #include "support/serializer.hpp" - #include -> #include - #include -> #include "diagnostics/X.hpp" - #include -> #include "chain/X.hpp" - #include -> #include "shader/X.hpp" -``` - -## File Changes - -### New files (standalone project skeleton) - -| File | Action | Description | -|------|--------|-------------| -| `filter-chain/CMakeLists.txt` | Create | Top-level standalone project definition: project(), option for STATIC/SHARED, OBJECT library composition, install/export rules, test enablement | -| `filter-chain/cmake/FilterChainDependencies.cmake` | Create | Resolves Vulkan, expected-lite, spdlog, slang, stb_image, Catch2 via standard find_package() | -| `filter-chain/cmake/GogglesFilterChainConfig.cmake.in` | Create | Package config template: includes FilterChainDependencies for transitive deps, includes targets export file | -| `filter-chain/cmake/CompilerConfig.cmake` | Create | C++20 enforcement, ccache, warning flags, sanitizer helpers (subset of goggles cmake/CompilerConfig.cmake) | -| `filter-chain/cmake/CodeQuality.cmake` | Create | clang-tidy helper function (subset of goggles cmake/CodeQuality.cmake) | -| `filter-chain/.clang-format` | Create | Symlink or copy of root `.clang-format` for standalone formatting | -| `filter-chain/.clang-tidy` | Create | Symlink or copy of root `.clang-tidy` for standalone linting | - -### New files (library-owned support shims) - -| File | Action | Description | -|------|--------|-------------| -| `filter-chain/src/support/logging.hpp` | Create | spdlog facade with GOGGLES_LOG_* macros matching util/logging.hpp contract | -| `filter-chain/src/support/logging.cpp` | Create | Logger initialization and singleton, matching util/logging.cpp | -| `filter-chain/src/support/profiling.hpp` | Create | Tracy facade with GOGGLES_PROFILE_* macros matching util/profiling.hpp contract (header-only) | -| `filter-chain/src/support/serializer.hpp` | Create | BinaryWriter/BinaryReader + read_file_binary() matching util/serializer.hpp contract (header-only) | - -### New files (consumer validation) - -| File | Action | Description | -|------|--------|-------------| -| `filter-chain/tests/consumer/static/CMakeLists.txt` | Create | Out-of-tree consumer that links GogglesFilterChain::goggles-filter-chain-static | -| `filter-chain/tests/consumer/static/main.cpp` | Create | Minimal consumer: includes public headers, instantiates types, verifies linkage | -| `filter-chain/tests/consumer/shared/CMakeLists.txt` | Create | Out-of-tree consumer that links GogglesFilterChain::goggles-filter-chain-shared | -| `filter-chain/tests/consumer/shared/main.cpp` | Create | Minimal consumer: same as static variant | - -### Moved files (source migration) - -| File | Action | Description | -|------|--------|-------------| -| `src/render/chain/*.cpp` (14 files) | Move | `chain_runtime.cpp`, `chain_builder.cpp`, `chain_resources.cpp`, `chain_executor.cpp`, `chain_controls.cpp`, `filter_controls.cpp`, `filter_pass.cpp`, `framebuffer.cpp`, `output_pass.cpp`, `downsample_pass.cpp`, `preset_parser.cpp`, `frame_history.cpp`, `vulkan_dispatch.cpp`, `api/c/goggles_filter_chain.cpp`, `api/cpp/goggles_filter_chain.cpp` -> `filter-chain/src/chain/` | -| `src/render/chain/*.hpp` (16 files) | Move | All private headers (`chain_runtime.hpp`, `chain_builder.hpp`, `chain_resources.hpp`, `chain_executor.hpp`, `chain_controls.hpp`, `filter_pass.hpp`, `framebuffer.hpp`, `output_pass.hpp`, `downsample_pass.hpp`, `preset_parser.hpp`, `frame_history.hpp`, `debug_label_scope.hpp`, `pass.hpp`, `semantic_binder.hpp`, `vulkan_result.hpp`) -> `filter-chain/src/chain/` | -| `src/render/chain/include/goggles/filter_chain/*.hpp` (5 files) | Move | `error.hpp`, `filter_controls.hpp`, `result.hpp`, `scale_mode.hpp`, `vulkan_context.hpp` -> `filter-chain/include/goggles/filter_chain/` | -| `src/render/chain/api/c/goggles_filter_chain.h` | Move | -> `filter-chain/include/goggles_filter_chain.h` | -| `src/render/chain/api/cpp/goggles_filter_chain.hpp` | Move | -> `filter-chain/include/goggles_filter_chain.hpp` | -| `src/render/shader/*.cpp` (3 files) | Move | `shader_runtime.cpp`, `retroarch_preprocessor.cpp`, `slang_reflect.cpp` -> `filter-chain/src/shader/` | -| `src/render/shader/*.hpp` (3 files) | Move | `shader_runtime.hpp`, `retroarch_preprocessor.hpp`, `slang_reflect.hpp` -> `filter-chain/src/shader/` | -| `src/render/texture/texture_loader.cpp` | Move | -> `filter-chain/src/texture/` | -| `src/render/texture/texture_loader.hpp` | Move | -> `filter-chain/src/texture/` | -| `src/util/diagnostics/*.cpp` (4 files) | Move | `log_sink.cpp`, `test_harness_sink.cpp`, `diagnostic_session.cpp`, `gpu_timestamp_pool.cpp` -> `filter-chain/src/diagnostics/` | -| `src/util/diagnostics/*.hpp` (~20 files) | Move | All diagnostics headers -> `filter-chain/src/diagnostics/` | - -### Moved files (test migration -- ~22 contract test files) - -| File | Action | Description | -|------|--------|-------------| -| `tests/render/test_filter_chain.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_filter_chain_c_api_contracts.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_filter_chain_retarget_contract.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_filter_controls.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_preset_parser.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_retroarch_preprocessor.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_slang_reflect.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_shader_runtime.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_semantic_binder.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_zfast_integration.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_shader_validation.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_runtime_diagnostics.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_diagnostic_event_model.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_diagnostic_sinks.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_binding_ledger.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_diagnostic_ledgers.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_diagnostic_session.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_diagnostic_reporting.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_source_provenance.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_compile_report.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_authoring_validation.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_gpu_timestamp_pool.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/test_shader_batch_report.cpp` | Move | -> `filter-chain/tests/contract/` | -| `tests/render/shader_batch_report.cpp` | Move | -> `filter-chain/tests/contract/` (test helper) | -| `tests/render/shader_batch_report.hpp` | Move | -> `filter-chain/tests/contract/` (test helper) | -| `tests/render/shader_batch_report_main.cpp` | Move | -> `filter-chain/tests/contract/` (batch report tool) | - -### Moved files (asset migration) - -| File | Action | Description | -|------|--------|-------------| -| `shaders/retroarch/test/*` (13 files) | Copy | -> `filter-chain/assets/shaders/test/` (Goggles retains original for its own tests) | -| `tests/util/test_data/filter_chain_diagnostics/*` (13 files) | Move | -> `filter-chain/assets/diagnostics/` | -| `shaders/retroarch/crt/crt-lottes-fast.*` + deps | Copy | -> `filter-chain/assets/shaders/upstream/crt/` (curated subset for zfast tests) | - -### Modified files (Goggles integration) - -| File | Action | Description | -|------|--------|-------------| -| `src/render/CMakeLists.txt` | Modify | Replace chain/shader/texture OBJECT library definitions and goggles-filter-chain composition with `find_package(GogglesFilterChain)` or `add_subdirectory(filter-chain/)`. Remove in-tree chain/shader/texture `add_subdirectory()` calls. | -| `src/render/chain/CMakeLists.txt` | Remove | Sources moved to standalone project | -| `src/render/shader/CMakeLists.txt` | Remove | Sources moved to standalone project | -| `src/render/texture/CMakeLists.txt` | Remove | Sources moved to standalone project | -| `src/util/CMakeLists.txt` | Modify | Remove `goggles_diagnostics` target definition. Remove `goggles_util_logging_obj` inclusion in filter-chain composition. Keep `goggles_util_logging_obj` for `goggles_util` itself. Remove `add_subdirectory(diagnostics)`. | -| `src/util/diagnostics/CMakeLists.txt` | Remove | Sources moved to standalone project | -| `tests/CMakeLists.txt` | Modify | Remove ~22 contract test source references from `goggles_tests`. Keep 3 host integration tests. Keep shader_batch_report tool if it stays in Goggles. | -| `CMakePresets.json` | Modify | Remove `.shared` hidden preset and `test-shared` configure/build/test presets after package validation proves both linkage modes | -| `src/render/backend/filter_chain_controller.hpp` | Modify | Includes already use installed-surface paths (``, ``). Remove `` if the config type reference can be decoupled. | -| `src/render/backend/filter_chain_controller.cpp` | Modify | Includes already use `` and `` -- these stay (they are Goggles-owned host code, not filter-chain library code). No change needed. | -| `CMakeLists.txt` | Modify | Add `option(GOGGLES_USE_BUNDLED_FILTER_CHAIN ...)` and conditional `add_subdirectory(filter-chain/)` or `find_package(GogglesFilterChain)` | - -### Files NOT changed - -| File | Reason | -|------|--------| -| `src/render/backend/vulkan_backend.cpp` | Already uses only Goggles-local `util/` includes. Not part of filter-chain library. | -| `src/render/backend/vulkan_backend.hpp` | Includes are backend-specific. No filter-chain include changes needed. | -| `src/util/logging.hpp` | Goggles retains its own copy for non-filter-chain code. | -| `src/util/profiling.hpp` | Goggles retains its own copy for non-filter-chain code. | -| `src/util/serializer.hpp` | Goggles retains its own copy for non-filter-chain code. | -| `src/util/error.hpp` | Goggles retains its own copy. The `#ifndef GOGGLES_ERROR_TYPES_DEFINED` guard prevents ODR violations. | -| `shaders/retroarch/` (full mirror) | Not moved. Goggles retains the full upstream mirror. | -| `tests/render/test_filter_boundary_contracts.cpp` | Host integration test -- stays in Goggles | -| `tests/render/test_vulkan_backend_subsystem_contracts.cpp` | Host integration test -- stays in Goggles | -| `tests/render/test_filter_chain_retarget.cpp` | Host integration test -- stays in Goggles | - -## Interfaces / Contracts - -### Package export model - -Consumers use: -```cmake -find_package(GogglesFilterChain CONFIG REQUIRED) -target_link_libraries(my_target PRIVATE GogglesFilterChain::goggles-filter-chain) -``` - -Exported targets: - -| Target | Purpose | -|--------|---------| -| `GogglesFilterChain::goggles-filter-chain` | Canonical consumer target (resolves to installed STATIC or SHARED) | -| `GogglesFilterChain::goggles-filter-chain-static` | Explicit STATIC target for validation | -| `GogglesFilterChain::goggles-filter-chain-shared` | Explicit SHARED target for validation | - -Transitive dependencies forwarded through package config: -- PUBLIC: `Vulkan::Vulkan`, `nonstd::expected-lite` -- PRIVATE (not forwarded): `spdlog::spdlog`, `slang::slang`, `stb_image` - -### Asset resolution contract - -Build-tree tests: -```cmake -target_compile_definitions(fc_tests PRIVATE - FILTER_CHAIN_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets") -``` - -Installed-surface tests: -```cmake -target_compile_definitions(fc_tests PRIVATE - FILTER_CHAIN_ASSET_DIR="${CMAKE_INSTALL_PREFIX}/share/goggles-filter-chain/assets") -``` - -Test code resolves fixtures as: -```cpp -auto preset = std::filesystem::path(FILTER_CHAIN_ASSET_DIR) / "shaders/test/format.slangp"; -``` - -### Support shim interface contracts - -**Logging shim** (`filter-chain/src/support/logging.hpp`): -- Provides: `goggles::initialize_logger()`, `goggles::get_logger()`, `goggles::set_log_level()` -- Provides: `GOGGLES_LOG_TRACE/DEBUG/INFO/WARN/ERROR/CRITICAL` macros with `GOGGLES_LOG_TAG` -- Depends on: `spdlog::spdlog` - -**Profiling shim** (`filter-chain/src/support/profiling.hpp`): -- Provides: `GOGGLES_PROFILE_FRAME/FUNCTION/SCOPE/TAG/VALUE` macros -- Depends on: Tracy when `TRACY_ENABLE` is defined; no-op otherwise - -**Serializer shim** (`filter-chain/src/support/serializer.hpp`): -- Provides: `goggles::util::BinaryWriter`, `goggles::util::BinaryReader`, `goggles::util::read_file_binary()` -- Depends on: standard library + `goggles/filter_chain/error.hpp` - -### Goggles integration option - -```cmake -# In root CMakeLists.txt or src/render/CMakeLists.txt -option(GOGGLES_USE_BUNDLED_FILTER_CHAIN - "Build filter-chain from in-repo subdirectory instead of finding installed package" OFF) - -if(GOGGLES_USE_BUNDLED_FILTER_CHAIN) - add_subdirectory(${CMAKE_SOURCE_DIR}/filter-chain) -else() - find_package(GogglesFilterChain CONFIG REQUIRED) -endif() - -# Either path makes GogglesFilterChain::goggles-filter-chain available -target_link_libraries(goggles_render PUBLIC GogglesFilterChain::goggles-filter-chain) -``` - -### Standalone project CMakeLists.txt structure (key sections) - -```cmake -cmake_minimum_required(VERSION 3.20) -project(GogglesFilterChain VERSION 0.1.0 LANGUAGES CXX) - -# Options -set(FILTER_CHAIN_LIBRARY_TYPE "STATIC" CACHE STRING "Library type (STATIC or SHARED)") -option(FILTER_CHAIN_BUILD_TESTS "Build tests" ON) -option(ENABLE_CLANG_TIDY "Enable clang-tidy" OFF) -option(ENABLE_ASAN "Enable AddressSanitizer" OFF) - -# CMake modules -include(cmake/CompilerConfig.cmake) -include(cmake/CodeQuality.cmake) -include(cmake/FilterChainDependencies.cmake) -include(GNUInstallDirs) -include(CMakePackageConfigHelpers) - -# OBJECT libraries with per-module log tags -add_library(fc_chain_obj OBJECT ...) -target_compile_definitions(fc_chain_obj PRIVATE GOGGLES_LOG_TAG="render.chain") - -add_library(fc_shader_obj OBJECT ...) -target_compile_definitions(fc_shader_obj PRIVATE GOGGLES_LOG_TAG="render.shader") - -add_library(fc_texture_obj OBJECT ...) -target_compile_definitions(fc_texture_obj PRIVATE GOGGLES_LOG_TAG="render.texture") - -add_library(fc_diagnostics_obj OBJECT ...) -add_library(fc_support_obj OBJECT ...) # logging.cpp only -add_library(fc_logging_obj OBJECT ...) # => merged into fc_support_obj - -# Composed library -add_library(goggles-filter-chain ${FILTER_CHAIN_LIBRARY_TYPE} - $ - $ - $ - $ - $ -) - -# Include directories: only standalone-relative paths -target_include_directories(goggles-filter-chain - PUBLIC - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src -) - -# Public compile definitions -target_compile_definitions(goggles-filter-chain PUBLIC - VULKAN_HPP_NO_EXCEPTIONS - VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 -) - -# Link libraries -target_link_libraries(goggles-filter-chain - PUBLIC Vulkan::Vulkan nonstd::expected-lite - PRIVATE spdlog::spdlog slang::slang stb_image -) - -# ALIAS for add_subdirectory() consumers -add_library(GogglesFilterChain::goggles-filter-chain ALIAS goggles-filter-chain) - -# Install rules (Phase 5) -install(TARGETS goggles-filter-chain EXPORT GogglesFilterChainTargets ...) -install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install(DIRECTORY assets/ DESTINATION share/goggles-filter-chain/assets) -install(EXPORT GogglesFilterChainTargets NAMESPACE GogglesFilterChain:: ...) - -# Tests -if(FILTER_CHAIN_BUILD_TESTS) - enable_testing() - add_subdirectory(tests) -endif() -``` - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|-------------|----------| -| Unit (standalone) | Preset parser, preprocessor, reflection, control helpers, diagnostics models, ledgers, sessions, sinks, source provenance, compile report | ~22 contract test files under `filter-chain/tests/contract/`, compiled against library-owned headers and linked to `goggles-filter-chain` target. Asset paths resolved via `FILTER_CHAIN_ASSET_DIR`. | -| Integration (standalone) | C API contracts, C++ wrapper contracts, retarget contract, zfast shader integration, shader validation | Same test executable, using curated upstream shader subset from `filter-chain/assets/shaders/upstream/`. | -| Consumer validation (standalone) | Package discoverability and linkage for STATIC and SHARED | Two minimal out-of-tree CMake projects under `filter-chain/tests/consumer/` that configure, build, and link against the installed package via `find_package(GogglesFilterChain)`. Run as CTest fixtures after install. | -| Host integration (Goggles) | FilterChainController wiring, VulkanBackend subsystem contracts, filter boundary behavior | 3 test files remain in `tests/render/`: `test_filter_boundary_contracts.cpp`, `test_vulkan_backend_subsystem_contracts.cpp`, `test_filter_chain_retarget.cpp`. These compile against the installed or subdirectory-provided target. | -| Asset verification (standalone) | Packaged fixture resolution | Tested implicitly by all contract tests resolving through `FILTER_CHAIN_ASSET_DIR`. Explicit test: verify key asset files exist at expected paths. | -| Include-path isolation (CI gate) | No Goggles-layout includes in standalone tree | `grep -r '#include ` and `` to - standalone-relative paths. -6. Update Goggles `src/render/CMakeLists.txt` to use `add_subdirectory(filter-chain/)` bridge. -7. Remove `src/render/chain/CMakeLists.txt`, `shader/CMakeLists.txt`, `texture/CMakeLists.txt`. - -**Gate**: `cmake -S filter-chain/ -B /tmp/fc-build && cmake --build /tmp/fc-build` succeeds. -**Gate**: `pixi run build -p asan && pixi run test -p asan` passes (Goggles via bridge). - -### Phase 4: Diagnostics and assets under library ownership - -1. Move diagnostics sources and headers to `filter-chain/src/diagnostics/`. -2. Replace `goggles_util` link with direct deps (Vulkan, spdlog). -3. Update diagnostics `#include ` to ``. -4. Create `filter-chain/assets/` with three categories. -5. Move ~22 contract test files to `filter-chain/tests/contract/`. -6. Update moved tests to use `FILTER_CHAIN_ASSET_DIR` instead of `GOGGLES_SOURCE_DIR`. -7. Remove moved test sources from Goggles `tests/CMakeLists.txt`. -8. Remove `src/util/diagnostics/CMakeLists.txt` and update `src/util/CMakeLists.txt`. - -**Gate**: `ctest --test-dir /tmp/fc-build` passes all contract tests. -**Gate**: `pixi run build -p quality` passes (no clang-tidy regressions). - -### Phase 5: Install, export, and package-first switch - -1. Add install/export rules to `filter-chain/CMakeLists.txt`. -2. Create `GogglesFilterChainConfig.cmake.in` template. -3. Add consumer validation projects under `filter-chain/tests/consumer/`. -4. Install to test prefix and run consumer validation. -5. Switch Goggles from `add_subdirectory()` to `find_package()` with - `GOGGLES_USE_BUNDLED_FILTER_CHAIN` option. -6. Remove `.shared` and `test-shared` presets from `CMakePresets.json`. - -**Gate**: Consumer validation succeeds for both STATIC and SHARED. -**Gate**: `pixi run build -p asan && pixi run test -p asan` passes with `find_package()` path. - -### Phase 6: End-to-end verification - -1. Clean-checkout standalone build: configure, build, test, install from scratch. -2. Run installed-surface contract tests. -3. Run Goggles build + host integration tests against installed package. -4. Verify include-path isolation (grep audit). -5. Verify `cmake --graphviz` on standalone project shows no Goggles target edges. - -**Gate**: Full CI-parity: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. - -## Open Questions - -- [ ] None. All technical decisions have been resolved through the exploration, proposal, and - archived design. The diagnostics audit confirmed no hidden type-level coupling to goggles_util. diff --git a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/exploration.md b/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/exploration.md deleted file mode 100644 index e6adbca1..00000000 --- a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/exploration.md +++ /dev/null @@ -1,133 +0,0 @@ -# Exploration: Phase 3-6 Standalone Filter-Chain Extraction - -## Current State - -Phase 1-2 monorepo groundwork is complete and archived. The `goggles-filter-chain` target is -decoupled at the public header level — 5 public headers under `include/goggles/filter_chain/` are -self-contained, internal headers use canonical paths, and no PUBLIC linkage to `goggles_util` -remains. The target composes 5 OBJECT libraries (chain, shader, texture, diagnostics, logging) into -a single STATIC or SHARED artifact with a `GogglesFilterChain::goggles-filter-chain` ALIAS ready -for future `find_package()` consumption. - -## Remaining Coupling (must sever for standalone build) - -### 1. util/logging.hpp and util/profiling.hpp (13 .cpp files each) -Every implementation file in chain/, shader/, texture/ includes both. These are lightweight facades -over spdlog and Tracy. The standalone project needs its own logging/profiling shims. - -### 2. util/serializer.hpp (1 .cpp file) -Only `shader/shader_runtime.cpp`. Small serialization utility for shader cache. - -### 3. Diagnostics implementation (util/diagnostics/) -The `goggles_diagnostics` OBJECT library (4 .cpp files) is already bundled into the filter-chain -artifact. However, it currently links `goggles_util` which transitively pulls in `toml11`, -`BS_thread_pool`, and `Threads` — none needed by the filter chain. This transitive coupling must be -severed: the diagnostics code needs to be self-contained under standalone ownership. - -Chain/shader headers reference 10 distinct diagnostics headers (`diagnostic_policy.hpp`, -`diagnostic_session.hpp`, `gpu_timestamp_pool.hpp`, `compile_report.hpp`, -`source_provenance.hpp`, `diagnostic_event.hpp`, `log_sink.hpp`, `diagnostic_sink.hpp`, -`test_harness_sink.hpp`, `binding_ledger.hpp`). - -### 4. Include path coupling -PRIVATE include dirs `${CMAKE_SOURCE_DIR}/src` and `${CMAKE_SOURCE_DIR}/src/render` are used by -the filter-chain target. All internal `#include ` and `` paths assume -this layout. - -## Test Split - -**Contract tests (move to standalone — ~22 files):** -- C API contracts, retarget contract, filter controls, chain resources, preset parser, - retroarch preprocessor, slang reflect, shader runtime, semantic binder, zfast integration, - runtime diagnostics, GPU timestamp pool, 10+ diagnostics unit tests - -**Host integration tests (stay in Goggles — 3 files):** -- `test_filter_boundary_contracts.cpp` (reads VulkanBackend/Controller source) -- `test_vulkan_backend_subsystem_contracts.cpp` (tests controller as backend subsystem) -- `test_filter_chain_retarget.cpp` (tests via FilterChainController) - -## Asset Inventory - -Three asset categories need standalone ownership: - -1. **Test shader fixtures** (`shaders/retroarch/test/`): format.slangp, history.slangp, - feedback.slangp, frame_count.slangp, pragma-name.slangp, decode-format.slang (~12 files) -2. **Diagnostics test corpus** (`tests/util/test_data/filter_chain_diagnostics/`): authoring - corpus (valid/invalid/reflection), semantic probes (~14 files) -3. **Real upstream shaders** (`shaders/retroarch/crt/`, etc.): used by zfast integration and - shader validation. The standalone project should own a curated test subset, not the full mirror. - -## Third-Party Dependencies - -| Library | Link | Source | Standalone needs | -|---------|------|--------|------------------| -| Vulkan::Vulkan | PUBLIC | System SDK | find_package(Vulkan) | -| nonstd::expected-lite | PUBLIC | Pixi/conda | find_package(expected-lite) | -| spdlog::spdlog | PRIVATE | Pixi/conda | find_package(spdlog) | -| slang::slang | PRIVATE | Pixi/conda | find_package(slang CONFIG) | -| stb_image | PRIVATE | Pixi/conda (header-only) | INTERFACE target or find_package | -| Catch2 | TEST | Pixi/conda | find_package(Catch2) | - -All are resolved via standard `find_package()` — no Goggles-specific Find modules needed. The -standalone `cmake/FilterChainDependencies.cmake` can wrap these cleanly. - -## Approaches - -### Approach A: Sibling directory extraction (recommended) - -Create the standalone project as a sibling directory (e.g., `../goggles-filter-chain/`) with its -own git repo. Copy sources (not move — keep Goggles building during transition), establish the -standalone build, prove it works, then switch Goggles to `find_package()` consumption and remove -the in-repo copies. - -- Pros: Clean separation from day one, standalone CI possible immediately, Goggles never breaks - during transition -- Cons: Temporary code duplication during transition, two repos to manage -- Effort: High (but matches the design goal of "independent project") - -### Approach B: In-repo subdirectory then extract - -Create `filter-chain/` inside the Goggles repo as a self-contained CMake subproject. Prove it -builds standalone via `cmake -S filter-chain/`. Then move to its own repo. - -- Pros: Single repo during development, easier to iterate, atomic commits -- Cons: Muddies "standalone" claim until final extraction, risk of re-coupling -- Effort: Medium - -### Approach C: Incremental in-place extraction - -Keep sources where they are but incrementally make the existing OBJECT libraries buildable from a -standalone CMakeLists.txt that references `src/render/chain/` etc. relative to a different root. - -- Pros: No file moves until the very end -- Cons: Fragile path assumptions, doesn't prove standalone ownership, hard to verify -- Effort: Low initially, high to finalize - -## Recommendation - -**Approach B** (in-repo subdirectory) provides the best balance for this workspace. It keeps -everything in one git history for review, allows atomic commits that move files + update consumers, -and still produces a provably standalone CMake project. The final repo extraction is a clean -`git subtree split` or directory copy once verification passes. - -The key ordering constraint: sever the diagnostics → goggles_util coupling FIRST (Phase 4.3), -because that transitive dependency on toml11/BS_thread_pool is the deepest remaining coupling. -Everything else (logging shim, profiling shim, serializer) is straightforward. - -## Risks - -- Diagnostics decoupling may require interface changes if diagnostic types reference goggles_util - types (config, job_system) — needs careful audit -- Shader validation tests scan the full `shaders/retroarch/` mirror — the standalone project - should own only a curated subset to avoid mirroring upstream content -- The `test-shared` transitional presets must not be removed until the standalone project proves - both STATIC and SHARED consumption — ordering matters - -## Open Questions - -- Where should the standalone project root live during development? (sibling dir vs in-repo subdir) -- Should the standalone project get its own Pixi environment or rely on system/vcpkg for deps? - -## Ready for Proposal - -Yes — with a decision on project location. Recommend asking the user. diff --git a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/proposal.md b/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/proposal.md deleted file mode 100644 index a549841c..00000000 --- a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/proposal.md +++ /dev/null @@ -1,279 +0,0 @@ -# Proposal: Standalone Filter-Chain Extraction - -## Intent - -### Problem - -The `goggles-filter-chain` library is architecturally independent -- it owns chain orchestration, -shader runtime, texture loading, diagnostics, and the public C/C++ boundary contract -- but it -cannot build, test, or verify outside the Goggles source tree. Every implementation file in chain/, -shader/, and texture/ includes `util/logging.hpp` and `util/profiling.hpp`. The diagnostics OBJECT -library links `goggles_util` which transitively pulls in `toml11`, `BS_thread_pool`, and `Threads` --- dependencies the filter chain does not use. Internal `#include ` and -`` paths assume the Goggles `${CMAKE_SOURCE_DIR}/src` layout. Contract tests compile -against `goggles_render` and `goggles_util` rather than the installed public surface. - -This coupling prevents: -- Independent release and versioning of the filter-chain library. -- External consumers using `find_package(GogglesFilterChain)` without the Goggles repo. -- Verification that the public surface is self-contained. -- Clear ownership boundaries between library-owned and host-owned concerns. - -Phase 1-2 monorepo groundwork (archived) established the public header boundary, canonical include -paths, and the `GogglesFilterChain::goggles-filter-chain` ALIAS target. This change completes the -extraction by creating a provably standalone CMake project, severing remaining coupling, moving -ownership of assets and tests, and switching Goggles to package-first consumption. - -### Why now - -The public header surface is clean and self-contained. The OBJECT library composition model is -proven. The boundary contract specs (`goggles-filter-chain`, `filter-chain-c-api`, -`filter-chain-cpp-wrapper`, `filter-chain-assets-package`, `build-system`) already describe the -standalone target state. What remains is implementation: moving code, severing coupling, packaging, -and verifying. - -## Scope - -### In Scope - -1. **Standalone project skeleton** (`filter-chain/`): Create an in-repo CMake subproject with the - layout `CMakeLists.txt`, `cmake/`, `include/`, `src/`, `tests/`, `assets/` that configures and - builds independently via `cmake -S filter-chain/`. - -2. **Source migration**: Move the reusable implementation from `src/render/chain/`, - `src/render/shader/`, `src/render/texture/`, and the diagnostics subset from - `src/util/diagnostics/` into standalone-owned modules under `filter-chain/src/`. - -3. **Support code ownership**: Create library-owned logging, profiling, and serializer shims under - `filter-chain/src/support/` to replace the 13+ file dependency on `util/logging.hpp`, - `util/profiling.hpp`, and `util/serializer.hpp`. - -4. **Diagnostics decoupling**: Sever the `goggles_diagnostics` -> `goggles_util` link. Move the 4 - diagnostics `.cpp` files and their 10 referenced headers under standalone ownership. Eliminate - the transitive dependency on `toml11`, `BS_thread_pool`, and `Threads`. - -5. **Asset package**: Move test shader fixtures (`shaders/retroarch/test/`), diagnostics test corpus - (`tests/util/test_data/filter_chain_diagnostics/`), and a curated subset of upstream shaders - under `filter-chain/assets/`. Make runtime and test asset lookup package-oriented rather than - Goggles-checkout-relative. - -6. **Contract test migration**: Move ~22 reusable contract test files to `filter-chain/tests/` - (C API contracts, retarget contract, filter controls, preset parser, preprocessor, reflect, - shader runtime, semantic binder, zfast integration, all diagnostics unit tests). Keep 3 host - integration tests in Goggles (`test_filter_boundary_contracts.cpp`, - `test_vulkan_backend_subsystem_contracts.cpp`, `test_filter_chain_retarget.cpp`). - -7. **Build, install, and export**: Publish `STATIC` and `SHARED` library targets. Install headers, - libraries, assets, and CMake package metadata. Export - `GogglesFilterChain::goggles-filter-chain` plus explicit static/shared targets via config-file - packages. Create `cmake/FilterChainDependencies.cmake` for third-party discovery and - `cmake/GogglesFilterChainConfig.cmake.in` for package metadata. - -8. **Downstream consumer validation**: Add out-of-tree consumer validation tests that use - `find_package(GogglesFilterChain CONFIG REQUIRED)` for both static and shared linkage. - -9. **Goggles integration switch**: Update Goggles to consume the installed package through - `find_package(...)` as the primary path, with `add_subdirectory(filter-chain/)` kept only as - an explicit development convenience. Remove the transitional `.shared` and `test-shared` presets - from `CMakePresets.json` once standalone package validation proves both linkage modes. - -10. **End-to-end verification**: Prove the standalone project can configure, build, test, install, - and validate from a clean checkout. Run Goggles build + host verification against the installed - package. - -### Out of Scope - -- **Contract redesign**: The public C/C++ API surface, diagnostics event model, and boundary - behavior are preserved as-is. No new APIs or behavioral changes. -- **Repository extraction**: The final `git subtree split` or move to a separate repository is - deferred. This change produces a provably standalone subdirectory that is ready for extraction. -- **New Pixi environment for standalone project**: The standalone project uses standard - `find_package()` for all dependencies. It does not get its own Pixi environment. -- **Upstream shader mirror changes**: The full `shaders/retroarch/` mirror stays in Goggles. The - standalone project owns only a curated test subset under `filter-chain/assets/`. -- **CI pipeline for standalone project**: CI configuration for the extracted project is future work. - This change validates via local build/test/install workflows. -- **MODULE library variant**: Not part of the supported package surface per existing specs. - -## Approach - -The extraction follows the phased strategy from the archived design, implemented as an in-repo -subdirectory (`filter-chain/`) per the key decision already made. Each phase produces a verifiable -intermediate state. - -### Phase 3: Create standalone skeleton and move implementation - -Create the `filter-chain/` root with a top-level `CMakeLists.txt` that defines the -`goggles-filter-chain` target from source files under `filter-chain/src/`. Move (not copy) the -chain, shader, and texture implementation files. Create library-owned support shims for logging, -profiling, and serialization that satisfy the same interface contracts as the `util/` originals but -depend only on spdlog (logging), Tracy (profiling), and standard library (serializer). Update all -internal include paths from `` and `` to standalone-relative paths. -Move the ~22 contract test files to `filter-chain/tests/`. - -The key ordering constraint: the standalone project must configure and build after this phase. -Goggles will temporarily use `add_subdirectory(filter-chain/)` to consume the target. - -### Phase 4: Move assets and diagnostics under library ownership - -Move diagnostics implementation (4 `.cpp` files, ~10 headers) under `filter-chain/src/diagnostics/`. -Sever the `goggles_util` link by replacing the 2 actual goggles_util dependencies in diagnostics -code (the `goggles_util` PUBLIC link exists because `goggles_diagnostics` lives under `src/util/` -and uses `${CMAKE_SOURCE_DIR}/src` includes -- not because it needs config, job_system, or toml11). -Create the library-owned asset package under `filter-chain/assets/` and update test fixtures to -resolve assets from the package rather than Goggles checkout paths. - -### Phase 5: Build, install, export, and switch to package-first - -Add CMake install rules, export sets, and package config templates. Publish both STATIC and SHARED -variants. Add downstream consumer validation projects. Switch Goggles `src/render/CMakeLists.txt` -from `add_subdirectory(filter-chain/)` to `find_package(GogglesFilterChain CONFIG REQUIRED)`. -Remove the transitional `.shared` and `test-shared` presets from `CMakePresets.json`. - -### Phase 6: End-to-end verification - -Prove clean-checkout standalone build. Run installed-surface contract tests. Run Goggles build + -host integration tests against the installed package. Verify preserved post-retarget behavior. - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `filter-chain/` (new) | New | Standalone CMake project root with full layout | -| `filter-chain/CMakeLists.txt` | New | Top-level project definition, targets, install/export rules | -| `filter-chain/cmake/` | New | `FilterChainDependencies.cmake`, `GogglesFilterChainConfig.cmake.in` | -| `filter-chain/include/` | New | Installed public headers (moved from `src/render/chain/include/` and `api/`) | -| `filter-chain/src/chain/` | New | Chain runtime sources (moved from `src/render/chain/`) | -| `filter-chain/src/shader/` | New | Shader runtime sources (moved from `src/render/shader/`) | -| `filter-chain/src/texture/` | New | Texture loading sources (moved from `src/render/texture/`) | -| `filter-chain/src/diagnostics/` | New | Diagnostics implementation (moved from `src/util/diagnostics/`) | -| `filter-chain/src/support/` | New | Library-owned logging, profiling, serializer shims | -| `filter-chain/tests/` | New | ~22 contract test files (moved from `tests/render/`) | -| `filter-chain/assets/` | New | Test fixtures, curated shader subset, diagnostics test corpus | -| `src/render/CMakeLists.txt` | Modified | Remove in-tree chain/shader/texture targets; consume via `find_package()` | -| `src/render/chain/` | Removed | Sources moved to `filter-chain/src/chain/` | -| `src/render/shader/` | Removed | Sources moved to `filter-chain/src/shader/` | -| `src/render/texture/` | Removed | Sources moved to `filter-chain/src/texture/` | -| `src/util/diagnostics/` | Modified | Sources moved to `filter-chain/src/diagnostics/`; Goggles keeps the subdirectory only if host-side diagnostics config remains | -| `src/util/CMakeLists.txt` | Modified | Remove `goggles_diagnostics` target and `goggles_util_logging_obj` bundling into filter chain | -| `tests/CMakeLists.txt` | Modified | Remove ~22 contract test sources; keep 3 host integration tests | -| `CMakePresets.json` | Modified | Remove `.shared` and `test-shared` presets after package validation proves both modes | -| `src/render/backend/filter_chain_controller.cpp` | Modified | Switch from in-tree chain includes to installed package headers | -| `src/render/backend/filter_chain_controller.hpp` | Modified | Switch includes | -| `src/render/backend/vulkan_backend.cpp` | Modified | Switch includes | - -## Impacted OpenSpec Specs - -| Spec | Impact | -|------|--------| -| `goggles-filter-chain` | Validates: Standalone Filter Library Target, Library-Owned Support Boundary, Installed Public-Surface Verification Boundary requirements are now implementable and verifiable | -| `build-system` | Validates: CMake-First Standalone Filter Project Workflow, Goggles External Dependency Primary Path, Paired Static and Shared Package Outputs requirements | -| `filter-chain-c-api` | Validates: Installed C ABI Consumer Contract, Post-Retarget Output Contract requirements outside Goggles tree | -| `filter-chain-cpp-wrapper` | Validates: Installed Wrapper Consumer Contract, Wrapper Retarget Contract Survives Standalone Packaging requirements | -| `filter-chain-assets-package` | Validates: All 3 requirements (Library-Owned Asset Package, Asset Resolution Is Package-Oriented, Assets Support Public-Surface Validation) | -| `diagnostics` | Affected by: Ownership move of diagnostics implementation; diagnostic event model, sinks, session, and ledger contracts are preserved but built from standalone project | -| `render-pipeline` | Affected by: Shader runtime, preset parser, and compile report contracts now built from standalone project; host pipeline integration switches to package consumption | - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| Diagnostics decoupling surfaces hidden `goggles_util` type dependencies (config types, job_system references) | Medium | Audit all 10 diagnostics headers for type-level coupling before moving. The exploration found the `goggles_util` link is primarily include-path-based, not type-based, but edge cases may exist in `diagnostic_policy.hpp` (config mapping) and `gpu_timestamp_pool.hpp`. | -| Include path migration breaks compilation in non-obvious ways (template instantiation, ADL, forward declarations) | Medium | Maintain compilation at each phase boundary. Use `pixi run build -p quality` (clang-tidy as errors) as the gate after every file move batch. | -| Asset path changes break tests that use `GOGGLES_SOURCE_DIR` compile definition for fixture lookup | High | Phase 4 explicitly addresses this. Define a `FILTER_CHAIN_ASSET_DIR` compile definition pointing to the standalone asset root. Update all ~22 moved tests to use the new path. | -| Goggles build breaks during transition when chain/shader/texture sources are moved but `find_package()` is not yet configured | Medium | Use `add_subdirectory(filter-chain/)` as the transitional bridge (Phase 3). Switch to `find_package()` only in Phase 5 after install/export is proven. | -| Shared library symbol visibility issues when diagnostics and support shims are compiled into the filter-chain SHARED target | Low | The existing `GOGGLES_CHAIN_BUILD_SHARED` / `POSITION_INDEPENDENT_CODE` pattern already handles this for chain/shader/texture OBJECT libraries. Apply the same pattern to diagnostics and support objects. | -| Curated shader subset is insufficient for zfast integration and shader validation tests | Low | Start with the exact files referenced by the moved tests. Add files incrementally if tests fail. The exploration inventoried 3 asset categories with specific file lists. | -| Transitional `add_subdirectory()` period allows re-coupling (new Goggles code adds `#include ` paths) | Low | The Phase 1-2 boundary contract tests already prevent this. Keep those tests active during the transition. The filter-chain target's include directories will only expose installed-surface paths. | - -## Policy-Sensitive Impacts - -- **Error handling**: The standalone project preserves the `tl::expected` / `Result` - pattern. The library-owned `error.hpp` already exists under `include/goggles/filter_chain/`. - No change to error handling policy. - -- **Logging**: The library-owned logging shim wraps spdlog with the same facade contract as - `util/logging.hpp`. Log tags (`render.chain`, `render.shader`, `render.texture`) are preserved. - No silent failure paths introduced. - -- **Threading**: The filter chain is single-threaded by design. The diagnostics decoupling removes - the transitive `BS_thread_pool` and `Threads` dependencies, which is correct -- the filter chain - does not use `JobSystem`. No threading policy change. - -- **Vulkan API split**: All `vk::` usage is preserved. The standalone project links - `Vulkan::Vulkan` PUBLIC and defines `VULKAN_HPP_NO_EXCEPTIONS` and - `VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1` as it does today. - -- **Lifetime/ownership**: RAII patterns preserved. No raw `new`/`delete` introduced. The - standalone project owns its support code lifetime; Goggles consumes through the installed - package boundary. - -## Rollback Plan - -Each phase is independently revertable: - -1. **Phase 3** (skeleton + source move): Revert the file moves and restore `src/render/chain/`, - `shader/`, `texture/` CMakeLists. The `filter-chain/` directory can be deleted. Goggles builds - as before. - -2. **Phase 4** (assets + diagnostics): Revert diagnostics source moves back to - `src/util/diagnostics/`. Restore `goggles_util` link. Revert asset path changes in tests. - -3. **Phase 5** (install/export + find_package switch): Revert Goggles back to - `add_subdirectory(filter-chain/)` or restore the in-tree chain target. Re-add `.shared` and - `test-shared` presets if removed. - -4. **Phase 6** (verification): No destructive changes -- this phase only validates. - -At any point, `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` must -pass. If it fails after a phase, revert that phase before investigating. - -## Dependencies - -- Phase 1-2 monorepo groundwork is complete (archived at - `openspec/changes/archive/2026-03-13-extract-filter-chain-standalone-project/`). -- Public header boundary under `src/render/chain/include/goggles/filter_chain/` is established. -- `GogglesFilterChain::goggles-filter-chain` ALIAS target exists. -- All third-party dependencies (Vulkan, expected-lite, spdlog, slang, stb_image, Catch2) are - available via standard `find_package()` through the Pixi/conda environment. - -## Success Criteria - -- [ ] `cmake -S filter-chain/ -B filter-chain/build` configures without errors and without - requiring Goggles source tree, Pixi wrappers, or Conda-specific paths. -- [ ] `cmake --build filter-chain/build` produces both STATIC and SHARED library artifacts. -- [ ] `ctest --test-dir filter-chain/build` passes all ~22 contract tests using library-owned - assets and fixtures. -- [ ] `cmake --install filter-chain/build --prefix /tmp/fc-install` installs headers, libraries, - assets, and package metadata. -- [ ] An out-of-tree consumer project successfully resolves - `find_package(GogglesFilterChain CONFIG REQUIRED)` and links both static and shared variants. -- [ ] Goggles builds and passes host integration tests when consuming the installed package - through `find_package(GogglesFilterChain)`. -- [ ] `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` passes - for the Goggles project after the switch. -- [ ] The standalone `filter-chain/` directory contains no `#include ` or - `#include ` paths that assume the Goggles source layout. -- [ ] The `goggles_diagnostics` target in the standalone project does not link `goggles_util`, - `toml11`, `BS_thread_pool`, or `Threads`. -- [ ] The transitional `.shared` and `test-shared` presets are removed from `CMakePresets.json`. - -## Validation Plan - -### Per-Phase Gates - -| Phase | Gate Command | What It Proves | -|-------|-------------|----------------| -| Phase 3 | `cmake -S filter-chain/ -B /tmp/fc-build && cmake --build /tmp/fc-build` | Standalone project configures and builds without Goggles tree | -| Phase 3 | `pixi run build -p asan && pixi run test -p asan` | Goggles still builds and passes tests via `add_subdirectory()` bridge | -| Phase 4 | `ctest --test-dir /tmp/fc-build` | Contract tests pass with library-owned assets | -| Phase 4 | `pixi run build -p quality` | No clang-tidy regressions in remaining Goggles code | -| Phase 5 | `cmake --install /tmp/fc-build --prefix /tmp/fc-prefix && cmake -S consumer/ -B /tmp/consumer-build -DCMAKE_PREFIX_PATH=/tmp/fc-prefix` | Installed package is discoverable and consumable | -| Phase 5 | `pixi run build -p asan && pixi run test -p asan` (with find_package path) | Goggles works with external package consumption | -| Phase 6 | Full CI-parity gate: `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality` | End-to-end pass | - -### Boundary Verification - -- Grep `filter-chain/src/` for `#include ` paths -- **AND** zero matches SHALL be found for `#include ` paths that reference Goggles-layout modules - -#### Scenario: Standalone internal includes use project-relative paths - -- **GIVEN** implementation files under `filter-chain/src/` -- **WHEN** they include library-internal headers -- **THEN** include paths SHALL resolve through the standalone project's own include directories -- **AND** include paths SHALL NOT depend on Goggles `${CMAKE_SOURCE_DIR}/src` being on the include path - -### Requirement: Library-Owned Support Shim Contracts - -The standalone project SHALL provide library-owned support shims for logging, profiling, and -serialization that replace the Goggles `util/logging.hpp`, `util/profiling.hpp`, and -`util/serializer.hpp` dependencies. Each shim SHALL satisfy the same interface contract as the -Goggles original while depending only on the shim's own third-party dependency (spdlog for logging, -Tracy for profiling, standard library for serializer). - -#### Scenario: Logging shim preserves tag-based facade contract - -- **GIVEN** the standalone project's library-owned logging shim -- **WHEN** chain, shader, and texture implementation files use the logging facade -- **THEN** the shim SHALL support the same tagged logging macros (e.g., `GOGGLES_LOG_INFO`, `GOGGLES_LOG_WARN`) -- **AND** log tags (`render.chain`, `render.shader`, `render.texture`) SHALL be preserved -- **AND** the shim SHALL depend only on spdlog, not on Goggles `util/logging.hpp` - -#### Scenario: Profiling shim compiles without Tracy when Tracy is unavailable - -- **GIVEN** the standalone project is built without Tracy available in the build environment -- **WHEN** profiling macros are expanded in implementation files -- **THEN** profiling macros SHALL expand to no-op expressions -- **AND** compilation SHALL succeed without Tracy headers or libraries - -#### Scenario: Serializer shim is self-contained - -- **GIVEN** the standalone project's serializer shim under `filter-chain/src/support/` -- **WHEN** `shader_runtime.cpp` uses serialization utilities -- **THEN** the shim SHALL provide the required serialization interface using only the standard library -- **AND** the shim SHALL NOT include Goggles `util/serializer.hpp` - -## MODIFIED Requirements - -_(No existing requirements are modified by this change. The main spec requirements for -Standalone Filter Library Target, Library-Owned Support Boundary, and Installed Public-Surface -Verification Boundary are being implemented as specified.)_ - -## REMOVED Requirements - -_(No requirements are removed by this change.)_ diff --git a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/tasks.md b/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/tasks.md deleted file mode 100644 index 03be2599..00000000 --- a/openspec/changes/archive/2026-03-14-standalone-filter-chain-extraction/tasks.md +++ /dev/null @@ -1,494 +0,0 @@ -# Tasks: Standalone Filter-Chain Extraction - -## Phase 3: Standalone Skeleton + Source Migration - -### 3A: Project skeleton and CMake infrastructure - -- [x] 3.1 Create `filter-chain/` directory structure with subdirectories `cmake/`, `include/`, - `include/goggles/filter_chain/`, `src/chain/`, `src/shader/`, `src/texture/`, - `src/support/`, `src/diagnostics/`, `tests/`, `tests/contract/`, `assets/`. - **Verify**: `ls filter-chain/{cmake,include,src,tests,assets}` succeeds. - -- [x] 3.2 Create `filter-chain/cmake/CompilerConfig.cmake` with C++20 enforcement, warning flags, - ccache support, sanitizer helpers, and `POSITION_INDEPENDENT_CODE` handling for SHARED builds. - Adapt the relevant subset from the root `cmake/CompilerConfig.cmake`. - **Verify**: The file defines `goggles_enable_sanitizers()` and sets `CMAKE_CXX_STANDARD 20`. - -- [x] 3.3 Create `filter-chain/cmake/CodeQuality.cmake` with the `goggles_enable_clang_tidy()` - helper function. Adapt from root `cmake/CodeQuality.cmake`. - **Verify**: The file defines the `goggles_enable_clang_tidy()` function. - -- [x] 3.4 Create `filter-chain/cmake/FilterChainDependencies.cmake` that resolves all third-party - dependencies via standard `find_package()`: Vulkan, expected-lite, spdlog, slang, stb_image, - and Catch2 (conditionally, only when tests are enabled). No Goggles-specific Find modules. - **Verify**: File contains `find_package(Vulkan REQUIRED)`, `find_package(expected-lite REQUIRED)`, - `find_package(spdlog REQUIRED)`, `find_package(slang CONFIG REQUIRED)`. - -- [x] 3.5 Create `filter-chain/CMakeLists.txt` with: - - `project(GogglesFilterChain VERSION 0.1.0 LANGUAGES CXX)` - - Options: `FILTER_CHAIN_LIBRARY_TYPE` (STATIC/SHARED), `FILTER_CHAIN_BUILD_TESTS`, - `ENABLE_CLANG_TIDY`, `ENABLE_ASAN` - - `include()` calls for CompilerConfig, CodeQuality, FilterChainDependencies, GNUInstallDirs - - Empty OBJECT library targets as placeholders: `fc_chain_obj`, `fc_shader_obj`, - `fc_texture_obj`, `fc_support_obj` (sources added in later tasks) - - Composed `goggles-filter-chain` library from OBJECT targets - - PUBLIC include dirs: `$` - - PRIVATE include dirs: `${CMAKE_CURRENT_SOURCE_DIR}/src` - - PUBLIC compile defs: `VULKAN_HPP_NO_EXCEPTIONS`, `VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1` - - Link libraries: PUBLIC `Vulkan::Vulkan`, `nonstd::expected-lite`; - PRIVATE `spdlog::spdlog`, `slang::slang`, `stb_image` - - `GogglesFilterChain::goggles-filter-chain` ALIAS - - SHARED/PIC handling matching existing pattern in `src/render/CMakeLists.txt` - - Conditional `add_subdirectory(tests)` when `FILTER_CHAIN_BUILD_TESTS` is ON - **Verify**: `cmake -S filter-chain/ -B /tmp/fc-skeleton -DFILTER_CHAIN_BUILD_TESTS=OFF` - configures without errors (with deps available via Pixi env). - -- [x] 3.6 Copy `.clang-format` and `.clang-tidy` from project root into `filter-chain/` for - standalone formatting and linting consistency. - **Verify**: `diff filter-chain/.clang-format .clang-format` shows no differences. - -### 3B: Library-owned support shims - -- [x] 3.7 Create `filter-chain/src/support/logging.hpp` with spdlog facade providing - `goggles::initialize_logger()`, `goggles::get_logger()`, `goggles::set_log_level()`, and - `GOGGLES_LOG_TRACE/DEBUG/INFO/WARN/ERROR/CRITICAL` macros using `GOGGLES_LOG_TAG`. - Replicate the interface contract from `src/util/logging.hpp`. Depend only on `spdlog::spdlog`. - **Verify**: Header compiles standalone: include it from a trivial .cpp with spdlog available. - -- [x] 3.8 Create `filter-chain/src/support/logging.cpp` with logger initialization and singleton - management matching `src/util/logging.cpp` behavior. - **Verify**: File compiles as part of `fc_support_obj` OBJECT library. - -- [x] 3.9 Create `filter-chain/src/support/profiling.hpp` (header-only) with Tracy facade providing - `GOGGLES_PROFILE_FRAME/FUNCTION/SCOPE/TAG/VALUE` macros. When `TRACY_ENABLE` is not defined, - macros expand to no-ops. Replicate interface from `src/util/profiling.hpp`. - **Verify**: Compiles with and without `TRACY_ENABLE` defined. - -- [x] 3.10 Create `filter-chain/src/support/serializer.hpp` (header-only) with - `goggles::util::BinaryWriter`, `goggles::util::BinaryReader`, and - `goggles::util::read_file_binary()`. Depends only on standard library + - ``. Replicate interface from `src/util/serializer.hpp`. - **Verify**: `shader_runtime.cpp` (once moved) can compile against this shim. - -- [x] 3.11 Add `fc_support_obj` OBJECT library to `filter-chain/CMakeLists.txt` with - `src/support/logging.cpp` as its source. Link PRIVATE `spdlog::spdlog`. Set - `GOGGLES_LOG_TAG="render.support"`. Handle PIC for SHARED builds. - **Verify**: `cmake --build /tmp/fc-skeleton --target fc_support_obj` succeeds. - -### 3C: Source migration -- public headers and API headers - -- [x] 3.12 Move (`git mv`) the 5 public headers from `src/render/chain/include/goggles/filter_chain/` - (`error.hpp`, `filter_controls.hpp`, `result.hpp`, `scale_mode.hpp`, `vulkan_context.hpp`) - to `filter-chain/include/goggles/filter_chain/`. - **Verify**: `ls filter-chain/include/goggles/filter_chain/*.hpp | wc -l` returns 5. - -- [x] 3.13 Move (`git mv`) the C API header `src/render/chain/api/c/goggles_filter_chain.h` - to `filter-chain/include/goggles_filter_chain.h`. - **Verify**: `test -f filter-chain/include/goggles_filter_chain.h` - -- [x] 3.14 Move (`git mv`) the C++ wrapper header `src/render/chain/api/cpp/goggles_filter_chain.hpp` - to `filter-chain/include/goggles_filter_chain.hpp`. - **Verify**: `test -f filter-chain/include/goggles_filter_chain.hpp` - -### 3D: Source migration -- chain module - -- [x] 3.15 Move (`git mv`) all 15 chain `.cpp` files from `src/render/chain/` to - `filter-chain/src/chain/`: `chain_runtime.cpp`, `chain_builder.cpp`, `chain_resources.cpp`, - `chain_executor.cpp`, `chain_controls.cpp`, `filter_controls.cpp`, `filter_pass.cpp`, - `framebuffer.cpp`, `output_pass.cpp`, `downsample_pass.cpp`, `preset_parser.cpp`, - `frame_history.cpp`, `vulkan_dispatch.cpp`, `api/c/goggles_filter_chain.cpp` (to - `filter-chain/src/chain/c_api.cpp`), `api/cpp/goggles_filter_chain.cpp` (to - `filter-chain/src/chain/cpp_wrapper.cpp`). - **Verify**: `ls filter-chain/src/chain/*.cpp | wc -l` returns 15. - -- [x] 3.16 Move (`git mv`) all 15 chain private `.hpp` files from `src/render/chain/` to - `filter-chain/src/chain/`: `chain_runtime.hpp`, `chain_builder.hpp`, `chain_resources.hpp`, - `chain_executor.hpp`, `chain_controls.hpp`, `filter_pass.hpp`, `framebuffer.hpp`, - `output_pass.hpp`, `downsample_pass.hpp`, `preset_parser.hpp`, `frame_history.hpp`, - `debug_label_scope.hpp`, `pass.hpp`, `semantic_binder.hpp`, `vulkan_result.hpp`. - Also move `README.md` if relevant. - **Verify**: `ls filter-chain/src/chain/*.hpp | wc -l` returns 15. - -- [x] 3.17 Update include paths in all moved chain source files: - - `#include ` -> `#include "support/logging.hpp"` - - `#include ` -> `#include "support/profiling.hpp"` - - `#include ` -> `#include ` - - `#include ` -> `#include "chain/X.hpp"` - - `#include ` -> `#include "shader/X.hpp"` - - `#include ` -> `#include "texture/X.hpp"` - - `#include ` -> `#include "diagnostics/X.hpp"` - **Verify**: `grep -r '#include ` -> `#include "support/logging.hpp"` - - `#include ` -> `#include "support/profiling.hpp"` - - `#include ` -> `#include "support/serializer.hpp"` - - `#include ` -> `#include ` - - `#include ` -> `#include "shader/X.hpp"` - - `#include ` -> `#include "diagnostics/X.hpp"` - **Verify**: `grep -r '#include ` -> `#include "support/logging.hpp"` - - `#include ` -> `#include "support/profiling.hpp"` - - `#include ` -> `#include ` - - `#include ` -> `#include "texture/X.hpp"` - **Verify**: `grep -r '#include ` -> `#include ` - - `#include ` -> `#include "support/logging.hpp"` - - `#include ` -> `#include "support/profiling.hpp"` - - Any `#include ` -> `#include "diagnostics/X.hpp"` - **Verify**: `grep -r '#include ` -> `#include "diagnostics/X.hpp"` - - `#include ` -> `#include "chain/X.hpp"` - - `#include ` -> `#include "shader/X.hpp"` - - `#include ` -> `#include "texture/X.hpp"` - - `#include ` -> `#include ` - - Replace `GOGGLES_SOURCE_DIR` usage with `FILTER_CHAIN_ASSET_DIR` for asset path resolution. - **Verify**: `grep -r 'GOGGLES_SOURCE_DIR' filter-chain/tests/` returns zero matches. - -- [x] 4.12 Create `filter-chain/tests/CMakeLists.txt` defining the `fc_tests` executable: - - Source files: all moved contract test `.cpp` files under `contract/` - - Link: `goggles-filter-chain`, `Catch2::Catch2WithMain` - - Compile definition: `FILTER_CHAIN_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/../assets"` - - Include dir: `${CMAKE_CURRENT_SOURCE_DIR}/../src` (for PRIVATE header access in tests) - - Register with CTest: `add_test(NAME fc_contract_tests COMMAND fc_tests)` - - Optionally add `fc_shader_batch_report` executable for batch report tool. - **Verify**: `cmake -S filter-chain/ -B /tmp/fc-build && cmake --build /tmp/fc-build` - builds the test executable. - -- [x] 4.13 Update Goggles `tests/CMakeLists.txt`: remove the ~22 moved contract test source - references from `goggles_tests`. Keep the 3 host integration tests: - `test_filter_boundary_contracts.cpp`, `test_vulkan_backend_subsystem_contracts.cpp`, - `test_filter_chain_retarget.cpp`. Remove the `shader_batch_report` sources and executable - if moved to standalone. Remove the visual test runner reference if it was moved. - **Verify**: `pixi run build -p debug` succeeds. The `goggles_tests` target compiles - with only host integration + utility tests. - -### 4D: Phase 4 build gate - -- [x] 4.14 Verify standalone contract tests pass: - `cmake -S filter-chain/ -B /tmp/fc-build && cmake --build /tmp/fc-build && ctest --test-dir /tmp/fc-build --output-on-failure`. - Fix any test failures from asset path changes or include path migration. - **Verify**: All contract tests pass (exit status 0). - -- [x] 4.15 Verify Goggles builds and tests pass after test migration: - `pixi run build -p asan && pixi run test -p asan`. - **Verify**: Both commands exit with status 0. - -- [x] 4.16 Verify code quality gate: - `pixi run build -p quality`. - Fix any clang-tidy regressions in remaining Goggles code. - **Verify**: Exit status 0. - -- [x] 4.17 Verify include-path isolation is preserved after diagnostics move: - `grep -r '#include ` and - ``, instantiates a type, returns 0. - **Verify**: `cmake -S filter-chain/tests/consumer/static -B /tmp/fc-consumer-static - -DCMAKE_PREFIX_PATH=/tmp/fc-static-install && cmake --build /tmp/fc-consumer-static` - succeeds. - -- [x] 5.6 Create `filter-chain/tests/consumer/shared/CMakeLists.txt` and - `filter-chain/tests/consumer/shared/main.cpp`: - - Same structure as static consumer but links shared variant. - **Verify**: `cmake -S filter-chain/tests/consumer/shared -B /tmp/fc-consumer-shared - -DCMAKE_PREFIX_PATH=/tmp/fc-shared-install && cmake --build /tmp/fc-consumer-shared` - succeeds. - -### 5C: Goggles find_package switch - -- [x] 5.7 Add `option(GOGGLES_USE_BUNDLED_FILTER_CHAIN ...)` to the root `CMakeLists.txt` or - `src/render/CMakeLists.txt`. When OFF (default), use - `find_package(GogglesFilterChain CONFIG REQUIRED)`. When ON, use - `add_subdirectory(${CMAKE_SOURCE_DIR}/filter-chain ...)`. - Both paths make `GogglesFilterChain::goggles-filter-chain` available for - `target_link_libraries(goggles_render ...)`. - **Verify**: `pixi run build -p debug` succeeds with - `-DCMAKE_PREFIX_PATH=/tmp/fc-static-install` (find_package path). - -- [x] 5.8 Update CMake presets in `CMakePresets.json` to include - `CMAKE_PREFIX_PATH` or `GOGGLES_USE_BUNDLED_FILTER_CHAIN` as needed so existing presets - (debug, release, asan, quality, test) work with the find_package path. - **Verify**: `pixi run build -p asan && pixi run test -p asan` passes using the installed - filter-chain package. - -- [x] 5.9 Remove the transitional `.shared` hidden preset and `test-shared` configure/build/test - presets from `CMakePresets.json`. The standalone project now owns shared-variant validation. - **Verify**: `grep -c 'shared' CMakePresets.json` returns 0 (or only references that are - not preset names). `pixi run build -p asan` still succeeds. - -### 5D: Phase 5 build gate - -- [x] 5.10 Verify consumer validation for STATIC linkage: - `cmake -S filter-chain/tests/consumer/static -B /tmp/consumer-s - -DCMAKE_PREFIX_PATH=/tmp/fc-static-install && - cmake --build /tmp/consumer-s && /tmp/consumer-s/static_consumer`. - **Verify**: Consumer configures, builds, and runs (exit 0). - -- [x] 5.11 Verify consumer validation for SHARED linkage: - `cmake -S filter-chain/tests/consumer/shared -B /tmp/consumer-d - -DCMAKE_PREFIX_PATH=/tmp/fc-shared-install && - cmake --build /tmp/consumer-d && - LD_LIBRARY_PATH=/tmp/fc-shared-install/lib /tmp/consumer-d/shared_consumer`. - **Verify**: Consumer configures, builds, and runs (exit 0). - -- [x] 5.12 Verify Goggles build + test with find_package consumption: - `pixi run build -p asan && pixi run test -p asan`. - **Verify**: Both commands exit with status 0. - -- [x] 5.13 Verify quality gate: `pixi run build -p quality`. - **Verify**: Exit status 0. - -## Phase 6: End-to-End Verification - -- [x] 6.1 Clean-checkout standalone build verification: from a clean build directory - (remove any previous `/tmp/fc-*` artifacts), configure, build, test, and install the - standalone project: - ``` - rm -rf /tmp/fc-e2e - cmake -S filter-chain/ -B /tmp/fc-e2e - cmake --build /tmp/fc-e2e - ctest --test-dir /tmp/fc-e2e --output-on-failure - cmake --install /tmp/fc-e2e --prefix /tmp/fc-e2e-install - ``` - **Verify**: All 4 commands exit with status 0. All ~22 contract tests pass. - -- [x] 6.2 Run Goggles full CI-parity gate against the installed package: - `pixi run build -p asan && pixi run test -p asan && pixi run build -p quality`. - **Verify**: All 3 commands exit with status 0. - -- [x] 6.3 Verify include-path isolation audit: - `grep -r '#include ` and `` -- no source-tree paths | COMPLIANT | -| Host Test Split After Extraction | Goggles retains host integration tests | `tests/CMakeLists.txt` includes `test_filter_chain_retarget.cpp`, `test_filter_boundary_contracts.cpp`, `test_vulkan_backend_subsystem_contracts.cpp` | COMPLIANT | -| Host Test Split After Extraction | Moved contract tests are absent from Goggles | Only 3 `test_*.cpp` files remain in `tests/render/`; no moved contract test files present | COMPLIANT | -| CMake-First Standalone Filter Project Workflow | Package config template generates valid export | `GogglesFilterChainConfig.cmake.in` contains `@PACKAGE_INIT@`, defines `GogglesFilterChain::goggles-filter-chain-static` and `GogglesFilterChain::goggles-filter-chain-shared` | COMPLIANT | - -### Spec: diagnostics (filter-chain/specs/diagnostics/spec.md) - -| Requirement | Scenario | Test | Result | -|-------------|----------|------|--------| -| Diagnostics Builds Without goggles_util | Standalone diagnostics target has no goggles_util dependency | `fc_diagnostics_obj` links only `Vulkan::Vulkan` and `spdlog::spdlog`; `grep goggles_util filter-chain/` = 0 matches | COMPLIANT | -| Diagnostics Builds Without goggles_util | Diagnostics compiles outside Goggles source tree | Standalone build succeeds; all 4 `.cpp` files compile | COMPLIANT | -| Diagnostics Builds Without goggles_util | Diagnostics headers are self-contained | 17 headers under `filter-chain/src/diagnostics/`; no `#include ` restores `goggles::input` everywhere. - -## Open Questions - -None. All decisions are resolved in the proposal. The rename is fully mechanical with no ambiguity. diff --git a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/proposal.md b/openspec/changes/archive/2026-03-19-rename-compositor-namespace/proposal.md deleted file mode 100644 index 06b305c8..00000000 --- a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/proposal.md +++ /dev/null @@ -1,133 +0,0 @@ -# Proposal: Rename compositor namespace from goggles::input to goggles::compositor - -## Problem - -The compositor module (`src/compositor/`) uses `goggles::input` as its C++ namespace. Every other module follows the project convention that namespace matches directory name (`app` → `goggles::app`, `render` → `goggles::render`, `ui` → `goggles::ui`, `util` → `goggles::util`). The compositor is the sole exception, affecting 14 source files internally and 4 files externally. This violates the ALWAYS rule "Follow namespace convention: `goggles::{module_name}`" (PROJECT_RULES.md, I3) and was confirmed as a historical anomaly in RFC.md (I5, Q1 — resolved: rename). - -## Intent - -Rename the `goggles::input` namespace to `goggles::compositor` so the project-wide namespace convention holds uniformly with zero exceptions. - -## Scope - -### In Scope - -- Rename all `namespace goggles::input` declarations in `src/compositor/` to `namespace goggles::compositor`. -- Update all qualified references (`goggles::input::*`) in consuming modules and tests. -- Update the forward declaration in `src/ui/imgui_layer.hpp`. -- Run `pixi run format` after changes. -- Verify with `pixi run ci --runner container --cache-mode warm --lane all`. - -### Out of Scope - -- Renaming or reorganizing the `src/compositor/` directory structure. -- Changing any type names, function signatures, or class APIs within the compositor module. -- Modifying the filter-chain submodule (confirmed: zero references to `goggles::input`). -- Updating archived openspec documents (historical records remain as-is). -- Any functional or behavioral changes to the compositor. - -## Approach - -This is a single-phase mechanical rename. No architectural changes, no phased rollout. - -### Step 1: Namespace declarations (14 files) - -Replace `namespace goggles::input` with `namespace goggles::compositor` in all compositor module files: - -| File | Type | -|------|------| -| `src/compositor/compositor_server.hpp` | Public header | -| `src/compositor/compositor_server.cpp` | Implementation | -| `src/compositor/compositor_state.hpp` | Internal header | -| `src/compositor/compositor_protocol_hooks.hpp` | Internal header | -| `src/compositor/compositor_targets.hpp` | Internal header | -| `src/compositor/compositor_runtime_metrics.hpp` | Internal header | -| `src/compositor/compositor_core.cpp` | Implementation | -| `src/compositor/compositor_cursor.cpp` | Implementation | -| `src/compositor/compositor_input.cpp` | Implementation | -| `src/compositor/compositor_focus.cpp` | Implementation | -| `src/compositor/compositor_layer_shell.cpp` | Implementation | -| `src/compositor/compositor_present.cpp` | Implementation | -| `src/compositor/compositor_xdg.cpp` | Implementation | -| `src/compositor/compositor_xwayland.cpp` | Implementation | - -### Step 2: Forward declaration (1 file) - -Update `src/ui/imgui_layer.hpp`: -- `namespace goggles::input { struct SurfaceInfo; }` → `namespace goggles::compositor { struct SurfaceInfo; }` - -### Step 3: Qualified references in tests (3 files) - -Update all `goggles::input::` qualifications: - -| File | Symbols referenced | -|------|-------------------| -| `tests/input/auto_input_forwarding_wayland.cpp` | `goggles::input::CompositorServer::create()` | -| `tests/input/auto_input_forwarding_x11.cpp` | `goggles::input::CompositorServer::create()` | -| `tests/render/test_filter_boundary_contracts.cpp` | `goggles::input::wlr_surface*`, `goggles::input::RuntimeMetricsState` | - -### Step 4: Format and verify - -1. `pixi run format` -2. `pixi run ci --runner container --cache-mode warm --lane all` - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `src/compositor/*.hpp` | Modified | Namespace declaration changed in 6 headers | -| `src/compositor/*.cpp` | Modified | Namespace declaration changed in 8 implementation files | -| `src/ui/imgui_layer.hpp` | Modified | Forward declaration updated | -| `tests/input/auto_input_forwarding_*.cpp` | Modified | Qualified name references updated (2 files) | -| `tests/render/test_filter_boundary_contracts.cpp` | Modified | Qualified name references updated | -| `filter-chain/` | None | Zero references to `goggles::input` — no changes needed | -| CMake build files | None | No namespace-dependent configuration exists | - -**Total: 18 files modified, 0 files created or deleted.** - -## Non-goals - -- Introduce a new `compositor` sub-namespace hierarchy or reorganize types within the module. -- Rename or split any types (e.g., keeping `InputEventType` as-is — the type name is semantic, not a namespace artifact). -- Update archived design documents that reference the old namespace. - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| Missed reference causes build failure | Low | Full CI gate catches all compile errors; `goggles::input` does not exist after rename, so any remaining reference is a hard compile error | -| Symbol mangling change breaks something | None | Monolithic build; no binary distribution or ABI stability contract | -| Name collision with existing `goggles::compositor` | None | Confirmed: `goggles::compositor` does not exist anywhere in the codebase | - -## Rollback Plan - -Single commit. Revert the commit to restore `goggles::input` everywhere. - -## Dependencies - -None. No external consumers, no ABI boundary, no submodule impact. - -## Validation Plan - -- `pixi run build -p debug` — confirms compilation after rename. -- `pixi run test -p test` — confirms all tests pass with new namespace. -- `pixi run ci --runner container --cache-mode warm --lane all` — full CI gate (ALWAYS rule, I2/I7). - -## Success Criteria - -- [ ] Zero occurrences of `goggles::input` in `src/` and `tests/` (excluding archived docs). -- [ ] All compositor module files use `namespace goggles::compositor`. -- [ ] The forward declaration in `src/ui/imgui_layer.hpp` references `goggles::compositor`. -- [ ] Full CI passes: `pixi run ci --runner container --cache-mode warm --lane all`. -- [ ] The ALWAYS rule "Follow namespace convention: `goggles::{module_name}`" applies uniformly with no exceptions. - -## PROJECT_RULES.md Compliance - -| Rule | Status | -|------|--------| -| ALWAYS: Follow namespace convention `goggles::{module_name}` (I3) | This change enforces it | -| ALWAYS: Run `pixi run format` before committing (I7) | Step 4 | -| ALWAYS: Use `pixi run ci ...` for final verification (I2, I7) | Step 4 | -| NEVER: Create circular module dependencies (I8) | No dependency changes | -| NEVER: Modify files in `shaders/retroarch/` or `research/` (I3) | Not touched | -| ASK FIRST: Modifying filter-chain library boundary (I4) | Not touched — confirmed zero impact | diff --git a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/spec.md b/openspec/changes/archive/2026-03-19-rename-compositor-namespace/spec.md deleted file mode 100644 index 93c5bc69..00000000 --- a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/spec.md +++ /dev/null @@ -1,143 +0,0 @@ -# Spec: Rename compositor namespace from goggles::input to goggles::compositor - -**Change:** rename-compositor-namespace -**Proposal:** [proposal.md](proposal.md) - ---- - -## Requirement: Namespace Declaration Convention - -All source files in the `src/compositor/` directory SHALL declare their namespace as `namespace goggles::compositor`. No file in the compositor module SHALL use `namespace goggles::input` or any other namespace that does not match the directory name. - -### Scenario: Compositor headers use correct namespace - -- **GIVEN** the compositor public and internal headers: - - `src/compositor/compositor_server.hpp` - - `src/compositor/compositor_state.hpp` - - `src/compositor/compositor_protocol_hooks.hpp` - - `src/compositor/compositor_targets.hpp` - - `src/compositor/compositor_runtime_metrics.hpp` -- **WHEN** namespace declarations in each file are inspected -- **THEN** every namespace declaration SHALL be `namespace goggles::compositor` -- **AND** zero occurrences of `namespace goggles::input` SHALL exist - -### Scenario: Compositor implementation files use correct namespace - -- **GIVEN** the compositor implementation files: - - `src/compositor/compositor_server.cpp` - - `src/compositor/compositor_core.cpp` - - `src/compositor/compositor_cursor.cpp` - - `src/compositor/compositor_input.cpp` - - `src/compositor/compositor_focus.cpp` - - `src/compositor/compositor_layer_shell.cpp` - - `src/compositor/compositor_present.cpp` - - `src/compositor/compositor_xdg.cpp` - - `src/compositor/compositor_xwayland.cpp` -- **WHEN** namespace declarations in each file are inspected -- **THEN** every namespace declaration SHALL be `namespace goggles::compositor` -- **AND** zero occurrences of `namespace goggles::input` SHALL exist - ---- - -## Requirement: No Residual References to Old Namespace - -After the rename, zero occurrences of the string `goggles::input` SHALL exist anywhere in `src/` or `tests/`. This ensures no stale qualified references, using-declarations, or comments referencing the old namespace survive the rename. - -### Scenario: Source tree contains no old namespace references - -- **GIVEN** the directories `src/` and `tests/` -- **WHEN** a text search for the literal string `goggles::input` is executed across all files -- **THEN** zero matches SHALL be found - -### Scenario: No residual using-declarations reference old namespace - -- **GIVEN** the directories `src/` and `tests/` -- **WHEN** a text search for `using namespace goggles::input` or `namespace input = goggles::input` is executed -- **THEN** zero matches SHALL be found - ---- - -## Requirement: Forward Declaration Consistency - -The forward declaration of compositor types in consumer modules SHALL reference the `goggles::compositor` namespace, not `goggles::input`. - -### Scenario: ImGui layer forward declaration updated - -- **GIVEN** the file `src/ui/imgui_layer.hpp` -- **WHEN** forward declarations of compositor types are inspected -- **THEN** the forward declaration of `SurfaceInfo` SHALL appear within `namespace goggles::compositor` -- **AND** zero occurrences of `namespace goggles::input` SHALL exist in the file - ---- - -## Requirement: Qualified Reference Consistency in Tests - -All test files that reference compositor types via fully qualified names SHALL use the `goggles::compositor::` prefix. - -### Scenario: Input forwarding test files use new namespace - -- **GIVEN** the test files: - - `tests/input/auto_input_forwarding_wayland.cpp` - - `tests/input/auto_input_forwarding_x11.cpp` -- **WHEN** qualified references to `CompositorServer::create()` are inspected -- **THEN** every reference SHALL use `goggles::compositor::CompositorServer::create()` -- **AND** zero occurrences of `goggles::input::CompositorServer` SHALL exist - -### Scenario: Filter boundary contract tests use new namespace - -- **GIVEN** the file `tests/render/test_filter_boundary_contracts.cpp` -- **WHEN** qualified references to compositor types are inspected -- **THEN** references to `wlr_surface` and `RuntimeMetricsState` SHALL use the `goggles::compositor::` prefix -- **AND** zero occurrences of `goggles::input::` SHALL exist in the file - ---- - -## Requirement: Build Verification - -The project SHALL compile cleanly and pass all tests after the namespace rename. Because the old namespace `goggles::input` ceases to exist, any missed reference will produce a hard compile error — there is no risk of a silent partial rename. - -### Scenario: Debug build succeeds - -- **GIVEN** all namespace changes are applied -- **WHEN** `pixi run build -p debug` is executed -- **THEN** the build SHALL succeed with zero errors - -### Scenario: Full test suite passes - -- **GIVEN** all namespace changes are applied -- **WHEN** `pixi run test -p test` is executed -- **THEN** all tests SHALL pass - -### Scenario: Full CI pipeline passes - -- **GIVEN** all namespace changes are applied and formatted via `pixi run format` -- **WHEN** `pixi run ci --runner container --cache-mode warm --lane all` is executed -- **THEN** all CI lanes SHALL pass (format, build+test with ASAN, package install + consumer validation, semgrep, clang-tidy quality gate) - ---- - -## Requirement: No Functional Changes - -The rename SHALL be a purely mechanical transformation. No type names, function signatures, class APIs, or runtime behavior SHALL change. Only the enclosing namespace identifier changes from `input` to `compositor`. - -### Scenario: Type names are preserved - -- **GIVEN** the compositor module types: `CompositorServer`, `CompositorState`, `SurfaceInfo`, `RuntimeMetricsState`, and all other public and internal types -- **WHEN** their declarations are inspected after the rename -- **THEN** every type name SHALL be identical to its pre-rename name -- **AND** only the enclosing namespace SHALL differ - -### Scenario: Function signatures are preserved - -- **GIVEN** all public and internal function signatures in the compositor module -- **WHEN** they are inspected after the rename -- **THEN** every function name, parameter list, and return type SHALL be identical to pre-rename -- **AND** no function SHALL be added, removed, or modified - -### Scenario: No files created or deleted - -- **GIVEN** the set of files modified by this change -- **WHEN** the changeset is inspected -- **THEN** zero files SHALL be created -- **AND** zero files SHALL be deleted -- **AND** exactly 18 files SHALL be modified (14 compositor module files, 1 UI forward declaration, 3 test files) diff --git a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/tasks.md b/openspec/changes/archive/2026-03-19-rename-compositor-namespace/tasks.md deleted file mode 100644 index bcf2f0de..00000000 --- a/openspec/changes/archive/2026-03-19-rename-compositor-namespace/tasks.md +++ /dev/null @@ -1,36 +0,0 @@ -# Tasks: Rename compositor namespace from goggles::input to goggles::compositor - -- [x] T1 Rename namespace declarations in all compositor module files - - Description: Replace every `namespace goggles::input` declaration with `namespace goggles::compositor` across all 14 source files in `src/compositor/`. This covers both the opening declaration and any closing comment annotations. No type names, function signatures, or class APIs change. - - Files involved: `src/compositor/compositor_server.hpp`, `src/compositor/compositor_server.cpp`, `src/compositor/compositor_state.hpp`, `src/compositor/compositor_protocol_hooks.hpp`, `src/compositor/compositor_targets.hpp`, `src/compositor/compositor_runtime_metrics.hpp`, `src/compositor/compositor_core.cpp`, `src/compositor/compositor_cursor.cpp`, `src/compositor/compositor_input.cpp`, `src/compositor/compositor_focus.cpp`, `src/compositor/compositor_layer_shell.cpp`, `src/compositor/compositor_present.cpp`, `src/compositor/compositor_xdg.cpp`, `src/compositor/compositor_xwayland.cpp` - - Verification method: `rg -n "goggles::input" src/compositor/` returns zero matches; `pixi run build -p debug` succeeds - - Dependencies: None - - Estimated complexity: S - -- [x] T2 Update forward declaration in UI module - - Description: Update the forward declaration `namespace goggles::input { struct SurfaceInfo; }` to `namespace goggles::compositor { struct SurfaceInfo; }` in the UI module header. - - Files involved: `src/ui/imgui_layer.hpp` - - Verification method: `rg -n "goggles::input" src/ui/` returns zero matches - - Dependencies: T1 - - Estimated complexity: S - -- [x] T3 Update qualified references in test files - - Description: Replace all `goggles::input::` qualified references with `goggles::compositor::` in the three test files that consume compositor types. This includes `goggles::input::CompositorServer::create()` in both input-forwarding tests and `goggles::input::wlr_surface*` / `goggles::input::RuntimeMetricsState` in the filter boundary contracts test. - - Files involved: `tests/input/auto_input_forwarding_wayland.cpp`, `tests/input/auto_input_forwarding_x11.cpp`, `tests/render/test_filter_boundary_contracts.cpp` - - Verification method: `rg -n "goggles::input" tests/` returns zero matches; `pixi run build -p debug` succeeds - - Dependencies: T1 - - Estimated complexity: S - -- [x] T4 Format and full CI verification - - Description: Run the formatter to normalize any style drift introduced by the rename, then run the full CI pipeline to confirm the entire codebase compiles, links, passes all tests, and satisfies static analysis with the new namespace. - - Files involved: CI output only - - Verification method: `pixi run format`; `pixi run ci --runner container --cache-mode warm --lane all` exits with code 0 - - Dependencies: T1, T2, T3 - - Estimated complexity: S - -- [x] T5 Final grep sweep - - Description: Confirm zero occurrences of the old namespace remain anywhere in the source and test trees. This is the terminal acceptance gate for the rename. - - Files involved: `src/`, `tests/` - - Verification method: `rg -n "goggles::input" src/ tests/` returns empty output (no matches) - - Dependencies: T4 - - Estimated complexity: S diff --git a/openspec/changes/extract-filter-chain/design.md b/openspec/changes/extract-filter-chain/design.md deleted file mode 100644 index e7479880..00000000 --- a/openspec/changes/extract-filter-chain/design.md +++ /dev/null @@ -1,267 +0,0 @@ -# Design: Extract Filter Chain - -## Technical Approach - -This change remains the original four-phase extraction program from `openspec/changes/extract-filter-chain/proposal.md`: prepare the monorepo, create the standalone repository, switch goggles to the standalone repository via submodule, then complete validation and cleanup. The design is updated to converge the library boundary without changing that program. - -The end-state boundary is: - -- `goggles-filter-chain` owns the executable runtime, preset/program loading, shader compilation/reflection, embedded assets, control enumeration/mutation, record-time rendering, and bounded inspection that is meaningful to non-goggles consumers. -- Goggles owns host orchestration, end-to-end application integration, swapchain/import/presentation concerns, and host-specific visual verification workflows. -- Intermediate pass capture is not treated as a proven stable public API requirement. If it exists during migration, it is transitional or test-support surface that must be internalized or otherwise clearly marked non-stable before this change is complete. -- Public diagnostics, if retained, are narrowed to the minimum justified caller-facing surface. The stable shape is passive metadata/reporting, not a broad session-lifecycle-driven contract unless later evidence explicitly justifies that broader surface. - -This keeps the original extraction goals intact while reconciling the diagnostics/test-boundary conclusions into the source-of-truth design. - -## Architecture Decisions - -### Decision: Preserve the original extraction program and phase structure - -**Choice**: Keep the existing four phases and the standalone-repo goal exactly as proposed, but make the diagnostics/test-boundary cleanup an explicit completion requirement inside those phases. -**Alternatives considered**: Start a follow-up change for boundary cleanup; freeze the current broader API as-is. -**Rationale**: The extraction is still the right program. The design gap is not whether to extract, but how to converge the public boundary before declaring the original change complete. - -### Decision: FC stays centered on runtime mechanism and bounded inspection - -**Choice**: Treat lifecycle, record, reports, errors, control enumeration/query/mutation, semantic control lookup, source loading, and log routing as the intended stable caller-facing surface. -**Alternatives considered**: Expand the stable surface to include all currently exported diagnostics/capture affordances; reduce FC to a thinner internal engine with goggles-owned wrappers. -**Rationale**: The extracted library is meant to be reusable on its own. Stable API should cover reusable runtime behavior and bounded inspection, not host-specific debugging workflows. - -### Decision: Diagnostics policy narrows to the minimum justified public surface - -**Choice**: Keep only the minimum stable diagnostics contract justified by external-consumer needs. A public diagnostics summary, if retained, is passive metadata retrieved from chain state. Diagnostics mode is the only caller-facing policy knob presumed stable by this change unless stronger evidence appears during implementation. -**Alternatives considered**: Treat diagnostic session lifecycle as stable public API; expose broad diagnostics configuration and artifact plumbing as part of the extracted contract. -**Rationale**: Current evidence supports a narrow inspection surface, not a broad lifecycle-driven diagnostics subsystem. Passive summary metadata is easier to support across hosts and is less likely to encode goggles-specific testing assumptions. - -### Decision: Intermediate pass capture is not part of the stable library boundary - -**Choice**: Do not define intermediate pass capture as a required stable public API outcome of this change. If pass capture remains needed during migration, implement it as internal/test-support plumbing owned by standalone FC tests or clearly transitional code that is removed or internalized before archive. -**Alternatives considered**: Canonize public pass-capture ABI/C++ wrappers as part of the extracted library contract; keep goggles visual tests dependent on private FC headers. -**Rationale**: Pass capture solves a testing problem, not a proven reusable-consumer problem. The extraction boundary should not permanently widen just to preserve goggles-side golden-test mechanics. - -### Decision: Test ownership splits by responsibility boundary - -**Choice**: Standalone `goggles-filter-chain` owns intermediate-pass golden tests and any library-level diagnostics/capture validation. Goggles keeps only end-to-end host integration coverage that proves the host can consume and drive FC correctly through the public boundary. -**Alternatives considered**: Keep all visual golden coverage in goggles; duplicate golden coverage in both repositories. -**Rationale**: Golden validation of FC internals and pass-by-pass execution belongs with the library that owns those mechanics. Goggles should validate host integration, not become the long-term owner of library-internal verification. - -### Decision: Migration is incomplete until transitional boundary debt is removed - -**Choice**: The change is not complete when the standalone repo merely exists or when goggles builds against it. Completion also requires removal of stale transitional APIs, shims, private-header escapes, outdated docs/spec language, and test arrangements that preserve the pre-extraction boundary. -**Alternatives considered**: Declare success after repo extraction and submodule switchover, leaving broader diagnostics/capture cleanup for later. -**Rationale**: Without cleanup, the extracted library would ship a caller-facing boundary shaped by temporary migration pressure instead of the intended durable contract. - -### Decision: Goggles keeps the same `filter-chain/` integration path with local override support - -**Choice**: Replace the tracked directory with a submodule at `filter-chain/` and keep `GOGGLES_FILTER_CHAIN_SOURCE_DIR` as the documented local override. -**Alternatives considered**: Move the dependency to `external/filter-chain/`; replace `add_subdirectory()` with a package-only workflow. -**Rationale**: This preserves the original integration plan, keeps build churn low, and does not conflict with the narrowed diagnostics boundary. - -## Data Flow - -### Phase 1-2 boundary convergence - -```text -monorepo FC runtime + host boundary fixes - | - +--> move FC-owned namespaces and host type references to goggles::fc - +--> remove host dependence on FC private/result headers - +--> migrate goggles visual helpers off FC private headers - | - v -standalone extraction with reusable runtime boundary preserved - | - +--> FC standalone tests own pass-level/golden validation - +--> public diagnostics surface narrowed to passive metadata/minimal policy - +--> transitional pass-capture/session APIs cleaned up or internalized -``` - -### Standalone library ownership model - -```text -host Vulkan handles + preset source + control requests - | - v -FC Instance -> Device -> Program -> Chain - | - +--> parse preset / resolve imports / compile shaders - +--> build executable pass graph / manage history and resources - +--> enumerate controls / apply caller mutations - +--> record rendering work - +--> expose bounded report/summary state -``` - -### Final repository/test ownership - -```text -standalone goggles-filter-chain repo - | - +--> contract tests - +--> consumer validation - +--> intermediate-pass golden tests - v -stable FC package/submodule consumed by goggles - | - +--> end-to-end host integration tests - +--> clean-checkout submodule CI -``` - -## File Changes - -| File | Action | Description | -|------|--------|-------------| -| `filter-chain/include/goggles/filter_chain/vulkan_context.hpp` | Modify | Keep FC-owned C++ support types under `goggles::fc` and trim stale host-assumption fields during convergence if needed. | -| `filter-chain/include/goggles/filter_chain/filter_controls.hpp` | Modify | Preserve stable caller-facing control descriptors under `goggles::fc`. | -| `filter-chain/include/goggles/filter_chain.hpp` | Modify | Converge the canonical public C++20 entrypoint to the final stable runtime/inspection boundary; avoid treating pass capture as durable public API unless explicitly justified. | -| `filter-chain/include/goggles/filter_chain.h` | Modify | Narrow the canonical public C entrypoint to the final justified surface; remove transitional APIs that only exist to unblock migration tests. | -| `filter-chain/include/goggles_filter_chain.h` | Remove | Delete the legacy top-level C entrypoint with no compatibility shim. | -| `filter-chain/src/api/c_api.cpp` | Modify | Match the C ABI implementation to the narrowed diagnostics boundary and any internalized transitional helpers. | -| `filter-chain/src/api/cpp_wrapper.cpp` | Modify | Match RAII wrappers to the final public contract after cleanup. | -| `filter-chain/src/runtime/chain.hpp` | Modify | Keep diagnostics/capture machinery available internally as needed for library-owned tests without forcing it into the stable public boundary. | -| `filter-chain/src/runtime/chain.cpp` | Modify | Support passive summary metadata and any internal test-support capture flow needed by standalone golden tests. | -| `filter-chain/tests/contract/test_filter_chain.cpp` | Modify | Validate only the stable public contract that survives extraction cleanup. | -| `filter-chain/tests/contract/test_runtime_diagnostics.cpp` | Modify | Focus on the justified diagnostics surface; move any pass-capture expectations to internal or standalone golden-test coverage. | -| `filter-chain/tests/visual/**/*` or equivalent standalone golden-test area | Create/Modify | Own intermediate-pass golden tests in the standalone repo rather than goggles. | -| `tests/visual/runtime_capture.hpp` | Modify | Keep goggles helper limited to end-to-end host integration needs; remove dependence on FC-private headers and eventually any dependency on non-stable FC capture/session APIs. | -| `tests/visual/runtime_capture.cpp` | Modify | Use only the stable FC runtime boundary that goggles needs for host integration. If pass-level capture is still required during migration, treat it as temporary and remove before completion. | -| `tests/visual/test_intermediate_golden.cpp` | Delete or migrate | Move intermediate-pass golden ownership to the standalone FC repository. | -| `tests/visual/test_temporal_golden.cpp` | Modify or migrate | Keep in goggles only if it remains true host end-to-end coverage; otherwise move library-owned golden behavior to standalone FC tests. | -| `tests/visual/CMakeLists.txt` | Modify | Keep goggles visual targets limited to host integration coverage after migration. | -| `src/render/backend/vulkan_error.hpp` | Modify | Preserve host-owned error/result independence. | -| `src/render/backend/vulkan_debug.hpp` | Modify | Preserve host-owned error/result independence. | -| `src/render/backend/render_output.hpp` | Modify | Preserve host-owned error/result independence. | -| `src/render/backend/external_frame_importer.hpp` | Modify | Preserve host-owned error/result independence. | -| `CMakeLists.txt` | Modify | Keep the `filter-chain/` submodule bridge with local override support. | -| `.gitmodules` | Create/Modify | Point goggles to `git@github.com:goggles-dev/goggles-filter-chain.git` at the stable path. | -| `.github/workflows/ci.yml` | Modify | Initialize the FC submodule in goggles CI before configure/build/test. | -| `README.md`, `CONTRIBUTING.md`, issue templates, and standalone CI files in the extracted repo | Modify/Create | Document the final stable boundary and final testing ownership after transitional cleanup. | - -## Interfaces / Contracts - -### Stable FC public boundary - -The stable public boundary for this change is centered on reusable runtime behavior: - -- instance/device/program/chain lifecycle -- file and memory preset loading plus import/base-path handling -- record-time execution -- chain reports and last-error queries -- control enumeration, lookup, mutation, and justified reset helpers -- caller-facing runtime policy only where explicitly supported across hosts - - retained stable runtime-policy helpers for this change are `goggles_fc_chain_set_stage_mask`, `goggles_fc_chain_set_prechain_resolution`, and `goggles_fc_chain_get_prechain_resolution` - - retained stable reset helpers for this change are `goggles_fc_chain_reset_control_value` and `goggles_fc_chain_reset_all_controls` -- log routing hooks -- bounded passive diagnostics metadata only where justified - -### Diagnostics contract - -The design converges diagnostics to the narrowest justified stable shape: - -- `mode` is the only diagnostics-policy control assumed stable by default. -- Any public diagnostics summary is passive metadata queried from chain state. -- Explicit diagnostic-session lifecycle is not presumed stable merely because current code exports it. -- If lifecycle APIs remain temporarily during migration, they are transitional and must be either justified and retained intentionally or removed/internalized before archive. - -### Runtime policy and reset helper contract - -The runtime-policy ambiguity for the local boundary migration is resolved as follows: - -- `goggles_fc_chain_set_stage_mask` remains part of the stable public runtime surface because selective prechain/effect/postchain enablement is a reusable cross-host execution policy, not a Goggles-only diagnostics seam. -- `goggles_fc_chain_set_prechain_resolution` and `goggles_fc_chain_get_prechain_resolution` remain stable because prechain sizing is durable chain state that callers need to set and observe across retarget/rebuild flows. -- `goggles_fc_chain_reset_control_value` and `goggles_fc_chain_reset_all_controls` remain stable as the justified reset helpers referenced by the boundary design; they restore caller-visible control state without widening diagnostics or capture policy. -- No additional session-lifecycle, capture-oriented, or diagnostics-policy helpers are implied by retaining these APIs. - -### Pass capture contract - -Intermediate pass capture is treated as internal or test-support behavior unless a concrete non-test consumer requirement is documented during implementation. Therefore: - -- goggles host code and goggles tests must not rely on FC private headers -- standalone FC tests may use internal/test-support seams to validate pass-by-pass results -- the extracted library's installed public contract must not promise pass capture as a durable consumer feature without explicit justification - -### Host contract after extraction - -Goggles remains responsible for: - -- Vulkan instance/device selection and lifetime -- queue ownership, command submission, swapchain, and presentation -- external frame import and synchronization -- application-level policy and UI translation into FC inputs -- end-to-end host integration verification - -FC remains responsible for: - -- preset parsing and import resolution -- shader compilation/reflection -- executable pass graph and internal resources -- frame history and runtime control state -- deterministic runtime behavior behind the stable public API - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|--------------|----------| -| FC contract | Stable public runtime and bounded inspection surface | Standalone contract tests validate lifecycle, reports, controls, source loading, errors, and any intentionally retained diagnostics metadata. | -| FC golden/internal validation | Intermediate-pass behavior and pass-level artifacts | Standalone `goggles-filter-chain` tests own golden coverage for intermediate outputs and any internal capture-based assertions. | -| FC packaging | Consumer usability | Standalone CI continues to require static and shared consumer validation through installed packages. | -| Goggles integration | Host can consume FC through the stable boundary | Goggles tests build and run using only installed/submodule public FC headers and targets. | -| Goggles E2E visual | End-to-end host behavior only | Keep only coverage that proves goggles integration, presentation, and app-level flow; remove library-internal golden ownership from goggles. | -| Repo integration | Submodule and clean checkout behavior | Goggles CI initializes the submodule and passes full CI; standalone FC CI passes independently. | - -## Migration / Rollout - -### Phase 1: Prepare the monorepo - -1. Complete the namespace migration and host reference updates to `goggles::fc`. -2. Preserve host-owned `Result`/error independence. -3. Remove goggles dependence on FC private headers. -4. If temporary public diagnostics/session/capture affordances are needed to unblock migration work, isolate them as transitional rather than final boundary commitments. -5. Keep Phase 1 complete only when the monorepo is green under full CI. - -Phase 1 gate remains `pixi run ci --runner container --cache-mode warm --lane all`. - -### Phase 2: Create the standalone repository - -1. Extract `filter-chain/` history into `goggles-dev/goggles-filter-chain`. -2. Add standalone build metadata, presets, CI, docs, and governance scaffolding. -3. Move intermediate-pass golden coverage and any test-support capture plumbing ownership into the standalone repository. -4. Converge standalone tests so they validate the runtime boundary directly, without depending on goggles-host test structure. -5. Update standalone docs to describe the narrowed stable diagnostics boundary. - -Phase 2 is not complete until the standalone repo both builds independently and owns its library-level golden coverage. - -### Phase 3: Consumer switchover in goggles - -1. Replace the tracked `filter-chain/` directory with the standalone submodule at the same path. -2. Keep the local checkout override via `GOGGLES_FILTER_CHAIN_SOURCE_DIR`. -3. Remove or migrate goggles tests that still depend on transitional pass-capture/session-oriented FC APIs rather than true host integration behavior. -4. Update goggles CI and docs for submodule-aware clean checkouts. - -Phase 3 is not complete until goggles uses FC through the same stable public boundary external consumers receive. - -### Phase 4: Validation and cleanup - -1. Re-run standalone CI and goggles full CI with the final submodule arrangement. -2. Tag `goggles-filter-chain` `v0.1.0` and pin goggles to that release. -3. Remove stale transitional APIs, wrappers, shims, tests, and docs that preserved a migration-only diagnostics/capture boundary. -4. Confirm specs, design, tasks, and repo documentation all describe the same final boundary and test ownership. -5. Archive the change only after both migration and cleanup are done. - -No data migration is required, but contract migration and cleanup are mandatory. - -## End-State Cleanup Requirements - -The change remains open until all of the following are true: - -- goggles no longer owns intermediate-pass golden tests that belong to FC runtime verification -- standalone `goggles-filter-chain` owns those golden tests and any internal capture support they require -- goggles visual/integration tests use only the final stable FC boundary needed for host integration -- no FC private headers are needed anywhere in goggles -- no stale public diagnostics/session/capture APIs remain merely because they were convenient during migration -- standalone and goggles docs describe the same final ownership boundary -- tasks/spec language that still implies a broader stable diagnostics surface is updated before archive - -## Open Questions - -- [ ] If any diagnostics lifecycle API remains public after cleanup, what non-goggles consumer use case justifies it as stable rather than transitional? -- [ ] Which existing goggles temporal visual checks remain true host end-to-end coverage and which should move fully into standalone FC golden coverage during Phase 2/3 cleanup? diff --git a/openspec/changes/extract-filter-chain/exploration.md b/openspec/changes/extract-filter-chain/exploration.md deleted file mode 100644 index d0244364..00000000 --- a/openspec/changes/extract-filter-chain/exploration.md +++ /dev/null @@ -1,173 +0,0 @@ -# Exploration: Extract filter-chain into standalone GitHub repository - -## Current State - -The `filter-chain/` directory in the goggles monorepo is already structured as a near-standalone CMake sub-project: - -- **Own `project(GogglesFilterChain VERSION 0.1.0)`** with full install targets, package config generation, and version compatibility -- **Clean C ABI** (40+ functions via `src/api/c_api.cpp`) + C++ RAII wrappers (`src/api/cpp_wrapper.cpp`) -- **Consumer validation tests**: C11, static C++, and shared C++ consumers in `tests/consumer/` -- **Embedded asset pipeline**: Internal shaders are compiled into the binary via `EmbedAssets.cmake` - no runtime asset directory needed -- **Independent infrastructure**: Own `.clang-format`, `.clang-tidy`, CMake modules (`cmake/CompilerConfig.cmake`, `cmake/CodeQuality.cmake`, `cmake/FilterChainDependencies.cmake`) -- **Public headers**: Canonical C++ entrypoint `include/goggles/filter_chain.hpp` plus 6 support headers in `include/goggles/filter_chain/` (`common.hpp`, `error.hpp`, `filter_controls.hpp`, `result.hpp`, `scale_mode.hpp`, `vulkan_context.hpp`) -- **Duplicated shared types**: `goggles::Error`, `goggles::Result`, `goggles::ErrorCode` defined identically in both `filter-chain/include/goggles/filter_chain/error.hpp` and `src/util/error.hpp` with `GOGGLES_ERROR_TYPES_DEFINED` ODR guards - -### How goggles monorepo currently consumes filter-chain - -The top-level `CMakeLists.txt` simply does `add_subdirectory(filter-chain)`. The host project accesses FC through: - -1. **C++ wrapper API**: `src/render/backend/filter_chain_controller.hpp` includes `` -2. **Public headers**: `vulkan_context.hpp`, `filter_controls.hpp` for cross-boundary types -3. **Result types**: 4 host files (`vulkan_error.hpp`, `vulkan_debug.hpp`, `external_frame_importer.hpp`, `render_output.hpp`) include `` instead of `src/util/error.hpp` - -### Dependencies - -FC depends on (via `FilterChainDependencies.cmake`): -- **Public**: Vulkan SDK, expected-lite (nonstd::expected) -- **Private**: spdlog, slang (shader compiler), stb_image, Catch2 (tests only) -- All currently provided through pixi/conda (`$CONDA_PREFIX` hints in cmake) -- `expected-lite` is a local pixi package in `packages/expected-lite/` (recipe builds from upstream git) - -## Affected Areas - -### In the new standalone repo (to be created) -- `filter-chain/` entire directory moves to become the repo root -- Needs: own `pixi.toml`, `CMakePresets.json`, CI workflow, LICENSE, README -- Namespace: internal code uses `goggles::render` extensively (47 matches in private headers) -- Public headers use `goggles::render` for `VulkanContext` and `FilterControlDescriptor` - -### In the goggles monorepo (post-extraction) -- `CMakeLists.txt`: Replace `add_subdirectory(filter-chain)` with external dependency consumption -- `cmake/Dependencies.cmake`: Add `find_package(GogglesFilterChain)` or equivalent -- `src/render/backend/`: 4 files using `` should switch to `src/util/error.hpp` -- `tests/visual/runtime_capture.cpp`: Includes FC internal headers (`chain/chain_runtime.hpp`, `diagnostics/diagnostic_policy.hpp`) directly -- `tests/render/test_filter_boundary_contracts.cpp`, `test_vulkan_backend_subsystem_contracts.cpp`: Include FC headers -- CI scripts: Consumer validation test would move to the new repo - -## Approaches - -### 1. Git History Extraction - -#### A. `git filter-repo` (Recommended) -- Pros: Clean rewrite, preserves commit history for `filter-chain/` subtree, fastest tool -- Cons: Rewrites SHAs (expected), requires `pip install git-filter-repo` -- Effort: Low -- Method: `git filter-repo --subdirectory-filter filter-chain/` on a clone, then push to new remote - -#### B. `git subtree split` -- Pros: Built into git, no external tools, creates clean linear history -- Cons: Loses merge commits, can be slow on large repos -- Effort: Low -- Method: `git subtree split --prefix=filter-chain/ -b filter-chain-standalone && git push filter-chain-standalone:main` - -#### C. Fresh repo (copy, no history) -- Pros: Cleanest start, no historical baggage -- Cons: Loses valuable commit history and blame context -- Effort: Low -- Method: Copy directory, `git init`, commit - -**Recommendation**: Option A (`git filter-repo`) - standard tool for this, preserves meaningful history. - -### 2. Consumer Integration (How goggles consumes the extracted library) - -#### A. Git submodule -- Pros: Exact version pinning, works well with `add_subdirectory()`, minimal CMake changes -- Cons: Submodule workflow friction, nested git repos, clone complexity -- Effort: Low (minimal CMake changes) - -#### B. CMake FetchContent -- Pros: Automatic download at configure time, no submodule friction, version pinning via git tag -- Cons: Download at configure time, no offline builds without cache, harder to develop both simultaneously -- Effort: Medium - -#### C. Pixi/conda package -- Pros: Consistent with existing dependency model, clean package boundary, versioned releases -- Cons: Requires packaging infrastructure (recipe.yaml), publish workflow, version lag during development -- Effort: High (need conda package build + publish pipeline) - -#### D. Vendored copy (manual or scripted) -- Pros: Simple, offline-capable, full control -- Cons: Manual sync burden, no version tracking -- Effort: Low initially, high ongoing - -**Recommendation**: Option A (git submodule) for initial extraction - least friction, allows `add_subdirectory()` to keep working exactly as today. Migrate to Option C (pixi/conda package) once the library stabilizes and a release cadence is established. - -### 3. Standalone CI Pipeline - -The new repo needs its own CI workflow. Based on the goggles CI pattern: - -``` -Jobs: -1. format-check - clang-format + taplo (if TOML files present) -2. build-and-test - cmake build + ctest (debug, asan presets) -3. consumer-validation - validate-installed-consumers.sh (C, static C++, shared C++) -4. static-analysis - clang-tidy quality build -``` - -Key decisions: -- Needs own `pixi.toml` with FC-specific dependencies only (Vulkan, spdlog, slang, expected-lite, stb, Catch2, clang-tools) -- Needs own `CMakePresets.json` (can start with subset of goggles presets) -- Consumer validation is already self-contained in `scripts/task/validate-installed-consumers.sh` and `tests/consumer/` -- No Semgrep rules needed initially (those are goggles-app-specific) - -### 4. Packaging Format - -#### A. Conda package via pixi-build -- Pros: Matches goggles dependency model, existing `packages/` pattern as template -- Cons: Need rattler-build recipe, publish to conda-forge or private channel -- Effort: Medium - -#### B. CMake install + find_package (FetchContent or submodule) -- Pros: Already works (`GogglesFilterChainConfig.cmake.in` fully functional), standard C++ approach -- Cons: Not a "package" in the distribution sense -- Effort: Already done - -#### C. System packages (deb/rpm) -- Pros: Standard for Linux distribution -- Cons: Heavy infrastructure, narrow user base for now -- Effort: High - -**Recommendation**: Start with B (CMake install - already works). Add A (conda package) when the library needs wider distribution. The existing `GogglesFilterChainConfig.cmake.in` is production-ready. - -## Coupling Issues (Pre-extraction vs Post-extraction) - -### Fix BEFORE extraction -1. **Result coupling** (4 host files): These should include `src/util/error.hpp` instead of ``. Both files are identical with ODR guards, but the host should use its own copy. Quick fix, reduces coupling score. - -2. **Visual test internal header includes**: `tests/visual/runtime_capture.cpp` includes `chain/chain_runtime.hpp` and `diagnostics/diagnostic_policy.hpp` (FC private headers). This will break after extraction. Needs refactoring to use only the public API. - -### Fix AFTER extraction (or as part of it) -3. **`$CONDA_PREFIX` hints**: These are fine for now - they'll work in the standalone pixi environment too. Can be cleaned up later with proper CMake find module paths. - -4. **Namespace `goggles::render`**: This is internal to FC and doesn't affect the extraction. The public API uses C linkage. Renaming to `goggles::filter_chain` would be a large internal refactor with no functional benefit for extraction. - -5. **Missing standalone infrastructure**: pixi.toml, CMakePresets.json, CI workflow, LICENSE - these are created as part of the extraction, not prerequisites. - -## Key Risks - -1. **Visual test breakage**: `tests/visual/runtime_capture.cpp` directly includes FC internal headers. After extraction, these internal headers won't be available. The visual tests need refactoring to use only public FC API before or during extraction. - -2. **ODR fragility**: The shared `goggles::Error`/`goggles::Result` types are guarded by `GOGGLES_ERROR_TYPES_DEFINED`, but if the types ever diverge between the two copies, hard-to-debug ODR violations will occur. The extraction is an opportunity to decide: does FC export its own error types, or does it share a common error library? - -3. **CI parity gap**: Until the standalone repo has its own CI, there's a window where FC changes aren't validated independently. The goggles monorepo CI currently validates FC as part of its build. - -4. **`expected-lite` packaging**: FC publicly depends on `nonstd::expected`. Currently provided by the goggles local pixi package `packages/expected-lite/`. The standalone repo needs its own way to get this dependency (own pixi recipe, or rely on conda-forge). - -5. **Slang compiler dependency**: The `slang` shader compiler is a private dependency provided by pixi. Need to verify it's available on conda-forge or package it for the standalone repo. - -6. **Two-repo development friction**: During active development, changes that span FC and goggles will require coordinated commits across two repos. Git submodules help but add workflow complexity. - -## Questions Requiring User Input - -See structured questions below - these cover the key decisions that need user input before a concrete proposal can be created. - -## Ready for Proposal - -**No** - Several key decisions need user input first: -1. Git history preservation strategy -2. Consumer integration method -3. Namespace/error type strategy -4. Versioning and release cadence -5. Whether to fix coupling issues before or after extraction -6. License for the new repo -7. CI/issue tracker decisions diff --git a/openspec/changes/extract-filter-chain/proposal.md b/openspec/changes/extract-filter-chain/proposal.md deleted file mode 100644 index c94e5bb4..00000000 --- a/openspec/changes/extract-filter-chain/proposal.md +++ /dev/null @@ -1,165 +0,0 @@ -# Proposal: Extract filter-chain into standalone GitHub repository - -## Problem - -The `filter-chain/` sub-project has reached the point where its reusable Vulkan runtime, package/install surface, and independent validation story no longer fit cleanly inside the goggles monorepo. Keeping it embedded makes external consumption harder, couples its release cadence to the viewer, and leaves ownership boundaries blurry. - -The extraction goal remains unchanged: move filter-chain into its own repository without weakening the host/library boundary. The accepted boundary is now tighter than some later transition planning assumed. FC should remain centered on reusable runtime mechanism plus bounded inspection. Intermediate pass capture is not a proven stable public API requirement, and any public diagnostics summary that survives should be passive metadata rather than a session-lifecycle-driven contract. - -## Intent - -Extract `filter-chain/` into `goggles-dev/goggles-filter-chain` as a first-class standalone library while preserving the original four-phase program: - -1. **Reusability** - external Vulkan applications can consume FC without the goggles monorepo. -2. **Independent release cycle** - FC can ship and version independently, starting at `0.1.0`. -3. **Clear ownership boundary** - goggles keeps host integration responsibilities; FC owns runtime execution and its own diagnostics-heavy/intermediate-pass verification. - -## Scope - -### In Scope - -- Namespace migration of FC-owned C++ types from `goggles::render` to `goggles::fc` across FC code and host callers. -- Removal of goggles host coupling to FC-owned `Result` headers by switching host backend headers to `src/util/error.hpp`. -- Boundary convergence for diagnostics and test ownership: goggles host tests use only the durable public runtime boundary, while standalone `filter-chain` owns intermediate-pass golden coverage and other diagnostics-heavy verification. -- Stable caller-facing diagnostics policy narrowed to the minimum justified surface; any retained summary/readout remains passive metadata, not a public session-management promise. -- `FilterChainDependencies.cmake` portability fixes so standalone FC resolves dependencies through standard CMake discovery. -- Asset audit to ensure only `crt-lottes-fast` remains in upstream packaged presets while required test/diagnostic/internal shader assets stay intact. -- Standalone repository creation at `git@github.com:goggles-dev/goggles-filter-chain.git` via `git filter-repo --subdirectory-filter filter-chain/`, including standalone `pixi.toml`, `CMakePresets.json`, CI, license, docs, and repository governance. -- Goggles consumer switchover from tracked in-repo `filter-chain/` content to a pinned git submodule while preserving `add_subdirectory()` semantics. -- Final migration and stale transitional code cleanup across both repos before the change is considered complete. - -### Out of Scope - -- Rolling back, replacing, or de-scoping the standalone extraction itself. -- Treating public intermediate-pass capture as part of the durable stable FC API without a later separately justified change. -- Expanding public diagnostics/session lifecycle APIs beyond the minimum stable inspection surface required by the accepted boundary. -- Shared error/util library extraction. -- Migration from git submodule consumption to conda/pixi packaging. -- Unrelated FC feature work or broad host/render refactors beyond what extraction requires. - -## Approach - -The extraction remains a four-phase process. The phase structure stays intact, but the accepted boundary is tightened inside that plan. - -### Phase 1: Prepare the monorepo - -Land all boundary-hardening work in goggles first so the existing CI validates the extracted shape before the split. - -1. **Namespace migration** - rename FC-owned C++ namespaces from `goggles::render` to `goggles::fc` across FC code, tests, and host references. -2. **Host error-type decoupling** - stop goggles backend headers from depending on FC-owned result/error headers. -3. **Host/test boundary cleanup** - remove goggles reliance on FC private headers and migrate goggles-side verification to the durable public runtime boundary only. -4. **Diagnostics boundary tightening** - stop treating intermediate pass capture as a stable caller-facing requirement; if diagnostics summary remains public, treat it as passive inspection metadata rather than public session lifecycle control. -5. **Verification ownership split** - move intermediate-pass golden coverage and other diagnostics-heavy verification into standalone `filter-chain` ownership; goggles keeps end-to-end host integration coverage. -6. **Portability and asset audit** - make dependency discovery standalone-safe and confirm packaged shader contents match the curated asset boundary. - -**Gate**: Full monorepo CI green (`pixi run ci --runner container --cache-mode warm --lane all`). - -### Phase 2: Create standalone repository - -Extract clean history and make FC independently buildable, testable, documented, and governable. - -1. Run `git filter-repo --subdirectory-filter filter-chain/` from a fresh clone. -2. Push to `git@github.com:goggles-dev/goggles-filter-chain.git`. -3. Add standalone repo infrastructure: `pixi.toml`, `CMakePresets.json`, CI workflow, `LICENSE`, `README.md`, issue templates, and repository settings. -4. Keep FC's own verification authoritative for runtime internals, including diagnostics-heavy and intermediate-pass golden workflows that no longer belong to goggles-host coverage. -5. Version the standalone project independently as `0.1.0`. - -**Gate**: Standalone CI green, including build/test, consumer validation, static analysis, and standalone-owned verification coverage. - -### Phase 3: Consumer switchover in goggles - -Replace the monorepo-owned FC tree with consumption of the extracted repository. - -1. Remove the tracked `filter-chain/` directory from goggles. -2. Add `goggles-filter-chain` back as a git submodule at `filter-chain/`. -3. Keep `add_subdirectory()` integration, with an overrideable local checkout path for co-development. -4. Update goggles CI and path-sensitive docs/scripts so clean checkouts initialize the submodule before build/test. -5. Verify goggles host integration coverage still passes against the extracted dependency. - -**Gate**: Goggles CI green with submodule integration (`pixi run ci --runner container --cache-mode warm --lane all`). - -### Phase 4: Validation and cleanup - -Complete the extraction by finishing migration, removing stale transitional scaffolding, and validating both repositories in their final state. - -1. Tag `goggles-filter-chain` `v0.1.0` and pin goggles to that release. -2. Remove or internalize stale transitional code, wrappers, API surface, and docs that existed only to bridge pre-extraction goggles-specific test needs. -3. Confirm no in-tree caller still depends on transitional diagnostics/session/capture paths that are outside the accepted stable boundary. -4. Re-run final standalone and goggles verification from clean checkouts. -5. Archive the change only after migration and cleanup are both complete. - -**Gate**: Both repos green, release tag published, goggles pinned to the tagged submodule ref, and stale transitional extraction scaffolding removed. - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `filter-chain/include/goggles/filter_chain/*.hpp` | Modified | FC namespace boundary and durable public C++ surface updates | -| `filter-chain/include/goggles/filter_chain.h` | Modified | Canonical public C entrypoint narrowed to the minimum justified runtime/inspection surface | -| `filter-chain/include/goggles/filter_chain.hpp` | Modified | Canonical public C++20 entrypoint exposes the installed wrapper surface | -| `filter-chain/include/goggles_filter_chain.h` | Removed | Legacy top-level C entrypoint removed with no compatibility shim | -| `filter-chain/src/**/*.{hpp,cpp}` | Modified | Runtime extraction, namespace migration, diagnostics/capture internalization, and cleanup | -| `filter-chain/tests/**/*` | Modified | Standalone ownership of diagnostics-heavy and intermediate-pass verification | -| `filter-chain/cmake/FilterChainDependencies.cmake` | Modified | Standard CMake dependency discovery for standalone use | -| `filter-chain/assets/shaders/**/*` | Modified/Reviewed | Curated packaged shader contents and required internal/test assets | -| `src/render/backend/**/*` | Modified | Host boundary cleanup, FC type namespace updates, and submodule consumption support | -| `tests/render/**/*` | Modified | Host integration tests aligned to durable public FC surface only | -| `tests/visual/**/*` | Modified | Goggles visual coverage narrowed to end-to-end host behavior rather than intermediate artifact ownership | -| `CMakeLists.txt` | Modified | Submodule-based `add_subdirectory()` bridge with local override support | -| `.gitmodules` | New | Git submodule configuration for extracted FC repository | -| `.github/workflows/ci.yml` | Modified | Goggles CI submodule initialization and extracted-boundary validation | -| `openspec/changes/extract-filter-chain/specs/**/*.md` | Modified | Change specs updated to match tightened diagnostics boundary and verification ownership split | -| `openspec/specs/**/*.md` | Modified | Living specs updated where extraction changes the durable system contract | - -## Non-goals - -- Preserve transitional testing-oriented API surface just because it unblocked goggles during migration. -- Make goggles the long-term owner of FC intermediate-pass artifact generation or golden baselines. -- Expand the public diagnostics contract beyond what is justified for external consumers. - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| Boundary convergence stalls and leaves transition-era diagnostics/capture APIs treated as permanent | Medium | Make cleanup an explicit completion gate and align proposal/spec/design/tasks around the narrowed durable boundary | -| Goggles host coverage loses useful debugging visibility when intermediate workflows move out | Medium | Keep goggles end-to-end integration assertions, but relocate intermediate-pass and diagnostics-heavy golden coverage into standalone FC verification where it belongs | -| Namespace and dependency-boundary changes break builds in unexpected places | Medium | Land all prep work in the monorepo first and gate Phase 1 with full CI | -| Two-repo development and submodule adoption introduce workflow friction | Medium | Keep stable `filter-chain/` path, support local override, and document the co-development workflow in both repos | -| Extraction appears done before migration debt is actually removed | High | Treat stale transitional code/docs/API cleanup as mandatory Phase 4 work, not optional follow-up | - -## Rollback Plan - -Each phase remains independently reversible: - -- **Phase 1** - revert the monorepo preparation commits that rename namespaces and harden the boundary. -- **Phase 2** - delete the new standalone repository if extraction infrastructure is not ready. -- **Phase 3** - remove the submodule, restore the in-repo `filter-chain/` tree from the last pre-switchover commit, and revert the goggles build/CI changes. -- **Phase 4** - revert cleanup commits and retag/repin as needed if final convergence exposes regressions. - -Rollback restores a working monorepo state, but Phase 4 cleanup is not optional in the forward direction. - -## Dependencies - -- GitHub repository creation and admin access for `goggles-dev/goggles-filter-chain`. -- `git-filter-repo` availability. -- Agreement that standalone `filter-chain` owns intermediate-pass golden tests and diagnostics-heavy runtime verification. -- Follow-through to update both change artifacts and living specs so the accepted boundary is consistent across proposal, design, specs, tasks, and code. - -## Validation Plan - -- Run full goggles CI after monorepo preparation (`pixi run ci --runner container --cache-mode warm --lane all`). -- Run standalone FC CI after extraction, including build/test, static analysis, and installed-consumer validation. -- Verify goggles clean-checkout + submodule initialization succeeds and full goggles CI passes against the extracted dependency. -- Verify no remaining caller-facing dependency on transitional diagnostics session/capture surface that falls outside the accepted stable boundary. -- Verify migration is not declared complete until stale transitional code and docs are removed. - -## Success Criteria - -- [ ] FC is extracted to `goggles-dev/goggles-filter-chain` with independent versioning, CI, docs, and governance. -- [ ] FC-owned C++ types use `goggles::fc`, and goggles host code no longer depends on FC-owned result/error headers. -- [ ] Goggles host/tests consume only the durable public FC boundary; no goggles-owned test relies on FC private headers or owns intermediate-pass golden workflows. -- [ ] Intermediate-pass golden tests and diagnostics-heavy verification live under standalone `filter-chain` ownership. -- [ ] The stable caller-facing diagnostics policy is narrowed to the minimum justified surface, and any retained public summary is passive metadata rather than session-lifecycle-driven API control. -- [ ] Goggles consumes FC via a pinned submodule and passes full CI from a clean checkout. -- [ ] Standalone FC passes its own required CI and consumer validation gates. -- [ ] Migration is complete only after stale transitional code, wrappers, docs, and boundary scaffolding are cleaned up across both repos. diff --git a/openspec/changes/extract-filter-chain/specs/build-system/spec.md b/openspec/changes/extract-filter-chain/specs/build-system/spec.md deleted file mode 100644 index 193caf22..00000000 --- a/openspec/changes/extract-filter-chain/specs/build-system/spec.md +++ /dev/null @@ -1,179 +0,0 @@ -# Delta for build-system - -## MODIFIED Requirements - -### Requirement: Standalone Dependency Discovery Module - -(Previously: `FilterChainDependencies.cmake` MAY have used `$ENV{CONDA_PREFIX}` hints for dependency discovery.) - -The standalone project SHALL provide a `cmake/FilterChainDependencies.cmake` module that uses exclusively standard `find_package()` calls for all third-party dependency discovery. The module SHALL NOT reference `$ENV{CONDA_PREFIX}`, `$ENV{CONDA_BUILD_SYSROOT}`, or any Conda/Pixi-specific environment variables for path hints. Dependencies SHALL be discoverable through standard CMake search paths (`CMAKE_PREFIX_PATH`, system paths, or package-specific `_DIR` variables). - -#### Scenario: No Conda environment variable references - -- **GIVEN** the file `filter-chain/cmake/FilterChainDependencies.cmake` -- **WHEN** a text search for `CONDA_PREFIX`, `CONDA_BUILD_SYSROOT`, or `ENV{CONDA` is executed -- **THEN** zero matches SHALL be found -- **AND** all dependency discovery SHALL use standard CMake `find_package()` mechanisms - -#### Scenario: Standalone build without Pixi wrapper - -- **GIVEN** a clean checkout of the filter-chain project with dependencies available on standard CMake search paths -- **WHEN** `cmake -S . -B build` is executed without any Pixi/Conda environment active -- **THEN** configuration SHALL succeed by discovering all dependencies through standard `find_package()` calls -- **AND** no configuration step SHALL require `$ENV{CONDA_PREFIX}` to be set - -#### Scenario: Dependency module resolves all required libraries - -- **GIVEN** the standalone project is configured from a clean checkout -- **WHEN** `cmake/FilterChainDependencies.cmake` is included during configuration -- **THEN** the module SHALL resolve Vulkan, expected-lite, spdlog, slang, stb_image, and Catch2 through standard `find_package()` calls -- **AND** configuration SHALL NOT require Goggles-owned CMake Find modules - -### Requirement: In-Repo Subdirectory Bridge During Extraction - -(Previously: Specified `add_subdirectory(filter-chain/)` as transitional bridge before `find_package()` switch.) - -**Replaced by**: Submodule Integration (see ADDED section below). The filter-chain directory is replaced by a git submodule. The `add_subdirectory()` integration pattern is preserved but now targets the submodule path instead of an in-repo directory. - -## ADDED Requirements - -### Requirement: Cross-Repository Verification Ownership Split - -After extraction, build and test ownership SHALL be split by boundary responsibility. Standalone `filter-chain` SHALL own intermediate-pass golden tests and other diagnostics-heavy verification that depends on internal capture-oriented capabilities. Goggles SHALL retain end-to-end host integration coverage only. - -#### Scenario: Standalone project owns intermediate-pass verification - -- **GIVEN** verification assets that assert on intermediate pass outputs or diagnostics-heavy golden baselines -- **WHEN** test ownership is reviewed after extraction -- **THEN** those assets SHALL live in standalone `filter-chain` build and test targets -- **AND** they SHALL NOT remain Goggles-owned requirements for normal host builds - -#### Scenario: Goggles build keeps host integration coverage - -- **GIVEN** the Goggles repository after extraction -- **WHEN** Goggles FC-facing test targets are inspected -- **THEN** they SHALL cover host wiring and end-to-end integration behavior against the public boundary -- **AND** they SHALL NOT duplicate standalone `filter-chain` intermediate-pass golden coverage - -### Requirement: Migration Cleanup Completes the Build/Test Transition - -The extraction build/test transition SHALL NOT be considered complete until stale transitional build wiring, test wiring, and supported-surface assumptions have been cleaned up after migration. Temporary accommodations that keep deprecated caller-facing diagnostics-session or pass-capture behavior alive for Goggles-owned verification SHALL be removed, internalized, or reassigned once the new ownership split is in place. - -#### Scenario: Transitional Goggles-owned capture wiring is removed - -- **GIVEN** standalone `filter-chain` owns the intermediate and diagnostics-heavy verification flows -- **WHEN** Goggles build and test wiring is inspected at change completion -- **THEN** Goggles-owned targets SHALL no longer depend on transitional public pass-capture or diagnostics-session lifecycle accommodations -- **AND** stale transitional test/build wiring SHALL not remain as part of the supported extracted boundary - -### Requirement: Standalone Build Independence - -The filter-chain project SHALL build independently as a complete CMake project from a clean checkout without requiring the goggles parent repository context, Pixi task wrappers, or Conda-specific environment assumptions. - -#### Scenario: Standalone configure and build - -- **GIVEN** a clean checkout of the filter-chain standalone repository -- **WHEN** `cmake -S . -B build` and `cmake --build build` are executed with dependencies on standard search paths -- **THEN** configuration and build SHALL succeed -- **AND** no file from the goggles parent repository SHALL be required - -#### Scenario: Standalone test execution - -- **GIVEN** a successfully built standalone filter-chain project -- **WHEN** `ctest --test-dir build` is executed -- **THEN** all contract tests SHALL pass -- **AND** test execution SHALL NOT depend on goggles repository test fixtures or infrastructure - -#### Scenario: Standalone install and consume - -- **GIVEN** a successfully built standalone filter-chain project -- **WHEN** the project is installed to a prefix via `cmake --install build --prefix /tmp/fc-install` -- **AND** a consumer validation project configures with `CMAKE_PREFIX_PATH=/tmp/fc-install` -- **THEN** `find_package(GogglesFilterChain CONFIG REQUIRED)` SHALL succeed -- **AND** both STATIC and SHARED consumer validation projects SHALL compile and link without errors - -### Requirement: Standalone Pixi Environment - -The standalone filter-chain repository SHALL have its own `pixi.toml` for dependency management, independent of the goggles monorepo `pixi.toml`. - -#### Scenario: Own pixi.toml with FC-specific dependencies - -- **GIVEN** the standalone filter-chain repository root -- **WHEN** `pixi.toml` is inspected -- **THEN** it SHALL declare FC-specific dependencies: Vulkan, spdlog, slang, expected-lite, stb, Catch2, and clang-tools -- **AND** it SHALL NOT reference goggles-specific dependencies (SDL3, CLI11, toml11, BS_thread_pool, wayland-client) - -#### Scenario: Pixi environment builds the project - -- **GIVEN** the standalone filter-chain repository with its own `pixi.toml` -- **WHEN** `pixi install` and the documented pixi build task are executed -- **THEN** all dependencies SHALL be resolved from the standalone `pixi.toml` -- **AND** the project SHALL build successfully within the pixi environment - -### Requirement: Standalone CMake Presets - -The standalone filter-chain repository SHALL have its own `CMakePresets.json` providing build presets appropriate for the library project. - -#### Scenario: CMakePresets.json contains required presets - -- **GIVEN** the standalone filter-chain repository root -- **WHEN** `CMakePresets.json` is inspected -- **THEN** it SHALL contain at minimum: debug, release, asan, quality, and test presets -- **AND** presets SHALL be self-contained (no `include` of goggles preset files) - -#### Scenario: Presets work for standalone builds - -- **GIVEN** the standalone filter-chain `CMakePresets.json` -- **WHEN** `cmake --preset test` is executed -- **THEN** configuration SHALL succeed using only the standalone preset definitions -- **AND** the resulting build directory SHALL be independent of any goggles build artifacts - -### Requirement: Submodule Integration for Goggles - -The goggles monorepo SHALL consume the extracted filter-chain via git submodule, preserving `add_subdirectory()` build semantics. The submodule SHALL be pinned to a specific commit or tag for deterministic builds. - -#### Scenario: Goggles build with submodule - -- **GIVEN** the goggles repository with filter-chain configured as a git submodule -- **WHEN** `git submodule update --init` is executed followed by a full build -- **THEN** the build SHALL succeed with `add_subdirectory()` consuming the submodule path -- **AND** all goggles tests SHALL pass - -#### Scenario: Submodule pinned to specific ref - -- **GIVEN** the `.gitmodules` file in the goggles repository -- **WHEN** the submodule configuration is inspected -- **THEN** the filter-chain submodule SHALL reference `git@github.com:goggles-dev/goggles-filter-chain.git` -- **AND** the submodule working copy SHALL be pinned to a specific commit (tag preferred) - -#### Scenario: Goggles CI passes with submodule integration - -- **GIVEN** the goggles repository with the filter-chain submodule -- **WHEN** `pixi run ci --runner container --cache-mode warm --lane all` is executed -- **THEN** all CI lanes SHALL pass -- **AND** the filter-chain submodule SHALL be initialized and available during the build - -#### Scenario: Local development override documented - -- **GIVEN** a developer working on both goggles and filter-chain simultaneously -- **WHEN** they need to test local filter-chain changes without pushing to the submodule remote -- **THEN** the documented workflow SHALL describe how to use a local path override (e.g., `add_subdirectory()` with a local checkout path via CMake variable) -- **AND** the override mechanism SHALL NOT require modifying `.gitmodules` or committed CMake files - -### Requirement: Filter-Chain Directory Removal - -After submodule integration, the `filter-chain/` in-repo directory SHALL be removed from the goggles repository and replaced entirely by the git submodule. - -#### Scenario: No in-repo filter-chain source after extraction - -- **GIVEN** the goggles repository after extraction is complete -- **WHEN** the repository contents are inspected (excluding the submodule) -- **THEN** no `filter-chain/` directory with FC source code SHALL exist as tracked repository content -- **AND** the path SHALL be occupied by the git submodule reference only - -#### Scenario: Git history preserved in standalone repo - -- **GIVEN** the standalone filter-chain repository created via `git filter-repo --subdirectory-filter filter-chain/` -- **WHEN** the git log is inspected -- **THEN** the commit history relevant to filter-chain files SHALL be preserved -- **AND** the history SHALL be clean (no goggles-only commits) diff --git a/openspec/changes/extract-filter-chain/specs/ci/spec.md b/openspec/changes/extract-filter-chain/specs/ci/spec.md deleted file mode 100644 index 8bb2d296..00000000 --- a/openspec/changes/extract-filter-chain/specs/ci/spec.md +++ /dev/null @@ -1,79 +0,0 @@ -# Delta for ci - -## ADDED Requirements - -### Requirement: Standalone Filter-Chain CI Pipeline - -The standalone filter-chain repository SHALL have its own GitHub Actions CI pipeline that validates the library independently of the goggles monorepo CI. The pipeline SHALL be a required merge gate on the standalone repository's main branch. - -#### Scenario: Format check lane - -- **GIVEN** code is pushed to the standalone filter-chain repository -- **WHEN** the CI pipeline executes the format check lane -- **THEN** clang-format SHALL be applied to all `.c`, `.h`, `.cpp`, `.hpp` files -- **AND** the lane SHALL fail if formatting violations are detected (or auto-fix and push on non-fork branches) - -#### Scenario: Build and test lane - -- **GIVEN** the standalone CI pipeline runs -- **WHEN** the build-and-test lane executes -- **THEN** the project SHALL configure, build, and run all contract tests -- **AND** the lane SHALL fail if any test fails - -#### Scenario: Consumer validation lane - -- **GIVEN** the standalone CI pipeline runs -- **WHEN** the consumer validation lane executes -- **THEN** the project SHALL build, install, and run consumer validation for both STATIC and SHARED library outputs -- **AND** `find_package(GogglesFilterChain CONFIG REQUIRED)` SHALL succeed for both consumer validation projects -- **AND** the lane SHALL fail if any consumer validation project fails to compile, link, or execute - -#### Scenario: Static analysis lane - -- **GIVEN** the standalone CI pipeline runs -- **WHEN** the static analysis lane executes -- **THEN** clang-tidy (or equivalent quality build) SHALL analyze FC source files -- **AND** the lane SHALL fail if blocking findings are reported - -#### Scenario: CI pipeline is the merge gate - -- **GIVEN** a pull request is opened against the standalone repository's main branch -- **WHEN** CI pipeline results are evaluated for merge eligibility -- **THEN** all lanes (format, build+test, consumer validation, static analysis) SHALL be required checks -- **AND** the PR SHALL NOT be mergeable until all lanes pass - -### Requirement: Goggles CI Submodule Awareness - -The goggles monorepo CI pipeline SHALL correctly initialize and build with the filter-chain git submodule. CI SHALL NOT assume the filter-chain source exists as an in-repo directory. - -#### Scenario: CI initializes submodule before build - -- **GIVEN** the goggles CI pipeline runs on a clean checkout -- **WHEN** the build step begins -- **THEN** git submodules SHALL be initialized and updated before CMake configuration -- **AND** the filter-chain submodule SHALL be present at the expected path - -#### Scenario: Goggles CI passes with submodule - -- **GIVEN** the goggles CI pipeline with submodule integration -- **WHEN** `pixi run ci --runner container --cache-mode warm --lane all` executes -- **THEN** all existing CI lanes SHALL pass -- **AND** no lane SHALL fail due to missing filter-chain source files - -### Requirement: Cross-Repository Verification Scope Split - -CI ownership SHALL follow the narrowed public boundary. Standalone `filter-chain` CI SHALL own intermediate-pass golden and diagnostics-heavy verification that depends on internal capture-oriented capabilities. Goggles CI SHALL retain end-to-end host integration coverage against the durable public boundary. - -#### Scenario: Standalone CI owns diagnostics-heavy FC verification - -- **GIVEN** verification flows that assert on intermediate outputs or diagnostics-heavy golden baselines -- **WHEN** CI responsibility is reviewed after extraction -- **THEN** those flows SHALL run in standalone `filter-chain` CI -- **AND** they SHALL NOT remain required Goggles CI coverage for normal host integration validation - -#### Scenario: Goggles CI validates only host-facing boundary behavior - -- **GIVEN** the Goggles CI pipeline after extraction -- **WHEN** FC-related coverage is evaluated -- **THEN** Goggles CI SHALL validate host wiring and end-to-end public-boundary behavior -- **AND** it SHALL NOT depend on stable caller-facing pass capture or diagnostics-session lifecycle APIs to satisfy its coverage obligations diff --git a/openspec/changes/extract-filter-chain/specs/filter-chain-assets-package/spec.md b/openspec/changes/extract-filter-chain/specs/filter-chain-assets-package/spec.md deleted file mode 100644 index b466c2da..00000000 --- a/openspec/changes/extract-filter-chain/specs/filter-chain-assets-package/spec.md +++ /dev/null @@ -1,37 +0,0 @@ -# Delta for filter-chain-assets-package - -## MODIFIED Requirements - -### Requirement: Curated Upstream Shader Content - -(Previously: The standalone asset package SHALL include only the specific upstream shader files referenced by zfast integration tests and shader validation tests, and SHALL NOT mirror the full `shaders/retroarch/` upstream collection.) - -The standalone asset package SHALL retain exactly one upstream shader preset: `crt-lottes-fast`. All other upstream presets from the RetroArch shader collection SHALL be removed from the standalone repository. Test/diagnostic shaders and internal blit/downsample shaders SHALL be retained regardless of upstream origin. - -#### Scenario: Only crt-lottes-fast retained from upstream - -- **GIVEN** the standalone filter-chain asset directory `assets/shaders/upstream/` -- **WHEN** preset files (`.slangp`) in the upstream directory are enumerated -- **THEN** exactly one upstream preset SHALL be present: `crt-lottes-fast` -- **AND** no other upstream presets (e.g., full CRT, scanline, NTSC variants) SHALL be present - -#### Scenario: Test and diagnostic shaders retained - -- **GIVEN** the standalone filter-chain asset directory -- **WHEN** test shader fixtures and diagnostic shaders are inspected -- **THEN** all test shaders used by contract tests (format handling, history, feedback, frame count, pragma parsing, format decoding) SHALL remain present -- **AND** diagnostic shader presets used by diagnostics unit tests SHALL remain present - -#### Scenario: Internal shaders retained - -- **GIVEN** the standalone filter-chain asset directory -- **WHEN** internal shader content is inspected -- **THEN** blit shaders and downsample shaders used by the filter-chain pipeline SHALL remain present -- **AND** their presence SHALL NOT depend on upstream shader selection - -#### Scenario: Embedded asset binary contains expected content - -- **GIVEN** the standalone filter-chain library is built with embedded assets -- **WHEN** the embedded asset binary content is inspected or validated at runtime -- **THEN** the binary SHALL contain `crt-lottes-fast` preset data, all test/diagnostic shader data, and all internal shader data -- **AND** the binary SHALL NOT contain data from removed upstream presets diff --git a/openspec/changes/extract-filter-chain/specs/goggles-filter-chain/spec.md b/openspec/changes/extract-filter-chain/specs/goggles-filter-chain/spec.md deleted file mode 100644 index 1fb73798..00000000 --- a/openspec/changes/extract-filter-chain/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,218 +0,0 @@ -# Delta for goggles-filter-chain - -## MODIFIED Requirements - -### Requirement: Namespace Identity for FC-Owned Types - -(Previously: FC-owned public types resided in `goggles::render` namespace alongside host render code, creating ambiguity about ownership boundaries.) - -All filter-chain-owned C++ types, functions, and constants SHALL use the `goggles::fc` namespace. The `goggles::render` namespace SHALL NOT contain any FC-owned type definitions after migration. The canonical C ABI entrypoint (`goggles/filter_chain.h`) SHALL NOT be affected by this namespace change, as C headers do not use C++ namespaces. - -#### Scenario: Public header namespace migration - -- **GIVEN** the public headers `vulkan_context.hpp` and `filter_controls.hpp` under `filter-chain/include/goggles/filter_chain/` -- **WHEN** namespace declarations are inspected -- **THEN** all type definitions SHALL use `namespace goggles::fc` -- **AND** no `namespace goggles::render` declarations SHALL remain in those headers - -#### Scenario: Internal source namespace migration - -- **GIVEN** all implementation files under `filter-chain/src/` (~48 files) -- **WHEN** namespace declarations and qualified name references are inspected -- **THEN** all FC-owned type references SHALL use `goggles::fc` namespace -- **AND** zero occurrences of `goggles::render` SHALL remain in FC-owned source files - -#### Scenario: Contract test namespace migration - -- **GIVEN** contract test files under `filter-chain/tests/contract/` (~11 files) -- **WHEN** namespace references are inspected -- **THEN** all FC type references SHALL use `goggles::fc` namespace -- **AND** zero occurrences of `goggles::render` for FC-owned types SHALL remain - -#### Scenario: Host code namespace update - -- **GIVEN** host files that reference FC-owned public types (`VulkanContext`, `FilterControlDescriptor`, `FilterControlId`, `FilterControlStage`, `make_filter_control_id()`, `clamp_filter_control_value()`) -- **WHEN** qualified name references in host backend and test files are inspected -- **THEN** all references SHALL use `goggles::fc::` qualification -- **AND** no host file SHALL reference these types via `goggles::render::` - -#### Scenario: C ABI unaffected by namespace change - -- **GIVEN** the canonical C ABI header `goggles/filter_chain.h` -- **WHEN** the header contents are inspected after namespace migration -- **THEN** the header SHALL be unchanged -- **AND** all C ABI function signatures and type definitions SHALL remain identical - -#### Scenario: Compilation succeeds after namespace migration - -- **GIVEN** the namespace rename has been applied across all FC and host files -- **WHEN** the full monorepo CI runs (`pixi run ci --runner container --cache-mode warm --lane all`) -- **THEN** compilation SHALL succeed with zero errors -- **AND** all tests SHALL pass - -#### Scenario: No residual goggles::render in FC code - -- **GIVEN** the completed namespace migration -- **WHEN** a grep for `goggles::render` is executed across all files under `filter-chain/` -- **THEN** zero matches SHALL be found -- **AND** the only `goggles::render` references in the repository SHALL be in host-owned code for host-owned types - -### Requirement: Semgrep fixture namespace update - -Semgrep test fixtures that reference FC-owned types SHALL use the `goggles::fc` namespace to match the migrated codebase. - -#### Scenario: Semgrep fixtures use updated namespace - -- **GIVEN** semgrep fixture files under `tests/semgrep/fixtures/src/render/chain/` -- **WHEN** fixture source files referencing FC-owned types are inspected -- **THEN** those references SHALL use `goggles::fc` namespace -- **AND** fixtures SHALL remain valid inputs for their associated semgrep rules - -## ADDED Requirements - -### Requirement: Host Error Type Independence - -Host backend files SHALL NOT include any filter-chain headers for error/result types. Host code that requires `Result` or equivalent error types SHALL use its own `util/error.hpp` (or equivalent host-owned header) instead of ``. - -#### Scenario: Host backend headers use own error types - -- **GIVEN** the host backend headers `vulkan_error.hpp`, `vulkan_debug.hpp`, `render_output.hpp`, and `external_frame_importer.hpp` -- **WHEN** include directives are inspected -- **THEN** none of these files SHALL include `` -- **AND** each SHALL use a host-owned error type header for `Result` definitions - -#### Scenario: Host builds without FC source tree for error types - -- **GIVEN** the host codebase with FC consumed as an installed package (headers only) -- **WHEN** host backend files are compiled -- **THEN** compilation SHALL succeed without FC private headers being available -- **AND** no error type resolution SHALL depend on FC internal header paths - -#### Scenario: ODR safety with duplicated error types - -- **GIVEN** both host and FC define compatible error/result types independently -- **WHEN** both are linked in the same binary -- **THEN** ODR guards (e.g., `GOGGLES_ERROR_TYPES_DEFINED`) SHALL prevent duplicate definitions -- **AND** the linked binary SHALL exhibit no ODR violations - -### Requirement: Visual Test Public API Boundary - -Goggles host-side visual and integration tests SHALL NOT include FC private/internal headers. All Goggles-side interactions with the filter-chain runtime SHALL use only the durable, caller-facing public API surface. Goggles host tests SHALL NOT require the stable public boundary to preserve intermediate pass capture or public diagnostics-session lifecycle affordances. - -#### Scenario: Visual test uses public API only - -- **GIVEN** the visual test file `tests/visual/runtime_capture.cpp` -- **WHEN** include directives are inspected -- **THEN** no includes of FC internal headers (e.g., `chain_runtime.hpp`, `diagnostic_policy.hpp`) SHALL be present -- **AND** all FC interactions SHALL use durable public API functions for lifecycle, record, controls, reports, or passive metadata queries only - -#### Scenario: Visual test compiles with installed FC headers only - -- **GIVEN** the filter-chain is consumed as an installed package -- **WHEN** `tests/visual/runtime_capture.cpp` is compiled -- **THEN** compilation SHALL succeed using only installed public FC headers -- **AND** no FC `src/` internal header paths SHALL be required - -#### Scenario: Visual test passes after refactor - -- **GIVEN** the visual test has been refactored to use the public API -- **WHEN** the full test suite is executed -- **THEN** `tests/visual/runtime_capture.cpp` SHALL compile and pass -- **AND** any host-visible diagnostics retained for Goggles SHALL be available through passive public metadata/report queries rather than public diagnostics-session lifecycle control - -#### Scenario: Goggles tests do not rely on stable pass capture - -- **GIVEN** Goggles host-side tests after boundary convergence -- **WHEN** their FC-facing assertions are inspected -- **THEN** they SHALL validate final host-observable behavior through durable public runtime APIs -- **AND** they SHALL NOT require stable caller-facing intermediate pass capture APIs - -### Requirement: Host Integration Test Boundary Enforcement - -Goggles host integration tests that exercise the FC boundary SHALL use only the durable public API surface and SHALL focus on end-to-end host integration behavior. No host test file SHALL include FC private headers from `filter-chain/src/`. - -#### Scenario: Host boundary tests use public headers only - -- **GIVEN** host integration test files (`test_filter_boundary_contracts.cpp`, `test_vulkan_backend_subsystem_contracts.cpp`, `test_filter_chain_retarget.cpp`) -- **WHEN** include directives are inspected -- **THEN** no includes referencing paths under `filter-chain/src/` SHALL be present -- **AND** all FC interactions SHALL use the public installed header surface - -#### Scenario: Host tests compile with FC as installed dependency - -- **GIVEN** the filter-chain is consumed via `add_subdirectory()` or `find_package()` -- **WHEN** host integration tests are compiled -- **THEN** compilation SHALL succeed using only the target's public include interface -- **AND** no `-I` flags pointing into FC `src/` directories SHALL be required - -#### Scenario: Goggles retains host-only verification scope - -- **GIVEN** the verification split after extraction -- **WHEN** Goggles-owned FC-facing tests are reviewed -- **THEN** they SHALL cover host wiring, integration, and end-to-end runtime behavior -- **AND** standalone `filter-chain` SHALL own intermediate-pass golden and other diagnostics-heavy verification that depends on internal capture-oriented capabilities - -### Requirement: Stable Caller-Facing Diagnostics Boundary - -The stable caller-facing diagnostics surface SHALL be limited to the minimum justified boundary. If a public diagnostics summary is retained, it SHALL be exposed as passive chain metadata or report state. Stable external-consumer diagnostics SHALL NOT depend on a public diagnostics-session lifecycle contract unless a later approved change explicitly justifies and re-specifies that lifecycle as durable API. - -#### Scenario: Passive diagnostics summary - -- **GIVEN** a caller needs host-visible diagnostic state from the runtime -- **WHEN** the caller queries the stable diagnostics surface -- **THEN** any returned summary SHALL be available as passive metadata or report state -- **AND** retrieving that summary SHALL NOT require the caller to create, manage, or destroy a public diagnostics session - -#### Scenario: Minimal stable diagnostics policy - -- **GIVEN** a caller configures stable public diagnostics behavior -- **WHEN** the stable public policy surface is inspected -- **THEN** the caller-facing policy SHALL be limited to the minimum justified surface -- **AND** broader session-lifecycle-driven or capture-oriented policy controls SHALL NOT be treated as stable caller-facing requirements without explicit later justification - -### Requirement: Stable Runtime Policy and Reset Helpers - -The local-boundary convergence for this change SHALL retain only the following provisional runtime-policy/reset APIs as supported public surface: `goggles_fc_chain_set_stage_mask`, `goggles_fc_chain_set_prechain_resolution`, `goggles_fc_chain_get_prechain_resolution`, `goggles_fc_chain_reset_control_value`, and `goggles_fc_chain_reset_all_controls`. These APIs SHALL remain aligned across the installed C API, the public C++ wrapper, and contract coverage. - -#### Scenario: Stable runtime policy helpers remain public - -- **GIVEN** the converged stable caller-facing boundary for the extracted library -- **WHEN** the retained runtime-policy APIs are reviewed -- **THEN** `goggles_fc_chain_set_stage_mask`, `goggles_fc_chain_set_prechain_resolution`, and `goggles_fc_chain_get_prechain_resolution` SHALL remain present in the installed public API -- **AND** they SHALL be treated as durable cross-host runtime policy controls rather than migration-only diagnostics or capture seams - -#### Scenario: Stable reset helpers remain public - -- **GIVEN** the converged stable caller-facing boundary for the extracted library -- **WHEN** the retained reset helpers are reviewed -- **THEN** `goggles_fc_chain_reset_control_value` and `goggles_fc_chain_reset_all_controls` SHALL remain present in the installed public API -- **AND** they SHALL be treated as part of the stable caller-facing control-mutation workflow - -#### Scenario: Public C and C++ surfaces stay aligned - -- **GIVEN** the retained runtime-policy and reset-helper API set -- **WHEN** the installed public headers are inspected -- **THEN** the C API and `goggles::filter_chain::Chain` wrapper SHALL expose the same retained helper set where the wrapper provides coverage for the underlying chain surface -- **AND** no retained helper SHALL exist only as an undocumented provisional export - -### Requirement: Intermediate Pass Capture Is Non-Stable - -Intermediate pass capture SHALL be treated as internal or standalone-test infrastructure unless a later approved change explicitly justifies it as durable cross-host public API. The extraction change SHALL NOT be considered complete by merely preserving pass capture as stable caller-facing surface for Goggles-hosted tests. - -#### Scenario: Stable contract excludes pass capture by default - -- **GIVEN** the converged stable caller-facing boundary for the extracted library -- **WHEN** stable public requirements are reviewed -- **THEN** intermediate pass capture SHALL NOT be required as part of the stable caller-facing contract -- **AND** any remaining capture capability SHALL be treated as non-stable or internal until explicitly re-justified - -### Requirement: Migration Completion Requires Transitional Cleanup - -The extraction change SHALL NOT be considered complete until migration to the narrowed public boundary is finished and stale transitional code is removed, internalized, or reassigned. This includes stale public diagnostics-session or pass-capture affordances kept only for migration, plus Goggles-side tests or wrappers that preserve those affordances after their ownership has moved. - -#### Scenario: Completion requires cleanup after migration - -- **GIVEN** consumers and tests have been migrated to the narrowed public boundary and standalone verification ownership split -- **WHEN** completion of the extraction change is evaluated -- **THEN** stale transitional public surface and compatibility code for superseded diagnostics-session or pass-capture behavior SHALL have been removed, internalized, or reassigned -- **AND** the change SHALL NOT be considered complete while those stale transitional elements still ship as supported caller-facing boundary diff --git a/openspec/changes/extract-filter-chain/specs/repository-infrastructure/spec.md b/openspec/changes/extract-filter-chain/specs/repository-infrastructure/spec.md deleted file mode 100644 index 55d86b8e..00000000 --- a/openspec/changes/extract-filter-chain/specs/repository-infrastructure/spec.md +++ /dev/null @@ -1,113 +0,0 @@ -# repository-infrastructure Specification - -## Purpose - -Define the standalone repository infrastructure requirements for the extracted filter-chain project at `goggles-dev/goggles-filter-chain`, covering licensing, versioning, branch protection, and repository configuration. - -## Requirements - -### Requirement: MIT License - -The standalone filter-chain repository SHALL be published under the MIT license, consistent with the goggles parent project license. - -#### Scenario: License file present and correct - -- **GIVEN** the standalone filter-chain repository root -- **WHEN** the `LICENSE` file is inspected -- **THEN** it SHALL contain a valid MIT license text -- **AND** the copyright holder and year SHALL be consistent with the goggles project license - -#### Scenario: License discoverable by package managers - -- **GIVEN** the standalone repository metadata (README, CMakeLists.txt) -- **WHEN** license information is queried -- **THEN** the project SHALL identify its license as MIT -- **AND** the `LICENSE` file SHALL be present at the repository root - -### Requirement: Independent Semantic Versioning - -The standalone filter-chain project SHALL maintain its own independent semantic version, starting at 0.1.0. The version SHALL NOT be coupled to or derived from the goggles project version. - -#### Scenario: Initial version is 0.1.0 - -- **GIVEN** the standalone filter-chain `CMakeLists.txt` -- **WHEN** the `project()` directive is inspected -- **THEN** the VERSION parameter SHALL be `0.1.0` -- **AND** the project name SHALL be `GogglesFilterChain` - -#### Scenario: Version is independent of goggles - -- **GIVEN** the goggles project at version X.Y.Z -- **WHEN** the standalone filter-chain project version is inspected -- **THEN** the filter-chain version SHALL be independently managed -- **AND** version bumps in either project SHALL NOT require corresponding bumps in the other - -#### Scenario: Version tag at extraction completion - -- **GIVEN** the standalone filter-chain repository with all CI lanes passing -- **WHEN** extraction is complete and validated -- **THEN** a git tag `v0.1.0` SHALL be created on the main branch -- **AND** the goggles submodule SHALL be pinned to this tagged release - -### Requirement: GitHub Repository Configuration - -The standalone filter-chain repository SHALL be configured as a public GitHub repository with appropriate branch protection and issue tracking. - -#### Scenario: Repository is public - -- **GIVEN** the repository at `github.com/goggles-dev/goggles-filter-chain` -- **WHEN** repository visibility is inspected -- **THEN** the repository SHALL be publicly accessible -- **AND** external consumers SHALL be able to clone without authentication - -#### Scenario: Main branch protection enabled - -- **GIVEN** the standalone filter-chain repository -- **WHEN** branch protection rules for `main` are inspected -- **THEN** direct pushes to `main` SHALL be prohibited -- **AND** pull request reviews SHALL be required before merging -- **AND** CI status checks SHALL be required to pass before merging - -#### Scenario: Issue tracker enabled - -- **GIVEN** the standalone filter-chain repository -- **WHEN** the repository settings are inspected -- **THEN** the GitHub issue tracker SHALL be enabled -- **AND** external users SHALL be able to file issues - -### Requirement: Repository Documentation - -The standalone filter-chain repository SHALL include a README.md with essential project documentation for external consumers. - -#### Scenario: README contains build instructions - -- **GIVEN** the standalone filter-chain repository README.md -- **WHEN** a new user reads the documentation -- **THEN** the README SHALL document how to build the project from source -- **AND** it SHALL document how to run tests -- **AND** it SHALL document how to consume the library as a CMake dependency - -#### Scenario: README documents local development override - -- **GIVEN** the standalone filter-chain README.md -- **WHEN** a developer working on both goggles and filter-chain reads the documentation -- **THEN** the README SHALL describe the local path override mechanism for co-development -- **AND** it SHALL explain how to use a local checkout instead of the submodule - -### Requirement: Clean Git History - -The standalone filter-chain repository SHALL have a clean git history derived from the monorepo using `git filter-repo --subdirectory-filter filter-chain/`. - -#### Scenario: History contains only filter-chain commits - -- **GIVEN** the standalone filter-chain repository -- **WHEN** the git log is inspected -- **THEN** commits SHALL contain only changes relevant to the filter-chain subdirectory -- **AND** no commits that only modified goggles-specific files (outside `filter-chain/`) SHALL be present - -#### Scenario: File paths are repository-root-relative - -- **GIVEN** the standalone filter-chain repository after history rewrite -- **WHEN** file paths in the git history are inspected -- **THEN** paths SHALL be relative to the repository root (e.g., `src/`, `include/`, not `filter-chain/src/`) -- **AND** the `filter-chain/` prefix SHALL have been stripped from all paths diff --git a/openspec/changes/extract-filter-chain/tasks.md b/openspec/changes/extract-filter-chain/tasks.md deleted file mode 100644 index d5fd4333..00000000 --- a/openspec/changes/extract-filter-chain/tasks.md +++ /dev/null @@ -1,171 +0,0 @@ -# Tasks: Extract filter-chain into standalone GitHub repository - -## Phase 1: Prepare Monorepo - -- [x] T1.1 Rename FC-owned namespaces inside filter-chain while preserving the public C ABI - - Description: Mechanically migrate FC-owned C++ namespaces from `goggles::render` to `goggles::fc` across FC public headers, internal headers, implementation files, runtime wrappers, and contract tests. Keep the exported C ABI symbol set unchanged after the rename. - - Files involved: `filter-chain/include/goggles/filter_chain.h`, `filter-chain/include/goggles/filter_chain/vulkan_context.hpp`, `filter-chain/include/goggles/filter_chain/filter_controls.hpp`, `filter-chain/src/**/*.hpp`, `filter-chain/src/**/*.cpp`, `filter-chain/tests/contract/*.cpp` - - Verification method: `rg -n "goggles::render" filter-chain/include filter-chain/src filter-chain/tests/contract`; build a shared FC library before and after the rename and compare `nm -D --defined-only | sort` vs `nm -D --defined-only | sort`; `cmake --preset asan`; `cmake --build --preset asan`; `ctest --preset asan --output-on-failure` - - Dependencies: None - - Estimated complexity: L - -- [x] T1.2 Update every goggles host/test namespace reference to FC-owned public types and helpers - - Description: Replace host-side references to FC-owned symbols with `goggles::fc::...`, including unqualified `FilterControl*` usages in `filter_chain_controller.hpp`, `render::FilterControl*` usages in the app/UI files, `goggles::render::VulkanContext`, `make_filter_control_id()`, `to_string()`, and `clamp_filter_control_value()` call sites. Leave host-owned `goggles::render` types unchanged. - - Files involved: `src/render/backend/vulkan_context.hpp`, `src/render/backend/vulkan_context.cpp`, `src/render/backend/filter_chain_controller.hpp`, `src/render/backend/filter_chain_controller.cpp`, `src/app/application.cpp`, `src/ui/imgui_layer.hpp`, `src/ui/imgui_layer.cpp`, `tests/render/test_filter_boundary_contracts.cpp`, `tests/render/test_vulkan_backend_subsystem_contracts.cpp`, `tests/render/test_filter_chain_retarget.cpp` - - Verification method: `rg -n "goggles::render::(VulkanContext|FilterControlDescriptor|FilterControlId|FilterControlStage|make_filter_control_id|to_string|clamp_filter_control_value)|render::FilterControl(Descriptor|Id|Stage)" src tests/render`; verify `clamp_filter_control_value()` call sites compile after the namespace migration and that the existing clamping-behavior unit coverage in `filter-chain/tests/contract/test_filter_controls.cpp` passes unchanged; `cmake --preset asan`; `cmake --build --preset asan --target goggles_render test_filter_controls test_filter_boundary_contracts test_vulkan_backend_subsystem_contracts test_filter_chain_retarget`; `ctest --preset asan --output-on-failure --tests-regex "filter_controls|filter_boundary|vulkan_backend_subsystem|filter_chain_retarget"` - - Dependencies: T1.1 - - Estimated complexity: M - -- [x] T1.3 Migrate semgrep fixtures that model FC-owned types to the new namespace - - Description: Inspect `filter-chain/tests/semgrep/` fixtures and update any fixture source that still models FC-owned types under `goggles::render` so the fixtures track the extracted `goggles::fc` ownership boundary. - - Files involved: `filter-chain/tests/semgrep/**/*`, `tests/semgrep/fixtures/src/render/chain/**/*` - - Verification method: `rg -n "goggles::render::(VulkanContext|FilterControlDescriptor|FilterControlId|FilterControlStage|make_filter_control_id|clamp_filter_control_value)" filter-chain/tests/semgrep tests/semgrep/fixtures/src/render/chain`; `pixi run ci --runner container --cache-mode warm --lane all` - - Dependencies: T1.1 - - Estimated complexity: S - -- [x] T1.4 Remove goggles host dependence on FC-owned result/error headers - - Description: Replace `` with `util/error.hpp` in the four backend headers that only need host-owned `Result` definitions, preserving the ODR guard behavior expected by the specs. - - Files involved: `src/render/backend/vulkan_error.hpp`, `src/render/backend/vulkan_debug.hpp`, `src/render/backend/render_output.hpp`, `src/render/backend/external_frame_importer.hpp` - - Verification method: `rg -n "goggles/filter_chain/result.hpp" src/render/backend`; `cmake --preset asan`; `cmake --build --preset asan --target goggles_render` - - Dependencies: None - - Estimated complexity: S - -- [x] T1.5 Converge the FC public diagnostics boundary to the minimum justified stable surface - - Description: Update the installed FC C and C++ API so the stable caller-facing diagnostics contract is limited to reusable runtime mechanism plus bounded inspection. Keep only the minimum justified policy surface, ensure any retained summary/readout is passive metadata queried from chain state, and stop treating diagnostics-session lifecycle control or intermediate pass capture as accepted stable end-state API. The installed package story SHALL converge on canonical language-binding entrypoints at `goggles/filter_chain.h` and `goggles/filter_chain.hpp`, with no compatibility shim for `goggles_filter_chain.h`. - - Files involved: `filter-chain/include/goggles/filter_chain.h`, `filter-chain/include/goggles/filter_chain.hpp`, `filter-chain/src/api/c_api.cpp`, `filter-chain/src/api/cpp_wrapper.cpp`, `filter-chain/src/runtime/chain.hpp`, `filter-chain/src/runtime/chain.cpp`, `filter-chain/tests/contract/test_filter_chain.cpp`, `filter-chain/tests/contract/test_runtime_diagnostics.cpp` - - Verification method: `rg -n "diagnostic_session|capture_pass_output|get_diagnostic_summary|diagnostic_mode" filter-chain/include filter-chain/src filter-chain/tests/contract`; verify public contract tests assert passive metadata/report access rather than public session lifecycle; `cmake --preset asan`; `cmake --build --preset asan --target test_runtime_diagnostics test_filter_chain`; `ctest --preset asan --output-on-failure --tests-regex "runtime_diagnostics|filter_chain"` - - Dependencies: T1.1 - - Estimated complexity: L - -- [x] T1.6 Rewrite goggles-side FC helpers and host tests around the durable public boundary only - - Description: Keep `tests/visual/runtime_capture.*`, `tests/render/*`, and related visual CMake wiring limited to installed/public FC headers and the final durable runtime boundary used by host integration. Any temporary reliance on migration-only diagnostics/session/capture seams must be isolated, documented as transitional, and scheduled for removal before Phase 4 completes; goggles-owned coverage must no longer imply stable caller-facing pass capture. - - Files involved: `tests/visual/runtime_capture.hpp`, `tests/visual/runtime_capture.cpp`, `tests/visual/CMakeLists.txt`, `tests/visual/test_temporal_golden.cpp`, `tests/render/test_filter_boundary_contracts.cpp`, `tests/render/test_vulkan_backend_subsystem_contracts.cpp`, `tests/render/test_filter_chain_retarget.cpp` - - Verification method: `rg -n "chain_runtime.hpp|diagnostic_policy.hpp|diagnostic_session.hpp|test_harness_sink.hpp|filter-chain/src" tests/visual tests/render`; inspect goggles-side FC assertions for passive metadata/report usage only; `cmake --preset asan`; `cmake --build --preset asan --target test_temporal_golden test_filter_boundary_contracts test_vulkan_backend_subsystem_contracts test_filter_chain_retarget`; `ctest --preset asan --output-on-failure --tests-regex "temporal_golden|filter_boundary|vulkan_backend_subsystem|filter_chain_retarget"` - - Dependencies: T1.2, T1.5 - - Estimated complexity: L - -- [x] T1.7 Portabilize `FilterChainDependencies.cmake` for standalone use - - Description: Remove Conda/Pixi-specific path hints from FC dependency discovery and switch the module to standard `find_package()` / `find_path()` resolution so the extracted project configures from normal CMake search paths. - - Files involved: `filter-chain/cmake/FilterChainDependencies.cmake` - - Verification method: `rg -n "CONDA_PREFIX|CONDA_BUILD_SYSROOT|ENV\{CONDA" filter-chain/cmake/FilterChainDependencies.cmake`; `cmake -S filter-chain -B build/filter-chain-standalone`; `cmake --build build/filter-chain-standalone` - - Dependencies: None - - Estimated complexity: M - -- [x] T1.8 Audit FC asset contents and prove required shader classes remain packaged - - Description: Confirm the standalone asset set retains exactly one upstream preset (`crt-lottes-fast`) while still carrying every required test shader, diagnostic shader, and internal blit/downsample shader into the embedded asset payload. - - Files involved: `filter-chain/assets/shaders/upstream/**/*`, `filter-chain/assets/shaders/**/*`, `filter-chain/tests/contract/test_shader_validation.cpp`, `filter-chain/tests/contract/test_runtime_diagnostics.cpp`, `filter-chain/tests/contract/test_zfast_integration.cpp` - - Verification method: enumerate upstream presets and confirm only `crt-lottes-fast` remains; verify test/diagnostic/internal shader directories still contain the files referenced by contract tests; `cmake --preset asan`; `cmake --build --preset asan --target test_shader_validation test_runtime_diagnostics test_zfast_integration`; `ctest --preset asan --output-on-failure --tests-regex "shader_validation|runtime_diagnostics|zfast_integration"` - - Dependencies: None - - Estimated complexity: M - -- [x] T1.9 Run the monorepo preparation gate against the narrowed end-state boundary - - Description: Revalidate the prepared monorepo only after the diagnostics-boundary convergence and goggles-host migration tasks are updated to the accepted end state. Phase 1 is complete only when the monorepo is green without requiring goggles-owned stable pass capture or public diagnostics-session lifecycle assumptions. - - Files involved: CI output only - - Verification method: `pixi run ci --runner container --cache-mode warm --lane all` - - Dependencies: T1.3, T1.4, T1.5, T1.6, T1.7, T1.8 - - Estimated complexity: M - -## Phase 2: Create Standalone Repository - -- [x] T2.1 Extract filter-chain history into a standalone repository root - - Description: Clone from a fresh monorepo checkout, run `git filter-repo --subdirectory-filter filter-chain/`, rename the old remote, add `git@github.com:goggles-dev/goggles-filter-chain.git`, and verify the new repository contains only FC-root-relative history. - - Files involved: extracted repository root, git history, git remotes - - Verification method: `git log --stat`; `git remote -v`; `test -f CMakeLists.txt`; `test -d src`; `test -d include`; `test ! -d filter-chain` - - Dependencies: T1.9 - - Estimated complexity: M - -- [x] T2.2 Establish standalone build metadata, versioning, and local Pixi package carry-over - - Description: Add the standalone `pixi.toml`, `CMakePresets.json`, and `project(GogglesFilterChain VERSION 0.1.0)` updates, and carry over the local Pixi package recipes required by the design (`packages/expected-lite/`, `packages/stb/`, `packages/slang-shaders/`) so the extracted repo is self-contained. - - Files involved: `pixi.toml`, `CMakePresets.json`, `CMakeLists.txt`, `packages/expected-lite/**/*`, `packages/stb/**/*`, `packages/slang-shaders/**/*` - - Verification method: `test -d packages/expected-lite`; `test -d packages/stb`; `test -d packages/slang-shaders`; `pixi install`; `cmake --list-presets`; `cmake --preset debug`; `cmake --preset release`; `cmake --preset asan`; `cmake --preset quality`; `cmake --preset test`; `rg -n "include.*goggles|configurePresets.*goggles" CMakePresets.json` returns no matches so the standalone `CMakePresets.json` is self-contained and does not include goggles presets - - Dependencies: T2.1 - - Estimated complexity: L - -- [x] T2.3 Move standalone-owned golden and diagnostics-heavy verification into the extracted repository - - Description: Migrate intermediate-pass golden coverage and any diagnostics-heavy runtime verification that depends on internal capture-oriented capabilities out of goggles and into standalone FC-owned targets. Keep any required capture plumbing internal or clearly non-stable within the standalone repo rather than widening the installed public contract. - - Files involved: `tests/visual/test_intermediate_golden.cpp` (source of migrated coverage), `filter-chain/tests/visual/**/*`, `filter-chain/tests/contract/test_runtime_diagnostics.cpp`, `filter-chain/src/runtime/chain.hpp`, `filter-chain/src/runtime/chain.cpp`, standalone `CMakeLists.txt` - - Verification method: verify intermediate-pass/golden targets exist only in standalone FC build files; `rg -n "intermediate_golden|capture_pass_output|diagnostic_session" filter-chain/tests filter-chain/src`; `cmake --preset test`; `cmake --build --preset test`; `ctest --preset test --output-on-failure --tests-regex "intermediate|golden|runtime_diagnostics"` - - Dependencies: T2.1, T2.2 - - Estimated complexity: L - -- [x] T2.4 Rewrite installed-consumer validation for the standalone repo layout - - Description: Update `scripts/validate-installed-consumers.sh` to use repository-root-relative paths after extraction, preserve the `tests/consumer/{c_api,static,shared}` fixtures in the standalone tree, and require both static and shared install validation with no skip path. - - Files involved: `scripts/validate-installed-consumers.sh`, `tests/consumer/c_api/**/*`, `tests/consumer/static/**/*`, `tests/consumer/shared/**/*` - - Verification method: `rg -n "filter-chain/|\$REPO_ROOT/filter-chain|tests/consumer" scripts/validate-installed-consumers.sh`; `bash scripts/validate-installed-consumers.sh --preset test` - - Dependencies: T2.1, T2.2 - - Estimated complexity: M - -- [x] T2.5 Add standalone CI lanes and ignore rules for the extracted layout and new verification ownership - - Description: Create or update the standalone GitHub Actions workflow with `format-check`, `build-and-test`, `consumer-validation`, and `static-analysis` jobs, and make the standalone pipeline the authoritative home for intermediate-pass golden and diagnostics-heavy verification that no longer belongs to goggles host CI. - - Files involved: `.github/workflows/ci.yml`, `.semgrepignore`, `pixi.toml`, `CMakePresets.json`, standalone `CMakeLists.txt`, `filter-chain/tests/visual/**/*` - - Verification method: `rg -n "format-check|build-and-test|consumer-validation|static-analysis" .github/workflows/ci.yml`; `rg -n "intermediate|golden|runtime_diagnostics" .github/workflows/ci.yml CMakePresets.json`; `pixi run format-check`; `cmake --preset quality`; `cmake --build --preset quality`; `bash scripts/validate-installed-consumers.sh --preset test` - - Dependencies: T2.3, T2.4 - - Estimated complexity: M - -- [x] T2.6 Add standalone repository documentation and issue scaffolding that describe the final boundary - - Description: Create or update `README.md`, `LICENSE`, `CONTRIBUTING.md`, and GitHub issue templates so they document builds, tests, `find_package(GogglesFilterChain CONFIG REQUIRED)`, the local goggles override flow, the narrowed stable diagnostics boundary, and the fact that standalone FC owns intermediate-pass golden coverage. - - Files involved: `README.md`, `LICENSE`, `CONTRIBUTING.md`, `.github/ISSUE_TEMPLATE/**/*`, `CMakeLists.txt` - - Verification method: `rg -n "find_package\(GogglesFilterChain|GOGGLES_FILTER_CHAIN_SOURCE_DIR|0.1.0|MIT|intermediate-pass|diagnostics|consumer-validation|static-analysis" README.md CONTRIBUTING.md LICENSE CMakeLists.txt`; `test -d .github/ISSUE_TEMPLATE` - - Dependencies: T2.3, T2.5 - - Estimated complexity: M - -- [x] T2.7 Run the standalone validation gate with final verification ownership in place - - Description: Prove the extracted repository is independently buildable, testable, and consumable, and that standalone FC now owns the intermediate-pass golden and diagnostics-heavy verification previously carried by goggles. - - Files involved: CI output only - - Verification method: `pixi install`; `cmake --preset test`; `cmake --build --preset test`; `ctest --preset test --output-on-failure`; `bash scripts/validate-installed-consumers.sh --preset test`; `cmake --preset quality`; `cmake --build --preset quality` - - Dependencies: T2.3, T2.5, T2.6 - - Estimated complexity: M - -## Phase 3: Consumer Switchover in Goggles - -- [x] T3.1 Replace the tracked FC directory with a submodule at the same path and keep local override support - - Description: Remove the tracked in-repo `filter-chain/` contents, add the extracted repository back as a git submodule at `filter-chain/`, and update the root `CMakeLists.txt` to use an overrideable `GOGGLES_FILTER_CHAIN_SOURCE_DIR` that still defaults to `${CMAKE_SOURCE_DIR}/filter-chain`. - - Files involved: `filter-chain/` (gitlink), `.gitmodules`, `CMakeLists.txt` - - Verification method: `git submodule status`; `test -f .gitmodules`; `git config --file .gitmodules --get submodule.filter-chain.path`; `git config --file .gitmodules --get submodule.filter-chain.url`; configure goggles with default path and with `-DGOGGLES_FILTER_CHAIN_SOURCE_DIR=/abs/path/to/local/checkout` - - Dependencies: T2.7 - - Estimated complexity: M - -- [x] T3.2 Update goggles CI checkout and remaining path-sensitive files for submodule-aware builds - - Description: Change goggles CI jobs to initialize the `filter-chain/` submodule before any configure/build step, and update remaining path-sensitive files such as `.semgrepignore`, scripts, ROADMAP/docs, and any clean-checkout instructions that assumed FC was tracked directly in-repo. - - Files involved: `.github/workflows/ci.yml`, `.semgrepignore`, `scripts/**/*`, `ROADMAP.md`, `docs/**/*` - - Verification method: `rg -n "submodules: recursive|submodule update --init --recursive" .github/workflows/ci.yml`; `rg -n "filter-chain/" .semgrepignore scripts ROADMAP.md docs`; `git submodule update --init --recursive` - - Dependencies: T3.1 - - Estimated complexity: M - -- [x] T3.3 Remove or migrate goggles-owned FC verification that exceeds host integration scope - - Description: Delete or migrate `tests/visual/test_intermediate_golden.cpp` and any other goggles-owned FC assertions that depend on intermediate outputs, public diagnostics-session lifecycle, or stable pass capture. Keep only goggles coverage that proves host wiring and end-to-end application behavior through the durable public FC boundary. - - Files involved: `tests/visual/test_intermediate_golden.cpp`, `tests/visual/test_temporal_golden.cpp`, `tests/visual/runtime_capture.hpp`, `tests/visual/runtime_capture.cpp`, `tests/visual/CMakeLists.txt` - - Verification method: `rg -n "intermediate_golden|capture_pass_output|diagnostic_session" tests/visual`; verify any remaining goggles FC-facing tests exercise host integration rather than pass-level ownership; `cmake --preset asan`; `cmake --build --preset asan --target test_temporal_golden`; `ctest --preset asan --output-on-failure --tests-regex "temporal_golden"` - - Dependencies: T3.1, T3.2 - - Estimated complexity: M - -- [x] T3.4 Run the goggles submodule integration gate from a clean checkout - - Description: Validate goggles exactly as consumers and CI will see it: clean checkout, submodule init, configure/build/test, and full CI with FC coming only from the submodule and goggles retaining only host integration coverage. - - Files involved: CI output only - - Verification method: `git submodule update --init --recursive`; `pixi run ci --runner container --cache-mode warm --lane all` - - Dependencies: T3.3 - - Estimated complexity: M - -## Phase 4: Validation and Cleanup - -- [x] T4.1 Tag `goggles-filter-chain` `v0.1.0` and repin goggles to the tagged release - - Description: Create and push the standalone `v0.1.0` tag after both repositories are green, then update the goggles submodule pointer so the monorepo is pinned to the release tag instead of an arbitrary commit. - - Files involved: standalone git tags, `filter-chain/` gitlink in goggles - - Verification method: `git -C ../goggles-filter-chain tag --list`; `git -C ../goggles-filter-chain push --tags`; `git submodule status`; verify the submodule commit matches the tagged ref - > **Completed**: Remote tag `v0.1.0` now resolves to `6e8d68922da5fca9c379b85debae3fe390ebea89`, and `goggles/filter-chain` is repinned to that tagged release state. - - Dependencies: T3.4 - - Estimated complexity: S - -- [x] T4.2 Remove stale transitional diagnostics/capture surface, wrappers, and docs across both repos - - Description: Remove, internalize, or clearly de-support any remaining transitional public diagnostics-session lifecycle APIs, pass-capture affordances, wrappers, test seams, and documentation that were kept only to bridge pre-extraction goggles verification. For final convergence, retain `goggles_fc_chain_set_stage_mask`, `goggles_fc_chain_set_prechain_resolution`, `goggles_fc_chain_get_prechain_resolution`, `goggles_fc_chain_reset_control_value`, and `goggles_fc_chain_reset_all_controls` as the approved stable runtime-policy/reset subset, make `goggles/filter_chain.h` and `goggles/filter_chain.hpp` the canonical installed entrypoints, and remove `goggles_filter_chain.h` with no compatibility shim. The change is not complete until the stale transitional surface is gone from installed FC headers, goggles-owned tests/helpers, and both repositories' docs/spec-aligned guidance. - - Files involved: `filter-chain/include/goggles/filter_chain.h`, `filter-chain/include/goggles/filter_chain.hpp`, `filter-chain/include/goggles_filter_chain.h`, `filter-chain/src/api/c_api.cpp`, `filter-chain/src/api/cpp_wrapper.cpp`, `filter-chain/src/runtime/chain.hpp`, `filter-chain/src/runtime/chain.cpp`, `tests/visual/runtime_capture.hpp`, `tests/visual/runtime_capture.cpp`, `README.md`, `CONTRIBUTING.md`, `openspec/changes/extract-filter-chain/tasks.md` - - Verification method: `rg -n "diagnostic_session|capture_pass_output|goggles_fc_captured_image|public capture|intermediate pass capture" filter-chain/include filter-chain/src tests/visual README.md CONTRIBUTING.md openspec/changes/extract-filter-chain`; verify any remaining matches are explicitly internal/test-only and not installed public contract; rebuild FC contract tests and goggles host-integration tests with the final public surface only - - Dependencies: T4.1 - - Estimated complexity: L - -- [x] T4.3 Run final dual-repository release verification and prove cleanup completion before archive - - Description: Re-run the standalone required checks and the goggles clean-checkout/full-CI flow after the release tag, submodule repin, and transitional-surface cleanup. Do not hand off to archive until both repos are green and verification proves no stale caller-facing migration surface remains. - - Files involved: CI output only - - Verification method: standalone required checks on `main`; `git submodule update --init --recursive`; `pixi run ci --runner container --cache-mode warm --lane all`; repeat the T4.2 stale-surface grep and confirm zero supported-surface matches remain - - Dependencies: T4.2 - - Estimated complexity: M diff --git a/openspec/changes/extract-filter-chain/verify-report.md b/openspec/changes/extract-filter-chain/verify-report.md deleted file mode 100644 index 9e92117f..00000000 --- a/openspec/changes/extract-filter-chain/verify-report.md +++ /dev/null @@ -1,58 +0,0 @@ -# Verification Report: extract-filter-chain release pin - -## status - -passed - -## executive_summary - -The standalone `goggles-filter-chain` release tag `v0.1.0` now points to the intended final commit `6e8d68922da5fca9c379b85debae3fe390ebea89`, and `goggles/filter-chain` is repinned to that tagged release state. - -Final release verification passed in both repositories: the standalone repo passed its release-state verification lanes, and the Goggles monorepo passed the required full CI gate with the release-pinned submodule. - -## completed_scope - -- Remote standalone tag `v0.1.0` moved from `5f14bae36baf5364e5884d40ba43e4ae4a840fc5` to `6e8d68922da5fca9c379b85debae3fe390ebea89`. -- Goggles submodule `filter-chain/` updated to detached `v0.1.0` at `6e8d68922da5fca9c379b85debae3fe390ebea89`. -- Final standalone and Goggles verification rerun on the release-pinned state. -- `openspec/changes/extract-filter-chain/tasks.md` updated to mark `T4.1` and `T4.3` complete. - -## verification_results - -### Standalone repo: `/home/kingstom/workspaces/goggles-filter-chain` - -- `pixi install` -> passed -- `pixi run format-check` -> passed -- `pixi run build -p asan` -> passed -- `ctest --preset asan --output-on-failure` -> passed - - Included `fc_contract_tests`, `fc_test_temporal_golden`, and `fc_test_semantic_probes` - - Expected fixture skips remained limited to missing optional MBZ/internal preset content -- `pixi run consumer-validation -p test` -> passed - - C API consumer passed - - Static C++ consumer passed - - Shared consumer reported skipped because the install tree for this verification was static-only -- `pixi run static-analysis` -> passed - -### Goggles monorepo: `/home/kingstom/workspaces/goggles` - -- `pixi run ci --runner container --cache-mode warm --lane all` -> passed -- Build/test, format, semgrep, and quality lanes all completed successfully against `filter-chain` pinned to `v0.1.0` - -## release_pin_evidence - -- `git -C /home/kingstom/workspaces/goggles-filter-chain ls-remote --tags origin v0.1.0` -> `6e8d68922da5fca9c379b85debae3fe390ebea89 refs/tags/v0.1.0` -- `git -C /home/kingstom/workspaces/goggles-filter-chain rev-parse v0.1.0^{commit}` -> `6e8d68922da5fca9c379b85debae3fe390ebea89` -- `git -C /home/kingstom/workspaces/goggles submodule status` -> `+6e8d68922da5fca9c379b85debae3fe390ebea89 filter-chain (v0.1.0)` - -## risks - -- The Goggles worktree still contains many pre-existing uncommitted extraction changes outside this release-pin closure; this verification proves the current worktree passes, but the pin is not yet recorded in a commit. -- Standalone consumer validation still skips the shared-consumer leg when only the static install artifact is present; that is the current scripted behavior observed during release verification. - -## t2_7_refresh_2026_03_16 - -- Re-ran the standalone T2.7 validation gate from `/home/kingstom/workspaces/goggles-filter-chain` using the authoritative task commands: `pixi install`, `cmake --preset test`, `cmake --build --preset test`, `ctest --preset test --output-on-failure`, `bash scripts/validate-installed-consumers.sh --preset test`, `cmake --preset quality`, and `cmake --build --preset quality`. -- The gate passed end-to-end. -- The standalone test run covered the FC-owned verification split now required by the change, including `fc_contract_tests`, `fc_test_temporal_golden`, and `fc_test_semantic_probes` in the standalone repository rather than Goggles host CI. -- Consumer validation completed successfully via `scripts/validate-installed-consumers.sh --preset test`. -- `openspec/changes/extract-filter-chain/tasks.md` was updated to mark `T2.7` complete. diff --git a/openspec/changes/filter-chain-gate-refactor/.openspec.yaml b/openspec/changes/filter-chain-gate-refactor/.openspec.yaml deleted file mode 100644 index f34a3859..00000000 --- a/openspec/changes/filter-chain-gate-refactor/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-03-10 diff --git a/openspec/changes/filter-chain-gate-refactor/design.md b/openspec/changes/filter-chain-gate-refactor/design.md deleted file mode 100644 index d4e9902e..00000000 --- a/openspec/changes/filter-chain-gate-refactor/design.md +++ /dev/null @@ -1,178 +0,0 @@ -## Context - -The current filter-chain subsystem already exposes stable public gates through -`src/render/chain/api/cpp/goggles_filter_chain.hpp` and `src/render/chain/api/c/goggles_filter_chain.h`, -but long-lived ownership still spans `FilterChain`, `FilterChainCore`, and -`FilterChainController`. The controller currently owns async rebuild orchestration plus filter -semantics such as stage-policy application, control forwarding, and prechain resolution shaping, -while `FilterChain` and `FilterChainCore` still mix preset loading, runtime allocations, control -state, and record-path execution. - -This refactor crosses `src/render/chain`, `src/render/backend`, and render regression tests, so the -design MUST preserve the existing wrapper/runtime contract, keep render-pipeline behavior stable, -and leave the backend on boundary-owned interfaces only. - -## Goals / Non-Goals - -**Goals:** -- Keep the C++ wrapper and C ABI as the only stable filter-chain gates. -- Move filter semantics behind one gate-owned runtime boundary. -- Split internal responsibilities into build, resources, execution, and controls without changing - stage ordering or gate behavior. -- Shrink `FilterChainController` to async rebuild submission, safe swap timing, and runtime - retirement orchestration. -- Preserve active policy, control state, and prechain configuration across successful runtime swaps. -- Keep failed reloads non-destructive and keep record-path work bounded. - -**Non-Goals:** -- Change shader semantic contracts, pass ordering, or public wrapper/C ABI surface shape. -- Move filter semantics back into backend code. -- Require explicit fence-backed retirement if the current backend plumbing cannot support it yet. -- Perform unrelated backend/render cleanup outside the gate-centered refactor. - -## Decisions - -### Decision: Make the stable gates the only supported runtime boundary - -`FilterChainRuntime` in `src/render/chain/api/cpp/goggles_filter_chain.hpp` and the C ABI in -`src/render/chain/api/c/goggles_filter_chain.h` SHALL remain the only stable filter-chain -integration gates. Backend code under `src/render/backend/` SHALL continue to interact through the -wrapper-owned runtime only, while internal runtime types remain hidden behind the C ABI handle. - -Rationale: -- The living wrapper spec already treats the C++ wrapper as the runtime integration boundary. -- Keeping the gate stable lets the deep refactor stay internal instead of turning into a broad - backend migration. - -Alternatives considered: -- Keep the current facade/core split and only rename classes: rejected because it preserves the same - ownership leakage across controller, facade, and core. -- Let backend code talk to new internal runtime types directly: rejected because it breaks boundary - isolation and widens the change surface unnecessarily. - -### Decision: Preserve the backend-owned Vulkan handoff seam - -`VulkanBackend` SHALL retain ownership of swapchain, external-frame import, synchronization, render -output, and root backend Vulkan state. The filter-chain runtime SHALL continue to receive only -boundary-scoped Vulkan inputs from `VulkanBackend::make_filter_chain_build_config()` and -`backend_internal::VulkanContext::boundary_context(...)`; the refactor SHALL NOT move root backend -Vulkan or render-output ownership behind `ChainRuntime`. - -Rationale: -- The current backend/chain seam already isolates root Vulkan and presentation ownership from filter - semantics. -- Preserving that seam keeps the refactor focused on filter-chain ownership instead of collapsing the - backend/render-output split. - -Alternatives considered: -- Move root backend Vulkan state into the new chain runtime: rejected because it breaks the current - boundary handoff contract and widens the refactor into swapchain/import/present ownership. -- Let the chain runtime reach back into backend render-output state ad hoc: rejected because it - recreates cross-module coupling through a different seam. - -### Decision: Collapse facade/core ownership into one runtime-owned boundary, then split by role - -The C ABI handle SHALL own one internal `ChainRuntime` object that becomes the single runtime owner -behind the stable gates. `ChainRuntime` SHALL coordinate an immutable compiled-chain product plus -runtime resources, control state, and requested policy/configuration. Internal decomposition SHALL -separate at least these responsibilities: -- `ChainBuilder` for preset parsing, include/texture resolution, shader compilation, and immutable - compiled output creation. -- `ChainResources` for framebuffers, history, feedback, texture registry, and resize-sensitive - allocations. -- `ChainExecutor` for hot-path `record()` execution in fixed stage order. -- `ChainControls` for stable control ids, descriptors, defaults, normalization, and rebuild replay. - -Rationale: -- One runtime owner makes async install/swap semantics explicit. -- Separating immutable build output from mutable runtime state keeps hot-path and rebuild-path logic - from drifting back together. - -Alternatives considered: -- Keep `FilterChain` as the top-level owner and extract helpers under it: rejected because the facade - would still remain a second runtime boundary behind the stable gates. -- Split everything immediately without a single runtime owner: rejected because swap/install - semantics become harder to reason about during migration. - -### Decision: Keep controller scope to orchestration only - -`FilterChainController` SHALL own active/pending runtime instances, async `JobSystem` submission, -swap-at-safe-point behavior, and retired-runtime cleanup. Stage-policy interpretation, prechain -resolution normalization, and control semantics SHALL move behind `ChainRuntime` so backend code only -forwards desired values into the gate. - -Rationale: -- The controller should manage runtime lifetimes, not encode filter semantics. -- Moving semantics behind the gate aligns the implementation with the wrapper boundary contract. - -Alternatives considered: -- Leave control normalization or prechain math in the controller: rejected because it keeps the - backend coupled to filter semantics and makes reload/state replay harder to centralize. - -### Decision: Treat reload and record as separate contracts - -Async reload SHALL build a complete pending runtime payload without mutating the active runtime. A -successful swap SHALL preserve policy, control values, and prechain configuration before the new -runtime renders its first frame. The active `record()` path SHALL consume prepared compiled state and -resources only, and SHALL NOT parse presets, compile shaders, touch the filesystem, or block on -background job completion. - -Rationale: -- Failure isolation is necessary for a deep refactor that preserves runtime continuity. -- Explicit hot-path constraints prevent architectural cleanup from regressing frame-loop behavior. - -Alternatives considered: -- Mutate the active runtime in place during background rebuild: rejected because it couples failure - handling to the live frame path. -- Permit record-path rebuild work when state drifts: rejected because it hides unbounded work in the - frame loop. - -### Decision: Prefer explicit retirement tracking but keep a bounded fallback - -The refactor SHALL isolate swapped-out runtime retirement into a helper with explicit safety rules. -If current backend plumbing supports submission-epoch or fence-backed retirement, the implementation -SHALL prefer that mechanism. Otherwise it SHALL keep the existing delayed-retire strategy as an -explicit fallback with dedicated verification coverage. - -Rationale: -- Runtime destruction safety is part of the swap contract, not incidental cleanup. -- An isolated fallback keeps the heuristic bounded instead of scattering it through controller logic. - -Alternatives considered: -- Keep the frame-delay heuristic inlined in controller code forever: rejected because it hides safety - assumptions in orchestration logic. -- Block the refactor on immediate fence plumbing: rejected because the architecture improvement can - still proceed with a bounded fallback path. - -## Risks / Trade-offs - -- [Ownership drift during migration] -> Introduce `ChainRuntime` first so later splits still route - through one owner. -- [Controller still knows too much] -> Move policy/prechain/control interpretation behind the gate - before treating the refactor as complete. -- [Record-path regressions] -> Keep explicit tests and contract language around stage ordering, - record-path boundedness, and control stability. -- [Retirement safety remains heuristic] -> Isolate the fallback, keep it bounded, and verify it with - dedicated boundary-contract coverage. - -## Migration Plan - -1. Introduce `ChainRuntime` and route the C ABI handle plus C++ wrapper through it while keeping the - public gate surface unchanged. -2. Move current long-lived state from `FilterChain` / `FilterChainCore` into `ChainRuntime` and keep - `FilterChain` only as a temporary adapter if needed during migration. -3. Extract immutable build output into `ChainBuilder` / `CompiledChain` and separate mutable runtime - allocations into `ChainResources`. -4. Extract hot-path recording into `ChainExecutor` and control/state replay into `ChainControls`. -5. Reduce `FilterChainController` to orchestration-only scope and isolate runtime retirement logic. -6. Remove the obsolete facade/core split once the wrapper, controller, and tests all target the new - boundary. - -Rollback strategy: -- Revert the change as one refactor unit if ownership or swap safety regresses; do not leave a - partially split runtime where both the old facade/core and the new boundary remain authoritative. - -## Open Questions - -- None. The only allowed implementation-time branch is whether runtime retirement can move to - submission/fence tracking immediately or must use the explicit bounded fallback first. diff --git a/openspec/changes/filter-chain-gate-refactor/implementation-context.json b/openspec/changes/filter-chain-gate-refactor/implementation-context.json deleted file mode 100644 index 8326c860..00000000 --- a/openspec/changes/filter-chain-gate-refactor/implementation-context.json +++ /dev/null @@ -1,247 +0,0 @@ -{ - "schema_version": 1, - "change_id": "filter-chain-gate-refactor", - "artifact_digest": "sha256:288222ad13f0a2f3a9bde35a6dd06428bf76de00dfc24996780a101ed1834611", - "authoritative_contract_paths": [ - "openspec/changes/filter-chain-gate-refactor/proposal.md", - "openspec/changes/filter-chain-gate-refactor/design.md", - "openspec/changes/filter-chain-gate-refactor/tasks.md", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md", - "openspec/specs/render-pipeline/spec.md", - "openspec/specs/filter-chain-cpp-wrapper/spec.md" - ], - "readiness": { - "ambiguity_closed": true, - "implementation_ready": true - }, - "cross_cutting": { - "locked_constraints": [ - "Keep `src/render/chain/api/cpp/goggles_filter_chain.hpp` and `src/render/chain/api/c/goggles_filter_chain.h` as the only stable filter-chain gates.", - "Do not move filter semantics back into `src/render/backend/`; controller scope is orchestration only.", - "Preserve stage order `prechain -> effect -> postchain`, policy continuity, and postchain presentation behavior.", - "Async rebuild must not mutate the active runtime before swap; failed rebuilds stay non-destructive.", - "Record-path work must stay bounded: no preset parse, shader compilation, filesystem I/O, or blocking waits.", - "Preserve C ABI continuity and wrapper source compatibility while internal runtime ownership changes." - ], - "divergence_triggers": [ - "Backend code starts including internal chain runtime/build/resource/control headers or types.", - "Controller continues to own prechain-resolution normalization, control semantics, or stage-policy interpretation after the refactor.", - "Active runtime state is mutated in place during background rebuild or lost across a successful swap.", - "Record-path execution reintroduces compile/I/O/blocking work or changes stage ordering.", - "Retirement safety remains implicit or untested after changing the swap lifecycle." - ], - "verification_contract": { - "baseline_gates": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality" - ], - "targeted_checks": [ - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure", - "pixi run test -p asan" - ], - "environment_sensitive_checks": [ - "ctest --preset test -R \"^(headless_smoke|goggles_headless_integration)$\" --output-on-failure" - ], - "manual_fallback_scope": "Allowed only for the environment-sensitive checks when the local runtime cannot provide the required GPU/display path; record prerequisites, observations, and proof location.", - "mandatory_no_fallback": [ - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run build -p quality", - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure" - ] - } - }, - "task_groups": [ - { - "group_id": "1", - "contract_refs": [ - "openspec/changes/filter-chain-gate-refactor/tasks.md#1-gate-owned-runtime-boundary", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:3", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:15", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md:3", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md:35" - ], - "candidate_paths": [ - "src/render/chain/api/cpp/goggles_filter_chain.hpp", - "src/render/chain/api/cpp/goggles_filter_chain.cpp", - "src/render/chain/api/c/goggles_filter_chain.h", - "src/render/chain/api/c/goggles_filter_chain.cpp", - "src/render/chain/CMakeLists.txt", - "src/render/backend/vulkan_context.hpp", - "src/render/backend/vulkan_backend.cpp", - "src/render/backend/filter_chain_controller.hpp", - "tests/render/test_filter_boundary_contracts.cpp" - ], - "candidate_symbols": [ - "goggles::render::FilterChainRuntime", - "goggles::render::ChainCreateInfo", - "goggles::render::ChainRecordInfo", - "goggles_chain", - "goggles_chain_create_vk_ex", - "goggles_chain_record_vk" - ], - "first_reads": [ - "src/render/chain/api/cpp/goggles_filter_chain.hpp:59", - "src/render/chain/api/cpp/goggles_filter_chain.cpp:233", - "src/render/chain/api/c/goggles_filter_chain.h:64", - "src/render/chain/api/c/goggles_filter_chain.cpp:51", - "src/render/chain/CMakeLists.txt:3", - "src/render/backend/vulkan_context.hpp:35", - "src/render/backend/vulkan_backend.cpp:589", - "tests/render/test_filter_boundary_contracts.cpp:305" - ], - "suggested_checks": [ - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure", - "pixi run build -p debug" - ], - "risks": [ - "Internal runtime types can leak into backend or public headers if the gate migration is incomplete.", - "C ABI continuity can regress if the opaque handle ownership changes without preserving wrapper/C tests." - ], - "confidence": 0.92 - }, - { - "group_id": "2", - "contract_refs": [ - "openspec/changes/filter-chain-gate-refactor/tasks.md#2-internal-runtime-responsibility-split", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:45", - "openspec/specs/render-pipeline/spec.md:1428" - ], - "candidate_paths": [ - "src/render/chain/filter_chain.hpp", - "src/render/chain/filter_chain.cpp", - "src/render/chain/filter_chain_core.hpp", - "src/render/chain/filter_chain_core.cpp", - "src/render/chain/preset_parser.cpp", - "src/render/chain/output_pass.cpp", - "src/render/chain/frame_history.cpp", - "src/render/chain/framebuffer.cpp", - "tests/render/test_filter_chain.cpp", - "tests/render/test_filter_controls.cpp" - ], - "candidate_symbols": [ - "goggles::render::FilterChain", - "goggles::render::FilterChainCore", - "goggles::render::FilterChain::record", - "goggles::render::FilterChainCore::load_preset", - "goggles::render::FilterChainCore::record", - "goggles::render::FilterChainCore::handle_resize" - ], - "first_reads": [ - "src/render/chain/filter_chain.hpp:23", - "src/render/chain/filter_chain_core.hpp:41", - "src/render/chain/filter_chain_core.cpp:386", - "src/render/chain/CMakeLists.txt:3", - "tests/render/test_filter_chain.cpp:84", - "tests/render/test_filter_controls.cpp:12" - ], - "suggested_checks": [ - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure", - "pixi run build -p quality" - ], - "risks": [ - "Builder/resource/executor/control concerns can drift back together if `ChainRuntime` does not stay the single owner.", - "Stage-order or hot-path regressions can slip in if extraction work changes how record traverses prechain/effect/postchain." - ], - "confidence": 0.87 - }, - { - "group_id": "3", - "contract_refs": [ - "openspec/changes/filter-chain-gate-refactor/tasks.md#3-controller-orchestration-and-swap-safety", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:27", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:57", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md:69", - "openspec/specs/render-pipeline/spec.md:1448" - ], - "candidate_paths": [ - "src/render/backend/filter_chain_controller.hpp", - "src/render/backend/filter_chain_controller.cpp", - "src/render/backend/vulkan_backend.cpp", - "src/render/chain/filter_chain.hpp", - "src/render/chain/filter_chain.cpp", - "src/render/chain/api/c/goggles_filter_chain.cpp", - "src/render/backend/render_output.cpp", - "tests/render/test_filter_boundary_contracts.cpp" - ], - "candidate_symbols": [ - "goggles::render::backend_internal::FilterChainController", - "FilterChainController::reload_shader_preset", - "FilterChainController::check_pending_chain_swap", - "FilterChainController::cleanup_deferred_destroys", - "goggles::render::FilterChain::set_stage_policy", - "goggles::render::FilterChain::set_prechain_resolution", - "goggles::render::FilterChain::set_control_value", - "goggles::render::FilterChain::reset_controls", - "VulkanBackend::reload_shader_preset", - "VulkanBackend::make_filter_chain_build_config" - ], - "first_reads": [ - "src/render/backend/filter_chain_controller.hpp:18", - "src/render/backend/filter_chain_controller.cpp:81", - "src/render/backend/filter_chain_controller.cpp:175", - "src/render/backend/filter_chain_controller.cpp:237", - "src/render/backend/vulkan_backend.cpp:574", - "src/render/chain/filter_chain.hpp:40", - "src/render/chain/filter_chain.cpp:137", - "src/render/chain/api/c/goggles_filter_chain.cpp:51", - "tests/render/test_filter_boundary_contracts.cpp:223" - ], - "suggested_checks": [ - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure", - "ctest --preset test -R \"^(headless_smoke|goggles_headless_integration)$\" --output-on-failure", - "pixi run test -p asan" - ], - "risks": [ - "Controller semantics may not fully move behind the gate, leaving policy/prechain/control logic split across layers.", - "Retirement changes can break shutdown or frames-in-flight safety if the fallback path is not explicit and verified." - ], - "confidence": 0.9 - }, - { - "group_id": "4", - "contract_refs": [ - "openspec/changes/filter-chain-gate-refactor/tasks.md#4-verification-and-contract-alignment", - "openspec/changes/filter-chain-gate-refactor/proposal.md#validation-plan", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md", - "openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md" - ], - "candidate_paths": [ - "tests/render/test_filter_boundary_contracts.cpp", - "tests/render/test_filter_chain.cpp", - "tests/render/test_filter_controls.cpp", - "tests/CMakeLists.txt", - "src/render/chain/CMakeLists.txt", - "src/render/backend/CMakeLists.txt" - ], - "candidate_symbols": [ - "TEST_CASE(\"Filter chain boundary control contract coverage\")", - "TEST_CASE(\"Async swap and resize safety contract coverage\")", - "TEST_CASE(\"Filter chain wrapper boundary contract coverage\")", - "TEST_CASE(\"FilterChain stage ordering parity\")", - "TEST_CASE(\"Filter control id semantics\")", - "goggles_tests" - ], - "first_reads": [ - "tests/render/test_filter_boundary_contracts.cpp:74", - "tests/render/test_filter_chain.cpp:84", - "tests/render/test_filter_controls.cpp:17", - "tests/CMakeLists.txt:12" - ], - "suggested_checks": [ - "ctest --preset test -R \"^goggles_unit_tests$\" --output-on-failure", - "pixi run build -p debug", - "pixi run build -p asan", - "pixi run test -p asan", - "pixi run build -p quality" - ], - "risks": [ - "Contract tests may continue to encode the old facade/core structure unless assertions are retargeted to the new boundary.", - "Environment-sensitive verification can be skipped accidentally if fallback eligibility is not recorded explicitly." - ], - "confidence": 0.94 - } - ] -} diff --git a/openspec/changes/filter-chain-gate-refactor/proposal.md b/openspec/changes/filter-chain-gate-refactor/proposal.md deleted file mode 100644 index 67eb49b7..00000000 --- a/openspec/changes/filter-chain-gate-refactor/proposal.md +++ /dev/null @@ -1,119 +0,0 @@ -## Why - -The current filter-chain implementation still splits long-lived ownership and behavior across -`FilterChain`, `FilterChainCore`, and `FilterChainController`, so the public C++/C gates are not the -real runtime boundary yet. That makes async reload, control ownership, prechain resolution, and -stage-policy behavior harder to reason about than the stable gate contracts imply. - -This change is needed now because the filter-chain C++ wrapper and render-pipeline living specs -already depend on stable gate behavior, atomic policy application, and safe async swaps. A deeper -refactor should tighten the architecture around those guarantees before more runtime features stack -onto the current facade/core split. - -## Problem - -- `src/render/backend/filter_chain_controller.cpp` still owns filter semantics such as stage-policy - interpretation, prechain resolution math, and control forwarding instead of only runtime - orchestration. -- `src/render/chain/filter_chain.hpp` and `src/render/chain/filter_chain_core.hpp` split ownership of - preset loading, record-path behavior, runtime allocations, and control state across overlapping - abstractions. -- The stable gate intent in `src/render/chain/api/cpp/goggles_filter_chain.hpp` and - `src/render/chain/api/c/goggles_filter_chain.h` is harder to preserve when internal runtime state - still leaks responsibility boundaries into backend code. -- Async reload safety and deferred destruction are present today, but the ownership model makes it - difficult to isolate failure handling, state replay, and retirement improvements cleanly. - -## Scope - -- Make the C++ wrapper and C ABI the only stable filter-chain gates while keeping them - source-compatible. -- Replace the current facade/core split with one gate-owned runtime boundary that can be decomposed - internally by responsibility. -- Reduce `FilterChainController` to async build submission, pending swap, and runtime retirement - orchestration. -- Preserve existing render-pipeline behavior for stage ordering, policy continuity, postchain - presentation, and reload safety while moving ownership behind the gate. -- Add contract coverage for boundary isolation, bounded record-path behavior, and runtime swap / - retirement guarantees. - -## What Changes - -- Introduce a dedicated filter-chain runtime-boundary capability that defines gate-owned runtime - ownership, async rebuild isolation, bounded record-path behavior, and controller-scope limits. -- Strengthen the `filter-chain-cpp-wrapper` capability so the C++ wrapper and C ABI remain the sole - stable filter-chain integration gates while internal runtime ownership changes behind them. -- Refactor the render-chain implementation so builder, executor, resources, and controls can evolve - behind one runtime-owned boundary without changing backend-facing contracts. -- Keep existing render-pipeline stage-order and policy-preservation guarantees intact while the - internals move behind the stable gates. - -## Capabilities - -### New Capabilities -- `filter-chain-runtime-boundary`: defines the gate-owned filter runtime, async swap guarantees, - controller scope, and bounded record-path responsibilities required for the deep refactor. - -### Modified Capabilities -- `filter-chain-cpp-wrapper`: the wrapper requirements change to keep the C++ wrapper and C ABI as - the only stable filter-chain gates while the internal runtime implementation changes behind them. - -## Non-goals - -- Change shader semantic names, pass ordering, or postchain presentation behavior. -- Bypass the stable C++ or C gates from `src/render/backend/`. -- Lock the current proposed internal class names if a safer equivalent ownership split is found - during implementation. -- Deliver unrelated render/backend cleanup outside the runtime-boundary refactor. - -## Impact - -- Affected modules: `src/render/chain`, `src/render/backend`, and `tests/render`. -- Likely affected files: `src/render/chain/filter_chain.hpp`, `src/render/chain/filter_chain.cpp`, - `src/render/chain/filter_chain_core.hpp`, `src/render/chain/filter_chain_core.cpp`, - `src/render/chain/api/c/goggles_filter_chain.cpp`, - `src/render/chain/api/cpp/goggles_filter_chain.cpp`, - `src/render/backend/filter_chain_controller.hpp`, - `src/render/backend/filter_chain_controller.cpp`, `src/render/chain/CMakeLists.txt`, - `tests/render/test_filter_boundary_contracts.cpp`, `tests/render/test_filter_chain.cpp`, and - `tests/render/test_filter_controls.cpp`. -- Impacted OpenSpec specs: `openspec/specs/filter-chain-cpp-wrapper/spec.md` and the new - `filter-chain-runtime-boundary` capability. -- No new external dependencies or packaging changes are expected. - -## Risks - -- Runtime ownership can still leak back into backend code if controller responsibilities are not - narrowed decisively. -- Async reload and shutdown behavior can regress if pending runtime state is not isolated from the - active runtime throughout failure and swap paths. -- A deep refactor can accidentally alter hot-path behavior unless record-path obligations stay - explicit and testable. -- Retirement safety can remain heuristic if explicit submission/fence tracking is not feasible in the - current backend plumbing. - -## Validation Plan - -Verification contract: -- Baseline gates: - - `pixi run build -p debug` - - `pixi run build -p asan` - - `pixi run build -p quality` -- Environment-agnostic automated checks: - - `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` - - `pixi run test -p asan` -- Environment-sensitive checks: - - `ctest --preset test -R "^(headless_smoke|goggles_headless_integration)$" --output-on-failure` - when the local runtime supports the required GPU/display path -- Manual fallback: - - allowed only for the environment-sensitive checks above - - record prerequisites, observations, and proof location if fallback is used -- Mandatory checks with no fallback: - - the baseline build/static-analysis gates above - - the targeted render/filter unit coverage in `goggles_unit_tests` -- Pass criteria: - - backend-facing code continues to use the stable filter-chain gates only - - failed async reload leaves the active runtime usable - - successful swaps preserve policy, controls, and prechain configuration before first render - - record-path behavior preserves `prechain -> effect -> postchain` ordering without preset parse, - shader compilation, filesystem I/O, or blocking waits diff --git a/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md b/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md deleted file mode 100644 index b9647ce1..00000000 --- a/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-cpp-wrapper/spec.md +++ /dev/null @@ -1,45 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Boundary Isolation -The C header SHALL remain the ABI boundary contract only. Internal runtime C++ modules in runtime -backend scope SHALL use the C++ wrapper header for filter-chain integration, SHALL NOT directly -include or call the C ABI header, and SHALL NOT depend on internal chain runtime/build/resource/ -control types. Wrapper adoption SHALL avoid re-exporting C-header ceremony or internal runtime types -into runtime callsites. - -#### Scenario: Internal runtime include boundary -- **GIVEN** runtime backend migration scope is `src/render/backend/` -- **WHEN** the gate-centered refactor is complete -- **THEN** runtime backend paths under `src/render/backend/` SHALL include the C++ wrapper header for - filter-chain integration -- **AND** direct inclusion of `goggles_filter_chain.h` SHALL be absent from that path for both quote - and angle-bracket forms -- **AND** direct dependence on internal filter-chain runtime/build/resource/control types SHALL be - absent from that path -- **AND** include-boundary verification command output SHALL show zero matches for the forbidden - boundary leaks - -### Requirement: C ABI Continuity During Migration -Filter-chain runtime refactors SHALL preserve C ABI behavior and validation coverage while internal -runtime ownership changes behind the ABI handle. - -#### Scenario: C ABI contract test continuity -- **GIVEN** C ABI contract tests are already present for `goggles_filter_chain.h` -- **WHEN** the gate-centered refactor is integrated -- **THEN** C ABI contract tests SHALL continue to compile and pass using `goggles_filter_chain.h` -- **AND** ABI boundary implementation files SHALL remain C-header based -- **AND** the internal runtime replacement SHALL NOT require source changes in stable C ABI consumers - -## ADDED Requirements - -### Requirement: Stable Gate Surface During Internal Runtime Refactor -The C++ wrapper and C ABI SHALL remain the only stable filter-chain gates while internal runtime -ownership changes behind them. Public wrapper and ABI consumers SHALL continue to use the existing -gate entrypoints without learning about internal `ChainRuntime`, builder, executor, resource, or -control types. - -#### Scenario: Stable gate behavior survives internal replacement -- **GIVEN** a stable gate consumer uses `FilterChainRuntime` or `goggles_filter_chain.h` -- **WHEN** the internal runtime implementation is replaced or decomposed behind the gate -- **THEN** the consumer SHALL continue using the same stable gate entrypoints and ownership model -- **AND** internal runtime type names SHALL remain hidden from the stable gate surface diff --git a/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md b/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md deleted file mode 100644 index ed6aa6f1..00000000 --- a/openspec/changes/filter-chain-gate-refactor/specs/filter-chain-runtime-boundary/spec.md +++ /dev/null @@ -1,79 +0,0 @@ -## ADDED Requirements - -### Requirement: Gate-Owned Filter Chain Runtime Boundary -The filter-chain implementation SHALL treat `src/render/chain/api/cpp/goggles_filter_chain.hpp` and -`src/render/chain/api/c/goggles_filter_chain.h` as the only stable integration gates. Internal -runtime, build, resource, and control types SHALL remain behind the C ABI handle and SHALL NOT be -referenced from backend-facing code or the public install surface. - -#### Scenario: Backend stays on the stable gate -- **GIVEN** runtime backend code under `src/render/backend/` needs filter-chain services -- **WHEN** the gate-centered refactor is integrated -- **THEN** backend code SHALL interact through `FilterChainRuntime` -- **AND** direct dependence on internal chain runtime/build/resource/control types SHALL be absent - -### Requirement: Backend-Owned Vulkan Handoff Seam Remains Intact -`VulkanBackend` SHALL retain ownership of root backend Vulkan state, swapchain/render-output -behavior, external-frame import, and presentation synchronization. The filter-chain runtime SHALL -continue to receive only boundary-scoped Vulkan inputs produced by the backend handoff seam rather -than taking ownership of backend render-output concerns. - -#### Scenario: Runtime creation uses boundary-scoped Vulkan inputs only -- **GIVEN** `VulkanBackend` creates or rebuilds a filter-chain runtime -- **WHEN** it prepares the runtime build configuration for the stable gate -- **THEN** the backend SHALL pass only boundary-scoped Vulkan inputs into the filter-chain runtime -- **AND** root backend Vulkan or render-output ownership SHALL remain outside `ChainRuntime` - -### Requirement: Async Rebuild Isolation And State-Preserving Swap -The gate-owned runtime SHALL build pending preset/runtime state without mutating the active runtime. -If async rebuild fails, the active runtime SHALL remain usable with its prior preset, effective stage -policy, control values, and prechain configuration. If async rebuild succeeds, the swapped-in runtime -SHALL preserve that state before its first rendered frame. - -#### Scenario: Failed rebuild leaves active runtime intact -- **GIVEN** an active filter-chain runtime is rendering frames -- **WHEN** an async preset rebuild fails before swap -- **THEN** the active runtime SHALL continue using its existing preset and effective state -- **AND** no frame SHALL render with partially installed pending state - -#### Scenario: Successful swap preserves effective state -- **GIVEN** an active runtime has stage policy, control overrides, and prechain configuration applied -- **WHEN** an async preset rebuild completes and swaps in a new runtime -- **THEN** the new runtime SHALL render its first frame with the same effective policy, control - values, and prechain configuration - -### Requirement: Record Path Uses Prepared Runtime State Only -Once a preset/runtime is active, `record()` SHALL consume prepared compiled state and runtime -resources only. The record path SHALL NOT parse presets, compile shaders, perform filesystem I/O, or -block on background build completion. Record-path execution SHALL preserve stage order -`prechain -> effect -> postchain`. - -#### Scenario: Hot path stays bounded -- **GIVEN** a prepared active runtime and a valid frame record request -- **WHEN** `record()` executes for a frame -- **THEN** it SHALL use prepared compiled state and runtime resources only -- **AND** it SHALL preserve `prechain -> effect -> postchain` ordering - -### Requirement: Controller Scope Is Orchestration Only -`FilterChainController` SHALL manage active/pending runtime instances, async rebuild submission, -safe swap timing, and retired-runtime cleanup only. Stage-policy interpretation, prechain -resolution normalization, control descriptor generation, and control value semantics SHALL be owned -inside the gate-owned runtime. - -#### Scenario: Controller forwards desired semantics without owning them -- **GIVEN** backend code requests policy, resolution, or preset changes -- **WHEN** `FilterChainController` handles those requests -- **THEN** it SHALL forward the desired values into the stable runtime gate -- **AND** filter semantic interpretation SHALL remain inside the gate-owned runtime - -### Requirement: Runtime Retirement Has An Explicit Safety Contract -Swapped-out runtimes SHALL remain valid until it is safe to destroy them relative to frames in -flight. The implementation SHALL prefer explicit submission-epoch or fence-backed retirement when the -backend plumbing supports it; otherwise it SHALL use an isolated bounded fallback retirement helper -with dedicated verification coverage. - -#### Scenario: Fallback retirement remains bounded and explicit -- **GIVEN** explicit submission/fence-backed retirement is not available yet -- **WHEN** a runtime swap retires the previous runtime instance -- **THEN** destruction SHALL be deferred through one isolated fallback helper -- **AND** verification coverage SHALL name the fallback behavior and its safety assumptions diff --git a/openspec/changes/filter-chain-gate-refactor/tasks.md b/openspec/changes/filter-chain-gate-refactor/tasks.md deleted file mode 100644 index cb3a82bb..00000000 --- a/openspec/changes/filter-chain-gate-refactor/tasks.md +++ /dev/null @@ -1,49 +0,0 @@ -## 1. Gate-owned runtime boundary - -- [x] 1.1 Add `ChainRuntime` (and any supporting compiled-state type) behind the filter-chain C ABI - handle, and update `src/render/chain/CMakeLists.txt` so the render-chain target builds the new - runtime entrypoint without changing the public C/C++ gate surface. -- [x] 1.2 Update `src/render/chain/api/c/goggles_filter_chain.cpp` and - `src/render/chain/api/cpp/goggles_filter_chain.cpp` so create/load/resize/record/control flows - forward through the gate-owned runtime while keeping C ABI and wrapper behavior source-compatible. -- [x] 1.3 Remove backend dependence on facade/core internals by keeping `src/render/backend/` on - `FilterChainRuntime` only and preserving boundary isolation coverage in - `tests/render/test_filter_boundary_contracts.cpp`. -- [x] 1.4 Preserve the backend-owned Vulkan handoff seam so `VulkanBackend` keeps swapchain, - importer, synchronization, render-output, and root `VulkanContext` ownership while passing only - boundary-scoped Vulkan inputs into the stable filter-chain gate. - -## 2. Internal runtime responsibility split - -- [x] 2.1 Extract preset parse/resolve/compile/rebuild work from the current facade/core path into - `ChainBuilder` and an immutable compiled-chain product. -- [x] 2.2 Extract runtime allocations and resize-sensitive state into `ChainResources`, including - framebuffers, history, feedback targets, texture registry, and prechain/postchain targets. -- [x] 2.3 Extract per-frame execution into `ChainExecutor` so the record path preserves - `prechain -> effect -> postchain` ordering without preset parsing, shader compilation, - filesystem I/O, or blocking waits. -- [x] 2.4 Extract control catalog, normalization, defaults, deterministic ids, and rebuild replay into - `ChainControls` so control semantics remain behind the gate. - -## 3. Controller orchestration and swap safety - -- [x] 3.1 Reduce `FilterChainController` to active/pending runtime orchestration, async `JobSystem` - submission, swap-at-safe-point behavior, and retired-runtime tracking. -- [x] 3.2 Move stage-policy interpretation, prechain-resolution resolution, and control semantics - behind `ChainRuntime` while preserving active policy, prechain configuration, and control values - across successful swaps and keeping failed reloads non-destructive. -- [x] 3.3 Isolate runtime retirement into a helper that prefers explicit submission/fence-backed - retirement when available and otherwise keeps the delayed-retire fallback explicitly bounded and - tested. - -## 4. Verification and contract alignment - -- [x] 4.1 Update or add render tests so wrapper boundary isolation, async swap safety, stage-order - invariants, and control-id/control-replay semantics stay aligned with the OpenSpec contract. -- [x] 4.2 Run `ctest --preset test -R "^goggles_unit_tests$" --output-on-failure` and address any - filter-chain, control, or boundary regressions. -- [x] 4.3 Run `pixi run build -p debug`, `pixi run build -p asan`, `pixi run test -p asan`, and - `pixi run build -p quality`. -- [x] 4.4 Run `ctest --preset test -R "^(headless_smoke|goggles_headless_integration)$" --output-on-failure` - when the local runtime supports it; otherwise record the explicit environment limitation and the - proof for the allowed fallback. diff --git a/openspec/changes/standalone-filter-chain-api/design.md b/openspec/changes/standalone-filter-chain-api/design.md deleted file mode 100644 index 8a16701f..00000000 --- a/openspec/changes/standalone-filter-chain-api/design.md +++ /dev/null @@ -1,549 +0,0 @@ -# Design: Standalone Filter-Chain API - -## Technical Approach - -Rebuild `filter-chain/` around a package-first runtime boundary whose public surface is a C-first -ABI with an optional thin C++ wrapper. The new boundary splits responsibilities into four owned -object families: - -- `instance` owns process-local library policy such as log routing and immutable global options. -- `device` owns borrowed Vulkan device/queue bindings plus library-owned caches, upload/setup - command resources, and device-scoped services. -- `program` owns preset parsing, source resolution, shader compilation/reflection, and immutable - preset metadata derived from either file or memory sources. -- `chain` owns executable runtime state for one program on one device: pipelines, descriptors, - intermediate images, control state, frame history, and record-time validation. - -The existing `ChainRuntime`/`goggles_chain_*` model stays only as implementation source material -until replacement work is complete. The shipped end state removes that legacy public surface, -associated compatibility notes, and any build/install/export/docs/examples that preserve it. Public -Goggles-facing abstractions move out of `src/render/backend/` and into a dedicated adapter that -consumes the standalone API exactly like any external host. Internal shaders/assets currently -resolved through `shader_dir` become embedded library resources selected by internal asset IDs. - -This design intentionally does not preserve source or ABI compatibility with the current public -surface. - -Completion of this design means the repository no longer ships two public stories for filter-chain: -the installed package, public examples, public tests, and package metadata all describe only the -`goggles_fc_*` C-first contract plus the thin wrapper layered on top of it. - -## Architecture Decisions - -### Decision: Replace the monolithic runtime handle with explicit instance/device/program/chain objects - -**Choice**: Introduce opaque C handles `goggles_fc_instance_t`, `goggles_fc_device_t`, -`goggles_fc_program_t`, and `goggles_fc_chain_t`, with creation/destruction APIs and explicit -dependency ordering. - -**Alternatives considered**: Keep one `goggles_chain_t` runtime handle; split only internally while -retaining the existing public lifecycle. - -**Rationale**: The proposal requires a clean-slate reusable library boundary. Separate objects make -borrowed-vs-owned resources explicit, let Goggles preload/compile programs asynchronously, and keep -record-time hot paths isolated from preset parsing and compilation work. - -### Decision: Make the C ABI authoritative and keep C++ as a thin wrapper only - -**Choice**: Define the full public contract in `filter-chain/include/goggles_filter_chain.h` with -`goggles_fc_*` functions and `GOGGLES_FC_*` macros. Rebuild the C++ wrapper as a header-level RAII -consumer in `namespace goggles::filter_chain` over that C ABI. - -**Alternatives considered**: Continue treating the C++ wrapper as the primary API; keep dual public -surfaces with partially independent semantics. - -**Rationale**: A C-first surface is the easiest contract to package, bind from other languages, and -keep ABI-focused. A thin wrapper avoids duplicated behavior and keeps all ownership rules defined in -one place. - -### Decision: Bind Vulkan at the device object and remove public `shader_dir` and host command-pool inputs - -**Choice**: `goggles_fc_device_create_vk(...)` accepts borrowed `VkPhysicalDevice`, `VkDevice`, -`VkQueue`, and queue-family index. The library creates and owns its internal upload/setup command -pools, transient buffers, caches, and synchronization helpers. Public create APIs no longer accept -`shader_dir` or host-owned command pools. - -**Alternatives considered**: Keep the current create-time `shader_dir` contract; keep requiring a -boundary-owned `vk::CommandPool`; make the host allocate upload helpers. - -**Rationale**: The host/library split in the proposal says the host provides device/queue handles -and per-record inputs while the library owns reusable runtime machinery. Removing `shader_dir` -eliminates repository-relative asset assumptions and makes installed-package behavior predictable. - -### Decision: Represent presets as immutable programs loaded from file or memory sources - -**Choice**: Add `goggles_fc_preset_source_t` with `file` and `memory` variants. Program creation -parses and compiles a preset source into immutable metadata plus device-scoped compiled artifacts. -Memory-backed sources optionally use a host-supplied import callback for relative includes/textures -or an explicit filesystem base path when the host wants default file-backed relative resolution. - -**Alternatives considered**: Keep preset loading as a mutable chain operation only; support memory - only for top-level preset text with no relative resolution; store only file paths publicly. - -**Rationale**: A standalone library needs reusable precompiled state and testable source loading. -Separating `program` from `chain` lets Goggles compile/reload without mutating the active runtime -and makes file-vs-memory sourcing a program concern instead of a frame concern. - -Canonical public UTF-8/path fields use pointer-plus-byte-length pairs. File-backed program sources -derive provenance and relative-resolution base paths from the parent directory of the supplied path. -Memory-backed program sources carry memory provenance plus optional `source_name` metadata and MUST -use either registered import callbacks or an explicit `base_path` for relative include/texture -resolution. If neither is provided, any relative external reference is rejected during program -creation instead of consulting ambient process state. - -### Decision: Route all logs and diagnostics through host-owned callbacks instead of shared globals - -**Choice**: Replace `filter-chain/src/support/logging.*` global logger access with an instance-owned -log router. Hosts register callbacks/sinks through the instance, and diagnostic sessions on chains -reuse the same routing infrastructure for structured events. - -**Alternatives considered**: Keep `spdlog` globals and hide them behind exported functions; keep -diagnostic callbacks only for forensic mode while normal logs use library-global sinks. - -**Rationale**: Shared/global logger symbols are exactly the packaging problem called out in the -proposal. Instance-owned routing keeps the library embeddable in hosts with different logging -systems and avoids symbol collisions across static/shared consumption modes. - -Callback registration is replaceable at runtime through `goggles_fc_instance_set_log_callback(...)`. -The instance borrows the callback function pointer and `user_data` until the next replacement or -instance destruction. Delivery occurs synchronously on the thread that emits the log event; v1 does -not spawn a dedicated logging thread or promise cross-thread serialization beyond the API's normal -external-synchronization rules. Unstructured callback payloads provide human-readable log lines, -while structured report queries remain available on `program` and `chain` objects for machine- -readable diagnostics and last-error inspection. - -### Decision: Keep programs device-affine but shareable across multiple chains on that device - -**Choice**: `goggles_fc_program_t` is created against exactly one `goggles_fc_device_t` and carries -that device affinity for its full lifetime. A single program handle MAY be bound to multiple chains -created from the same device, but it MUST NOT be attached to chains on any other device. - -**Alternatives considered**: Make programs instance-scoped and lazily specialize on first chain use; -require one program per chain. - -**Rationale**: Device affinity keeps compiled artifacts and reflection caches concrete for Vulkan-only -v1, while multi-chain sharing allows cheap creation of multiple executable chains with independent -runtime state from one immutable preset/program. - -### Decision: Keep package identity stable while changing API naming - -**Choice**: Preserve the package target name `goggles-filter-chain` and exported CMake namespace -`GogglesFilterChain::goggles-filter-chain`, but rename API entry points/macros/types to -`goggles_fc_*` / `GOGGLES_FC_*` and move the C++ namespace to `goggles::filter_chain`. - -**Alternatives considered**: Rename the CMake target together with the API; keep old -`goggles_chain_*` naming for target-package consistency. - -**Rationale**: Downstream build integration is easier if the package identity stays stable, while -the public ABI still needs a clean break from the old object model and naming. - -### Decision: Remove legacy public surface and migration artifacts at cutover - -**Choice**: Treat the old public C header shape, legacy wrapper header/namespace, compatibility -examples, obsolete contract tests, stale install/export wiring, and package metadata that mentions -removed runtime assumptions as temporary migration scaffolding only. Before the change is complete, -all such artifacts must be deleted or rewritten so the shipped package presents one public contract. - -**Alternatives considered**: Leave deprecated headers or docs in place for reference; keep dual -consumer examples; preserve old install metadata as a compatibility convenience. - -**Rationale**: The proposal defines a clean-slate redesign. Leaving deprecated surfaces, shims, or -stale docs/examples behind would keep the migration ambiguous and continue exporting the old -contract in practice even if the new API exists. - -## Data Flow - -### High-Level Ownership - -```text -Host Process - | - +--> goggles_fc_instance - | \-- log router, global options - | - +--> goggles_fc_device - | \-- borrowed VkDevice/VkQueue - | \-- owned caches, command pools, upload helpers - | - +--> goggles_fc_program - | \-- parsed preset graph - | \-- compiled shaders/reflection - | \-- embedded built-in asset references - | - \--> goggles_fc_chain - \-- pipelines, descriptors, framebuffers, controls, history - \-- records into host command buffer using host-provided images/views -``` - -### Program Build Sequence - -```mermaid -sequenceDiagram - participant Host - participant Instance - participant Device - participant Program - participant Resolver as Source Resolver - participant Assets as Embedded Assets - - Host->>Instance: goggles_fc_instance_create(...) - Host->>Device: goggles_fc_device_create_vk(instance, vk_desc) - Host->>Program: goggles_fc_program_create(device, preset_source) - Program->>Resolver: load file bytes or import relative source - Resolver-->>Program: preset bytes + external asset bytes - Program->>Assets: request built-in shaders/assets by asset id - Assets-->>Program: embedded bytes - Program->>Program: parse preset, reflect shaders, populate cache entries - Program-->>Host: immutable program handle + control metadata -``` - -### Record-Time Sequence - -```mermaid -sequenceDiagram - participant Host - participant Chain - participant Device - participant Cmd as Host VkCommandBuffer - - Host->>Chain: goggles_fc_chain_record_vk(record_info) - Chain->>Chain: validate borrowed source/target inputs - Chain->>Device: fetch cached samplers, descriptors, upload helpers - Chain->>Cmd: record setup/upload commands if pending - Chain->>Cmd: record pass N pipelines and barriers - Chain->>Cmd: record final output pass to host target view - Chain-->>Host: status only (no submit/present) -``` - -### Goggles Integration Flow - -```text -src/render/backend/vulkan_backend.cpp - -> src/render/backend/filter_chain_controller.cpp - -> goggles_fc_instance / goggles_fc_device / goggles_fc_program / goggles_fc_chain - -> filter-chain runtime internals - -Host owns: -- swapchain lifecycle -- external image import -- queue submit / present -- async reload scheduling - -Library owns: -- preset parsing and include resolution -- shader compilation and reflection cache -- internal pipelines and framebuffers -- built-in shader assets -- diagnostics fanout -``` - -## File Changes - -| File | Action | Description | -|------|--------|-------------| -| `filter-chain/include/goggles_filter_chain.h` | Modify | Replace the current `goggles_chain_*` ABI with the new `goggles_fc_*` object model, status types, source descriptors, logging callbacks, and Vulkan create/record structs. | -| `filter-chain/include/goggles_filter_chain.hpp` | Delete | Remove the current `goggles::render::FilterChainRuntime` wrapper so the old API shape cannot leak forward. | -| `filter-chain/include/goggles/filter_chain.hpp` | Create | Define the canonical thin C++ wrapper entrypoint in `namespace goggles::filter_chain` over the C ABI. | -| `filter-chain/include/goggles/filter_chain/common.hpp` | Create | Hold public C++ enums and POD wrapper types shared by the wrapper surface. | -| `filter-chain/src/api/c_api.cpp` | Create | Implement handle validation, ABI marshaling, status mapping, and the exported `goggles_fc_*` functions. | -| `filter-chain/src/api/cpp_wrapper.cpp` | Create | Implement the thin RAII C++ wrapper using only the C ABI. | -| `filter-chain/src/api/abi_validation.hpp` | Create | Centralize struct-size/version validation and borrowed-handle checks for the C layer. | -| `filter-chain/src/runtime/instance.hpp` | Create | Define the internal instance object, callback registry, and log router ownership. | -| `filter-chain/src/runtime/instance.cpp` | Create | Implement instance lifecycle and callback dispatch rules. | -| `filter-chain/src/runtime/device.hpp` | Create | Define device-scoped caches, internal command resources, and Vulkan borrowed-handle storage. | -| `filter-chain/src/runtime/device.cpp` | Create | Implement device creation, cache initialization, and teardown in dependency order. | -| `filter-chain/src/runtime/program.hpp` | Create | Define immutable compiled preset/program state and source provenance. | -| `filter-chain/src/runtime/program.cpp` | Create | Implement file/memory preset loading, include resolution, built-in asset lookup, and compilation. | -| `filter-chain/src/runtime/chain.hpp` | Create | Define executable chain state bound to one device/program pair. | -| `filter-chain/src/runtime/chain.cpp` | Create | Implement output retargeting, resize, controls, and record-time execution. | -| `filter-chain/src/runtime/source_resolver.hpp` | Create | Define file and memory source resolution contracts for presets, includes, and textures. | -| `filter-chain/src/runtime/source_resolver.cpp` | Create | Implement path-based and callback-based source resolution with provenance tracking. | -| `filter-chain/src/runtime/embedded_assets.cpp` | Create | Register compiled-in built-in shaders/assets and expose lookup by internal asset id. | -| `filter-chain/src/support/logging.hpp` | Modify | Replace global logger getters/setters with internal log-router primitives only. | -| `filter-chain/src/support/logging.cpp` | Modify | Remove `spdlog::set_default_logger(...)` style global ownership and route through instance-bound sinks. | -| `filter-chain/src/chain/chain_runtime.hpp` | Modify | Retain only lower-level reusable execution helpers or migrate declarations into the new runtime layer. | -| `filter-chain/src/chain/chain_runtime.cpp` | Modify | Re-scope existing runtime internals behind `device/program/chain` objects instead of a public monolith. | -| `filter-chain/src/chain/preset_parser.hpp` | Modify | Generalize parser inputs away from filesystem-only loading so program creation can use file or memory sources. | -| `filter-chain/src/chain/preset_parser.cpp` | Modify | Implement parser integration with the new source resolver and provenance model. | -| `filter-chain/CMakeLists.txt` | Modify | Reorganize build targets around `api`, `runtime`, and embedded-assets sources; remove install-time dependency on public asset directories for built-ins. | -| `filter-chain/cmake/GogglesFilterChainConfig.cmake.in` | Modify | Stop exporting runtime asset-dir assumptions and keep package metadata focused on headers, library targets, and private dependency discovery. | -| `filter-chain/tests/contract/test_filter_chain_c_api_contracts.cpp` | Modify | Rewrite coverage for `goggles_fc_*`, object lifecycle splits, memory/file preset sources, and log callback contracts. | -| `filter-chain/tests/consumer/static/main.cpp` | Modify | Validate the installed C++ wrapper surface under the new namespace and header layout. | -| `filter-chain/tests/consumer/shared/main.cpp` | Modify | Validate shared-library consumption through the new wrapper surface. | -| public docs/examples/install checks | Modify | Remove or rewrite any example, package note, or usage snippet that references removed names, wrapper layouts, or `shader_dir`-style assumptions. | -| `src/render/backend/filter_chain_adapter.hpp` | Delete | Remove the intermediate Goggles-side adapter after its responsibilities move into the controller-owned consumer boundary. | -| `src/render/backend/filter_chain_adapter.cpp` | Delete | Remove the intermediate Goggles-side adapter implementation after controller consolidation. | -| `src/render/backend/filter_chain_controller.hpp` | Modify | Replace direct `FilterChainRuntime` ownership with controller-owned standalone instance/device/program/chain handles and controller-local boundary types. | -| `src/render/backend/filter_chain_controller.cpp` | Modify | Move reload, retarget, control, diagnostics, and standalone object-graph ownership into the controller instead of a separate adapter layer. | -| `src/render/backend/vulkan_backend.cpp` | Modify | Build controller/device descriptors and keep host responsibilities restricted to swapchain, import, submission, and presentation. | - -## Interfaces / Contracts - -### Public C ABI Shape - -```c -typedef struct goggles_fc_instance goggles_fc_instance_t; -typedef struct goggles_fc_device goggles_fc_device_t; -typedef struct goggles_fc_program goggles_fc_program_t; -typedef struct goggles_fc_chain goggles_fc_chain_t; - -typedef uint32_t goggles_fc_status_t; -typedef uint32_t goggles_fc_log_level_t; -typedef uint32_t goggles_fc_capability_flags_t; -typedef uint32_t goggles_fc_preset_source_kind_t; - -typedef struct GogglesFcUtf8View { - const char* data; - size_t size; -} goggles_fc_utf8_view_t; - -typedef struct GogglesFcLogMessage { - uint32_t struct_size; - goggles_fc_log_level_t level; - goggles_fc_utf8_view_t domain; - goggles_fc_utf8_view_t message; -} goggles_fc_log_message_t; - -typedef void(GOGGLES_FC_CALL* goggles_fc_log_callback_t)( - const goggles_fc_log_message_t* message, - void* user_data); - -typedef struct GogglesFcInstanceCreateInfo { - uint32_t struct_size; - goggles_fc_log_callback_t log_callback; - void* log_user_data; -} goggles_fc_instance_create_info_t; - -typedef struct GogglesFcVkDeviceCreateInfo { - uint32_t struct_size; - VkPhysicalDevice physical_device; - VkDevice device; - VkQueue graphics_queue; - uint32_t graphics_queue_family_index; - goggles_fc_utf8_view_t cache_dir; /* optional */ -} goggles_fc_vk_device_create_info_t; - -typedef struct GogglesFcPresetSource { - uint32_t struct_size; - goggles_fc_preset_source_kind_t kind; - goggles_fc_utf8_view_t source_name; - const void* bytes; - size_t byte_count; - goggles_fc_utf8_view_t path; - goggles_fc_utf8_view_t base_path; - const goggles_fc_import_callbacks_t* import_callbacks; /* optional for memory */ -} goggles_fc_preset_source_t; - -typedef struct GogglesFcChainCreateInfo { - uint32_t struct_size; - VkFormat target_format; - uint32_t frames_in_flight; - uint32_t initial_stage_mask; - GogglesFcExtent2D initial_prechain_resolution; -} goggles_fc_chain_create_info_t; - -typedef struct GogglesFcRecordInfoVk { - uint32_t struct_size; - VkCommandBuffer command_buffer; - VkImage source_image; - VkImageView source_view; - GogglesFcExtent2D source_extent; - VkImageView target_view; - GogglesFcExtent2D target_extent; - uint32_t frame_index; - uint32_t scale_mode; - uint32_t integer_scale; -} goggles_fc_record_info_vk_t; - -goggles_fc_status_t goggles_fc_instance_create( - const goggles_fc_instance_create_info_t* create_info, - goggles_fc_instance_t** out_instance); -void goggles_fc_instance_destroy(goggles_fc_instance_t* instance); - -uint32_t goggles_fc_get_api_version(void); -uint32_t goggles_fc_get_abi_version(void); -goggles_fc_capability_flags_t goggles_fc_get_capabilities(void); - -goggles_fc_status_t goggles_fc_instance_set_log_callback( - goggles_fc_instance_t* instance, - goggles_fc_log_callback_t log_callback, - void* user_data); - -goggles_fc_status_t goggles_fc_device_create_vk( - goggles_fc_instance_t* instance, - const goggles_fc_vk_device_create_info_t* create_info, - goggles_fc_device_t** out_device); -void goggles_fc_device_destroy(goggles_fc_device_t* device); - -goggles_fc_status_t goggles_fc_program_create( - goggles_fc_device_t* device, - const goggles_fc_preset_source_t* source, - goggles_fc_program_t** out_program); -void goggles_fc_program_destroy(goggles_fc_program_t* program); - -goggles_fc_status_t goggles_fc_program_get_source_info( - const goggles_fc_program_t* program, - goggles_fc_program_source_info_t* out_source_info); - -goggles_fc_status_t goggles_fc_program_get_report( - const goggles_fc_program_t* program, - goggles_fc_program_report_t* out_report); - -goggles_fc_status_t goggles_fc_chain_create( - goggles_fc_device_t* device, - const goggles_fc_program_t* program, - const goggles_fc_chain_create_info_t* create_info, - goggles_fc_chain_t** out_chain); -void goggles_fc_chain_destroy(goggles_fc_chain_t* chain); - -goggles_fc_status_t goggles_fc_chain_bind_program( - goggles_fc_chain_t* chain, - const goggles_fc_program_t* program); - -goggles_fc_status_t goggles_fc_chain_clear(goggles_fc_chain_t* chain); - -goggles_fc_status_t goggles_fc_chain_resize( - goggles_fc_chain_t* chain, - const goggles_fc_extent_2d_t* new_source_extent); - -goggles_fc_status_t goggles_fc_chain_retarget( - goggles_fc_chain_t* chain, - const goggles_fc_chain_target_info_t* target_info); - -goggles_fc_status_t goggles_fc_chain_record_vk( - goggles_fc_chain_t* chain, - const goggles_fc_record_info_vk_t* record_info); - -goggles_fc_status_t goggles_fc_chain_get_report( - const goggles_fc_chain_t* chain, - goggles_fc_chain_report_t* out_report); - -goggles_fc_status_t goggles_fc_chain_get_last_error( - const goggles_fc_chain_t* chain, - goggles_fc_chain_error_info_t* out_error); - -goggles_fc_status_t goggles_fc_chain_get_control_count( - const goggles_fc_chain_t* chain, - uint32_t* out_count); - -goggles_fc_status_t goggles_fc_chain_get_control_info( - const goggles_fc_chain_t* chain, - uint32_t index, - goggles_fc_control_info_t* out_control); - -goggles_fc_status_t goggles_fc_chain_set_control_value_f32( - goggles_fc_chain_t* chain, - uint32_t index, - float value); - -const char* goggles_fc_status_string(goggles_fc_status_t status); -bool goggles_fc_is_success(goggles_fc_status_t status); -bool goggles_fc_is_error(goggles_fc_status_t status); -``` - -### Internal Module Boundaries - -```text -filter-chain/include/ - public ABI and C++ wrapper only - -filter-chain/src/api/ - exported function entrypoints - ABI validation and status mapping - -filter-chain/src/runtime/ - instance/device/program/chain ownership - source resolution - embedded asset registry - -filter-chain/src/chain/ - reusable pass graph, executor, resources, controls - -filter-chain/src/shader/ - shader runtime, preprocessing, reflection - -filter-chain/src/diagnostics/ - structured diagnostic events and sinks - -src/render/backend/ - Goggles-only adapter and host lifecycle coordination -``` - -### Ownership Rules - -- `instance`, `device`, `program`, and `chain` are library-owned opaque handles destroyed by their - matching `*_destroy(...)` functions. -- `goggles_fc_program_t` is affine to the `goggles_fc_device_t` used at creation and MAY be shared by - multiple chains created from that same device. -- `VkPhysicalDevice`, `VkDevice`, and `VkQueue` passed into `device_create_vk` are borrowed and - MUST outlive every chain/program/device derived from that device. -- `VkCommandBuffer`, `VkImage`, and `VkImageView` passed into `chain_record_vk` are borrowed for the - duration of the call only. -- Preset source bytes passed as `memory` are copied or fully consumed during program creation; the - library MUST NOT retain caller memory after create returns. -- Log callback pointers are registered on the instance and remain borrowed until replaced or the - instance is destroyed. - -Destroy ordering is strict: chains MUST be destroyed before their bound program or device, programs -MUST be destroyed before their device, and devices MUST be destroyed before their instance. Each -`*_destroy(...)` function accepts `NULL` and treats it as a no-op. Re-destroying a non-`NULL` handle -after successful destruction is invalid caller behavior; v1 guarantees null-safe destroy, not handle -idempotence for stale pointers. - -### Threading and Synchronization Rules - -- The public C API is externally synchronized by default. Hosts MUST NOT call mutating operations on - the same `instance`, `device`, `program`, or `chain` concurrently unless a specific API is later - documented as concurrent-safe. -- Distinct handles MAY be used concurrently when they do not share the same object instance and the - host still satisfies borrowed Vulkan object synchronization requirements. -- `goggles_fc_chain_record_vk(...)`, resize/retarget operations, control mutation, and callback - replacement are all mutating operations and therefore require host-side serialization per object. -- Query-style helpers such as version/capability inspection and status-string lookup are process- - global pure reads and may be called concurrently. -- Log callbacks execute on the same thread that triggered the underlying event; hosts MUST keep the - callback non-blocking enough to avoid stalling the calling API unexpectedly. - -## Testing Strategy - -| Layer | What to Test | Approach | -|-------|-------------|----------| -| Unit | Struct validation, UTF-8/source validation, log-router dispatch, source-resolver rules | Extend `filter-chain/tests/contract/` with focused tests that avoid live Vulkan where possible. | -| Unit | Program creation from file and memory, built-in asset lookup, relative include resolution | Add parser/program tests that use library-owned fixture bytes and callback-backed memory imports. | -| Integration | Vulkan device/program/chain lifecycle, retargeting, controls, diagnostics, record validation | Rewrite `filter-chain/tests/contract/test_filter_chain_c_api_contracts.cpp` around `goggles_fc_*` and Vulkan-backed happy/validation paths. | -| Integration | Installed static/shared package consumption | Keep `filter-chain/tests/consumer/static/` and `filter-chain/tests/consumer/shared/`, updated to compile against the new wrapper/public headers only. | -| Integration | Legacy-surface absence | Add negative checks or install-tree inspection proving removed headers, deprecated aliases, old examples, and stale package metadata are not shipped. | -| Integration | Goggles controller boundary | Add backend tests under `tests/render/` that verify Goggles uses the standalone controller-owned consumer boundary rather than direct library internals. | -| E2E | Goggles preset reload, swapchain retarget, and control persistence | Reuse existing backend/manual coverage through `pixi run test -p test` and relevant render integration tests once the controller-owned consumer boundary is wired. | - -## Migration / Rollout - -This change rolls out as a single incompatible API transition on the change branch. - -1. Introduce the new internal runtime objects (`instance`, `device`, `program`, `chain`) and the - Goggles-side consumer boundary while reusing lower-level chain/shader modules where practical. -2. Replace the public C header and C++ wrapper in one pass; do not ship public compatibility - aliases for `goggles_chain_*`, `GOGGLES_CHAIN_*`, or `FilterChainRuntime`. -3. Switch Goggles backend code to `src/render/backend/filter_chain_controller.*` so no Goggles - module outside the render/backend controller boundary knows the library internals. -4. Rewrite contract and consumer tests against the new installed surface. -5. Remove public asset-dir packaging assumptions after embedded built-ins and testdata coverage are - in place. -6. Delete or rewrite obsolete public docs/examples, install/export wiring, package metadata, and - validation paths so the installed package exposes only the final standalone contract. - -No feature flag or runtime migration path is required because backward compatibility is explicitly -out of scope. - -## Open Questions - -- [x] Decide whether installed file-source contract fixtures should live under - `share/goggles-filter-chain/testdata` or be generated entirely by the test harness. - -### Resolved: Test harness generates validation fixtures at build/test time - -Installed file-source contract fixtures SHALL NOT ship under `share/goggles-filter-chain/testdata`. -Instead, contract and consumer tests SHALL generate minimal fixture presets (for example trivial -`.slangp` files) at CMake configure or CTest time within the build tree. Memory-source tests use -in-process byte buffers directly. This avoids adding public packaging surface area for test content -and keeps the installed package focused on headers, library targets, and embedded runtime assets. diff --git a/openspec/changes/standalone-filter-chain-api/proposal.md b/openspec/changes/standalone-filter-chain-api/proposal.md deleted file mode 100644 index b12d80f0..00000000 --- a/openspec/changes/standalone-filter-chain-api/proposal.md +++ /dev/null @@ -1,154 +0,0 @@ -# Proposal: Standalone Filter-Chain API - -## Intent - -Redesign `filter-chain` as a reusable standalone library with a clean public contract that Goggles -consumes as one host, rather than as an extension of Goggles internals. The current surface mixes -host concerns, Goggles-specific support assumptions, and evolving ABI decisions; this change -establishes a clean-slate public interface that is easier to embed, package, test, and evolve. - -## Problem - -The existing filter-chain boundary still reflects in-repo Goggles integration choices instead of a -package-first library contract. Public naming, lifecycle, logging, asset lookup, Vulkan -integration, packaging metadata, and supporting docs/examples are not yet aligned around an -external consumer model, which makes standalone reuse, FFI adoption, and installed-package -verification harder than necessary. - -The intended end state is a complete public cutover, not a staged compatibility outcome. When this -change is finished, the shipped standalone package must contain only the new clean-slate contract: -no deprecated APIs, no compatibility aliases or shims, no dual public surfaces, no stale -install-only runtime assumptions, and no stale docs/examples that reference removed names or -contracts. - -## Scope - -### In Scope -- Define a clean-slate standalone public model split around `instance`, `device`, `program`, and - `chain` responsibilities. -- Define a first-class C API with the `goggles_fc_*` prefix and clean host/library ownership rules. -- Redesign Vulkan host integration so the host provides device/queue handles and per-record inputs - while the library owns compilation, caches, pipelines, upload/setup command resources, and - intermediate runtime resources. -- Replace shared/global logging assumptions with host-routable callback or sink-based diagnostics. -- Define library-owned embedded/internal shader and asset handling plus preset loading from file and - memory sources. -- Keep Goggles as a consumer of the standalone package through its render/backend controller - integration rather than as the source of its public abstractions or a user of standalone - internals. -- Remove legacy public headers, symbols, wrapper shapes, build/install/export wiring, examples, - tests, and package metadata that would preserve or imply the old contract after cutover. -- Update the change artifacts so they describe the final shipped contract rather than an - intermediate migration state. - -### Out of Scope -- Source or ABI compatibility with the current filter-chain public naming or handle model. -- Non-Vulkan backends in v1. -- A compatibility layer, deprecated aliases, or migration shims for old `goggles_chain_*`/`fc_*` - style entry points. -- Final implementation details for every subsystem; those belong in follow-on spec, design, and - task artifacts. - -## Non-goals - -- Preserve current public naming when a cleaner standalone contract is available. -- Make Goggles application types, config objects, or helper utilities part of the library surface. -- Require consumers to provide shader directories or other Goggles-repository-relative assets. - -## Approach - -Adopt a package-first API redesign centered on explicit host/library boundaries. The host owns -process integration concerns such as Vulkan instance/device selection, queue ownership, submission, -presentation, and per-record source/target inputs. The standalone library owns reusable runtime -concerns such as preset parsing, shader compilation/reflection, internal caches, stage pipelines, -setup/upload command helpers, intermediate images/buffers, and embedded built-in assets. - -The public contract will prioritize a stable C surface first, with naming and lifecycle rebuilt - around `goggles_fc_*` entry points and object families that make FFI and downstream embedding - straightforward. Logging and diagnostics will route outward through callbacks/sinks so the library - does not expose or depend on shared/global `spdlog` symbols. Goggles integration will remain on - the consumer side of the package boundary through render/backend controller code that maps - existing runtime/backend needs onto the standalone contract. - -The implementation is only complete once all legacy public traces are removed from the shipped -surface: no retained `goggles_chain_*` or `fc_*` entry points, no deprecated wrapper header or -namespace, no install/export logic for removed surfaces, no public `shader_dir` or similar stale -runtime assumptions, and no docs/examples/tests that continue to teach the removed contract. - -## Affected Areas - -| Area | Impact | Description | -|------|--------|-------------| -| `filter-chain/include/` | Modified | Replace or reorganize the standalone public header surface around the new C API and object model. | -| `filter-chain/src/` | Modified | Realign internal ownership, logging, asset packaging, Vulkan boundary handling, and runtime lifecycle to match the standalone contract. | -| `filter-chain/tests/` | Modified | Shift contract coverage toward installed-surface and standalone-owned API validation. | -| `filter-chain/CMakeLists.txt` | Modified | Ensure packaging/export/install rules match the redesigned public interface. | -| `filter-chain/cmake/` | Modified | Remove stale exported package metadata and install-time assumptions that describe the old contract. | -| `src/render/` | Modified | Adapt Goggles runtime/backend integration to consume the standalone API through the render/backend controller boundary. | -| package-facing docs/examples | Modified | Remove or rewrite docs/examples that reference removed names, wrapper shapes, or asset assumptions. | -| `openspec/specs/filter-chain-c-api/spec.md` | Modified | Update the C ABI contract to the new naming, lifecycle, and ownership model. | -| `openspec/specs/filter-chain-cpp-wrapper/spec.md` | Modified | Recast the wrapper as a thin C++ consumer layer over the redesigned standalone C API. | -| `openspec/specs/goggles-filter-chain/spec.md` | Modified | Update host/library responsibility boundaries and Goggles-consumer expectations. | -| `openspec/specs/filter-chain-assets-package/spec.md` | Modified | Align asset ownership and preset source expectations with embedded/library-owned assets. | - -## Policy-Sensitive Impacts - -- Error handling: public failures stay explicit and non-exception-based, but error and diagnostic - transport move to host-routable callbacks/sinks. -- Logging: standalone code must stop depending on shared/global Goggles logging state or exported - `spdlog` symbols. -- Threading: the proposal keeps host/library boundaries explicit so threading guarantees can be - specified without hidden shared-state assumptions. -- Vulkan split: v1 stays Vulkan-only, but host-provided handles and library-owned runtime resources - must remain clearly separated. -- Lifetime/ownership: object model and API calls must make borrowed vs owned resources obvious for - FFI consumers and Goggles render/backend integration. - -## Risks - -| Risk | Likelihood | Mitigation | -|------|------------|------------| -| Clean-slate API scope grows into an uncontrolled refactor | Medium | Lock the next spec/design work to public-boundary decisions first and defer unrelated internals. | -| Goggles integration uncovers hidden assumptions about current runtime ownership | High | Capture controller-side consumer requirements explicitly in design and preserve Goggles as a consumer, not a boundary exception. | -| Logging/diagnostic redesign creates unclear host callback lifetime rules | Medium | Specify callback registration, threading, and ownership semantics in the C API spec before implementation. | -| Embedded assets and memory/file preset sources complicate packaging | Medium | Define asset resolution and packaging behavior in specs and validate against installed-package tests. | - -## Rollback Plan - -If the redesign proves unworkable, revert the standalone API changes within `filter-chain/`, keep -Goggles on the current in-repo integration path, and discard the controller-side consumer -integration changes before syncing any delta specs into living specs. Because backward -compatibility is explicitly out of scope, the rollback boundary is the change branch itself rather -than a compatibility preservation layer. - -## Dependencies - -- Follow-on delta specs for the standalone C API, C++ wrapper, Goggles integration boundary, and - asset package behavior. -- A technical design that defines object lifetimes, host callbacks, Vulkan handle flow, and - Goggles render/backend controller integration strategy. - -## Validation Plan - -- Validate that the proposal produces spec updates covering C API naming, lifecycle, ownership, - logging/diagnostics, asset sourcing, and Goggles host responsibilities. -- Verify the redesigned package can be built, installed, and consumed without Goggles-private - headers, repository-relative assets, or shared global logging state. -- Verify no shipped public header, example, test target, install/export rule, or package metadata - path still exposes removed names, compatibility shims, or install-only asset assumptions. -- Verify Goggles remains able to integrate through render/backend controller code rather than - direct dependence on standalone internals. - -## Success Criteria - -- [x] The change artifacts define a standalone public model centered on `instance`, `device`, - `program`, and `chain` responsibilities. -- [x] The planned C API is first-class, uses the `goggles_fc_*` prefix, and does not require a - compatibility layer. -- [x] The resulting specs make host/library ownership, Vulkan boundaries, logging sinks, and asset - sourcing explicit and testable. -- [x] Goggles is positioned as a consumer of the standalone package, not as the source of its public - interface assumptions. -- [x] The final intended shipped state is unambiguous: no legacy public surface, no deprecated or - dual public contract, no stale runtime/package assumptions, and no stale docs/examples left - behind. diff --git a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-assets-package/spec.md b/openspec/changes/standalone-filter-chain-api/specs/filter-chain-assets-package/spec.md deleted file mode 100644 index c98593d5..00000000 --- a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-assets-package/spec.md +++ /dev/null @@ -1,74 +0,0 @@ -# Delta for filter-chain-assets-package - -## MODIFIED Requirements - -### Requirement: Library-Owned Asset Package - -The standalone filter-chain project SHALL own built-in internal shaders and runtime assets required -for normal operation. Those built-in assets SHALL be resolved through library-owned packaging and -runtime lookup rules rather than through a required public `shader_dir` input from consumers. - -The final package contract MUST remove stale asset-era migration residue. Installed metadata, -consumer docs/examples, and runtime configuration guidance MUST NOT preserve deprecated `shader_dir` -style assumptions, compatibility toggles, or alternate instructions that imply library-owned built- -ins still depend on source-tree-relative or install-only public paths. - -#### Scenario: Runtime uses built-in assets without external asset directory input - -- GIVEN a host links the standalone library and loads a supported preset -- WHEN the runtime needs built-in internal shader or auxiliary asset content -- THEN it SHALL resolve that content through library-owned packaging -- AND the host SHALL NOT be required to provide a public `shader_dir` configuration value - -### Requirement: Asset Resolution Is Package-Oriented - -Package-oriented asset resolution SHALL support both file-based and memory-based preset entry paths. -When a preset is loaded from a file, asset resolution MUST use documented package-oriented rules -instead of Goggles-relative paths. When a preset is loaded from memory, the runtime MUST still be -able to resolve any library-owned built-in assets needed for execution. - -For file-backed preset sources, relative includes and external texture references MUST resolve from -the parent directory of the supplied preset path. For memory-backed preset sources, relative -includes and external texture references MUST resolve through the registered resolver callback or an -explicit host-supplied base path. If neither is provided, the runtime MUST reject relative external -references deterministically instead of consulting process working-directory state. - -#### Scenario: Memory-loaded preset still resolves built-in runtime assets - -- GIVEN a host loads a preset from an in-memory byte buffer -- WHEN the runtime evaluates preset dependencies needed for execution -- THEN library-owned built-in assets SHALL remain resolvable through the standalone package contract -- AND successful execution SHALL NOT depend on the host providing a source-tree-relative asset path - -### Requirement: Assets Support Public-Surface Validation - -The standalone asset package SHALL continue to support installed public-surface validation, while the -minimum normal-operation asset contract for embedded consumers SHALL remain library-owned and -self-contained. Installed validation SHALL prove that both file-source and memory-source preset flows -work without Goggles-owned assets. - -Installed validation and package-facing guidance MUST describe the final steady-state contract, not -temporary migration behavior. Obsolete examples, stale package notes, and validation targets that -depend on removed public asset assumptions MUST be deleted or rewritten before the change is -considered complete. - -#### Scenario: Installed validation covers both preset source forms - -- GIVEN maintainers validate the installed standalone package -- WHEN contract checks exercise preset loading behavior -- THEN validation SHALL cover at least one file-based preset source and one memory-based preset source -- AND neither flow SHALL require Goggles-owned assets or Goggles repository-relative lookup rules - -#### Scenario: Package-facing asset guidance contains no stale assumptions - -- GIVEN a maintainer reviews installed package metadata and consumer guidance for asset handling -- WHEN the standalone asset redesign is complete -- THEN all package-facing guidance SHALL describe only library-owned built-ins plus the documented file/memory source rules -- AND no stale example, release note, or exported config text SHALL instruct consumers to configure removed public asset-directory inputs - -## REMOVED Requirements - -### Requirement: Test Fixture Resolution Through Compile Definition - -(Reason: Public asset behavior should be defined in terms of package-visible runtime and validation -contracts, not by a specific internal compile-definition mechanism.) diff --git a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-c-api/spec.md b/openspec/changes/standalone-filter-chain-api/specs/filter-chain-c-api/spec.md deleted file mode 100644 index 860629b2..00000000 --- a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-c-api/spec.md +++ /dev/null @@ -1,289 +0,0 @@ -# Delta for filter-chain-c-api - -## MODIFIED Requirements - -### Requirement: Public Header and Export Surface - -The standalone filter-chain C API MUST be defined by a library-owned installed public surface and -MUST use the `goggles_fc_` prefix for public types, constants, and functions. The public surface -MUST represent the standalone library contract rather than Goggles integration details, and it MUST -NOT require deprecated aliases or compatibility entry points for prior `goggles_chain_*` or `fc_*` -names in v1. - -The final shipped public C surface MUST contain only the clean-slate contract. Installed headers, -exported symbols, package metadata, public examples, and public-facing usage notes MUST NOT retain -legacy names, deprecated annotations for removed APIs, or side-by-side documentation for both the -old and new contracts. - -#### Scenario: Host compiles against the standalone public header - -- GIVEN an external host integrates the installed standalone package -- WHEN it includes the public C header and resolves exported symbols -- THEN every required public declaration SHALL use the `goggles_fc_` prefix -- AND the host SHALL NOT need legacy `goggles_chain_*` or `fc_*` declarations to consume v1 - -#### Scenario: Installed package does not ship dual public C surfaces - -- GIVEN a maintainer inspects the installed standalone package and its package-facing examples/docs -- WHEN the clean-slate redesign is complete -- THEN the shipped surface SHALL expose only `goggles_fc_*` public declarations and examples -- AND it SHALL NOT ship deprecated legacy C headers, compatibility aliases, or stale usage text for removed names - -### Requirement: Version and Capability Negotiation - -The v1 standalone contract MUST report itself as Vulkan-only and MUST use version and capability -queries to describe optional behavior without implying non-Vulkan backend support. Capability -negotiation MUST allow a host to determine whether file-based preset loading, memory-based preset -loading, logging callbacks, and other optional extensions are available without probing symbol -presence dynamically. - -The required v1 query surface MUST include explicit `goggles_fc_get_api_version(...)`, -`goggles_fc_get_abi_version(...)`, and `goggles_fc_get_capabilities(...)` families. Those queries -MUST be callable without creating instance, device, program, or chain objects, and the capability -report MUST explicitly indicate that v1 is Vulkan-only while enumerating any optional source-loading -or diagnostics extensions. - -#### Scenario: Host verifies backend scope before creating objects - -- GIVEN a host queries the standalone library before creating runtime objects -- WHEN it inspects the reported API version, ABI version, and capabilities -- THEN it SHALL be able to determine that v1 supports Vulkan integration -- AND it SHALL NOT infer support for non-Vulkan backends unless a future capability explicitly adds them - -### Requirement: Runtime Lifecycle and State Semantics - -The standalone C API MUST expose a first-class object model centered on `instance`, `device`, -`program`, and `chain` responsibilities. Lifecycle rules MUST make ownership, dependency ordering, -and valid call sequencing explicit for FFI consumers, and each object family MUST expose creation -and destruction semantics that are independent of Goggles runtime types. - -The following matrix defines the normative minimum v1 API families and their lifecycle intent: - -| Function family | Owning object | v1 disposition | -|---|---|---| -| `goggles_fc_get_api_version`, `goggles_fc_get_abi_version`, `goggles_fc_get_capabilities` | library | reshaped from implicit/partial version reporting into explicit process-global queries | -| `goggles_fc_instance_create`, `goggles_fc_instance_destroy` | `instance` | reshaped from implicit global/session state into explicit instance ownership | -| `goggles_fc_instance_set_log_callback` | `instance` | renamed and reshaped from shared/global logger wiring into per-instance callback registration | -| `goggles_fc_device_create_vk`, `goggles_fc_device_destroy` | `device` | reshaped from monolithic runtime creation into explicit Vulkan device binding | -| `goggles_fc_program_create`, `goggles_fc_program_destroy` | `program` | reshaped from chain-local preset loading into immutable program creation | -| `goggles_fc_program_get_source_info`, `goggles_fc_program_get_report` | `program` | new first-class program provenance and diagnostics queries | -| `goggles_fc_chain_create`, `goggles_fc_chain_destroy` | `chain` | reshaped from monolithic runtime lifetime into executable chain lifetime | -| `goggles_fc_chain_bind_program`, `goggles_fc_chain_clear` | `chain` | bind is new explicit program attachment; clear preserves runtime reset intent but moves under chain ownership | -| `goggles_fc_chain_record_vk` | `chain` | preserved in purpose, renamed to `goggles_fc_` and narrowed to Vulkan-only v1; accepts `scale_mode` as a `GOGGLES_FC_SCALE_MODE_*` constant | -| `goggles_fc_chain_set_prechain_resolution` | `chain` | dedicated runtime update path for prechain resolution after chain creation | -| `goggles_fc_chain_resize`, `goggles_fc_chain_retarget` | `chain` | preserved in behavior, renamed and split into explicit resize/retarget families | -| `goggles_fc_chain_get_control_count`, `goggles_fc_chain_get_control_info`, `goggles_fc_chain_find_control_index`, `goggles_fc_chain_set_control_*` | `chain` | preserved in purpose, reshaped into explicit metadata/query and index-based or semantic-name-based set families so callers do not depend on transient control ordering | -| `goggles_fc_chain_get_report`, `goggles_fc_chain_get_last_error` | `chain` | reshaped from ad hoc diagnostics into explicit chain-scoped report/error queries | -| `goggles_fc_status_string`, `goggles_fc_is_success`, `goggles_fc_is_error` | library | new error/status helper families for FFI-friendly status inspection | - -No v1 requirement SHALL depend on deprecated `goggles_chain_*` or `fc_*` entry points, and any -legacy monolithic runtime family SHALL be treated as removed rather than carried forward as a public -compatibility layer. - -Control lookup and mutation MUST remain correct across runtime rebuilds, program rebinds, and other -successful replacements that can change control enumeration order. The standalone API therefore MUST -provide a semantic lookup path keyed by control stage plus control name, and hosts SHOULD prefer that -semantic path over caching enumeration indexes across reload boundaries. - -The v1 C API scale mode constants SHALL expose the full standalone record-time surface: -`GOGGLES_FC_SCALE_MODE_STRETCH=0`, `GOGGLES_FC_SCALE_MODE_FIT=1`, -`GOGGLES_FC_SCALE_MODE_INTEGER=2`, `GOGGLES_FC_SCALE_MODE_FILL=3`, and -`GOGGLES_FC_SCALE_MODE_DYNAMIC=4`. Runtime translation from C API values to internal values SHALL -use explicit mapping rather than raw casts so the standalone constants remain authoritative even when -internal enum ordering differs. - -#### Scenario: Host creates the standalone object graph explicitly - -- GIVEN a host wants to prepare a filter runtime through the public C API -- WHEN it follows the documented lifecycle -- THEN it SHALL create and manage standalone object families with explicit boundaries for instance, - device, program, and chain roles -- AND it SHALL NOT need Goggles-private objects or implicit global state to do so - -#### Scenario: Host selects fill or dynamic scaling through the public C contract - -- GIVEN a host records work through `goggles_fc_chain_record_vk(...)` -- WHEN it sets `record_info.scale_mode` to `GOGGLES_FC_SCALE_MODE_FILL` or - `GOGGLES_FC_SCALE_MODE_DYNAMIC` -- THEN the call SHALL be valid within the standalone public contract -- AND the runtime SHALL apply the requested public scale mode without requiring Goggles-private enum translation at the callsite - -#### Scenario: Host updates prechain resolution without rebuilding create info - -- GIVEN a live standalone chain already exists -- WHEN the host calls `goggles_fc_chain_set_prechain_resolution(...)` with a positive extent -- THEN the runtime SHALL update the chain's prechain resolution through that dedicated API -- AND the host SHALL NOT need to recreate the chain solely to change prechain resolution - -### Requirement: Vulkan Create Input Validation - -Vulkan-facing creation APIs MUST accept host-provided Vulkan handles and queue selection as borrowed -inputs at the host/library boundary, while the library MUST own internal compilation state, caches, -pipeline objects, setup/upload command helpers, and intermediate runtime resources derived from that -context. The public creation contract MUST make borrowed-versus-owned Vulkan resources explicit. - -#### Scenario: Host provides device and queue handles - -- GIVEN a host has already selected Vulkan instance, physical device, logical device, and queue ownership -- WHEN it creates standalone filter-chain device or chain objects -- THEN the host SHALL provide the required Vulkan handles through the public creation contract -- AND the library SHALL retain ownership only of its own derived runtime resources rather than host submission objects - -### Requirement: Error Model and Diagnostics Contract - -All fallible APIs MUST return standalone status codes and MUST NOT require exceptions. Logging and -diagnostic delivery MUST be host-routable through callback or sink registration contracts that are -scoped to standalone objects or sessions, and the library MUST NOT require shared/global logger -symbols as part of the public contract. - -The public contract MUST distinguish unstructured log delivery from structured diagnostic/report -queries. Logging callbacks MUST carry human-readable severity/domain/message data for immediate host -consumption, while report-query families MUST expose structured machine-readable status for program -and chain inspection without requiring the host to parse free-form log text. - -#### Scenario: Host routes diagnostics into its own logging system - -- GIVEN a host embeds the standalone library inside an application with its own logging stack -- WHEN it registers a logging callback or sink through the public API -- THEN standalone diagnostics SHALL be delivered through that host-provided route -- AND linking the library SHALL NOT require exporting or sharing global logger state between host and library - -### Requirement: UTF-8 Path and Length-Based API Contract - -The standalone C API MUST support preset loading from both file and memory sources. File-based -entry points MUST continue to define UTF-8 and explicit-length behavior for path input, and memory- -based entry points MUST accept caller-provided preset bytes without requiring a filesystem path or a -public shader directory contract. - -All public structs that expose textual identifiers, paths, diagnostics, or debug names MUST use the -same canonical representation: a UTF-8 byte pointer plus an explicit byte length, with NUL -termination optional and not semantically required. When a public struct accepts a UTF-8 path, the -path bytes MUST represent the host's intended filesystem path exactly as supplied, and the library -MUST treat the byte-length field rather than sentinel termination as authoritative. - -Preset provenance and relative resolution MUST follow these rules: - -- File-backed preset sources MUST record file provenance and derive the default base path from the - parent directory of the provided preset path. -- Memory-backed preset sources MUST record memory provenance and MAY additionally supply a UTF-8 - `source_name` for diagnostics plus either a UTF-8 `base_path` or import/resolver callbacks. -- Relative preset includes and external texture references from a file-backed source MUST resolve - relative to that source's derived base path. -- Relative preset includes and external texture references from a memory-backed source MUST resolve - through the host-provided resolver when one is registered; otherwise they MUST resolve relative to - the explicit memory-source `base_path` when present. -- If a memory-backed source references relative external content and neither a resolver nor a - `base_path` is provided, program creation MUST fail with a deterministic validation status rather - than probing process working-directory state. - -### Requirement: Callback and Returned-String Lifetime Contract - -The standalone C API MUST define explicit lifetime and ownership rules for all callback-delivered -data and for strings or buffers returned by API functions, so that FFI consumers in any language can -manage memory correctly without relying on C-specific conventions. - -#### Callback-delivered data - -- Pointers passed into a host-provided callback (such as `goggles_fc_log_callback_t`) are borrowed - by the host for the duration of that callback invocation only. The library owns the backing memory, - and the host MUST NOT retain, free, or write through those pointers after the callback returns. - If the host needs the data beyond the callback scope it MUST copy the content before returning. - -- For import callbacks (`goggles_fc_import_read_fn_t`), the host (callee) allocates and returns a - buffer through `out_bytes` / `out_byte_count`. The library borrows that buffer and MUST release it - by calling the paired `goggles_fc_import_free_fn_t` with the same pointer and size once it has - finished reading. The host MUST NOT free the buffer itself; it MUST wait for the library's - `free_fn` call. - -#### Strings returned by query functions - -- The string returned by `goggles_fc_status_string(...)` is a pointer to a library-owned static - string literal. It remains valid for the lifetime of the process and MUST NOT be freed by the - caller. - -- UTF-8 views written into caller-provided output structs by query functions (for example the `name` - and `description` fields in `goggles_fc_control_info_t`, or the `source_name` and `source_path` - fields in `goggles_fc_program_source_info_t`) point into library-owned internal storage. These - pointers remain valid only as long as the owning object (program, chain, etc.) is alive and has not - undergone a state change that invalidates the queried data (such as `goggles_fc_chain_bind_program` - replacing the bound program). Callers that need the strings beyond that scope MUST copy them. - -#### Thread safety of callbacks - -- The library MAY invoke a logging callback from any thread that performs library work. The host - callback implementation MUST be safe to call concurrently from multiple threads if the host - performs multi-threaded recording or object creation on the same instance. - -- Import callbacks (`read_fn`, `free_fn`) are invoked synchronously on the calling thread during - `goggles_fc_program_create`. The library SHALL NOT invoke import callbacks from background threads. - -#### Scenario: Host copies log message in callback for deferred processing - -- GIVEN a host registers a `goggles_fc_log_callback_t` with the standalone instance -- WHEN the library invokes the callback with a `goggles_fc_log_message_t` pointer -- THEN the host SHALL treat the message and its embedded UTF-8 views as borrowed for the callback - duration only -- AND the host SHALL copy any data it needs to retain before returning from the callback - -#### Scenario: Library frees import buffer through paired free callback - -- GIVEN a host provides `goggles_fc_import_read_fn_t` and `goggles_fc_import_free_fn_t` callbacks -- WHEN the library calls `read_fn` and receives a host-allocated buffer -- THEN the library SHALL release that buffer by calling `free_fn` with the original pointer and size -- AND the host SHALL NOT free the buffer independently - -#### Scenario: Host loads a preset from memory only - -- GIVEN a host has preset content in memory and no preset file on disk -- WHEN it calls the memory-based preset loading entry point -- THEN the standalone library SHALL initialize the preset from the provided byte buffer -- AND the host SHALL NOT need to materialize a temporary file or configure a public shader directory - -#### Scenario: Memory source omits resolver and base path for relative imports - -- GIVEN a host provides a memory-backed preset source whose content references a relative include or texture -- WHEN it does not provide import callbacks and does not provide an explicit memory-source base path -- THEN program creation SHALL fail with a deterministic validation status -- AND the runtime SHALL NOT attempt fallback resolution through the process working directory or Goggles-private paths - -### Requirement: Ownership, Handle Provenance, and Pointer Retention - -The public contract MUST make host/library ownership boundaries explicit. The library MUST NOT -retain pointers to caller-owned transient input buffers beyond the documented call lifetime, and it -MUST document which handles remain borrowed from the host versus which resources become library-owned -after object creation. - -#### Scenario: Host frees transient preset input after load request - -- GIVEN a host passes file-path bytes or preset-memory bytes into a load call -- WHEN the call returns successfully or with validation failure -- THEN the host SHALL be free to release or mutate its transient input buffers according to the documented call lifetime -- AND the library SHALL preserve memory safety without depending on continued caller ownership of those transient buffers - -### Requirement: Installed C ABI Consumer Contract - -The installed standalone C ABI MUST be consumable without Goggles-private headers, repository- -relative assets, or Goggles-defined public abstractions. Built-in internal shaders and related -library-owned assets required for normal runtime behavior MUST be packaged as library-owned runtime -content rather than exposed as a required public `shader_dir` input. - -#### Scenario: Installed consumer runs without configuring a shader directory - -- GIVEN an external C consumer links the installed standalone package -- WHEN it creates the runtime and loads a supported preset -- THEN the runtime SHALL resolve required built-in internal shader or asset content through library-owned packaging -- AND the consumer SHALL NOT need to supply a public `shader_dir` path merely to enable normal operation - -#### Scenario: Installed package ships no stale runtime assumptions - -- GIVEN a maintainer reviews the installed public contract and package metadata -- WHEN it verifies the final v1 standalone package -- THEN no exported config, documented environment, or public example SHALL require removed install-only runtime inputs such as `shader_dir` -- AND built-in runtime behavior SHALL be described entirely in terms of library-owned assets plus the documented file/memory source contracts - -## REMOVED Requirements - -### Requirement: Compatibility and Evolution Policy for v1.x - -(Reason: This change is a clean-slate standalone redesign where backward compatibility with prior -`goggles_chain_*` naming and handle families is explicitly out of scope for v1 planning.) diff --git a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-cpp-wrapper/spec.md b/openspec/changes/standalone-filter-chain-api/specs/filter-chain-cpp-wrapper/spec.md deleted file mode 100644 index 89c813ef..00000000 --- a/openspec/changes/standalone-filter-chain-api/specs/filter-chain-cpp-wrapper/spec.md +++ /dev/null @@ -1,123 +0,0 @@ -# Delta for filter-chain-cpp-wrapper - -## MODIFIED Requirements - -### Requirement: C++20 Wrapper Header for Filter Chain - -The standalone project SHALL provide a thin C++20 consumer wrapper over the standalone C API rather -than a Goggles-specific integration header. The wrapper SHALL mirror the standalone object model and -installed package surface, and it SHALL remain consumable without depending on Goggles-private -headers or backend-only types. - -The completed wrapper surface MUST NOT leave a second public C++ contract behind. Deprecated wrapper -headers, compatibility namespaces, and package-facing examples for removed wrapper shapes MUST be -deleted or rewritten so the installed package documents only the thin wrapper over the `goggles_fc_*` -C API. - -#### Scenario: External C++ consumer includes the wrapper - -- GIVEN an external C++ consumer integrates the installed standalone package -- WHEN it includes the wrapper header -- THEN the wrapper SHALL present a standalone consumer-facing API over the C layer -- AND the include path SHALL NOT require Goggles backend or application headers - -#### Scenario: Installed package ships only the final wrapper contract - -- GIVEN a maintainer inspects installed wrapper headers and package-facing examples -- WHEN the standalone redesign is complete -- THEN only the `goggles::filter_chain` wrapper layout SHALL be documented and shipped for normal consumer use -- AND legacy wrapper headers, compatibility aliases, or stale examples for removed contracts SHALL NOT remain public - -### Requirement: RAII Ownership for Chain Handle - -RAII ownership in the C++ wrapper MUST align with the standalone object families introduced by the C -API. Wrapper-owned objects for instance, device, program, and chain roles MUST destroy the -underlying C handles exactly once and MUST preserve explicit ownership boundaries between host- -provided Vulkan context and library-owned runtime objects. - -#### Scenario: Wrapper releases standalone objects in dependency order - -- GIVEN a C++ consumer owns wrapper objects created from the standalone package -- WHEN those wrapper objects leave scope -- THEN each underlying standalone C handle SHALL be destroyed exactly once in a valid dependency order -- AND host-owned Vulkan handles SHALL remain under host ownership rather than being implicitly destroyed by the wrapper - -### Requirement: Strongly Typed C++ Runtime Surface - -The C++ wrapper SHALL expose typed operations for the standalone public model, including program and -chain lifecycle, preset loading from file and memory sources, and dedicated chain update helpers for -operations that remain distinct in the C ABI such as prechain-resolution changes. Normal wrapper -usage SHALL NOT require callers to manage legacy Goggles naming, deprecated compatibility flows, or -public `shader_dir` configuration. - -The wrapper's `goggles::filter_chain::ScaleMode` enum SHALL mirror the C API `GOGGLES_FC_SCALE_MODE_*` -constant values exactly (`stretch=0`, `fit=1`, `integer=2`, `fill=3`, `dynamic=4`). This wrapper -enum SHALL define the standalone public scale-mode contract directly, and wrapper consumers SHALL be -able to select every public C API scale mode without falling back to raw integer constants. - -#### Scenario: Wrapper loads a preset from memory - -- GIVEN a C++ consumer has preset bytes in memory -- WHEN it invokes the wrapper's preset-loading API -- THEN the wrapper SHALL expose a typed memory-source load path over the standalone C API -- AND the callsite SHALL NOT need filesystem-only temporary paths or a public shader directory parameter - -#### Scenario: Wrapper updates prechain resolution through a dedicated helper - -- GIVEN a C++ consumer already owns a live wrapper `Chain` -- WHEN it updates prechain resolution after creation -- THEN the wrapper SHALL expose a dedicated `set_prechain_resolution(...)` operation over the standalone C API -- AND the callsite SHALL NOT need to overload resize or recreate the chain to express that change - -#### Scenario: Wrapper selects fill or dynamic scaling without raw constants - -- GIVEN a C++ consumer prepares record-time inputs for the standalone wrapper contract -- WHEN it chooses `goggles::filter_chain::ScaleMode::fill` or `goggles::filter_chain::ScaleMode::dynamic` -- THEN those enum values SHALL be valid public wrapper choices -- AND their numeric values SHALL match the corresponding `GOGGLES_FC_SCALE_MODE_*` constants exactly - -### Requirement: Result-Based Error Propagation - -Every fallible wrapper operation SHALL continue to use project-style result objects for expected -failures, but diagnostics emitted by the standalone library SHALL remain host-routable rather than -being coupled to Goggles logging internals. - -#### Scenario: Wrapper consumer forwards standalone diagnostics - -- GIVEN a wrapper consumer registers a host logging callback or sink -- WHEN an expected runtime failure occurs in the standalone library -- THEN the wrapper SHALL return a failed result to the caller -- AND library diagnostics SHALL remain routable through the configured host-facing callback path - -### Requirement: Boundary Isolation - -The C++ wrapper SHALL act as a consumer convenience layer over the standalone C ABI and SHALL NOT -define the public contract independently of that C surface. Goggles-specific adapters MAY consume the -wrapper, but the wrapper's semantics MUST remain valid for non-Goggles consumers. - -#### Scenario: Goggles consumes the same wrapper contract as other hosts - -- GIVEN Goggles integrates the standalone library through an adapter layer -- WHEN it uses the C++ wrapper -- THEN Goggles SHALL consume the same standalone wrapper contract available to other hosts -- AND no Goggles-only behavior SHALL be required to make the wrapper API meaningful - -### Requirement: Wrapper Surface Has No Legacy Compatibility Layer - -The final standalone C++ wrapper contract MUST be singular and clean-slate. The shipped wrapper MUST -NOT preserve deprecated adapter helpers, namespace aliases, or old header layouts merely to ease -migration from pre-redesign public surfaces. - -#### Scenario: Consumer cannot accidentally adopt removed wrapper names - -- GIVEN a downstream C++ consumer follows the installed wrapper docs and examples -- WHEN it integrates the completed standalone package -- THEN it SHALL encounter only the final wrapper names and header layout -- AND it SHALL NOT be taught or encouraged to use removed wrapper contracts through compatibility helpers or deprecation notices - -## REMOVED Requirements - -### Requirement: C ABI Continuity During Migration - -(Reason: The standalone redesign is intentionally clean-slate, so preserving the prior `goggles_chain_*` -C ABI behavior is not a v1 requirement for this change.) diff --git a/openspec/changes/standalone-filter-chain-api/specs/goggles-filter-chain/spec.md b/openspec/changes/standalone-filter-chain-api/specs/goggles-filter-chain/spec.md deleted file mode 100644 index f85260f9..00000000 --- a/openspec/changes/standalone-filter-chain-api/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,117 +0,0 @@ -# Delta for goggles-filter-chain - -## MODIFIED Requirements - -### Requirement: Complete Filter Runtime Ownership Boundary - -The standalone filter library SHALL own preset parsing, shader compilation/reflection, internal -caches, stage pipelines, setup/upload command helpers, intermediate images and buffers, and -library-owned built-in assets needed for normal runtime behavior. Goggles SHALL remain responsible -only for host integration concerns outside that boundary. - -#### Scenario: Goggles uses the standalone runtime as a consumer - -- GIVEN Goggles integrates the standalone filter library into its render pipeline -- WHEN it creates and uses filter runtime objects -- THEN the standalone library SHALL own reusable filter runtime internals and built-in assets -- AND Goggles SHALL consume those services without becoming the source of the library's public abstractions - -### Requirement: Host Backend Responsibility Boundary - -The host backend SHALL remain responsible for Vulkan instance and device selection, queue ownership, -submission, presentation, swapchain lifecycle, external image import, synchronization, and per-record -source/target bindings. The standalone library SHALL consume those host-provided inputs through -explicit contracts and SHALL NOT assume implicit control over host submission flow. - -The completed Goggles integration MUST NOT retain stale bypass paths or compatibility assumptions -from the pre-redesign contract. In particular, Goggles MUST NOT remain a hidden contract owner for -legacy public types, old asset-directory expectations, or alternate direct-internal integration -paths once the standalone consumer cutover is complete. - -#### Scenario: Goggles records with explicit host/library separation - -- GIVEN Goggles has already prepared the Vulkan command buffer and host-owned source/target resources -- WHEN it invokes standalone chain recording for a frame -- THEN Goggles SHALL provide the borrowed per-record inputs required by the public contract -- AND the standalone library SHALL record only its library-owned rendering work without taking over submission or presentation - -### Requirement: Goggles Integration Leaves No Legacy Contract Path - -Goggles SHALL consume the standalone library only through the final standalone boundary or approved -thin wrapper layered over it. When the migration is complete, Goggles-facing docs, adapters, tests, -and build wiring MUST NOT preserve alternate public stories based on removed `goggles_chain_*` -contracts or install-only asset assumptions. - -The `FILTER_CHAIN_ENABLE_LEGACY_API` CMake gating mechanism, all legacy `goggles_chain_*` C API -files, the legacy `goggles_filter_chain.hpp` C++ wrapper, and the internal -`goggles_chain_legacy.h` header have been removed. The Goggles application consumes the standalone -library through `FilterChainController`, which owns the C++ RAII wrapper object graph over the -`goggles_fc_*` C ABI. No Goggles-side source code references `FilterChainRuntime`, `shader_dir`, -`goggles_chain_*`, or standalone internals. - -#### Scenario: Goggles cutover removes alternate contract ownership - -- GIVEN the standalone redesign has fully landed -- WHEN maintainers inspect Goggles integration points and package-facing references -- THEN Goggles SHALL appear only as a consumer of the standalone contract -- AND no Goggles-owned docs, tests, or exported integration surface SHALL preserve the removed public contract - -### Requirement: Boundary-safe VulkanContext Contract Placement - -Boundary-owned Vulkan context contracts SHALL reflect the standalone object model and SHALL expose -only the host-provided information required to create standalone instance, device, program, or chain -objects. Goggles integration headers SHALL consume those standalone-owned contracts rather than -defining alternative public types. - -#### Scenario: Goggles controller integration includes standalone-owned boundary contracts - -- GIVEN Goggles implements controller-side consumer integration with the standalone library -- WHEN controller headers describe Vulkan initialization inputs -- THEN those headers SHALL consume standalone-owned contract types and naming -- AND they SHALL NOT redefine the public host/library boundary around Goggles-private abstractions - -### Requirement: Error Model and Diagnostics Contract - -Goggles integration SHALL route standalone diagnostics through host-facing callback or sink -registration rather than through shared/global logger symbols. Consumer integration code MAY -translate or enrich messages at subsystem boundaries, but it SHALL preserve the standalone library's host-routable -diagnostic model. - -#### Scenario: Goggles forwards standalone diagnostics into app logging - -- GIVEN Goggles configures a standalone logging callback or sink during integration -- WHEN the standalone library emits diagnostics during preset load or frame recording -- THEN Goggles SHALL receive those diagnostics through the documented callback path -- AND the integration SHALL NOT require shared global logging state between Goggles and the library - -### Requirement: Library-Owned Support Boundary - -The standalone library's public and internal support contracts SHALL be defined independently of -Goggles-private support code. Goggles-specific consumer integration MUST treat the library as an -external consumer-facing package and SHALL NOT rely on special-case access to define or reinterpret -the public contract. - -The installed standalone package MAY ship shared infrastructure headers (such as -`FilterControlDescriptor`, `VulkanContext`, and the `goggles::ScaleMode` enum) alongside the -`goggles_fc_*` C API surface, provided those headers are self-contained (no Goggles `util/` or -application dependencies). These shared types serve as standalone library-owned contracts consumed by -both the library and any host (including Goggles). The standalone library's public `goggles::ScaleMode` -enum defines five values — `fit`, `fill`, `stretch`, `integer`, and `dynamic` — which correspond -one-to-one with the C API constants `GOGGLES_FC_SCALE_MODE_FIT` through -`GOGGLES_FC_SCALE_MODE_DYNAMIC`. All five scale modes are part of the standalone library's public -surface and are available to any consumer. - -#### Scenario: Goggles consumer integration does not redefine the standalone API contract - -- GIVEN Goggles and another downstream host both consume the standalone package -- WHEN their integration layers are compared -- THEN both SHALL rely on the same standalone public contract for object ownership and lifecycle -- AND Goggles-specific controller integration SHALL NOT add hidden contract assumptions unavailable to other consumers - -## REMOVED Requirements - -### Requirement: In-Repo Subdirectory Bridge During Extraction - -(Reason: This change's public contract centers on Goggles as a consumer rather than on a -transitional in-repo integration mechanism. Build-bridge details belong in design or packaging work, -not in the host boundary spec.) diff --git a/openspec/changes/standalone-filter-chain-api/tasks.md b/openspec/changes/standalone-filter-chain-api/tasks.md deleted file mode 100644 index 6c592e37..00000000 --- a/openspec/changes/standalone-filter-chain-api/tasks.md +++ /dev/null @@ -1,68 +0,0 @@ -# Tasks: Standalone Filter-Chain API - -## Phase 1: Contract Baseline and Build Strategy - -- [x] 1.1 Resolve the installed validation fixture strategy from `openspec/changes/standalone-filter-chain-api/design.md` by choosing either packaged `share/goggles-filter-chain/testdata` fixtures or generated test-harness inputs, and record the expected consumers, install layout, and test dependencies before any consumer-test or packaging work depends on it. -- [x] 1.2 Replace `filter-chain/include/goggles_filter_chain.h` with the new `goggles_fc_*` public header skeleton for opaque `instance`/`device`/`program`/`chain` handles, API/ABI version queries, capability/status enums and `GOGGLES_FC_*` macros, status helper functions, and the clean-slate Vulkan-only v1 naming surface. -- [x] 1.3 Define the normative public input/output POD contracts in `filter-chain/include/goggles_filter_chain.h`: UTF-8 view structs, Vulkan device-create and record structs, preset source descriptors, import-callback types/contracts, target-info structs, control metadata structs, program/chain report structs, and last-error/report query payloads. -- [x] 1.4 Introduce the new wrapper header layout with `filter-chain/include/goggles/filter_chain.hpp` as the canonical C++ entrypoint plus `filter-chain/include/goggles/filter_chain/common.hpp` support types in `namespace goggles::filter_chain`, while treating `filter-chain/include/goggles_filter_chain.hpp` as temporary migration scaffolding that must be removed before the change is complete. -- [x] 1.5 Create `filter-chain/src/api/abi_validation.hpp` and `filter-chain/src/api/c_api.cpp` scaffolding for struct-size/version validation, borrowed-handle checks, status-code mapping, exported symbol organization, and clean separation between ABI entry points and runtime implementation. -- [x] 1.6 Reorganize `filter-chain/CMakeLists.txt` so the new `src/api/` and `src/runtime/` sources build in parallel with the migration, and stage the public install/export cutover behind the replacement path instead of deleting the legacy public build/install path up front. - -## Phase 2: Embedded Assets and Runtime Foundations - -- [x] 2.1 Update `filter-chain/src/support/logging.hpp` and `filter-chain/src/support/logging.cpp` to remove shared/global logger ownership and expose only internal log-router helpers used by the new runtime instance layer. -- [x] 2.2 Create `filter-chain/src/runtime/instance.hpp` and `filter-chain/src/runtime/instance.cpp` to own immutable library policy, replaceable log callback registration, borrowed callback/user-data lifetime tracking, and synchronous log delivery on the calling thread. -- [x] 2.3 Create `filter-chain/src/runtime/device.hpp` and `filter-chain/src/runtime/device.cpp` to bind borrowed `VkPhysicalDevice`/`VkDevice`/`VkQueue` inputs while owning device-scoped caches, setup/upload command resources, and teardown ordering. -- [x] 2.4 Create `filter-chain/src/runtime/source_resolver.hpp` and `filter-chain/src/runtime/source_resolver.cpp`, then update `filter-chain/src/chain/preset_parser.hpp` and `filter-chain/src/chain/preset_parser.cpp`, to support file-backed and memory-backed preset sources, provenance tracking, import callbacks, explicit base paths, and deterministic rejection of relative external references with no resolver/base path. -- [x] 2.5 Add the embedded-asset build pipeline for built-in shaders/runtime assets: choose the canonical source manifest, add any required generation step, produce compiled-in asset tables/sources, and wire those outputs into the `filter-chain` target so normal runtime behavior no longer depends on a public `shader_dir`. -- [x] 2.6 Create the runtime-facing embedded asset registry implementation (for example in `filter-chain/src/runtime/embedded_assets.cpp`) and hook preset/shader loading to library-owned asset IDs rather than repository-relative paths. -- [x] 2.7 Update install/package rules and `filter-chain/cmake/GogglesFilterChainConfig.cmake.in` so the installed package exports headers, targets, private dependency discovery, embedded-asset support, and any chosen installed testdata layout without reintroducing public asset-dir assumptions. -- [x] 2.8 Remove stale build/install/export wiring and outdated package metadata fields, cache variables, compile definitions, and generated config content that preserve removed names, dual surfaces, or install-only runtime assumptions. - > **Deviation (partial)**: Implemented as build gating behind `FILTER_CHAIN_ENABLE_LEGACY_API` CMake option rather than full removal. The new `goggles_fc_*` API is scaffolding-only (most methods return NOT_SUPPORTED), so the legacy `FilterChainRuntime` remains the only functional runtime path and cannot be removed yet. Full removal of legacy wiring is deferred to post-Phase 4 when the replacement API is functional. See Task 6.9 for final legacy removal. - -## Phase 3: Program and Chain Runtime Model - -- [x] 3.1 Create `filter-chain/src/runtime/program.hpp` and `filter-chain/src/runtime/program.cpp` to own immutable parsed preset state, source provenance, compiled shader/reflection artifacts, and structured program source/report query data. -- [x] 3.2 Create `filter-chain/src/runtime/chain.hpp` and `filter-chain/src/runtime/chain.cpp`, then refactor `filter-chain/src/chain/chain_runtime.hpp` and `filter-chain/src/chain/chain_runtime.cpp`, so executable chain state, clear/resize/retarget/record behavior, control metadata, and last-error/report tracking live behind the new `device` + `program` object graph. - > **Note**: New Chain/Program objects are implemented alongside the existing ChainRuntime rather than replacing it. ChainRuntime remains the functional backend for the legacy `goggles_chain_*` API. Full removal happens in Phase 6 (task 6.9) when the legacy API is cut. -- [x] 3.3 Implement device-affine program ownership rules in the runtime layer so one program can be shared across multiple chains created from the same device, while attachment or reuse across different devices is rejected deterministically. -- [x] 3.4 Ensure runtime diagnostics/report plumbing stays separate from log text so program and chain report structs remain queryable without requiring hosts to parse callback output. - -## Phase 4: C API Surface Implementation - -- [x] 4.1 Implement process-global query helpers in `filter-chain/src/api/c_api.cpp`: `goggles_fc_get_api_version`, `goggles_fc_get_abi_version`, `goggles_fc_get_capabilities`, `goggles_fc_status_string`, `goggles_fc_is_success`, and `goggles_fc_is_error`, matching the new status/capability enums and macro definitions. -- [x] 4.2 Implement `goggles_fc_instance_*` entry points with null-safe destroy behavior, callback replacement semantics, borrowed callback lifetime rules, and synchronous callback delivery behavior matching the public contract. -- [x] 4.3 Implement `goggles_fc_device_*` entry points and Vulkan create validation, keeping borrowed-vs-owned Vulkan handle semantics explicit and rejecting invalid dependency ordering. -- [x] 4.4 Implement `goggles_fc_program_*` entry points for file and memory preset creation, source provenance queries, import-callback handling, and structured program report queries. -- [x] 4.5 Implement `goggles_fc_chain_*` entry points for create/bind/clear/resize/retarget/record, target-info handling, control metadata queries and mutation, structured chain report queries, and last-error reporting. -- [x] 4.6 Remove legacy public `goggles_chain_*`, `fc_*`, `FilterChainRuntime`, and Goggles-private public contract types from the installed `filter-chain/include/` surface only after the replacement C ABI builds, links, and passes its direct contract coverage. - > **Deviation (partial)**: Changed `FILTER_CHAIN_ENABLE_LEGACY_API` default from `ON` to `OFF`. Legacy C++ wrapper header (`goggles_filter_chain.hpp`) is now excluded from the default installed surface. However, `goggles/filter_chain/` shared types (FilterControlDescriptor, VulkanContext, ScaleMode, Result, Error) remain installed because the Goggles application depends on them directly. These types move behind the Goggles render/backend consumer boundary in Phase 5. The build script explicitly passes `FILTER_CHAIN_ENABLE_LEGACY_API=ON` to keep the Goggles application build working until Phase 5 migrates it. -- [x] 4.7 Remove compatibility aliases, deprecated notes, and stale exported symbol/documentation references for removed public APIs so the C contract describes only `goggles_fc_*` families. - > **Note**: The public C header (`goggles_filter_chain.h`) already contained only `goggles_fc_*` declarations. The legacy C API declarations live in the internal-only `src/chain/goggles_chain_legacy.h` (not installed). Package config, new C++ wrapper headers, and all installed headers reference only `goggles_fc_*` families. - -## Phase 5: C++ Wrapper and Goggles Adapter Migration - -- [x] 5.1 Implement the thin RAII wrapper in `filter-chain/src/api/cpp_wrapper.cpp`, `filter-chain/include/goggles/filter_chain.hpp`, and `filter-chain/include/goggles/filter_chain/common.hpp` so `instance`, `device`, `program`, and `chain` wrapper types destroy C handles exactly once and keep fallible operations result-based. -- [x] 5.2 Update the wrapper's typed source-loading, report-query, control-query, and logging-registration helpers so it remains a consumer convenience layer over the C ABI rather than defining alternate contract semantics. -- [x] 5.3 Move Goggles render/backend integration onto standalone `goggles_fc_*` instance/device/program/chain calls in `src/render/backend/filter_chain_controller.hpp` and `src/render/backend/filter_chain_controller.cpp`, including host log forwarding, file-vs-memory preset loading, reload flow, and host-owned Vulkan boundary enforcement. -- [x] 5.4 Update `src/render/backend/filter_chain_controller.hpp` and `src/render/backend/filter_chain_controller.cpp` to replace direct `FilterChainRuntime` ownership with controller-owned program/chain handles while preserving reload, retarget, controls, and diagnostics behavior through the standalone boundary. -- [x] 5.5 Update `src/render/backend/vulkan_backend.cpp` and any affected backend build wiring so Goggles constructs the controller-side standalone consumer integration from host-owned Vulkan device/queue state, keeps submission/presentation on the Goggles side, and does not expose `filter-chain` internals outside the controller boundary. -- [x] 5.6 Remove Goggles-side bypass paths, compatibility helpers, and stale consumer-boundary assumptions that still treat `filter-chain` as owning install-only asset directories or legacy public runtime contracts. - -## Phase 6: Contract Tests, Installed Consumers, and Cutover - -- [x] 6.1 Rewrite `filter-chain/tests/contract/test_filter_chain_c_api_contracts.cpp` around `goggles_fc_*` lifecycle, version/capability queries, status helpers, Vulkan validation paths, file-backed preset loading, memory-backed preset loading, and the normative public struct families introduced in the new header. -- [x] 6.2 Add focused contract coverage for import-callback types/contracts, source provenance, report structs, control metadata structs, target-info structs, and deterministic rejection when memory-backed relative imports lack both callbacks and `base_path`. -- [x] 6.3 Add explicit logging contract tests covering callback replacement semantics and synchronous delivery expectations on the thread that emits the event. -- [x] 6.4 Add explicit runtime/API tests proving one program can be shared across multiple chains on the same device and is rejected when a different device attempts to reuse or bind it. -- [x] 6.5 Add or update focused runtime-facing tests in `filter-chain/tests/contract/` for embedded built-in asset lookup and execution, including both normal embedded-asset behavior and any installed fixture/testdata path chosen in task 1.1. -- [x] 6.6 Add an explicit installed C-consumer validation target that builds and runs against the installed package using only the public C header, validates at least one file-source flow and one memory-source flow, and avoids Goggles-private headers or public `shader_dir` inputs. -- [x] 6.7 Update `filter-chain/tests/consumer/static/main.cpp` and `filter-chain/tests/consumer/shared/main.cpp` to validate the installed C++ wrapper headers, new namespace, and standalone object model without Goggles-private headers or public `shader_dir` configuration. -- [x] 6.8 Add controller-focused regression coverage under `tests/render/` for Goggles-side integration through `filter_chain_controller.*`, proving Goggles remains a consumer of the standalone boundary instead of a contract owner. -- [x] 6.9 After the replacement C ABI, wrapper, packaging, and installed consumer paths all work end-to-end, remove `filter-chain/include/goggles_filter_chain.hpp` and any remaining legacy public build/install wiring so only the new standalone surface ships. - > **Note**: This task completes the deferred portion of Task 2.8. The `FILTER_CHAIN_ENABLE_LEGACY_API` CMake gating introduced in 2.8 should be removed here along with all legacy wiring it guards. -- [x] 6.10 Remove obsolete tests, examples, docs, release/install notes, and package-facing snippets that reference removed API names, removed wrapper layouts, compatibility shims, or public `shader_dir`/legacy asset assumptions. -- [x] 6.11 Add final cutover validation that the install tree, exported package metadata, public headers, examples, and consumer tests expose no deprecated API names, no compatibility aliases/shims, no dual public surfaces, and no stale runtime/package assumptions. -- [x] 6.12 If implementation reveals contract drift, update the delta specs under `openspec/changes/standalone-filter-chain-api/specs/` in the same change so the C API, C++ wrapper, Goggles boundary, and asset-package behavior remain aligned with the shipped code. -- [x] 6.13 Verify the completed change with `pixi run build -p test`, `pixi run test -p test`, and `pixi run ci --lane build-test`; also run the installed consumer and packaging validation targets through the existing preset-driven CMake/CTest flow before handing off to verification. diff --git a/openspec/config.yaml b/openspec/config.yaml deleted file mode 100644 index 906305dd..00000000 --- a/openspec/config.yaml +++ /dev/null @@ -1,45 +0,0 @@ -schema: spec-driven - -context: | - Goggles is a Linux C++20 Vulkan viewer and compositor system for Wayland-native frame capture. - Flow: target app inside nested compositor -> DMA-BUF + explicit sync -> viewer - (VulkanBackend -> FilterChain -> SDL3 window). - Optional input forwarding runs through the same nested wlroots/XWayland compositor. - - Core modules: src/compositor, src/render, src/app, src/util, src/ui. - - Authoritative policy: docs/project_policies.md. - In any conflict, docs/project_policies.md takes precedence. - Narrower-scope rules override broader rules in that scope. - Exceptions must be explicit and include reason, scope, and rollback/remediation plan. - - Non-negotiables: - - Fallible operations use tl::expected; expected runtime failures do not use exceptions. - - Errors are handled or propagated; no silent failure; avoid duplicate cascading logs. - - App Vulkan code uses vk::. - - Check vk::Result explicitly; no static_cast(...) on result-returning Vulkan calls. - - App code does not use raw new/delete; owned fds use goggles::util::UniqueFd. - - Build/test uses Pixi tasks and CMake/CTest presets (no ad-hoc non-preset build dirs). - - Render/pipeline concurrency uses goggles::util::JobSystem; no std::thread/std::jthread there. - - OpenSpec style: - - Use RFC-style normative words (MUST/SHOULD/MAY). - - Requirements are behavior-focused, observable, and testable. - - Use GIVEN/WHEN/THEN scenarios. - -rules: - proposal: - - MUST include Problem, Scope, Non-goals, Risks, and Validation Plan. - - MUST identify impacted modules/files and impacted OpenSpec specs. - - MUST explain why, not only implementation steps. - - MUST call out policy-sensitive impacts (error handling, logging, threading, Vulkan API split, lifetime/ownership) when touched. - tasks: - - MUST break work into small verifiable steps mapped to concrete files/subsystems. - - MUST include verification commands using Pixi/CMake/CTest presets. - - MUST include spec-update tasks when behavior/contracts change. - - MUST NOT include unrelated broad refactors unless explicitly in scope. - spec: - - MUST use explicit Requirement blocks with normative language. - - MUST include at least one GIVEN/WHEN/THEN scenario per requirement. - - MUST keep statements behavior-oriented and testable. - - MUST update Purpose from placeholder/TBD when editing a spec. diff --git a/openspec/specs/app-window/spec.md b/openspec/specs/app-window/spec.md deleted file mode 100644 index 1271c982..00000000 --- a/openspec/specs/app-window/spec.md +++ /dev/null @@ -1,266 +0,0 @@ -# app-window Specification - -## Purpose -TBD - created by archiving change add-sdl3-window-test. Update Purpose after archive. -## Requirements -### Requirement: SDL3 Window Creation -The application SHALL create an SDL3 window with Vulkan support enabled on startup **unless `--headless` is active**, in which case SDL SHALL NOT be initialized and no window SHALL be created. - -#### Scenario: Window creation success -- **GIVEN** SDL3 is properly initialized -- **WHEN** the application starts without `--headless` -- **THEN** a window titled "Goggles" SHALL be created -- **AND** the window SHALL have the Vulkan flag set - -#### Scenario: SDL3 initialization failure -- **GIVEN** SDL3 cannot be initialized -- **WHEN** the application starts without `--headless` -- **THEN** an error SHALL be logged -- **AND** the application SHALL exit with a non-zero code - -#### Scenario: Headless mode skips SDL entirely -- **GIVEN** the application is launched with `--headless` -- **WHEN** initialization runs -- **THEN** `SDL_Init` SHALL NOT be called -- **AND** no SDL window SHALL be created - -### Requirement: Window Event Loop -The application SHALL run an event loop that processes window events until the user closes the window. - -#### Scenario: Window close event -- **GIVEN** the window is open -- **WHEN** the user closes the window (X button or Alt+F4) -- **THEN** the event loop SHALL exit -- **AND** SDL3 resources SHALL be cleaned up - -### Requirement: Command Line Interface -The application SHALL support command-line arguments to override default behavior and provide information without throwing exceptions into the main execution flow. - -#### Scenario: Display help -- **WHEN** the application is run with `--help` -- **THEN** it SHALL print usage information -- **AND** it SHALL exit with code 0 - -#### Scenario: Display version -- **WHEN** the application is run with `--version` -- **THEN** it SHALL print "Goggles v0.1.0" -- **AND** it SHALL exit with code 0 - -#### Scenario: Exception encapsulation -- **GIVEN** the application uses CLI11 for parsing -- **WHEN** `parse_cli` is called -- **THEN** it MUST catch all library exceptions internally -- **AND** it MUST return a `nonstd::expected` value-based result - -#### Scenario: Override shader preset -- **GIVEN** a valid `.slangp` file exists -- **WHEN** run with `--shader ` -- **THEN** the specified preset SHALL be loaded regardless of config file settings - -### Requirement: SDL Resource Ownership via RAII - -The application SHALL manage SDL initialization and the SDL window lifetime via RAII wrappers within the app module to ensure SDL resources are cleaned up on all exit paths, including early returns due to initialization failures. - -#### Scenario: Window creation failure cleanup -- **GIVEN** SDL3 initializes successfully -- **WHEN** window creation fails -- **THEN** an error SHALL be logged -- **AND** SDL3 resources SHALL be cleaned up before exit - -### Requirement: Orchestrated Event Loop Boundary - -The application SHALL encapsulate window event handling and per-frame orchestration behind a dedicated component (e.g., `goggles::app::Application`), keeping `src/app/main.cpp` limited to composition and top-level error handling. - -#### Scenario: Quit event exits orchestration -- **GIVEN** the window is open -- **WHEN** the user closes the window (X button or Alt+F4) -- **THEN** the orchestrator SHALL stop the event loop -- **AND** SDL3 resources SHALL be cleaned up - -### Requirement: Child Process Death Signal - -The application SHALL configure spawned child processes to receive SIGTERM when the parent process terminates unexpectedly. - -#### Scenario: Parent crash terminates child - -- **GIVEN** a child process was spawned via `-- ` mode -- **WHEN** the parent goggles process is killed (SIGKILL, crash, or abnormal termination) -- **THEN** the child process SHALL receive SIGTERM automatically -- **AND** the child process SHALL terminate - -#### Scenario: Parent PID 1 reparenting race - -- **GIVEN** a child process is being spawned -- **WHEN** the parent dies between `fork()` and `prctl()` setup -- **THEN** the child SHALL detect reparenting to PID 1 -- **AND** SHALL exit immediately with failure code - -### Requirement: Target FPS CLI Override - -The application SHALL allow overriding the effective global pacing target FPS from the command line. - -#### Scenario: Override target fps via CLI -- **GIVEN** the application is started with `--target-fps 120` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `120` -- **AND** the override SHALL take precedence over the config file -- **AND** the effective global pacing target for the current session SHALL be `120` - -#### Scenario: Disable frame cap via CLI -- **GIVEN** the application is started with `--target-fps 0` -- **WHEN** configuration is loaded -- **THEN** `config.render.target_fps` SHALL be set to `0` -- **AND** the effective global pacing target SHALL be uncapped - -### Requirement: GPU Device Selection - -The application SHALL allow users to select a specific GPU and SHALL improve automatic GPU -selection for multi-GPU systems. - -#### Scenario: Explicit GPU selection by index - -- **GIVEN** multiple GPUs are available -- **WHEN** the user runs with `--gpu 0` -- **THEN** the application SHALL use the GPU at index 0 - -#### Scenario: Explicit GPU selection by name - -- **GIVEN** multiple GPUs are available including one with "AMD" in its name -- **WHEN** the user runs with `--gpu AMD` -- **THEN** the application SHALL use the GPU whose name contains "AMD" - -#### Scenario: Invalid GPU selector - -- **GIVEN** no GPU matches the selector -- **WHEN** the user runs with `--gpu nonexistent` -- **THEN** the application SHALL exit with an error message listing available GPUs - -#### Scenario: Ambiguous GPU selector - -- **GIVEN** multiple suitable GPUs match the name selector -- **WHEN** the user runs with a non-unique selector like `--gpu AMD` -- **THEN** the application SHALL exit with an error listing matching GPUs -- **AND** it SHALL instruct the user to choose a numeric index - -#### Scenario: Default GPU selection - -- **GIVEN** multiple GPUs are available and no `--gpu` option is specified -- **WHEN** the application selects a GPU -- **THEN** it SHALL prefer a GPU that supports presenting to the current surface -- **AND** it SHALL log all available GPUs with their indices - -### Requirement: Headless Mode CLI Flags - -The application SHALL accept `--headless`, `--frames `, and `--output ` as top-level CLI flags. `--frames` and `--output` are required when `--headless` is present; providing either without the other SHALL produce an error. - -#### Scenario: Valid headless invocation -- **WHEN** run with `--headless --frames 10 --output /tmp/frame.png -- ./app` -- **THEN** `CliOptions.headless` SHALL be `true`, `frames` SHALL be `10`, `output_path` SHALL be `/tmp/frame.png` - -#### Scenario: --frames without --output -- **WHEN** run with `--headless --frames 10 -- ./app` and `--output` is absent -- **THEN** the application SHALL print a descriptive error and exit with a non-zero code - -### Requirement: Filter Chain Control Scope and Precedence -The application UI SHALL expose three filter-related controls with distinct scope and precedence: -- `Application -> Window Management -> Filter Chain (All Surfaces)` controls global prechain/effect enablement. -- `Application -> Window Management -> Surface List` controls per-surface prechain/effect enablement. -- `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader` controls effect stage only. - -The application SHALL resolve an effective runtime policy that applies precedence as: -1) global toggle, -2) per-surface toggle, -3) effect-stage toggle. - -The application SHALL dispatch the resolved policy through a single runtime update path so prechain -and effect stage updates occur together. - -For first-time surface discovery, the application SHALL use deterministic defaulting rules: -- In direct Vulkan capture sessions, newly discovered active Vulkan-target surfaces default to - filter-chain enabled. -- Once the user toggles a surface, the user choice SHALL be preserved and SHALL NOT be overwritten - by subsequent auto-default evaluation. - -When a direct Vulkan capture session initializes prechain defaults and no explicit prechain target -is configured, the application SHALL initialize prechain target from viewer swapchain extent. - -#### Scenario: Global toggle disables all surfaces -- **GIVEN** the Window Management panel is visible -- **WHEN** the user disables `Filter Chain (All Surfaces)` -- **THEN** subsequent frames SHALL bypass prechain and effect stages for all surfaces - -#### Scenario: Per-surface toggle applies when global is enabled -- **GIVEN** `Filter Chain (All Surfaces)` is enabled -- **WHEN** the user disables a surface entry in Surface List -- **THEN** subsequent frames for that surface SHALL bypass prechain and effect stages -- **AND** other enabled surfaces SHALL continue using prechain/effect - -#### Scenario: Effect toggle does not disable prechain -- **GIVEN** global and per-surface toggles are enabled -- **WHEN** the user disables `Enable Shader` -- **THEN** subsequent frames SHALL bypass effect stage only -- **AND** prechain behavior SHALL remain controlled by global/per-surface toggles - -#### Scenario: Runtime updates are applied atomically -- **GIVEN** any toggle transition changes effective stage policy -- **WHEN** the application dispatches runtime state to the backend -- **THEN** prechain and effect stage updates SHALL be applied together in one policy update - -#### Scenario: First discovery defaults ON for direct Vulkan sessions -- **GIVEN** a direct Vulkan capture session and a newly discovered active surface -- **WHEN** the surface appears in Surface List without user override -- **THEN** its per-surface filter toggle SHALL default to enabled - -#### Scenario: User override remains authoritative -- **GIVEN** a surface has been manually toggled by the user -- **WHEN** the surface list is refreshed or source timing changes -- **THEN** the surface toggle SHALL keep the user-selected value - -### Requirement: Application Performance Panel Reports Gamer-Facing Metrics - -The Application performance panel SHALL report `Game FPS` and `Compositor Latency` instead of the -legacy `Render` and `Source` FPS metrics. - -The panel SHALL display compositor-provided `Game FPS` and `Compositor Latency` values. - -#### Scenario: Performance panel shows replacement metrics -- **WHEN** the Application performance panel is rendered -- **THEN** it SHALL display `Game FPS` and `Compositor Latency` -- **AND** it SHALL NOT display `Render` FPS or `Source` FPS - -#### Scenario: Legacy performance plots are removed -- **WHEN** the Application performance panel is rendered after this change -- **THEN** it SHALL NOT render the legacy frame-history plots associated with `Render` and `Source` - FPS - -#### Scenario: Game FPS follows active captured game surface only -- **GIVEN** a game surface is the current capture target -- **WHEN** the performance panel reports `Game FPS` -- **THEN** the reported value SHALL come from the compositor-provided metric snapshot for that - capture target only - -### Requirement: Application Window Runtime Frame Pacing Controls - -The Application window SHALL expose runtime controls for the effective global pacing target used by -the current Goggles session. - -The controls SHALL initialize from the resolved `render.target_fps` value and SHALL update the active -session pacing target without requiring restart. - -#### Scenario: Runtime controls reflect startup pacing target -- **GIVEN** the application window is rendered after config and CLI resolution -- **WHEN** the runtime pacing controls are shown -- **THEN** they SHALL reflect the current effective `render.target_fps` value for the session - -#### Scenario: Runtime controls update active pacing target -- **GIVEN** the Application window runtime pacing controls are visible -- **WHEN** the user selects a new non-zero target FPS -- **THEN** the effective global pacing target SHALL update for the current session -- **AND** the compositor and viewer pacing paths SHALL observe the same updated target - -#### Scenario: Runtime controls allow uncapped mode -- **GIVEN** the Application window runtime pacing controls are visible -- **WHEN** the user selects uncapped pacing -- **THEN** the effective global pacing target SHALL become `0` -- **AND** the session SHALL switch to uncapped pacing without restart - diff --git a/openspec/specs/build-system/spec.md b/openspec/specs/build-system/spec.md deleted file mode 100644 index d54aef6a..00000000 --- a/openspec/specs/build-system/spec.md +++ /dev/null @@ -1,326 +0,0 @@ -# build-system Specification - -## Purpose -TBD - created by archiving change add-version-management. Update Purpose after archive. -## Requirements -### Requirement: Single Source of Truth for Version - -The build system SHALL maintain project version in a single authoritative location that automatically propagates to all code via compile definitions. - -#### Scenario: Version defined in CMake project directive - -- **GIVEN** the root `CMakeLists.txt` file -- **WHEN** the `project()` directive is invoked -- **THEN** the VERSION parameter SHALL specify the project version in `MAJOR.MINOR.PATCH` format -- **AND** this SHALL be the only location where version is manually maintained - -#### Scenario: CMake version variables available after project directive - -- **GIVEN** `project(goggles VERSION 0.1.0)` has been invoked -- **WHEN** CMake configuration proceeds -- **THEN** variables `PROJECT_VERSION`, `PROJECT_VERSION_MAJOR`, `PROJECT_VERSION_MINOR`, `PROJECT_VERSION_PATCH` SHALL be defined -- **AND** variable `PROJECT_NAME` SHALL contain the project name - -### Requirement: Version Component Compile Definitions - -The build system SHALL define preprocessor macros for individual version components. - -#### Scenario: Major minor patch macros defined - -- **GIVEN** project version is `0.1.0` -- **WHEN** CMake processes compile definitions -- **THEN** `GOGGLES_VERSION_MAJOR` SHALL be defined as `0` -- **AND** `GOGGLES_VERSION_MINOR` SHALL be defined as `1` -- **AND** `GOGGLES_VERSION_PATCH` SHALL be defined as `0` -- **AND** these SHALL be defined via `add_compile_definitions()` - -#### Scenario: Vulkan version compatibility - -- **GIVEN** `GOGGLES_VERSION_MAJOR`, `GOGGLES_VERSION_MINOR`, `GOGGLES_VERSION_PATCH` macros are defined -- **WHEN** Vulkan application info is populated -- **THEN** `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` SHALL compile without errors -- **AND** SHALL produce correct Vulkan version encoding - -### Requirement: No Hardcoded Version Values - -Source code SHALL NOT contain hardcoded version numbers independent of CMake project version. - -#### Scenario: Vulkan backend uses version macros - -- **GIVEN** `VulkanBackend::create_instance()` sets Vulkan application info -- **WHEN** `applicationVersion` and `engineVersion` are assigned -- **THEN** they SHALL use `VK_MAKE_VERSION(GOGGLES_VERSION_MAJOR, GOGGLES_VERSION_MINOR, GOGGLES_VERSION_PATCH)` -- **AND** SHALL NOT use hardcoded values like `VK_MAKE_VERSION(0, 1, 0)` - -#### Scenario: No hardcoded version strings in source - -- **GIVEN** the version management system is implemented -- **WHEN** searching source files with `rg "0\.1\.0" src/` -- **THEN** no hardcoded version strings SHALL be found in source files -- **AND** all version references SHALL use compile definition macros - -### Requirement: Version Change Propagation - -Changes to the project version SHALL automatically propagate to all code without manual updates. - -#### Scenario: Version update workflow - -- **GIVEN** project version is `0.1.0` and code uses `GOGGLES_VERSION_*` macros -- **WHEN** `project(goggles VERSION 0.2.0)` is modified in `CMakeLists.txt` -- **AND** rebuild is performed -- **THEN** all macros SHALL reflect version `0.2.0` after recompilation -- **AND** no manual code changes SHALL be required -- **AND** Vulkan application info SHALL show version `0.2.0` - -### Requirement: Toolchain Version Pinning - -The build system SHALL pin all development tool versions in pixi.toml to prevent system tool leakage and ensure reproducible builds. - -#### Scenario: Clang toolchain version consistency -- **WHEN** building with pixi -- **THEN** clang, clang++, lld, and clang-tools SHALL use the same major version (21.x) - -#### Scenario: Build tool version pinning -- **WHEN** pixi.toml specifies cmake, ninja, ccache -- **THEN** each tool SHALL have a pinned version constraint (not `*`) - -#### Scenario: Format tool version consistency -- **WHEN** running format tasks in default or lint environment -- **THEN** taplo version SHALL be identical across environments - -### Requirement: Visual test targets build unconditionally -The build system SHALL build all visual test clients and the image comparison library as part of the default build, since all dependencies (wayland-client, wayland-protocols, stb_image, Catch2) are already project requirements. - -#### Scenario: Default build includes visual targets -- **GIVEN** a clean CMake configuration using any preset -- **WHEN** the build completes -- **THEN** all test client binaries (`solid_color_client`, `gradient_client`, `quadrant_client`, `multi_surface_client`) SHALL be built -- **AND** `goggles_image_compare` CLI binary SHALL be built -- **AND** the `image_compare` static library SHALL be built -- **AND** `test_image_compare` Catch2 test binary SHALL be built - -### Requirement: CTest label taxonomy -The build system SHALL register test targets under a consistent label taxonomy using `set_tests_properties(... LABELS ...)`. - -#### Scenario: Unit label unchanged -- **WHEN** `ctest -L unit` is run with any preset -- **THEN** existing Catch2 unit tests and `image_compare_unit_tests` SHALL run -- **AND** no integration tests SHALL be included - -#### Scenario: Integration label includes headless smoke -- **WHEN** `ctest -L integration` is run with any preset -- **THEN** the headless pipeline smoke test (`headless_smoke`, `headless_smoke_png_check`) SHALL be included -- **AND** existing integration tests (e.g., `auto_input_forwarding`, when available) SHALL also be included - -#### Scenario: Visual label for visual regression tests -- **WHEN** `ctest -L visual` is run with any preset -- **THEN** only visual regression test targets (Phase 2+) SHALL run -- **AND** unit and integration tests SHALL NOT be included unless also labeled `visual` - -### Requirement: Deterministic Semgrep Tooling - -The build system SHALL provide a Pixi-managed Semgrep toolchain and checked-in rule source so local and CI static analysis use the same deterministic inputs. - -#### Scenario: Pixi provides Semgrep for local and CI execution -- **GIVEN** the repository defines lint and developer workflow tooling in `pixi.toml` -- **WHEN** contributors or CI invoke the Semgrep entrypoint -- **THEN** the Semgrep binary SHALL come from the repository-managed Pixi environment -- **AND** the same Semgrep version surface SHALL be used locally and in CI - -#### Scenario: Semgrep provenance is observable during verification -- **GIVEN** the repository verifies the Semgrep tool surface before enforcing the gate -- **WHEN** maintainers inspect the Semgrep path and version under the repository-managed workflow -- **THEN** the resolved Semgrep executable SHALL originate from the Pixi-managed environment -- **AND** the reported version SHALL match the locked local and CI Semgrep surface - -#### Scenario: Pixi source-of-truth files stay synchronized -- **GIVEN** the repository adds Semgrep to the Pixi-managed tool surface -- **WHEN** the change updates Semgrep dependency configuration -- **THEN** `pixi.toml` SHALL declare the dependency version surface -- **AND** `pixi.lock` SHALL be updated in sync with that change - -#### Scenario: Dependency admission remains reviewable -- **GIVEN** the repository adds Semgrep as a new dependency for the static-analysis workflow -- **WHEN** the proposal and apply artifacts are reviewed -- **THEN** they SHALL include dependency rationale, license compatibility review, maintenance assessment, and team agreement evidence -- **AND** the dependency SHALL NOT be treated as implicitly admitted just because the tool is easy to install - -#### Scenario: Initial scan roots stay limited to repository-managed C and C++ code -- **GIVEN** the repository enables Semgrep policy checks -- **WHEN** the `pixi run semgrep` entrypoint runs in its initial configuration -- **THEN** it SHALL scan repository-managed code under `src/` and `tests/` -- **AND** it SHALL use narrower path filters for rules that apply only to selected subsystems - -#### Scenario: Semgrep rule sources are checked into the repository -- **GIVEN** the repository enables Semgrep policy checks -- **WHEN** the Semgrep entrypoint runs -- **THEN** it SHALL load configuration and rules from checked-in repository files -- **AND** it SHALL NOT depend on registry defaults or hosted rule configuration - -#### Scenario: Subsystem-sensitive rules stay path-scoped -- **GIVEN** some policy bans only apply to selected Goggles subsystems -- **WHEN** the repository defines Semgrep rules for Vulkan API split or render-path threading -- **THEN** those rules SHALL scope to the directories where the policy applies -- **AND** they SHALL exclude directories with explicit policy exceptions such as `src/capture/vk_layer/` - -### Requirement: CMake-First Standalone Filter Project Workflow - -The build system SHALL define the extracted filter runtime as a CMake-first standalone project with -repository layout rooted at `include/`, `src/`, `tests/`, `assets/`, and `cmake/`. The documented -configure, build, test, install, and export workflow SHALL be runnable from a clean checkout without -requiring Goggles-specific wrappers. - -#### Scenario: Clean checkout uses standalone CMake entry points -- **GIVEN** a clean checkout of the extracted filter-chain project -- **WHEN** a maintainer follows the documented standalone workflow -- **THEN** configure, build, test, and install steps SHALL execute through project-owned CMake entry points -- **AND** the workflow SHALL NOT require Pixi task wrappers, Goggles preset files, or Conda-specific environment assumptions - -#### Scenario: Separate consumer validates exported package -- **GIVEN** the standalone project has been installed to a prefix -- **WHEN** a separate CMake consumer project resolves that install tree -- **THEN** package discovery, target resolution, and public-header inclusion SHALL succeed through the exported package contract -- **AND** validation SHALL occur without adding the library sources back into the consumer source tree - -#### Scenario: Package config template generates valid export - -- **GIVEN** the standalone project is installed to a prefix -- **WHEN** a downstream consumer calls `find_package(GogglesFilterChain CONFIG REQUIRED)` -- **THEN** the generated config SHALL define `GogglesFilterChain::goggles-filter-chain` as an - imported target -- **AND** the config SHALL resolve transitive PUBLIC dependencies before defining the target - -### Requirement: Goggles In-Tree Subdirectory Primary Path - -Goggles SHALL consume the in-repo filter runtime through `add_subdirectory(filter-chain)` as the -primary and default integration path. The standalone build and `find_package(...)` path SHALL remain -available for validating the exported package contract (installed consumer tests) but SHALL NOT be -required for normal Goggles builds. - -#### Scenario: Goggles normal build uses subdirectory inclusion -- **GIVEN** the filter-chain source tree is present under `filter-chain/` in the Goggles repo -- **WHEN** Goggles is configured with any build preset -- **THEN** the build SHALL include the filter runtime via `add_subdirectory(filter-chain)` -- **AND** no separate pre-build, install, or `CMAKE_PREFIX_PATH` step SHALL be required - -#### Scenario: Installed package validation remains available -- **GIVEN** the standalone filter-chain project can be configured, built, and installed independently -- **WHEN** maintainers run installed-consumer validation (e.g. `validate-installed-consumers.sh`) -- **THEN** the validation script SHALL perform its own standalone build and install -- **AND** the exported `find_package(GogglesFilterChain CONFIG REQUIRED)` contract SHALL continue to work for downstream consumers outside the Goggles repo - -### Requirement: Paired Static and Shared Package Outputs - -The standalone build and export workflow SHALL publish supported `STATIC` and `SHARED` library -outputs for the filter runtime. The exported package contract SHALL validate both output forms and -SHALL NOT require a `MODULE` target. - -#### Scenario: Package exports static and shared variants -- **GIVEN** the standalone project is built for distribution -- **WHEN** install and export artifacts are inspected -- **THEN** supported package artifacts SHALL include both `STATIC` and `SHARED` library outputs -- **AND** consumers SHALL not be required to build or load a `MODULE` target to use the package - -#### Scenario: Downstream validation covers both supported output forms -- **GIVEN** downstream consumer validation is run against the installed standalone package -- **WHEN** maintainers verify supported linkage modes -- **THEN** the validation evidence SHALL cover consumption of both `STATIC` and `SHARED` outputs -- **AND** success criteria SHALL not treat a `MODULE` build as an acceptable substitute for either supported output form - -### Requirement: Transitional Preset Cleanup After Package Validation - -The build system SHALL remove the transitional `.shared` and `test-shared` CMake presets from -`CMakePresets.json` once the standalone project's package export proves both STATIC and SHARED -consumption paths. These presets SHALL NOT remain in the preset file after successful package -validation. - -#### Scenario: Transitional presets removed after both linkage modes proven - -- **GIVEN** the standalone project has been installed and downstream consumer validation has - proven both STATIC and SHARED linkage through `find_package(GogglesFilterChain)` -- **WHEN** maintainers inspect `CMakePresets.json` -- **THEN** the `.shared` and `test-shared` presets SHALL have been removed -- **AND** existing named presets (`debug`, `release`, `asan`, `quality`, `test`) SHALL remain unchanged - -#### Scenario: Presets not removed prematurely - -- **GIVEN** the standalone project has NOT yet completed consumer validation for both linkage modes -- **WHEN** maintainers inspect `CMakePresets.json` -- **THEN** the `.shared` and `test-shared` presets SHALL still be present -- **AND** those presets SHALL continue to function for transitional verification - -### Requirement: Standalone Dependency Discovery Module - -The standalone project SHALL provide a `cmake/FilterChainDependencies.cmake` module that -encapsulates third-party dependency discovery for all required external libraries. The module -SHALL use standard `find_package()` calls and SHALL NOT require Goggles-specific Find modules -or Pixi/Conda-specific path assumptions. - -#### Scenario: Dependency module discovers all required third-party libraries - -- **GIVEN** the standalone project is configured from a clean checkout with dependencies available - through standard CMake package discovery -- **WHEN** `cmake/FilterChainDependencies.cmake` is included during configuration -- **THEN** the module SHALL resolve Vulkan, expected-lite, spdlog, slang, stb_image, and Catch2 - through standard `find_package()` calls -- **AND** configuration SHALL NOT require Goggles-owned CMake Find modules - -#### Scenario: Dependency module is reusable by exported package config - -- **GIVEN** the standalone project has been installed and a downstream consumer resolves the package -- **WHEN** the exported `GogglesFilterChainConfig.cmake` is loaded by the consumer -- **THEN** the package config SHALL invoke dependency discovery for transitive PUBLIC dependencies - (Vulkan, expected-lite) -- **AND** PRIVATE dependencies (spdlog, slang, stb_image) SHALL NOT leak to the consumer - -### Requirement: Downstream Consumer Validation Projects - -The standalone project SHALL include out-of-tree consumer validation projects that prove the -installed package is discoverable and linkable for both STATIC and SHARED library outputs. These -validation projects SHALL use only `find_package(GogglesFilterChain CONFIG REQUIRED)` and the -installed public surface. - -#### Scenario: Static consumer validation succeeds - -- **GIVEN** the standalone project has been installed to a prefix with STATIC library output -- **WHEN** a consumer validation project configures with `CMAKE_PREFIX_PATH` pointing to the - install prefix -- **THEN** `find_package(GogglesFilterChain CONFIG REQUIRED)` SHALL succeed -- **AND** the consumer SHALL compile and link against the STATIC target without errors - -#### Scenario: Shared consumer validation succeeds - -- **GIVEN** the standalone project has been installed to a prefix with SHARED library output -- **WHEN** a consumer validation project configures with `CMAKE_PREFIX_PATH` pointing to the - install prefix -- **THEN** `find_package(GogglesFilterChain CONFIG REQUIRED)` SHALL succeed -- **AND** the consumer SHALL compile and link against the SHARED target without errors - -#### Scenario: Consumer validation uses only installed surface - -- **GIVEN** a consumer validation project -- **WHEN** its source files are inspected -- **THEN** it SHALL include only headers from the installed package include root -- **AND** it SHALL NOT reference standalone project source-tree paths or Goggles-owned headers - -### Requirement: Host Test Split After Extraction - -After contract tests are moved to the standalone project, the Goggles test suite SHALL retain -only host integration tests that exercise the wiring between Goggles backend subsystems and the -filter-chain boundary. The Goggles test target SHALL NOT duplicate contract tests that are -owned and run by the standalone project. - -#### Scenario: Goggles retains host integration tests - -- **GIVEN** the standalone project owns ~22 contract test files -- **WHEN** the Goggles `tests/CMakeLists.txt` is inspected -- **THEN** it SHALL include host integration tests (e.g., `test_filter_boundary_contracts.cpp`, - `test_vulkan_backend_subsystem_contracts.cpp`, `test_filter_chain_retarget.cpp`) -- **AND** those tests SHALL compile against the installed or subdirectory-provided filter-chain target - -#### Scenario: Moved contract tests are absent from Goggles - -- **GIVEN** ~22 contract test files have been moved to `filter-chain/tests/` -- **WHEN** the Goggles `tests/` directory is inspected -- **THEN** the moved contract test source files SHALL NOT be present in the Goggles test directory -- **AND** the Goggles test CMakeLists SHALL NOT reference the moved source files diff --git a/openspec/specs/ci/spec.md b/openspec/specs/ci/spec.md deleted file mode 100644 index 3efafc99..00000000 --- a/openspec/specs/ci/spec.md +++ /dev/null @@ -1,167 +0,0 @@ -# ci Specification - -## Purpose -Defines the Continuous Integration pipeline behavior for the Goggles project, including automated code formatting, building, testing, and static analysis. -## Requirements -### Requirement: Auto-format Code on Push - -The CI system SHALL automatically format code using the Pixi-managed clang-format and gate subsequent jobs on whether formatting changes were pushed. - -#### Scenario: Code with formatting issues is pushed -- **WHEN** code with clang-format violations is pushed to a branch -- **THEN** CI runs clang-format to fix the issues via Pixi -- **AND** CI commits the formatted code with message \"style: auto-format code\" when the branch is non-fork -- **AND** the format check job succeeds - -#### Scenario: Forked PR formatting without push -- **GIVEN** a pull request originates from a fork -- **WHEN** clang-format produces changes -- **THEN** CI SHALL skip auto-commit/push for safety -- **AND** it SHALL expose `formatted=false` so downstream jobs still run - -#### Scenario: Code is already properly formatted -- **WHEN** code that passes clang-format check is pushed -- **THEN** CI detects no changes needed -- **AND** no commit is created -- **AND** the format check job succeeds - -#### Scenario: All C/C++ file types are formatted -- **WHEN** clang-format is run in CI -- **THEN** files with extensions `.c`, `.h`, `.cpp`, `.hpp` are formatted -- **AND** the same clang-format version from Pixi is used - -### Requirement: Unified Clang-Format via Pixi - -The project SHALL use Pixi-managed formatting tools to ensure consistent formatting across all contributors regardless of their local system toolchain. - -#### Scenario: Formatting uses Pixi-managed tools -- **WHEN** a contributor runs `pixi run format` -- **THEN** C/C++ formatting SHALL use `clang-format` from the Pixi environment -- **AND** TOML formatting SHALL use `taplo` from the Pixi environment - -#### Scenario: Init installs the managed formatting hook -- **WHEN** a contributor runs `pixi run init` -- **THEN** the managed pre-commit hook SHALL be installed or repaired -- **AND** the hook SHALL format staged C/C++ and TOML files via Pixi-managed tools - -### Requirement: Scoped Sanitizer Instrumentation - -The build system SHALL apply sanitizer instrumentation only to first-party Goggles code, excluding all third-party dependencies. - -#### Scenario: First-party target with ASAN enabled -- **WHEN** building with `ENABLE_ASAN=ON` -- **AND** the target is a first-party Goggles library or executable -- **THEN** the target is compiled with `-fsanitize=address -fno-omit-frame-pointer` -- **AND** the target is linked with `-fsanitize=address` - -#### Scenario: Third-party dependency with ASAN enabled -- **WHEN** building with `ENABLE_ASAN=ON` -- **AND** the target is a third-party dependency (CPM or Conan) -- **THEN** the target is NOT compiled with sanitizer flags -- **AND** the target is NOT linked with sanitizer flags - -### Requirement: Runtime Leak Suppressions - -The build system SHALL configure LSAN suppressions via external file rather than compiled-in code, enabling transparent suppression of third-party library leaks while detecting first-party leaks. - -#### Scenario: CTest runs with LSAN suppressions -- **WHEN** running tests via CTest with `ENABLE_ASAN=ON` -- **THEN** `LSAN_OPTIONS` environment variable includes `suppressions=/lsan.supp` -- **AND** leaks originating entirely within suppressed patterns are not reported -- **AND** leaks in first-party code ARE reported even if called via third-party APIs - -### Requirement: Scoped Clang-Tidy Configuration - -The build system SHALL apply clang-tidy static analysis with per-directory configuration, allowing different strictness levels for different code categories. - -#### Scenario: Application code with clang-tidy enabled -- **WHEN** building with `ENABLE_CLANG_TIDY=ON` -- **AND** the source file is outside directories with a local `.clang-tidy` override -- **THEN** the file is analyzed with the root `.clang-tidy` configuration -- **AND** all checks are enforced with `-warnings-as-errors` - -#### Scenario: Generated protocol headers with clang-tidy enabled -- **WHEN** building with `ENABLE_CLANG_TIDY=ON` -- **AND** the source file is in `src/compositor/protocol/` -- **THEN** the file is analyzed with `src/compositor/protocol/.clang-tidy` -- **AND** all clang-tidy checks are disabled for those generated headers - -#### Scenario: C filter-chain API code with clang-tidy enabled -- **WHEN** building with `ENABLE_CLANG_TIDY=ON` -- **AND** the source file is in `src/render/chain/api/c/` -- **THEN** the file inherits the root `.clang-tidy` configuration -- **AND** identifier naming and selected modernization checks are relaxed for the C-facing API surface - -#### Scenario: Root clang-tidy enforces project conventions -- **WHEN** the root `.clang-tidy` is used -- **THEN** private members MUST use `m_` prefix -- **AND** enum constants MUST use `UPPER_SNAKE_CASE` -- **AND** functions MUST use `snake_case` -- **AND** C-style arrays are forbidden in favor of `std::array` - -### Requirement: Repo-Controlled Semgrep Gate - -The CI system SHALL run a repo-controlled Semgrep scan as a blocking step in the static-analysis -workflow. - -The CI system SHALL allow the Semgrep blocking step to run in a dedicated PR-gated job that MAY run -in parallel with other static-analysis checks, provided equivalent required PR coverage is preserved. - -#### Scenario: Static-analysis job runs checked-in Semgrep rules -- **GIVEN** the repository defines a Semgrep configuration and local ruleset -- **WHEN** the static-analysis workflow runs in CI -- **THEN** it SHALL execute Semgrep from repository-checked-in configuration -- **AND** it SHALL fail the job when Semgrep reports a blocking finding - -#### Scenario: Local and CI Semgrep use repository-defined entrypoints -- **GIVEN** the repository exposes repository-owned Semgrep entrypoints for local and CI execution -- **WHEN** contributors run the local command and CI runs the static-analysis job graph -- **THEN** both flows SHALL evaluate the same checked-in ruleset -- **AND** both flows SHALL remain deterministic from repository state - -#### Scenario: Semgrep complements the existing quality gate -- **GIVEN** the static-analysis workflow requires both Semgrep and `pixi run build -p quality` -- **WHEN** Semgrep is executed as part of the static-analysis PR checks -- **THEN** the workflow SHALL retain the existing quality build gate -- **AND** Semgrep SHALL complement rather than replace that gate - -#### Scenario: Semgrep scope stays limited to approved policy bans -- **GIVEN** the repository policy identifies both tool-enforceable and review-only rules -- **WHEN** the CI Semgrep gate evaluates the repository -- **THEN** it SHALL cover only the approved high-signal policy bans selected for Semgrep -- **AND** it SHALL NOT duplicate formatting, naming, include-order, lockfile/preset, or - runtime-validation checks that are owned by other tools - -### Requirement: Static Analysis Coverage Supports Parallel PR Execution - -The CI static-analysis coverage SHALL be partitioned into independently executable Semgrep and -quality-build surfaces that can run as separate required PR checks. - -The repository SHALL preserve a combined static-analysis entrypoint for deterministic local -reproduction while exposing split entrypoints for targeted execution. - -#### Scenario: PR checks run split static-analysis surfaces -- **GIVEN** pull-request CI executes static-analysis coverage -- **WHEN** the workflow schedules static-analysis jobs -- **THEN** it SHALL run Semgrep and quality-build static-analysis surfaces as separate jobs or lanes -- **AND** both surfaces SHALL remain required merge gates - -#### Scenario: Combined local static-analysis entrypoint remains available -- **GIVEN** a contributor runs the canonical local static-analysis command -- **WHEN** the command executes -- **THEN** it SHALL run both Semgrep and quality-build static-analysis surfaces from repository-owned - lane definitions -- **AND** it SHALL preserve deterministic behavior from repository state - -#### Scenario: Split local entrypoints support targeted reproduction -- **GIVEN** a contributor needs to isolate one static-analysis surface -- **WHEN** the contributor invokes the split Semgrep or quality-build lane directly -- **THEN** each lane SHALL execute only its owned static-analysis surface -- **AND** each lane SHALL preserve the same policy and blocking semantics used by CI - -#### Scenario: Static-analysis optimization keeps equivalent PR coverage -- **GIVEN** the static-analysis composition is changed for throughput -- **WHEN** CI protection rules evaluate required checks -- **THEN** PR coverage SHALL remain equivalent to running both Semgrep and quality-build checks -- **AND** required checks SHALL NOT be moved to non-PR triggers to satisfy this requirement - diff --git a/openspec/specs/compositor-capture/spec.md b/openspec/specs/compositor-capture/spec.md deleted file mode 100644 index 43e0a2cd..00000000 --- a/openspec/specs/compositor-capture/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -# compositor-capture Specification - -## Purpose -Defines how Goggles captures compositor-managed client surfaces and exports them for viewer presentation. -## Requirements -### Requirement: Non-Vulkan Surface Presentation -The system SHALL render a selected non-Vulkan client surface (Wayland or XWayland) into the -viewer using the compositor capture path when compositor presentation is available. - -The render pass for compositor-presented frames SHALL composite surfaces in the following order: -1. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `background` layer -2. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `bottom` layer -3. The primary capture surface tree (xdg_toplevel or XWayland surface) -4. XWayland override-redirect popup surfaces belonging to the primary surface -5. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `top` layer -6. Mapped `wlr-layer-shell-unstable-v1` surfaces on the `overlay` layer -7. The compositor software cursor - -#### Scenario: Present selected surface -- **GIVEN** a non-Vulkan client surface is connected to the compositor -- **AND** the surface is selected via the existing surface selector -- **WHEN** the compositor produces a new frame -- **THEN** the viewer presents the selected surface - -#### Scenario: Overlay layer surface composited above game -- **GIVEN** a game surface is the primary capture target -- **AND** a layer surface with layer `overlay` is mapped -- **WHEN** the compositor produces a frame -- **THEN** the overlay layer surface appears above the game surface in the presented frame - -#### Scenario: Background layer surface composited below game -- **GIVEN** a layer surface with layer `background` is mapped -- **WHEN** the compositor produces a frame -- **THEN** the background layer surface appears below the game surface in the presented frame - -#### Scenario: No layer surfaces does not affect existing behavior -- **GIVEN** no layer surfaces are connected -- **WHEN** the compositor produces a frame -- **THEN** the presented frame contains only the primary surface tree and cursor (existing behavior) - -#### Scenario: Presentation unavailable -- **GIVEN** compositor presentation cannot be initialized -- **WHEN** non-Vulkan clients connect for input -- **THEN** input forwarding continues without presenting non-Vulkan frames - -### Requirement: DMA-BUF Export for Compositor Frames -The compositor capture path SHALL export frames using DMA-BUF for zero-copy presentation. - -#### Scenario: Export compositor frame via DMA-BUF -- **WHEN** the compositor renders a frame for the selected surface -- **THEN** it exports a DMA-BUF with width, height, format, stride, and modifier metadata -- **AND** the viewer imports and presents the frame without CPU readback - -### Requirement: Compositor Capture Publishes Gameplay Metrics - -The compositor capture path SHALL publish the timing data required for the Application performance -panel to report `Game FPS` and `Compositor Latency`. - -`Game FPS` SHALL be derived from presents or commits for the currently captured game surface only. -`Compositor Latency` SHALL be derived from the interval between an eligible active-surface commit -and the corresponding compositor capture publication. - -#### Scenario: Active surface commit updates Game FPS source -- **GIVEN** a game surface is the current capture target -- **WHEN** that surface produces an eligible commit for capture -- **THEN** the compositor capture path SHALL update the `Game FPS` metric source from that event - -#### Scenario: Non-target surface does not change Game FPS source -- **GIVEN** a different surface is not the current capture target -- **WHEN** that non-target surface commits -- **THEN** the compositor capture path SHALL NOT count that event toward `Game FPS` - -#### Scenario: Commit-to-capture latency is published -- **GIVEN** an eligible active-surface commit produces a captured frame -- **WHEN** the compositor publishes the captured frame for viewer consumption -- **THEN** the compositor capture path SHALL publish `Compositor Latency` for that commit as the - elapsed commit-to-capture interval - -### Requirement: Compositor Capture Participates in Global Frame Pacing - -The compositor capture path SHALL participate in the same effective target FPS contract that drives -viewer presentation for the current Goggles session. - -For the active capture target, the compositor SHALL pace callback/publication flow so the nested -target application is not driven solely by immediate commit-triggered `frame_done` issuance. - -#### Scenario: Active capture target follows global pacing target -- **GIVEN** an active capture target and a non-zero effective target FPS -- **WHEN** the compositor is issuing callbacks and publishing captured frames for that target -- **THEN** the compositor SHALL apply pacing for that target using the effective global target FPS -- **AND** the target SHALL NOT be driven solely by immediate commit-triggered callback issuance - -#### Scenario: Uncapped mode bypasses compositor pacing delays -- **GIVEN** the effective global target FPS is `0` -- **WHEN** the compositor is issuing callbacks and publishing frames for the active target -- **THEN** the compositor SHALL bypass target-interval pacing delays -- **AND** the workflow SHALL remain explicitly uncapped - -#### Scenario: Host acceptance scope covers Wayland and X11 -- **GIVEN** Goggles is running on either a Wayland host or an X11 host -- **WHEN** the active capture target participates in the paced compositor path -- **THEN** the compositor pacing contract SHALL apply in both host environments -- **AND** acceptance SHALL use the same target-FPS rule in both environments - diff --git a/openspec/specs/compositor-module-layout/spec.md b/openspec/specs/compositor-module-layout/spec.md deleted file mode 100644 index 27610c18..00000000 --- a/openspec/specs/compositor-module-layout/spec.md +++ /dev/null @@ -1,136 +0,0 @@ -# compositor-module-layout Specification - -## Purpose -TBD - created by archiving change refactor-compositor-server-modules. Update Purpose after archive. - -## Requirements - -### Requirement: Compositor Server Facade Remains Stable -The compositor implementation SHALL preserve `CompositorServer` as the public integration facade while moving subsystem logic out of `compositor_server.cpp`. - -The refactor SHALL: - -- Keep `compositor_server.hpp` as the public API declaration surface. -- Keep `compositor_server.cpp` limited to public method entrypoints and high-level delegation. -- Preserve existing `CompositorServer` externally observable behavior unless a very small internal-only adjustment is explicitly documented in the change artifacts. - -#### Scenario: Public facade preserved after split -- **GIVEN** the compositor refactor is complete -- **WHEN** external callers integrate the nested compositor -- **THEN** external callers still integrate through `CompositorServer` -- **AND** compositor subsystem implementations no longer remain concentrated in one giant `compositor_server.cpp` - -### Requirement: Single Compositor State Authority -The compositor implementation SHALL retain one central implementation state object as the single source of truth for wlroots resources, synchronization primitives, listener storage, focus state, cursor state, pointer-constraint state, and presented-frame/export state. - -Subsystem modules SHALL operate on that central state and SHALL NOT duplicate ownership of compositor-global resources across separate subsystem owners. - -#### Scenario: Ownership remains centralized after extraction -- **GIVEN** subsystem code is split across multiple files -- **WHEN** ownership of compositor-global resources is inspected -- **THEN** wlroots handles, listener containers, input queues, focus metadata, cursor metadata, pointer-constraint state, and presented-frame state still resolve to one compositor state authority -- **AND** teardown ordering remains auditable from that central state - -### Requirement: Responsibility-Oriented Compositor Modules -The compositor implementation SHALL organize subsystem logic into responsibility-oriented modules so future edits can stay local to one compositor concern. - -The split SHALL provide module boundaries that match or closely approximate these responsibilities: - -- facade/public API in `compositor_server.*` -- bootstrap/thread lifecycle/teardown in `compositor_core.*` -- input queue injection and dispatch in `compositor_input.*` -- focus switching, hit-testing, and pointer-constraint ownership in `compositor_focus.*` -- cursor setup/update/rendering in `compositor_cursor.*` -- presented-frame/render/export logic in `compositor_present.*` -- XDG lifecycle in `compositor_xdg.*` -- XWayland lifecycle in `compositor_xwayland.*` -- layer-shell lifecycle/render integration in `compositor_layer_shell.*` - -The implementation SHALL NOT introduce a generic `misc`, `helpers`, or `utils` dumping-ground module for compositor extraction. -`compositor_core.*` SHALL contain only startup, shutdown, backend/output/event-loop orchestration, compositor thread lifecycle, and teardown responsibilities. -Layer-shell-originated `xdg_popup` hook creation and destruction SHALL remain owned by `compositor_xdg.*`, with `compositor_layer_shell.*` limited to forwarding popup creation events into that XDG-owned path. - -#### Scenario: Localized edit surface for protocol lifecycle -- **GIVEN** a future change only affects XWayland lifecycle behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `compositor_xwayland.*` -- **AND** unrelated input, cursor, and presentation logic does not need to remain in the same translation unit - -#### Scenario: Localized edit surface for input targeting -- **GIVEN** a future change only affects hit-testing, focus targeting, or pointer-constraint behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `compositor_focus.*` -- **AND** protocol lifecycle code does not need to be loaded to make that focused change - -### Requirement: Extraction Contract Is Explicit for Apply -The change artifacts SHALL define a compile-safe extraction order and verification plan that minimize behavior drift during apply. - -The change artifacts SHALL specify: - -- An initial declaration-seam step that creates only the narrow internal headers needed to make multi-file extraction compile-safe. -- Updating `src/compositor/CMakeLists.txt` as each compositor translation unit lands so the migration remains compile-complete. -- Extract XDG lifecycle before XWayland lifecycle. -- Extract XWayland lifecycle before splitting input dispatch from focus/hit-testing. -- Extract layer-shell after XDG popup ownership is isolated and after the input/focus split is stable, and before final core/facade cleanup. -- Extract cursor and presentation/export logic after protocol and input/focus seams are stable. -- Leave bootstrap/teardown consolidation for the end unless an earlier minimal core split is required for safe extraction. -- Include verification commands and a concrete checklist covering startup/shutdown, Wayland clients, XWayland clients, input forwarding, focus targeting, pointer constraints, layer-shell behavior, cursor behavior, and presented-frame acquisition/export. - -#### Scenario: Migration order is explicit for implementation -- **GIVEN** implementation starts from the repository artifacts alone -- **WHEN** the artifacts are read before editing code -- **THEN** the OpenSpec artifacts specify the required extraction order -- **AND** the implementation does not depend on undocumented external context to determine safe sequencing - -#### Scenario: Behavior preservation is verified explicitly -- **GIVEN** the refactor is implemented -- **WHEN** verification evidence is recorded -- **THEN** the recorded verification evidence includes preset-driven build/test/static checks -- **AND** it includes a compositor behavior checklist covering startup/shutdown, Wayland/XWayland handling, input, focus, pointer constraints, layer-shell behavior, cursor, and presentation/export preservation - -#### Scenario: Input-routing fallback is explicit and bounded -- **GIVEN** automated execution of `goggles_auto_input_forwarding_x11` or `goggles_auto_input_forwarding_wayland` is unavailable -- **WHEN** equivalent interactive runtime conditions exist -- **THEN** the verification plan allows manual fallback only for those unavailable input-routing checks -- **AND** it requires recorded prerequisites, observations, and stored proof for the fallback run - -#### Scenario: Presentation and export verification stays mandatory -- **GIVEN** implementation touches presented-frame or DMA-BUF export code -- **WHEN** verification is executed -- **THEN** `goggles_headless_integration*` remains a required check -- **AND** the verification plan does not replace that check with manual fallback - -### Requirement: Behavior Is Preserved Across the Module Split -The compositor refactor SHALL preserve existing compositor behavior while changing only the internal module layout. - -The preserved behavior SHALL include: - -- startup and shutdown ordering -- Wayland XDG toplevel and popup lifecycle behavior -- XWayland lifecycle behavior and X11-specific quirks -- input forwarding and focus targeting behavior -- pointer-constraint activation, confinement, and cursor-hint behavior -- layer-shell render ordering, popup forwarding, and exclusive keyboard-focus behavior -- cursor visibility, positioning, and overlay rendering behavior -- presented-frame acquisition, retained-frame refresh, and DMA-BUF export behavior - -#### Scenario: Protocol and input behavior remain unchanged -- **GIVEN** the compositor implementation has been split into subsystem-oriented files -- **WHEN** Wayland clients, XWayland clients, and forwarded input are exercised through the defined verification plan -- **THEN** their externally observable behavior matches the pre-refactor compositor behavior - -#### Scenario: Presentation and export behavior remain unchanged -- **GIVEN** the compositor implementation has been split into subsystem-oriented files -- **WHEN** the presented-frame path and export path are exercised through the defined verification plan -- **THEN** retained-frame behavior, cursor overlay behavior, and DMA-BUF export behavior remain unchanged - -### Requirement: Behavior-Critical Quirks Stay With Their Subsystems -The compositor refactor SHALL preserve existing behavior-critical quirks, workarounds, and constraint comments adjacent to the subsystem logic they govern. - -This includes XWayland-specific activation/input quirks, destroy-listener restrictions, pointer-constraint confinement/cursor-hint rules, layer-shell popup forwarding ownership, and hook-allocation/lifetime constraints for protocol handlers. - -#### Scenario: XWayland quirks remain isolated and documented -- **GIVEN** XWayland handling is extracted -- **WHEN** the subsystem ownership is inspected -- **THEN** XWayland-specific activation and destroy-listener constraints remain documented next to the XWayland lifecycle logic -- **AND** those constraints are not moved into an unrelated shared helper or omitted during extraction diff --git a/openspec/specs/config-loading/spec.md b/openspec/specs/config-loading/spec.md deleted file mode 100644 index 88828983..00000000 --- a/openspec/specs/config-loading/spec.md +++ /dev/null @@ -1,75 +0,0 @@ -# config-loading Specification - -## Purpose -TBD - created by archiving change refactor-path-resolution-and-config-loading. Update Purpose after archive. -## Requirements -### Requirement: Config Discovery Uses XDG Config Home -The system SHALL discover the default configuration file at: -`${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`. - -#### Scenario: Default config path is resolved -- **GIVEN** no explicit `--config` argument is provided -- **WHEN** Goggles starts -- **THEN** it SHALL resolve the default config path under XDG config home - -### Requirement: Config Is Validated Early With Fallback -The system SHALL validate the configuration file early during startup, before configuration-dependent -behavior is applied. - -If the config file is missing, unreadable, fails to parse, or fails semantic validation, the system -SHALL: -- log a single warning at the application boundary -- fall back to defaults (and continue) - -#### Scenario: Missing config falls back to defaults -- **GIVEN** no config file exists at the resolved default path -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning -- **AND** it SHALL continue using default configuration values - -#### Scenario: Invalid config falls back to defaults -- **GIVEN** a config file exists but contains invalid TOML or invalid values -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning describing the failure -- **AND** it SHALL continue using default configuration values - -#### Scenario: Explicit config path is invalid -- **GIVEN** the user provides an explicit `--config` path -- **AND** the config file is missing or invalid -- **WHEN** Goggles starts -- **THEN** it SHALL log a warning describing the failure -- **AND** it SHALL continue using default configuration values - -### Requirement: Optional Template-Based Bootstrap -The system SHALL support an optional template-based bootstrap flow for creating a user config when -no user config exists. - -If a shipped config template exists under `resource_dir`, the system MAY write a user config file into -XDG config home. - -The write operation SHALL be atomic (no partial writes on crash/disk-full). -If writing fails, the system SHALL continue using defaults or the template with a warning. - -#### Scenario: Template seeds a new user config -- **GIVEN** no user config exists -- **AND** a shipped template exists under `resource_dir` -- **WHEN** Goggles starts -- **THEN** it MAY write a new user config under XDG config home atomically -- **AND** it SHALL continue startup regardless of whether the write succeeds - -### Requirement: Config Supports Directory Root Overrides -The system SHALL support an optional `[paths]` table in the configuration file to override directory -roots: -- `resource_dir` -- `config_dir` -- `data_dir` -- `cache_dir` -- `runtime_dir` - -When provided, these overrides SHALL take precedence over environment variables and defaults. - -#### Scenario: Config overrides cache directory -- **GIVEN** the config file sets `[paths].cache_dir` -- **WHEN** Goggles starts -- **THEN** it SHALL use the configured cache directory root instead of XDG-derived defaults - diff --git a/openspec/specs/dependency-management/spec.md b/openspec/specs/dependency-management/spec.md deleted file mode 100644 index 31faaa88..00000000 --- a/openspec/specs/dependency-management/spec.md +++ /dev/null @@ -1,125 +0,0 @@ -# dependency-management Specification - -## Purpose -Defines the dual-layer dependency management strategy using Pixi for system dependencies, toolchains, and source-built/prebuilt packages. CPM is not used in the current configuration. -## Requirements -### Requirement: Pixi as Primary Dependency Manager - -The project SHALL use Pixi as the enforced environment for builds and dependency resolution, anchored to a Glibc 2.28 baseline for cross-distro compatibility. - -#### Scenario: Pixi environment enforcement -- **WHEN** CMake config runs for any target -- **THEN** it SHALL require `CONDA_PREFIX` to be set by Pixi -- **AND** it SHALL fail fast with guidance to run `pixi run build -p ` if invoked outside Pixi - -#### Scenario: Glibc 2.28 compatibility anchor -- **WHEN** dependencies are resolved via Pixi -- **THEN** `sysroot_linux-64` SHALL be pinned to the `2.28.*` series -- **AND** binaries built in the Pixi environment SHALL be compatible with RHEL8/Ubuntu 20.04 class distros - -### Requirement: Pixi for C++ Libraries (Primary) - -The build system SHALL consume C++ libraries from Pixi packages (including source-built recipes) as the primary source. - -#### Scenario: Pixi-managed C++ libraries -- **GIVEN** the `pixi.toml` configuration -- **THEN** the following libraries SHALL be provided by Pixi packages (built from source where applicable): - - expected-lite (error handling) - - spdlog (logging) - - toml11 (configuration) - - Catch2 (testing) - - stb (image loading) - - BS_thread_pool (concurrency) - - slang-shaders (Slang shader compiler) - intentionally managed as local package for independent version control - - Tracy (profiling, optional) - -#### Scenario: Pixi package discovery -- **GIVEN** `CPM_USE_LOCAL_PACKAGES=ON` is set during CMake configure -- **WHEN** `find_package()` is invoked for the above libraries -- **THEN** the Pixi-provided packages SHALL be found without CPM downloads -- **NOTE**: Slang shader compiler is intentionally managed as a local pixi-build package for independent version control -- **RATIONALE**: This allows the project to control Slang updates independently from conda-forge package updates - -### Requirement: Pixi-CPM Integration - -System libraries provided by Pixi SHALL be discovered by CMake using `find_package()` without CPM downloads. - -#### Scenario: SDL3 discovery -- **GIVEN** SDL3 is installed via Pixi -- **WHEN** CMake processes `cmake/Dependencies.cmake` -- **THEN** `find_package(SDL3 REQUIRED)` SHALL locate the Pixi-provided SDL3 -- **AND** CPM SHALL NOT be used - -#### Scenario: CLI11 discovery -- **GIVEN** CLI11 is installed via Pixi -- **WHEN** CMake processes `cmake/Dependencies.cmake` -- **THEN** `find_package(CLI11 REQUIRED)` SHALL locate the Pixi-provided CLI11 -- **AND** CPM SHALL NOT be used - -### Requirement: Dependency Version Pinning - -Dependency resolution SHALL be anchored by the sysroot version while allowing Pixi to solve other packages, with exact versions locked in `pixi.lock`. - -#### Scenario: Sysroot version constraint -- **GIVEN** `pixi.toml` -- **WHEN** dependencies are installed -- **THEN** `sysroot_linux-64` SHALL declare version constraint `2.28.*` -- **AND** 32-bit sysroot builds SHALL consume the matching baseline - -#### Scenario: Solver-driven versions with lockfile -- **WHEN** Pixi installs dependencies with wildcard constraints -- **THEN** exact resolved versions SHALL be captured in `pixi.lock` -- **AND** subsequent installs SHALL reproduce those versions from the lockfile - -### Requirement: Worktree-Compatible Hook Installation - -The pre-commit hook installation SHALL work correctly in git worktrees. - -#### Scenario: Hook installation in worktree -- **GIVEN** a git worktree created from the main repository -- **WHEN** `pixi run init` is executed -- **THEN** the pre-commit hook SHALL be installed to the correct hooks directory -- **AND** the script SHALL use `git rev-parse --git-path hooks` to locate the hooks directory - -#### Scenario: Hook installation with custom hooksPath -- **GIVEN** `core.hooksPath` is configured in git config -- **WHEN** `pixi run init` is executed -- **THEN** the pre-commit hook SHALL be installed to the configured hooksPath - -### Requirement: Vulkan Components from conda-forge - -Vulkan headers and validation layers SHALL be sourced from conda-forge packages instead of local pixi-build packages. - -#### Scenario: Vulkan package availability -- **GIVEN** the `pixi.toml` configuration -- **THEN** the following Vulkan components SHALL be provided by conda-forge: - - `libvulkan-headers = "1.4.328.*"` - Vulkan API headers - - `vulkan-validation-layers = "1.4.328.*"` - Debug validation layers for development - -#### Scenario: Validation layer activation -- **GIVEN** the pixi environment is activated -- **WHEN** a Vulkan application runs with validation enabled -- **THEN** `VK_ADD_LAYER_PATH` SHALL point to `$CONDA_PREFIX/share/vulkan/explicit_layer.d` -- **AND** `VULKAN_SDK` SHALL be set to `$CONDA_PREFIX` - -#### Rationale -- The project uses Slang for shader compilation (via `slang-shaders` local package) -- glslang, shaderc, and spirv-cross are not required dependencies -- Only validation layers are needed for development and debugging - -### Requirement: Sysroot Package Integrity - -Sysroot packages SHALL verify upstream artifacts and self-heal known packaging issues before use. - -#### Scenario: SHA256 verification for upstream debs -- **WHEN** the 32-bit sysroot recipe downloads Debian/Ubuntu archives -- **THEN** each archive SHALL be validated against an expected SHA256 -- **AND** the build SHALL fail if any checksum mismatches - -#### Scenario: Symlink self-repair -- **WHEN** GCC development libraries in the sysroot include broken absolute symlinks -- **THEN** the recipe SHALL repoint them to local targets or remove unusable links to avoid linker errors - -#### Scenario: Tracy source integrity -- **WHEN** Tracy sources are fetched for the sysroot build -- **THEN** the recipe SHALL verify the commit hash matches the expected revision before building diff --git a/openspec/specs/diagnostics/spec.md b/openspec/specs/diagnostics/spec.md deleted file mode 100644 index 0c62d562..00000000 --- a/openspec/specs/diagnostics/spec.md +++ /dev/null @@ -1,467 +0,0 @@ -# Diagnostics Specification - -## Purpose - -Defines the structured diagnostic event model, sink-agnostic adapter contracts, layered activation tiers, reporting modes, session identity, and validation workflows that form the filter-chain diagnostics system. This domain covers the core infrastructure that all other diagnostic capabilities (authoring analysis, runtime validation, capture, quality validation) are built upon. - -## Requirements - -### Requirement: Diagnostic Event Model - -The diagnostics system SHALL define a structured event type that carries severity, category, pass localization, and an evidence payload. Every diagnostic observation emitted by any layer of the system MUST use this event type. - -#### Scenario: Event carries required fields -- GIVEN a diagnostic observation is generated during filter-chain operation -- WHEN the observation is emitted as a diagnostic event -- THEN the event SHALL include a severity level (error, warning, info, debug) -- AND the event SHALL include a category (authoring, runtime, quality, capture) -- AND the event SHALL include a localization key identifying the pass ordinal, processing stage, and affected resource or semantic name where applicable -- AND the event SHALL include a session identity reference - -#### Scenario: Event carries optional evidence payload -- GIVEN a diagnostic event is emitted with supporting evidence -- WHEN a sink receives the event -- THEN the event MAY include a structured evidence payload containing resource identifiers, extent values, semantic values, image data references, or source location information -- AND the evidence payload format SHALL be defined by the event category - -#### Scenario: Events without pass context use chain-level localization -- GIVEN a diagnostic observation applies to the chain as a whole rather than a specific pass -- WHEN the observation is emitted -- THEN the localization key SHALL use a sentinel pass ordinal indicating chain-level scope -- AND the processing stage SHALL identify the chain-level operation (preset parsing, graph validation, policy evaluation) - -### Requirement: Severity Classification - -The diagnostics system SHALL define a closed set of severity levels with deterministic ordering and policy-driven promotion rules. - -#### Scenario: Severity levels are ordered -- GIVEN the set of diagnostic severity levels -- WHEN severity values are compared -- THEN the ordering SHALL be debug < info < warning < error -- AND no severity level outside this closed set SHALL be emittable - -#### Scenario: Policy-driven severity promotion -- GIVEN a diagnostic policy configured in strict mode -- WHEN a fallback substitution event is emitted that would normally be a warning -- THEN the diagnostics system SHALL promote the event to error severity -- AND the original severity SHALL be preserved in the event metadata for audit - -#### Scenario: Default severity in compatibility mode -- GIVEN a diagnostic policy configured in compatibility mode -- WHEN a fallback substitution event is emitted -- THEN the event SHALL retain warning severity -- AND the event SHALL be recorded in the degradation ledger - -### Requirement: Sink-Agnostic Adapter Interface - -The diagnostics system SHALL define a sink adapter interface that decouples diagnostic event production from event consumption. The interface SHALL consist of a single method that receives a diagnostic event. - -#### Scenario: Multiple sinks receive the same event -- GIVEN two or more sink adapters are registered with the diagnostic session -- WHEN a diagnostic event is emitted -- THEN each registered sink adapter SHALL receive the event -- AND sink delivery order SHALL be deterministic within a session - -#### Scenario: Sink failure does not block event emission -- GIVEN a sink adapter encounters an error during event processing -- WHEN the adapter returns from the event delivery call -- THEN the diagnostic session SHALL NOT halt event emission to other sinks -- AND the diagnostic session SHALL record the sink failure as a diagnostic event itself - -#### Scenario: No sinks registered -- GIVEN a diagnostic session is created with no sink adapters registered -- WHEN diagnostic events are emitted -- THEN events SHALL be silently discarded -- AND no runtime error SHALL occur - -### Requirement: Concrete Sink Adapters - -The diagnostics system SHALL provide at minimum two concrete sink adapters: a logging sink and a test-harness sink. - -#### Scenario: Logging sink routes to spdlog -- GIVEN a logging sink adapter is registered -- WHEN a diagnostic event is received -- THEN the adapter SHALL format the event and route it to spdlog using a dedicated logger name distinct from the main application logger -- AND the spdlog severity SHALL correspond to the diagnostic event severity - -#### Scenario: Test-harness sink collects events for assertion -- GIVEN a test-harness sink adapter is registered in a test context -- WHEN diagnostic events are emitted during a test run -- THEN the adapter SHALL collect all events in an ordered, queryable container -- AND tests SHALL be able to assert on event count, severity, category, and localization fields - -#### Scenario: Test-harness sink supports filtering -- GIVEN a test-harness sink with collected events -- WHEN a test queries events by category and severity -- THEN the sink SHALL return only events matching the specified filter criteria -- AND the returned events SHALL preserve emission order - -### Requirement: Diagnostic Session Identity - -Every diagnostic session SHALL carry an identity that uniquely identifies the validation context and is attached to every emitted event and every produced artifact. - -#### Scenario: Session identity contains content hashes -- GIVEN a diagnostic session is created for a loaded preset -- WHEN the session identity is constructed -- THEN the identity SHALL include the preset revision hash, the expanded-source hash, and the compiled-contract hash - -#### Scenario: Session identity contains runtime context -- GIVEN a diagnostic session is active during frame recording -- WHEN the session identity is inspected -- THEN the identity SHALL include the runtime generation identifier, the frame range, the capture mode, and an environment fingerprint - -#### Scenario: Session identity is stable across repeated runs -- GIVEN the same preset with the same source content and the same environment -- WHEN two diagnostic sessions are created -- THEN the content hashes in both session identities SHALL be identical -- AND the runtime generation identifiers MAY differ - -### Requirement: Layered Activation Tiers - -The diagnostics system SHALL support three activation tiers that control the cost and depth of diagnostic instrumentation. - -#### Scenario: Tier 0 baseline checks for an active session -- GIVEN a diagnostic session is active with the default diagnostics tier -- WHEN filter-chain frames are recorded -- THEN Tier 0 checks (binding coverage, semantic coverage, degradation detection) SHALL execute every frame for that session -- AND Tier 0 overhead SHALL be less than 1% of frame time - -#### Scenario: No diagnostic session keeps runtime behavior unchanged -- GIVEN a filter-chain runtime has no active diagnostic session -- WHEN filter-chain frames are recorded -- THEN the runtime SHALL preserve existing rendering behavior without creating diagnostic artifacts -- AND diagnostics-only ledgers and sink delivery SHALL remain inactive until a session is created - -#### Scenario: Tier 1 runtime opt-in -- GIVEN a build with Tier 1 diagnostics enabled via configuration -- WHEN filter-chain frames are recorded -- THEN additional diagnostic collection (GPU timestamp queries, selected readback, execution timeline) SHALL be active -- AND Tier 1 overhead SHALL be between 5% and 15% of frame time - -#### Scenario: Tier 2 compile-time gated forensic capture -- GIVEN a build compiled without the forensic instrumentation compile flag -- WHEN the binary is inspected -- THEN no Tier 2 instrumentation symbols or code paths SHALL be present -- AND no Tier 2 overhead SHALL exist - -#### Scenario: Tier 2 enabled at compile time -- GIVEN a build compiled with the forensic instrumentation compile flag enabled -- WHEN forensic capture is triggered -- THEN full intermediate readback, complete event timeline, and artifact bundle generation SHALL be available - -#### Scenario: Tier 2 macros are compiled out by default -- GIVEN a build where `GOGGLES_DIAGNOSTICS_FORENSIC` is not defined -- WHEN forensic helper macros are included from `src/util/diagnostics/forensic.hpp` -- THEN `GOGGLES_DIAG_FORENSIC_SCOPE(name)` and `GOGGLES_DIAG_FORENSIC_CAPTURE(session, pass, cmd)` SHALL expand to no-op expressions -- AND no Tier 2 capture code SHALL be required by downstream translation units - -### Requirement: Diagnostic Policy Configuration - -The diagnostics system SHALL support a library-internal policy configuration that controls strict versus compatibility behavior, capture depth, retention limits, and per-degradation severity rules without exposing the policy struct through the public boundary. - -#### Scenario: Strict mode forbids silent fallback -- GIVEN diagnostic policy is set to strict mode -- WHEN a texture binding resolves via fallback substitution -- THEN the diagnostics system SHALL emit an error-severity event -- AND the fallback SHALL be forbidden (the pass SHALL NOT execute with the substituted resource) - -#### Scenario: Compatibility mode records fallback -- GIVEN diagnostic policy is set to compatibility mode -- WHEN a texture binding resolves via fallback substitution -- THEN the diagnostics system SHALL emit a warning-severity event -- AND the fallback SHALL proceed and the pass SHALL execute with the substituted resource -- AND the substitution SHALL be recorded in the degradation ledger - -#### Scenario: Host boundary does not advertise diagnostics policy configuration -- GIVEN a Goggles host integrates through the current filter-chain boundary -- WHEN the application initializes -- THEN the host SHALL NOT expose a user-facing `[diagnostics]` policy surface that implies runtime policy control -- AND any diagnostics behavior beyond passive summary retrieval SHALL remain internal to the library - -#### Scenario: Concrete policy shape remains internal -- GIVEN the runtime diagnostics policy is derived from host configuration and library defaults -- WHEN the active policy is applied inside the filter-chain implementation -- THEN the library MAY use whatever internal fields it requires to enforce diagnostics behavior -- AND the public boundary SHALL NOT expose the concrete policy struct shape as a contract - -#### Scenario: Policy struct shape is not public API - -- **GIVEN** the standalone filter-chain library's diagnostics policy type -- **WHEN** host code interacts through the public boundary -- **THEN** the library SHALL keep policy fields and policy object layout internal -- **AND** the public diagnostics contract SHALL remain limited to summary retrieval via `get_diagnostic_summary()` - -#### Scenario: Host does not translate TOML diagnostics policy into runtime control - -- **GIVEN** the Goggles application loads its TOML configuration -- **WHEN** it initializes filter-chain integration -- **THEN** no `[diagnostics]` TOML keys SHALL be required or interpreted as runtime policy controls -- **AND** the standalone library SHALL NOT import or link TOML parsing libraries - -### Requirement: Reporting Modes - -The diagnostics system SHALL support four reporting modes that control the breadth and depth of diagnostic output. - -#### Scenario: Minimal mode output -- GIVEN reporting mode is set to Minimal -- WHEN a diagnostic report is generated -- THEN the report SHALL include the final verdict, compact failure summary, degraded-state markers, error counts by category, and session metadata -- AND the report SHALL NOT include intermediate image captures except final output on failure - -#### Scenario: Standard mode output -- GIVEN reporting mode is set to Standard -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Minimal plus normalized chain manifest, compile and reflection summaries, pass graph summary, binding coverage table, semantic coverage table, and one-frame execution trace - -#### Scenario: Investigate mode output -- GIVEN reporting mode is set to Investigate -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Standard plus selected intermediate outputs, uniform and push-constant dumps for selected passes, detailed source provenance, expanded degradation ledger, and history and feedback snapshots for selected frames - -#### Scenario: Forensic mode output -- GIVEN reporting mode is set to Forensic -- WHEN a diagnostic report is generated -- THEN the report SHALL include everything in Investigate plus all pass outputs for selected frame ranges, full temporal capture of history and feedback resources, full event timeline, and complete artifact bundle with hashes and environment fingerprint - -### Requirement: Degradation Ledger - -The diagnostics system SHALL maintain a degradation ledger that records every instance of fallback substitution, unresolved semantic binding, missing reflection data, or policy downgrade during a diagnostic session. - -#### Scenario: Fallback substitution is ledgered -- GIVEN a texture binding falls back to the source image during frame recording -- WHEN the binding is resolved -- THEN the degradation ledger SHALL record the pass ordinal, the expected resource identity, the substituted resource identity, and the frame index - -#### Scenario: Unresolved semantic is ledgered -- GIVEN a semantic destination member has no matching semantic assignment -- WHEN semantic values are populated for a pass -- THEN the degradation ledger SHALL record the pass ordinal, the unresolved member name, and the destination offset - -#### Scenario: Ledger is queryable by pass -- GIVEN a degradation ledger with multiple entries across different passes -- WHEN the ledger is queried for a specific pass ordinal -- THEN only degradation entries for that pass SHALL be returned -- AND entries SHALL be ordered by frame index - -### Requirement: Chain Manifest - -The diagnostics system SHALL produce a chain manifest that describes the normalized structure of a loaded preset including pass order, scaling configuration, texture declarations, alias declarations, and temporal requirements. - -#### Scenario: Manifest reflects loaded preset -- GIVEN a preset has been successfully loaded and compiled -- WHEN the chain manifest is generated -- THEN the manifest SHALL list every pass with its ordinal, shader source path, scale type, scale factor, format override, and wrap mode -- AND the manifest SHALL list every declared alias with its target pass -- AND the manifest SHALL list every declared preset texture with its path - -#### Scenario: Manifest includes temporal requirements -- GIVEN a preset uses history or feedback resources -- WHEN the chain manifest is generated -- THEN the manifest SHALL include the inferred history depth -- AND the manifest SHALL list every feedback-producing pass with its consumer passes - -#### Scenario: Manifest is deterministic -- GIVEN the same preset is loaded twice in identical environments -- WHEN chain manifests are generated for both loads -- THEN both manifests SHALL be byte-identical - -### Requirement: Binding Ledger - -The diagnostics system SHALL produce a per-pass, per-frame binding ledger that records the resolved resource table including source identity, fallback status, resource extents, and producer relationship for every texture binding. - -#### Scenario: All bindings are accounted for -- GIVEN an effect pass with reflected texture bindings -- WHEN the binding ledger is populated during frame recording -- THEN every reflected texture binding SHALL have an entry in the ledger -- AND each entry SHALL classify the binding as resolved, substituted (fallback), or unresolved - -#### Scenario: Binding ledger records producer chain -- GIVEN a pass samples the output of an earlier pass via alias -- WHEN the binding ledger entry is created -- THEN the entry SHALL record the producing pass ordinal and the alias name used for resolution - -#### Scenario: Binding ledger records extents -- GIVEN a texture binding is resolved to a concrete resource -- WHEN the binding ledger entry is created -- THEN the entry SHALL record the width, height, and format of the bound resource - -### Requirement: Semantic Assignment Ledger - -The diagnostics system SHALL produce a per-pass semantic assignment ledger that records resolved semantic values and their destination offsets in uniform buffers and push constants. - -#### Scenario: All semantic destinations are classified -- GIVEN a pass with reflected uniform buffer and push constant members -- WHEN the semantic assignment ledger is populated -- THEN every destination member SHALL be classified as parameter, semantic, static, or unresolved - -#### Scenario: Semantic values are recorded -- GIVEN a pass receives semantic injection for source size, output size, and frame count -- WHEN the semantic assignment ledger is populated -- THEN the ledger SHALL record the concrete values written for each semantic destination - -#### Scenario: Alias-size destinations are resolved -- GIVEN a pass has destination members with size suffixes that resolve via the alias-size table -- WHEN the semantic assignment ledger is populated -- THEN the ledger SHALL record the resolved producer pass and the concrete size values - -### Requirement: Execution Timeline - -The diagnostics system SHALL produce an ordered execution timeline that records allocation, recording, history push, feedback copy, and final composition events with timestamps when Tier 1 or higher is active. - -#### Scenario: Timeline records pass-level events -- GIVEN Tier 1 diagnostics are active -- WHEN effect passes are recorded for a frame -- THEN the execution timeline SHALL include a start and end event for each pass with timing data - -#### Scenario: GPU timing is populated on recycled frame slots -- GIVEN Tier 1 diagnostics are active with GPU timestamps available -- WHEN a frame slot is reused after the previous submission has completed -- THEN pass end events for that slot SHALL be annotated with GPU duration in microseconds -- AND the first use of a slot MAY omit GPU duration until results become available - -#### Scenario: Timeline records temporal operations -- GIVEN Tier 1 diagnostics are active and the preset uses history and feedback -- WHEN a frame completes recording -- THEN the execution timeline SHALL include history push events and feedback copy events with their respective timing data - -#### Scenario: Timeline ordering matches execution order -- GIVEN an execution timeline for a single frame -- WHEN the timeline events are enumerated -- THEN events SHALL appear in the order they were executed -- AND pass events SHALL appear in pass-ordinal order - -### Requirement: Authoring Verdict - -The diagnostics system SHALL produce an authoring verdict after static validation that summarizes whether the preset is ready for execution, with categorized findings. - -#### Scenario: Clean preset produces passing verdict -- GIVEN a preset where all passes compile, all reflections succeed, and all contracts validate -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "pass" -- AND the finding count for errors SHALL be zero - -#### Scenario: Compilation failure produces failing verdict -- GIVEN a preset where one pass fails to compile -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "fail" -- AND the findings SHALL include at least one error-severity entry identifying the failing pass ordinal and compilation stage - -#### Scenario: Reflection loss produces conditional verdict -- GIVEN a preset where compilation succeeds but reflection fails for one pass in compatibility mode -- WHEN the authoring verdict is generated -- THEN the verdict SHALL be "degraded" -- AND the findings SHALL include a warning identifying the pass ordinal and reflection stage -- AND the verdict SHALL note that strict mode would have produced a "fail" verdict - -### Requirement: Source Provenance Map - -The diagnostics system SHALL produce a source provenance map that links expanded and rewritten shader text back to original file paths and line numbers. - -#### Scenario: Include expansion is tracked -- GIVEN a shader source that uses #include directives -- WHEN the source provenance map is generated after preprocessing -- THEN every line in the expanded source SHALL map back to a file path and line number in the original source tree - -#### Scenario: Compatibility rewrites are tracked -- GIVEN a shader source that undergoes compatibility text rewrites during preprocessing -- WHEN the source provenance map is generated -- THEN rewritten regions SHALL map back to the original source location before rewriting -- AND the provenance entry SHALL indicate that a rewrite was applied - -#### Scenario: Provenance enables error localization -- GIVEN a compilation error at a specific line in the expanded source -- WHEN the source provenance map is consulted -- THEN the original file path and line number SHALL be recoverable -- AND the diagnostic event for the compilation error SHALL include the original source location - -### Requirement: Compile and Reflection Reports - -The diagnostics system SHALL produce per-pass compile reports and reflection reports as structured artifacts. - -#### Scenario: Compile report records per-stage results -- GIVEN shader compilation has been attempted for a pass -- WHEN the compile report is generated -- THEN the report SHALL include per-stage (vertex, fragment) compilation success or failure, diagnostic messages, timing, and cache-hit status - -#### Scenario: Reflection report records merged contract -- GIVEN shader reflection has been performed for a pass -- WHEN the reflection report is generated -- THEN the report SHALL include per-stage reflected resources (uniform buffer members, push constant members, texture bindings, vertex inputs) -- AND the report SHALL include the merged pass contract with any binding collisions or type mismatches noted - -#### Scenario: Reflection absence is explicitly reported -- GIVEN a pass where compilation succeeds but reflection produces an empty contract -- WHEN the reflection report is generated -- THEN the report SHALL explicitly flag reflection absence -- AND the severity SHALL be error in strict mode and warning in compatibility mode - -### Requirement: Diagnostics Implementation Builds Without goggles_util - -The diagnostics implementation (event model, sinks, session, ledger, policy, and related types) -SHALL build as part of the standalone filter-chain project without linking or depending on the -`goggles_util` CMake target. The diagnostics OBJECT library SHALL NOT transitively pull in -`toml11`, `BS_thread_pool`, or `Threads` as link dependencies. - -#### Scenario: Standalone diagnostics target has no goggles_util dependency - -- **GIVEN** the standalone project's diagnostics OBJECT library target -- **WHEN** its link dependencies are inspected (e.g., via `cmake --graphviz` or target property query) -- **THEN** `goggles_util` SHALL NOT appear as a direct or transitive link dependency -- **AND** `toml11`, `BS_thread_pool`, and `Threads` SHALL NOT appear as transitive link dependencies - -#### Scenario: Diagnostics compiles outside Goggles source tree - -- **GIVEN** the diagnostics implementation files have been moved to `filter-chain/src/diagnostics/` -- **WHEN** the standalone project is configured and built from a clean checkout -- **THEN** all diagnostics source files SHALL compile without errors -- **AND** compilation SHALL NOT require Goggles `src/util/` headers on the include path - -#### Scenario: Diagnostics headers are self-contained within standalone tree - -- **GIVEN** the ~10 diagnostics headers referenced by chain and shader code -- **WHEN** their transitive include dependencies are audited within the standalone tree -- **THEN** each header SHALL resolve all includes through standalone-owned paths -- **AND** no header SHALL include Goggles `util/config.hpp`, `util/job_system.hpp`, or other - `goggles_util`-owned types - -### Requirement: Host Boundary Does Not Expose Diagnostics Policy Configuration - -The Goggles host boundary SHALL not expose TOML-based diagnostics policy configuration or other -user-facing controls that imply caller ownership of diagnostics policy. The standalone diagnostics -library SHALL NOT read TOML configuration files directly, and the public boundary SHALL NOT expose -policy-setting or diagnostic-session-creation API parameters. - -#### Scenario: Standalone library keeps policy internal and avoids TOML - -- **GIVEN** the standalone library evaluates diagnostics behavior during runtime operation -- **WHEN** policy decisions are applied -- **THEN** the library SHALL use internal policy state rather than public boundary API parameters -- **AND** the library SHALL NOT read TOML configuration files or depend on `toml11` for policy resolution - -#### Scenario: Goggles host keeps diagnostics configuration out of runtime config - -- **GIVEN** the Goggles application reads its TOML configuration -- **WHEN** it initializes filter-chain diagnostics behavior -- **THEN** the host SHALL limit caller-visible diagnostics behavior to passive summary retrieval -- **AND** the TOML parsing dependency SHALL remain in the Goggles host, not in the standalone library - -### Requirement: Diagnostic Test Ownership Transfer - -The standalone project SHALL own and run all diagnostics unit tests that verify diagnostic event -model, sink behavior, session lifecycle, ledger operations, and policy enforcement. Goggles SHALL -NOT retain copies of these tests. - -#### Scenario: Diagnostics unit tests run from standalone project - -- **GIVEN** diagnostics unit tests have been moved to `filter-chain/tests/` -- **WHEN** `ctest --test-dir filter-chain/build` is executed -- **THEN** all diagnostics unit tests SHALL pass using library-owned diagnostics test corpus assets -- **AND** test assertions SHALL verify event model, sink delivery, session identity, and ledger behavior - -#### Scenario: Goggles does not retain diagnostics unit tests - -- **GIVEN** the diagnostics unit test files have been moved to the standalone project -- **WHEN** the Goggles `tests/` directory is inspected -- **THEN** the moved diagnostics unit test files SHALL NOT be present -- **AND** Goggles MAY retain host integration tests that exercise public diagnostic summary retrieval - through the filter-chain boundary API diff --git a/openspec/specs/documentation/spec.md b/openspec/specs/documentation/spec.md deleted file mode 100644 index 0ea4186b..00000000 --- a/openspec/specs/documentation/spec.md +++ /dev/null @@ -1,67 +0,0 @@ -# documentation Specification - -## Purpose -TBD - created by archiving change add-documentation-structure. Update Purpose after archive. -## Requirements -### Requirement: Documentation Policy - -The project SHALL define a documentation policy that specifies: -- What types of content to document (architecture, design rationale, constraints) -- What types of content NOT to document (implementation details AI can regenerate) -- Standard document pattern for consistency -- File naming convention (`snake_case.md`) -- Maintenance expectations - -#### Scenario: New contributor reads documentation policy -- **WHEN** a new contributor wants to add documentation -- **THEN** they can find clear guidelines in `openspec/project.md` under Documentation Policy - -### Requirement: Documentation Naming Convention - -All documentation files in `docs/` SHALL use `snake_case.md` naming, consistent with the project's code file naming conventions. - -#### Scenario: New documentation file created -- **WHEN** a contributor creates a new documentation file -- **THEN** the file name uses snake_case (e.g., `filter_chain.md`, not `FilterChain.md` or `FILTER_CHAIN.md`) - -### Requirement: Architecture Overview Document - -The project SHALL provide a high-level architecture overview document at `docs/architecture.md` that serves as the entry point for understanding the codebase. - -The document SHALL include: -- System context (what Goggles does, how it fits in the environment) -- Module overview with responsibilities -- Data flow between major components -- Links to topic-specific documentation - -#### Scenario: Maintainer needs to understand codebase -- **WHEN** a maintainer wants to understand how the project is structured -- **THEN** they can read `docs/architecture.md` and get a high-level map in under 10 minutes - -#### Scenario: Maintainer needs deeper topic knowledge -- **WHEN** a maintainer needs to understand a specific subsystem (threading, DMA-BUF, filter chain) -- **THEN** the architecture doc links to the relevant topic-specific document - -### Requirement: README Documentation Section - -The project README SHALL include a Documentation section that: -- Points to `docs/architecture.md` as the entry point -- Briefly describes what documentation is available - -#### Scenario: User discovers documentation from README -- **WHEN** a user reads the README -- **THEN** they can find a link to project documentation - -### Requirement: Consistent Document Pattern - -All topic-specific documentation SHALL follow a consistent pattern: -1. Purpose (1-2 sentences) -2. Overview (with diagram if helpful) -3. Key Components -4. How They Connect (data/control flow) -5. Constraints/Decisions (the "why") - -#### Scenario: Reading any topic doc feels familiar -- **WHEN** a maintainer reads any topic documentation -- **THEN** they find a consistent structure that matches other docs - diff --git a/openspec/specs/filter-chain-assets-package/spec.md b/openspec/specs/filter-chain-assets-package/spec.md deleted file mode 100644 index b9358c32..00000000 --- a/openspec/specs/filter-chain-assets-package/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -# filter-chain-assets-package Specification - -## Purpose - -Define the standalone filter-chain asset package contract used by installed verification and -downstream consumers. - -## Requirements - -### Requirement: Library-Owned Asset Package - -The standalone filter-chain project SHALL publish a library-owned asset package that contains the -fixtures, presets, shader assets, and related data needed for installed contract verification and -documented downstream consumption. Those assets SHALL be owned and versioned by the standalone -project rather than by the Goggles repository. - -#### Scenario: Installed project exposes standalone-owned assets -- **GIVEN** the standalone filter-chain project has been installed or staged for distribution -- **WHEN** maintainers inspect the installed asset content used for contract verification -- **THEN** required presets, shaders, and related fixtures SHALL be present as standalone project-owned assets -- **AND** those verification assets SHALL NOT be sourced from Goggles-owned directories - -### Requirement: Asset Resolution Is Package-Oriented - -Consumers and installed verification flows SHALL resolve standalone filter-chain assets through the -standalone project's documented package-oriented asset location rules. Asset lookup SHALL NOT depend -on Goggles checkout-relative paths or the caller's current working directory. - -#### Scenario: Installed tests resolve assets without repository context -- **GIVEN** installed contract tests or sample consumers run outside the standalone project source tree -- **WHEN** they load packaged presets or shader assets -- **THEN** asset resolution SHALL succeed through the standalone package's documented asset location contract -- **AND** success SHALL NOT depend on current working directory or Goggles repository-relative paths - -### Requirement: Assets Support Public-Surface Validation - -The standalone asset package SHALL provide the minimum reusable content needed to verify the public -surface from installed `STATIC` and `SHARED` distributions. - -#### Scenario: Public-surface validation reuses the same owned assets -- **GIVEN** maintainers validate both installed `STATIC` and installed `SHARED` package consumption paths -- **WHEN** contract verification is executed against each distribution form -- **THEN** both validation flows SHALL use the standalone library-owned asset package -- **AND** neither validation flow SHALL require a separate Goggles-owned fixture source - -### Requirement: Asset Category Coverage - -The standalone asset package SHALL contain three distinct asset categories that together provide -sufficient content for all library-owned contract and integration tests. - -#### Scenario: Test shader fixtures are present - -- **GIVEN** the standalone asset package under `filter-chain/assets/` -- **WHEN** test shader fixture content is inspected -- **THEN** the package SHALL include preset files (`.slangp`) and shader source files (`.slang`) - used by contract tests for format handling, history, feedback, frame count, pragma parsing, - and format decoding -- **AND** these fixtures SHALL NOT be symlinks to or copies resolved from `shaders/retroarch/test/` - in the Goggles repository at build time - -#### Scenario: Diagnostics test corpus is present - -- **GIVEN** the standalone asset package under `filter-chain/assets/` -- **WHEN** diagnostics test corpus content is inspected -- **THEN** the package SHALL include the authoring corpus (valid, invalid, and reflection test - cases) and semantic probe presets used by diagnostics unit tests -- **AND** these assets SHALL NOT be resolved from Goggles `tests/util/test_data/` paths at build time - -#### Scenario: Curated upstream shader subset is present - -- **GIVEN** the standalone asset package under `filter-chain/assets/` -- **WHEN** upstream shader content is inspected -- **THEN** the package SHALL include only the specific upstream shader files referenced by - zfast integration tests and shader validation tests -- **AND** the package SHALL NOT mirror the full `shaders/retroarch/` upstream collection - -### Requirement: Test Fixture Resolution Through Compile Definition - -Contract tests in the standalone project SHALL resolve asset paths through a compile-time -definition rather than relying on working directory, source-tree-relative paths, or -Goggles-owned path macros. - -#### Scenario: Tests use FILTER_CHAIN_ASSET_DIR for fixture lookup - -- **GIVEN** contract tests under `filter-chain/tests/` that load presets or shader fixtures -- **WHEN** those tests resolve asset file paths -- **THEN** path construction SHALL use a `FILTER_CHAIN_ASSET_DIR` compile definition (or - equivalent project-owned macro) pointing to the standalone asset root -- **AND** tests SHALL NOT use `GOGGLES_SOURCE_DIR` or any Goggles-checkout-relative path macro - -#### Scenario: Asset resolution works from installed test prefix - -- **GIVEN** the standalone project has been installed to a test prefix with assets -- **WHEN** installed contract tests resolve asset paths -- **THEN** the asset resolution mechanism SHALL locate installed assets through the package's - documented asset location contract -- **AND** resolution SHALL NOT depend on the standalone project source tree being present - -#### Scenario: Asset resolution works from build tree - -- **GIVEN** the standalone project is built but not installed -- **WHEN** tests run from the build tree (e.g., via `ctest --test-dir filter-chain/build`) -- **THEN** the `FILTER_CHAIN_ASSET_DIR` definition SHALL point to the source-tree asset directory -- **AND** all contract tests SHALL resolve their fixtures without installation diff --git a/openspec/specs/filter-chain-c-api/spec.md b/openspec/specs/filter-chain-c-api/spec.md deleted file mode 100644 index 0f4b2d41..00000000 --- a/openspec/specs/filter-chain-c-api/spec.md +++ /dev/null @@ -1,144 +0,0 @@ -# filter-chain-c-api Specification - -## Purpose -Defines the current standalone filter-chain C ABI: its canonical installed C entrypoint, exported symbol naming, opaque-handle lifecycle, version/capability queries, Vulkan-facing program/chain operations, and caller-visible diagnostics/control contracts. - -## Requirements -### Requirement: Public ABI naming and export surface -The public C ABI MUST use `goggles/filter_chain.h` as its canonical installed entrypoint. It MUST use the `goggles_fc_` symbol prefix and `GOGGLES_FC_` macro/constant prefix. Exported functions MUST use `GOGGLES_FC_API` and `GOGGLES_FC_CALL`, and the header-level ABI identity MUST be expressed through `GOGGLES_FC_API_VERSION` and `GOGGLES_FC_ABI_VERSION`. - -#### Scenario: Consumer links against the public ABI -- **GIVEN** a consumer compiles against `goggles/filter_chain.h` -- **WHEN** it links to a library reporting the same ABI major -- **THEN** the declared `goggles_fc_*` symbols SHALL be the supported callable ABI surface -- **AND** the consumer SHALL NOT rely on legacy `goggles_chain_*` naming - -### Requirement: Global query helpers -The ABI MUST expose process-global query helpers `goggles_fc_get_api_version()`, `goggles_fc_get_abi_version()`, `goggles_fc_get_capabilities()`, `goggles_fc_status_string(...)`, `goggles_fc_is_success(...)`, and `goggles_fc_is_error(...)`. The packed API version MUST use `major << 22 | minor << 12 | patch` bit packing. - -#### Scenario: Consumer checks runtime support -- **GIVEN** a consumer needs compatibility and feature negotiation before creating runtime objects -- **WHEN** it calls the global query helpers -- **THEN** it SHALL be able to determine ABI compatibility, optional capability flags, and human-readable status text without probing object handles first - -### Requirement: Stable scalar constants and fixed-width typedefs -Status, log-level, capability, and preset-source-kind public scalar typedefs MUST remain 32-bit unsigned values. Public constants for status codes, capabilities, log levels, preset-source kinds, scale modes, stage identifiers, stage masks, and provenance kinds MUST remain expressible as fixed-width integer ABI values. - -#### Scenario: FFI binding validates scalar widths -- **GIVEN** a foreign-language binding maps the public scalar typedefs -- **WHEN** it validates the ABI widths declared by the header -- **THEN** the mapped scalar storage SHALL be 32-bit unsigned for those typedef families - -### Requirement: Four opaque-handle lifecycle model -The ABI MUST model runtime state through four opaque handles: `goggles_fc_instance_t`, `goggles_fc_device_t`, `goggles_fc_program_t`, and `goggles_fc_chain_t`. Creation APIs MUST return handles through out-parameters, and destroy APIs for those handles MUST be void-returning object destructors rather than status-returning pointer-to-pointer teardown APIs. - -#### Scenario: Consumer owns distinct runtime objects -- **GIVEN** a consumer creates an instance, Vulkan device binding, preset program, and executable chain -- **WHEN** it destroys them through the public ABI -- **THEN** each object kind SHALL have its own dedicated destroy function -- **AND** the ABI SHALL NOT collapse those lifetimes into a single `goggles_chain_t` runtime model - -### Requirement: Struct-based call contract and initialization helpers -The ABI MUST expose POD structs for UTF-8 views, extents, log messages, instance creation, Vulkan device creation, import callbacks, preset sources, chain creation, chain retargeting, Vulkan record info, control info, program source info, program report, chain report, chain error info, and diagnostic summary. Each struct family with a `struct_size` field MUST support `GOGGLES_FC_STRUCT_SIZE(...)`-based initialization, and the header MUST provide inline `*_init()` helpers for the public struct families that callers are expected to initialize. - -#### Scenario: Caller prepares a public input struct -- **GIVEN** a caller is about to invoke a struct-based ABI entrypoint -- **WHEN** it initializes the struct with the corresponding `*_init()` helper or equivalent `struct_size` -- **THEN** the call SHALL use the documented v1 struct layout prefix -- **AND** the caller SHALL NOT need legacy `_ex` path-variant APIs to opt into extensible struct inputs - -### Requirement: Instance lifecycle and logging callback contract -The ABI MUST expose `goggles_fc_instance_create(...)`, `goggles_fc_instance_destroy(...)`, and `goggles_fc_instance_set_log_callback(...)`. Instance creation MUST accept optional log callback configuration through `goggles_fc_instance_create_info_t`, and the callback contract MUST deliver `goggles_fc_log_message_t` with log level plus UTF-8 domain and message views. - -#### Scenario: Host installs or replaces a log callback -- **GIVEN** a live instance handle -- **WHEN** the host configures a log callback at create time or later through `goggles_fc_instance_set_log_callback(...)` -- **THEN** the callback contract SHALL use `goggles_fc_log_message_t` -- **AND** the public ABI SHALL expose log levels through `GOGGLES_FC_LOG_LEVEL_*` constants - -### Requirement: Vulkan device binding contract -The ABI MUST expose `goggles_fc_device_create_vk(...)` and `goggles_fc_device_destroy(...)`. Vulkan device creation MUST accept `goggles_fc_vk_device_create_info_t` containing physical device, logical device, graphics queue, graphics queue family index, and optional cache-directory UTF-8 view. - -#### Scenario: Consumer binds an existing Vulkan device -- **GIVEN** a live filter-chain instance and an existing Vulkan device context -- **WHEN** the consumer creates a filter-chain device binding -- **THEN** the public ABI SHALL accept Vulkan handles and queue metadata through `goggles_fc_vk_device_create_info_t` - -### Requirement: Program creation uses preset-source descriptors -The ABI MUST expose `goggles_fc_program_create(...)`, `goggles_fc_program_destroy(...)`, `goggles_fc_program_get_source_info(...)`, and `goggles_fc_program_get_report(...)`. Program creation MUST consume a `goggles_fc_preset_source_t` descriptor rather than legacy preset-load functions. The preset source contract MUST support file-backed and memory-backed sources, plus import callbacks for memory-backed include resolution. - -#### Scenario: Consumer creates a program from memory-backed preset bytes -- **GIVEN** a consumer has preset bytes and import callbacks for dependent resources -- **WHEN** it creates a program with `kind = GOGGLES_FC_PRESET_SOURCE_MEMORY` -- **THEN** the ABI SHALL accept the program source through `goggles_fc_preset_source_t` -- **AND** the public contract SHALL use the import-callback struct rather than `preset_load[_ex]` APIs - -### Requirement: File-source passthrough contract -For `GOGGLES_FC_PRESET_SOURCE_FILE`, a non-null `path.data` with `path.size == 0` MUST represent passthrough mode: no preset file is loaded and the runtime uses a built-in single-pass blit pipeline. A null `path.data` for file sources MUST be rejected as invalid input. - -#### Scenario: Caller requests passthrough mode -- **GIVEN** a file-source preset descriptor with a non-null path pointer and zero path length -- **WHEN** the caller creates a program from that descriptor -- **THEN** the public contract SHALL treat the request as passthrough mode -- **AND** it SHALL NOT require a preset file on disk for that case - -### Requirement: Program metadata queries -`goggles_fc_program_get_source_info(...)` MUST populate `goggles_fc_program_source_info_t` with provenance, source name, source path, and pass count. `goggles_fc_program_get_report(...)` MUST populate `goggles_fc_program_report_t` with shader, pass, and texture counts. - -#### Scenario: Consumer inspects created program metadata -- **GIVEN** a successfully created program handle -- **WHEN** the consumer queries source info and report data -- **THEN** the ABI SHALL expose source provenance/name/path and aggregate shader-pass-texture counts through the corresponding report structs - -### Requirement: Chain creation and lifecycle are separate from program lifecycle -The ABI MUST expose `goggles_fc_chain_create(...)` and `goggles_fc_chain_destroy(...)` as the executable-chain lifecycle. Chain creation MUST consume an already-created device handle, an already-created program handle, and `goggles_fc_chain_create_info_t` containing target format, frames in flight, initial stage mask, and initial prechain resolution. - -#### Scenario: Consumer builds an executable chain from a program -- **GIVEN** a device handle and a program handle -- **WHEN** the consumer creates a chain -- **THEN** the chain lifecycle SHALL be a distinct step from program creation -- **AND** creation inputs SHALL include stage-mask and prechain-resolution configuration - -### Requirement: Chain runtime operations -The chain runtime surface MUST expose `goggles_fc_chain_bind_program(...)`, `goggles_fc_chain_clear(...)`, `goggles_fc_chain_resize(...)`, `goggles_fc_chain_set_prechain_resolution(...)`, `goggles_fc_chain_get_prechain_resolution(...)`, `goggles_fc_chain_set_stage_mask(...)`, `goggles_fc_chain_retarget(...)`, and `goggles_fc_chain_record_vk(...)`. Stage enablement MUST be controlled through stage masks, not a stage-policy API. - -#### Scenario: Consumer reconfigures an existing chain -- **GIVEN** a live chain handle -- **WHEN** the consumer changes source extent, prechain resolution, stage mask, target format, or bound program -- **THEN** it SHALL do so through the dedicated chain reconfiguration calls -- **AND** the public ABI SHALL use `goggles_fc_chain_set_stage_mask(...)` rather than legacy stage-policy naming - -### Requirement: Vulkan record call shape -`goggles_fc_chain_record_vk(...)` MUST consume `goggles_fc_record_info_vk_t`, including command buffer, source image, source view, source extent, target view, target extent, frame index, scale mode, and integer scale. The public scale-mode contract MUST use the `GOGGLES_FC_SCALE_MODE_*` constants. - -#### Scenario: Host records one frame -- **GIVEN** a live chain and a Vulkan command buffer already chosen by the host -- **WHEN** the host records filter-chain work for a frame -- **THEN** the call SHALL pass recording inputs through `goggles_fc_record_info_vk_t` -- **AND** scale behavior SHALL be expressed through the public scale-mode constants - -### Requirement: Chain diagnostics and report queries -The ABI MUST expose passive chain metadata queries through `goggles_fc_chain_get_report(...)`, `goggles_fc_chain_get_last_error(...)`, and `goggles_fc_chain_get_diagnostic_summary(...)`. The corresponding structs MUST expose pass-count/frame-count/stage-mask report data, status-vk-result-subsystem error data, and aggregate diagnostic event counts plus current frame. - -#### Scenario: Consumer inspects recent chain state -- **GIVEN** a live chain handle -- **WHEN** the consumer requests report, last-error, or diagnostic-summary data -- **THEN** the ABI SHALL provide those results through dedicated out-struct queries -- **AND** the public diagnostics surface SHALL be passive metadata retrieval rather than a diagnostics-session lifecycle API - -### Requirement: Control enumeration and mutation use count/index/info APIs -The ABI MUST expose controls through `goggles_fc_chain_get_control_count(...)`, `goggles_fc_chain_get_control_info(...)`, `goggles_fc_chain_find_control_index(...)`, `goggles_fc_chain_set_control_value_f32(...)`, `goggles_fc_chain_set_control_value_f32_by_name(...)`, `goggles_fc_chain_reset_control_value(...)`, and `goggles_fc_chain_reset_all_controls(...)`. `goggles_fc_control_info_t` MUST describe control index, stage, UTF-8 name/description, and current/default/min/max/step values. - -#### Scenario: Consumer discovers and edits a control -- **GIVEN** a chain with exposed controls -- **WHEN** the consumer enumerates controls, looks one up by stage and name, or mutates/resets values -- **THEN** the public contract SHALL use count/index/info and direct mutation/reset calls -- **AND** it SHALL NOT require snapshot ownership objects for control listing - -### Requirement: Installed standalone consumer contract -The standalone package MUST provide a self-sufficient installed C ABI for external consumers. External consumers MUST be able to compile and link against the installed package using `goggles/filter_chain.h`, the exported targets, and required third-party headers. The installed package SHALL NOT provide `goggles_filter_chain.h` as a compatibility shim. - -#### Scenario: External C consumer uses the installed package -- **GIVEN** an external consumer resolves the installed standalone filter-chain package -- **WHEN** it builds against the public C ABI -- **THEN** the installed package SHALL provide the required public declarations and linkable ABI surface without Goggles-private source-tree headers diff --git a/openspec/specs/filter-chain-cpp-wrapper/spec.md b/openspec/specs/filter-chain-cpp-wrapper/spec.md deleted file mode 100644 index 42a2f8f4..00000000 --- a/openspec/specs/filter-chain-cpp-wrapper/spec.md +++ /dev/null @@ -1,93 +0,0 @@ -# filter-chain-cpp-wrapper Specification - -## Purpose -Defines the current public C++ wrapper around the standalone filter-chain C ABI: the canonical installed C++20 entrypoint, RAII ownership, result-based error propagation, namespace boundaries, and the exact wrapper methods currently exposed to C++ consumers. - -## Requirements -### Requirement: Public wrapper surface and namespace contract -The standalone package MUST provide its public C++ wrapper surface through `goggles/filter_chain.hpp` as the canonical installed C++20 entrypoint. Supporting public types remain split by ownership: wrapper enums and `Extent2D` in `goggles::filter_chain`, plus filter-control and Vulkan-context support types in `goggles::fc`. - -#### Scenario: Consumer includes explicit installed wrapper headers -- **GIVEN** a C++ consumer integrates the standalone filter-chain package -- **WHEN** it includes `goggles/filter_chain.hpp` -- **THEN** the RAII runtime API SHALL be available from `goggles::filter_chain` -- **AND** the installed package SHALL NOT require or provide `goggles_filter_chain.h` as a compatibility shim -- **AND** support headers SHALL retain their current namespace ownership split instead of collapsing everything into one namespace - -### Requirement: RAII ownership for instance, device, program, and chain -The wrapper MUST provide move-only RAII classes `Instance`, `Device`, `Program`, and `Chain`. Each wrapper class MUST own exactly one corresponding `goggles_fc_*` handle, destroy that handle in its destructor, expose `handle()` for interop, and expose `explicit operator bool()` for validity checks. - -#### Scenario: Wrapper-owned object leaves scope -- **GIVEN** a wrapper object owns a live underlying C handle -- **WHEN** that wrapper object is destroyed or overwritten by move assignment -- **THEN** the corresponding C ABI destroy function SHALL run exactly once -- **AND** normal C++ callsites SHALL NOT manage raw destroy calls directly - -### Requirement: Result-based error propagation -All fallible wrapper operations MUST return `goggles::Result` or `goggles::Result`. The installed package MUST define that result/error surface in `goggles/filter_chain/error.hpp`, and `goggles/filter_chain/result.hpp` MAY remain as a convenience forwarding include to the same definitions. Expected runtime failures MUST NOT require exceptions as part of the public contract. - -#### Scenario: Wrapper call fails -- **GIVEN** a wrapper operation encounters an expected runtime failure from the C ABI -- **WHEN** the C++ consumer receives the result -- **THEN** the failure SHALL be represented as a failed `goggles::Result` -- **AND** the wrapper contract SHALL NOT require exception handling for that path - -### Requirement: Wrapper call signatures mirror C ABI structs -The current wrapper API MUST accept the public C ABI structs and callback types directly rather than re-expressing them as `vk::`-typed or bespoke C++ configuration objects. Wrapper creation, recording, retargeting, and metadata queries MUST use `goggles_fc_*` structs where applicable. - -#### Scenario: Consumer configures wrapper calls -- **GIVEN** a C++ consumer prepares instance, Vulkan device, preset source, chain create, retarget, or record inputs -- **WHEN** it calls the wrapper -- **THEN** it SHALL pass the corresponding `goggles_fc_*` public structs -- **AND** the current wrapper contract SHALL NOT promise a `vk::`-typed wrapper-only configuration surface - -### Requirement: Instance and device wrapper operations -`Instance` MUST expose `create(const goggles_fc_instance_create_info_t*)` and `set_log_callback(goggles_fc_log_callback_t, void*)`. `Device` MUST expose `create(Instance&, const goggles_fc_vk_device_create_info_t*)`. - -#### Scenario: Consumer bootstraps wrapper runtime state -- **GIVEN** a C++ consumer wants to initialize the standalone runtime -- **WHEN** it creates an instance, optionally installs a log callback, and binds a Vulkan device -- **THEN** the wrapper SHALL provide those operations through `Instance` and `Device` - -### Requirement: Program wrapper operations use preset-source creation -`Program` MUST expose `create(Device&, const goggles_fc_preset_source_t*)`, `get_source_info()`, and `get_report()`. The wrapper contract MUST use program creation from a preset-source descriptor rather than legacy `preset_load` naming. - -#### Scenario: Consumer creates a wrapper program from preset source -- **GIVEN** a device wrapper and a preset source descriptor -- **WHEN** the consumer creates a program and inspects its metadata -- **THEN** the public wrapper SHALL create through `Program::create(...)` -- **AND** source/report inspection SHALL be available through dedicated query methods - -### Requirement: Chain wrapper operations reflect current public methods -`Chain` MUST expose `create(Device&, const Program&, const goggles_fc_chain_create_info_t*)`, `bind_program(...)`, `clear()`, `resize(...)`, `set_prechain_resolution(...)`, `get_prechain_resolution()`, `set_stage_mask(...)`, `retarget(...)`, `record_vk(...)`, `get_diagnostic_summary()`, `get_report()`, `get_last_error()`, `get_control_count()`, `find_control_index(...)`, `get_control_info(...)`, both `set_control_value_f32(...)` overloads, `reset_control_value(...)`, and `reset_all_controls()`. - -#### Scenario: Consumer drives a chain through the wrapper -- **GIVEN** a live chain wrapper -- **WHEN** the consumer rebonds programs, clears state, resizes, queries prechain resolution, retargets, records, inspects reports/errors, or queries, mutates, and resets controls -- **THEN** the wrapper SHALL expose those operations as `Chain` methods -- **AND** stage enablement SHALL be expressed as `set_stage_mask(...)` - -### Requirement: Wrapper parity is intentionally partial -The current C++ wrapper MUST match the methods it actually publishes and SHALL NOT be treated as a full 1:1 mirror of every C ABI entrypoint. The retained runtime-policy and reset helpers SHALL be wrapped directly in `Chain`; consumers that need other unsupported ABI operations MAY call the C ABI directly. - -#### Scenario: Consumer needs an ABI feature not wrapped today -- **GIVEN** a C++ consumer needs a public C ABI operation that has no dedicated wrapper method -- **WHEN** it inspects the current wrapper contract -- **THEN** the absence of that wrapper method SHALL be considered current supported behavior -- **AND** direct C ABI interop through `handle()` SHALL remain the compatibility path - -### Requirement: Global helper forwarding -The wrapper MUST expose inline free functions `get_api_version()`, `get_abi_version()`, `get_capabilities()`, and `status_string(...)` that forward the corresponding global C ABI queries. - -#### Scenario: Consumer queries global runtime metadata in C++ -- **GIVEN** a C++ consumer needs version, capability, or status-string data without creating wrapper objects first -- **WHEN** it uses the public wrapper helpers -- **THEN** the wrapper SHALL forward those queries directly to the C ABI global helpers - -### Requirement: Installed wrapper consumer contract -The installed standalone package MUST provide a self-sufficient C++ wrapper surface for external consumers using only installed public headers, exported targets, and required third-party dependencies. External C++ consumers MUST NOT require Goggles-private source-tree headers to use the wrapper, and the installed surface SHALL be satisfied by `goggles/filter_chain.hpp`, `goggles/filter_chain.h`, and any required support headers under `goggles/filter_chain/`. - -#### Scenario: External C++ consumer uses installed wrapper -- **GIVEN** an external C++ consumer resolves the installed standalone package -- **WHEN** it includes the wrapper headers and builds against the exported library targets -- **THEN** the installed package SHALL provide the required public wrapper declarations without Goggles-private headers diff --git a/openspec/specs/goggles-filter-chain/spec.md b/openspec/specs/goggles-filter-chain/spec.md deleted file mode 100644 index 32f31a10..00000000 --- a/openspec/specs/goggles-filter-chain/spec.md +++ /dev/null @@ -1,451 +0,0 @@ -# goggles-filter-chain Specification - -## Purpose -Define the boundary contract between the host render/backend code and the standalone -`goggles-filter-chain` runtime, including ownership, lifecycle, controls, and diagnostics. -## Requirements -### Requirement: Standalone Filter Library Target - -The extracted filter runtime SHALL be an independently buildable, installable, and exportable -standalone CMake project that publishes the downstream target contract `goggles-filter-chain`. -The standalone project SHALL use repository layout rooted at `include/`, `src/`, `tests/`, -`assets/`, and `cmake/`. Release acceptance SHALL require `STATIC` and `SHARED` library outputs, -and SHALL NOT require or expose a `MODULE` library variant as part of the package contract. - -#### Scenario: Target dependency direction -- **GIVEN** build targets are configured for render and filter runtime -- **WHEN** dependency checks run for target link relationships -- **THEN** `goggles-filter-chain` SHALL compile and link without depending on host backend targets -- **AND** host backend targets SHALL depend on `goggles-filter-chain` for filter execution - -#### Scenario: Target dependency audit -- **GIVEN** the `goggles-filter-chain` target link dependency list -- **WHEN** dependency audit checks execute -- **THEN** `goggles-filter-chain` SHALL NOT link app- or UI-only targets -- **AND** `goggles-filter-chain` SHALL link only dependencies required for chain/shader/texture runtime behavior - -#### Scenario: Standalone checkout builds without Goggles repository -- **GIVEN** a clean checkout of the extracted filter-chain project -- **WHEN** the documented CMake workflow configures and builds the project -- **THEN** the project SHALL build without requiring the Goggles source tree, Pixi wrappers, or Conda-specific paths -- **AND** the project layout consumed by that workflow SHALL be rooted at `include/`, `src/`, `tests/`, `assets/`, and `cmake/` - -#### Scenario: Installed package preserves stable target identity -- **GIVEN** the standalone project has been installed and exported -- **WHEN** a downstream consumer resolves the package through CMake package discovery -- **THEN** the consumer SHALL obtain the filter runtime through the target contract `goggles-filter-chain` -- **AND** consuming the installed package SHALL NOT require downstream target renaming or source-tree include assumptions - -#### Scenario: Distribution excludes module-only success criteria -- **GIVEN** the standalone project is prepared for release validation -- **WHEN** library artifacts and exported targets are inspected -- **THEN** `STATIC` and `SHARED` outputs SHALL both be available as supported deliverables -- **AND** no `MODULE` library variant SHALL be required for success or documented as part of the supported package surface - -### Requirement: Complete Filter Runtime Ownership Boundary -The filter boundary SHALL own filter-chain orchestration, shader runtime ownership/creation, -shader processing, and preset texture loading internals. When the host retargets output format -without changing the active preset, the boundary SHALL preserve source-independent -preset-derived runtime state across that retarget. - -#### Scenario: Source-independent preset work survives output retarget -- **GIVEN** a preset runtime has already completed parsing, shader compilation/reflection, preset - texture loading, and effect-pass setup -- **WHEN** the host requests output-format retargeting for a source color-space change -- **THEN** that source-independent preset-derived work SHALL remain available after the retarget -- **AND** the boundary SHALL expose the same effect-stage behavior after activation - -### Requirement: Host Backend Responsibility Boundary -The host backend SHALL remain responsible for swapchain lifecycle, external image import, -synchronization, queue submission, and present. The host backend SHALL use boundary-facing -retarget behavior for swapchain/output-format changes and SHALL reserve full preset/runtime -rebuild behavior for explicit preset reload requests. - -#### Scenario: Format retarget is handed off without full preset rebuild -- **GIVEN** swapchain output format must change because the source color-space classification changed -- **WHEN** host backend recreation is triggered -- **THEN** host backend code SHALL recreate swapchain and present-path resources -- **AND** filter runtime retargeting SHALL be invoked through boundary-facing contracts without - forcing full preset rebuild behavior - -#### Scenario: Explicit preset reload still uses rebuild path -- **GIVEN** the user explicitly requests a preset reload or selects a different preset -- **WHEN** the host backend coordinates the change -- **THEN** the boundary interaction SHALL use the full preset/runtime rebuild path -- **AND** the request SHALL NOT be collapsed into output-format-only retarget behavior - -#### Scenario: Pending runtime is aligned before activation -- **GIVEN** the host backend has a pending runtime from an explicit reload -- **AND** the authoritative output target changes before that pending runtime becomes active -- **WHEN** the backend/controller prepares the pending runtime for activation -- **THEN** the boundary interaction SHALL align that pending runtime to the current output target -- **AND** activation SHALL occur only after the pending runtime matches the latest output format - -### Requirement: Boundary-safe VulkanContext Contract Placement -Host<->filter initialization contracts SHALL use a boundary-owned `VulkanContext` definition that does not pull backend internals into the filter boundary. - -#### Scenario: VulkanContext ownership and include safety -- **GIVEN** headers used to define `VulkanContext` for host<->filter initialization -- **WHEN** include/dependency checks run -- **THEN** the `VulkanContext` type SHALL be declared in a boundary-owned header -- **AND** that header SHALL include only boundary-allowed dependencies and SHALL NOT include backend-only helper headers - -#### Scenario: Host/backend consumption of VulkanContext contract -- **GIVEN** backend and filter runtime initialization paths -- **WHEN** host code passes initialization context into `goggles-filter-chain` -- **THEN** host code SHALL consume the boundary-owned `VulkanContext` contract -- **AND** backend public headers SHALL NOT expose filter-boundary internals beyond this contract - -### Requirement: Boundary-safe Vulkan Result Utility Contracts -Filter boundary code SHALL use boundary-safe Vulkan result utilities and SHALL NOT include backend-only helper headers. - -#### Scenario: Backend helper include removal -- **GIVEN** chain/shader/texture boundary source files -- **WHEN** include dependency checks are executed -- **THEN** backend helper headers SHALL NOT be included from boundary sources - -#### Scenario: Boundary-safe `VK_TRY` relocation -- **GIVEN** Vulkan result-checking macros used by boundary code -- **WHEN** boundary-safe utility contracts are applied -- **THEN** boundary call sites SHALL include `VK_TRY` from a boundary-safe helper header -- **AND** that helper header SHALL depend only on boundary-allowed headers - -### Requirement: Boundary-safe Control Descriptor Contract -The filter boundary SHALL expose curated control descriptors for both effect and prechain controls with a closed, deterministic stage contract. - -#### Scenario: Control descriptor enumeration -- **GIVEN** a preset with effect-stage and prechain controls is loaded -- **WHEN** controls are enumerated through the boundary API -- **THEN** each descriptor SHALL include `control_id`, `stage`, `name`, `current_value`, `default_value`, `min_value`, `max_value`, and `step` -- **AND** descriptors SHALL represent both effect and prechain controls through the same boundary-safe contract - -#### Scenario: Stage domain is explicit and closed -- **GIVEN** control descriptors returned by the boundary API -- **WHEN** descriptor stage values are validated -- **THEN** each descriptor `stage` SHALL be one of `prechain` or `effect` -- **AND** unknown stage values SHALL NOT be emitted without an explicit spec update - -#### Scenario: Deterministic descriptor ordering -- **GIVEN** the same preset is enumerated repeatedly without control-layout changes -- **WHEN** control descriptors are listed through the boundary API -- **THEN** descriptor order SHALL be deterministic across runs and equivalent reloads -- **AND** ordering SHALL group `prechain` controls before `effect` controls while preserving stable per-stage ordering - -#### Scenario: Optional description fallback -- **GIVEN** a control descriptor may omit `description` -- **WHEN** UI renders control metadata -- **THEN** UI SHALL render a control label from `name` -- **AND** UI SHALL apply no tooltip text when `description` is absent - -### Requirement: Control Identifier Semantics -The control identifier contract SHALL define uniqueness and stability rules for `control_id`. - -#### Scenario: Uniqueness within active preset -- **GIVEN** controls for a loaded preset are enumerated -- **WHEN** the boundary returns control descriptors -- **THEN** each `control_id` SHALL be unique within the active preset - -#### Scenario: Stability across equivalent reload -- **GIVEN** the same preset is reloaded without control-layout changes -- **WHEN** controls are enumerated after reload -- **THEN** `control_id` values for matching controls SHALL remain stable across reload - -#### Scenario: Different preset layouts -- **GIVEN** a different preset with different control layout is loaded -- **WHEN** controls are enumerated for the new preset -- **THEN** `control_id` values MAY differ from the previous preset - -### Requirement: Control Mutation Contract -Control mutation and callback contracts SHALL use `control_id` and SHALL define deterministic out-of-range handling. - -#### Scenario: Control mutation is `control_id`-only -- **GIVEN** boundary consumers set or reset control values -- **WHEN** control mutation APIs are invoked -- **THEN** operations SHALL address controls by `control_id` -- **AND** boundary surfaces SHALL NOT expose pass indices as mutation keys - -#### Scenario: Out-of-range value handling -- **GIVEN** a control descriptor defines `min_value` and `max_value` -- **WHEN** a set-value request provides a value outside `[min_value, max_value]` -- **THEN** the boundary SHALL clamp the request to the nearest valid bound before applying it -- **AND** subsequent control enumeration SHALL report the clamped `current_value` - -### Requirement: Adapter Ownership Isolation -Adapters from shader-internal metadata to curated control descriptors SHALL live behind the `goggles-filter-chain` boundary. - -#### Scenario: Adapter dependency boundary -- **GIVEN** backend, app, and UI modules consume boundary descriptors -- **WHEN** control metadata adaptation is performed -- **THEN** adaptation from shader-internal metadata SHALL occur inside `goggles-filter-chain` -- **AND** backend/app/UI modules SHALL NOT include shader-internal metadata types for control enumeration - -#### Scenario: Adapter mapping parity across effect and prechain sources -- **GIVEN** control metadata comes from effect-stage and prechain-stage sources with different field availability -- **WHEN** adapters build curated descriptors -- **THEN** both sources SHALL map to the same descriptor schema with documented, deterministic field mapping rules -- **AND** when a source omits `current_value`, adapters SHALL emit `current_value` equal to the effective runtime value (or `default_value` when no runtime override exists) - -#### Scenario: UI/include isolation from shader internals -- **GIVEN** non-boundary consumer paths such as `src/ui` and `src/app` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** non-boundary consumers SHALL NOT include `render/shader/*` headers for control metadata access - -### Requirement: No Concrete FilterChain Type Exposure Outside Boundary -Backend public APIs, app code, and UI code MUST NOT depend on concrete chain headers, concrete `FilterChain*` types, or chain accessors that expose concrete internals. - -#### Scenario: Include guard in app and UI -- **GIVEN** source files under `src/app` and `src/ui` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** files under `src/app` and `src/ui` SHALL NOT include `render/chain/filter_chain.hpp` - -#### Scenario: Backend public header guard -- **GIVEN** backend public headers consumed by app/UI and downstream tests -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** backend public headers SHALL NOT expose concrete `FilterChain` types or accessors returning concrete chain internals - -#### Scenario: Type and accessor guard in app and UI -- **GIVEN** source files under `src/app` and `src/ui` -- **WHEN** boundary compliance is validated through tests and source audit -- **THEN** no direct references to concrete `FilterChain*` SHALL exist in app or UI code -- **AND** app/UI code SHALL NOT call backend chain-accessor methods that expose concrete chain internals - -### Requirement: Stable Facade API Groups for Downstream Tests -The stable downstream test surface SHALL be limited to boundary facade groups for lifecycle/preset, frame submission, controls, and prechain/policy operations. - -#### Scenario: Downstream test compile surface -- **GIVEN** downstream contract tests compile against the boundary facade -- **WHEN** tests include boundary-facing headers only -- **THEN** tests SHALL be able to exercise lifecycle/preset, frame submission, controls, and prechain/policy operations -- **AND** tests SHALL NOT require concrete chain, pass, shader-runtime, or deferred-destroy internal types - -### Requirement: Facade Active-Chain Invocation Safety -Boundary facade methods SHALL resolve active chain/runtime references at call time and SHALL NOT cache concrete chain pointers across calls. - -#### Scenario: Async swap with subsequent facade calls -- **GIVEN** an async preset reload swaps in a new active chain/runtime -- **WHEN** subsequent facade operations are invoked -- **THEN** each facade call SHALL target the current active chain/runtime reference -- **AND** facade operations SHALL NOT use stale cached concrete chain pointers from prior calls - -### Requirement: Error Model and Diagnostics Contract - -All fallible APIs MUST return `goggles_chain_status_t` and MUST NOT surface exceptions as part of the public contract. `goggles_chain_status_to_string(...)` MUST return a stable static string and unknown status values MUST map to `"UNKNOWN_STATUS"`. Structured diagnostics MUST remain limited to `goggles_chain_error_last_info_get(...)` and `goggles_fc_chain_get_diagnostic_summary(...)`; the boundary MUST NOT expose diagnostic event, artifact, or session-query retrieval APIs. - -#### Scenario: Summary supplements last-error -- **GIVEN** a READY runtime records diagnostics internally and an API call fails -- **WHEN** the host queries diagnostics through the public boundary -- **THEN** `goggles_chain_error_last_info_get(...)` SHALL still return the last error info -- **AND** `goggles_fc_chain_get_diagnostic_summary(...)` SHALL remain the only public diagnostics summary surface beyond last-error details - -#### Scenario: Diagnostics-not-active status has a stable code -- **GIVEN** a READY runtime where internal diagnostics are not active -- **WHEN** the host calls `goggles_fc_chain_get_diagnostic_summary(...)` -- **THEN** the boundary SHALL return `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` -- **AND** `goggles_chain_status_to_string(...)` SHALL map that code to `"DIAGNOSTICS_NOT_ACTIVE"` - -### Requirement: Internal Diagnostic Session Lifecycle - -The filter-chain runtime MAY manage diagnostic sessions internally, but the public boundary SHALL NOT expose session creation, query, or teardown controls. - -#### Scenario: Host does not create diagnostic session with policy -- **GIVEN** a READY filter-chain runtime instance -- **WHEN** the host uses the public filter-chain boundary -- **THEN** no boundary API SHALL accept diagnostic session creation inputs or diagnostics policy parameters -- **AND** any diagnostics policy decisions SHALL remain library-internal - -#### Scenario: Host queries diagnostic summary only -- **GIVEN** internal diagnostics state exists on a filter-chain runtime -- **WHEN** the host queries diagnostics through the boundary API -- **THEN** the boundary SHALL expose only `goggles_fc_chain_get_diagnostic_summary(...)` -- **AND** the public contract SHALL NOT include policy mode or session-state query fields - -#### Scenario: No public session state is required -- **GIVEN** a READY filter-chain runtime with no host-visible diagnostics controls -- **WHEN** the runtime records frames -- **THEN** the runtime SHALL continue operating without any public session lifecycle interaction -- **AND** only the public summary API MAY report diagnostics availability - -### Requirement: No Public Diagnostic Sink Registration - -The filter-chain boundary SHALL NOT allow host code to register or unregister diagnostic sinks or callbacks. - -#### Scenario: Host cannot register a sink adapter -- **GIVEN** a host integrates through the public filter-chain boundary -- **WHEN** it inspects available diagnostics APIs -- **THEN** no sink registration entrypoint SHALL be present -- **AND** diagnostic delivery SHALL NOT depend on host callback registration - -#### Scenario: Host cannot unregister a sink adapter -- **GIVEN** a host integrates through the public filter-chain boundary -- **WHEN** it looks for a sink-unregistration API -- **THEN** no such public API SHALL exist -- **AND** the boundary SHALL expose no sink identifier contract - -#### Scenario: Diagnostics do not promise external callback delivery -- **GIVEN** diagnostics are emitted internally by the runtime -- **WHEN** external consumers use the public boundary -- **THEN** the public contract SHALL make no guarantees about callback delivery ordering or multi-sink fanout -- **AND** host-visible diagnostics SHALL remain limited to summary retrieval - -### Requirement: Public Diagnostics Retrieval Is Summary-Only - -The filter-chain boundary SHALL expose diagnostic retrieval only through `goggles_fc_chain_get_diagnostic_summary(...)` without requiring external consumers to access runtime internals. - -#### Scenario: Host retrieves diagnostic summary counts -- **GIVEN** internal diagnostics are active and events have been emitted -- **WHEN** the host calls `goggles_fc_chain_get_diagnostic_summary(...)` -- **THEN** the boundary SHALL return the public diagnostic summary data without exposing policy mode or event stream details -- **AND** the call SHALL return `GOGGLES_CHAIN_STATUS_DIAGNOSTICS_NOT_ACTIVE` when diagnostics are not active - -#### Scenario: Host cannot retrieve events through callback registration -- **GIVEN** a runtime emits diagnostic events internally -- **WHEN** the host uses the public diagnostics boundary -- **THEN** no callback registration API SHALL be available for event delivery -- **AND** event-by-event retrieval SHALL remain outside the public contract - -#### Scenario: Summary retrieval before preset load -- **GIVEN** a runtime in CREATED state with no preset loaded -- **WHEN** the host requests diagnostics through `goggles_fc_chain_get_diagnostic_summary(...)` -- **THEN** the boundary SHALL return a not-initialized status -- **AND** no event or artifact retrieval API SHALL be available to return partial or stale diagnostics data - -### Requirement: No Public Capture Control Through Boundary API - -The filter-chain boundary SHALL NOT expose capture control or other diagnostics expansion points as part of the current public contract. - -#### Scenario: Current boundary exposes no capture requests -- **GIVEN** a host is using the current C boundary header -- **WHEN** it inspects the exported diagnostics functions -- **THEN** the implemented public diagnostics surface SHALL stop at `goggles_fc_chain_get_diagnostic_summary(...)` -- **AND** no public session lifecycle, sink registration, event retrieval, or capture request API SHALL be present - -### Requirement: Boundary Diagnostic Event Emission Does Not Break Frame Recording Contract - -Diagnostic event emission through the boundary SHALL NOT violate the existing frame recording performance contract. - -#### Scenario: Tier 0 events during record -- **GIVEN** the filter-chain is recording commands for a frame -- **WHEN** Tier 0 diagnostic events are emitted -- **THEN** no heap allocation, file I/O, shader compilation, or blocking wait SHALL occur in the event emission path - -#### Scenario: Tier 1 events with GPU timestamps -- **GIVEN** Tier 1 diagnostics are active during frame recording -- **WHEN** GPU timestamp queries are inserted for pass-level timing -- **THEN** timestamp query commands SHALL be recorded into the same command buffer -- **AND** timestamp readback SHALL occur asynchronously after frame submission, not during recording - -### Requirement: Library-Owned Support Boundary - -The extracted library SHALL own every public contract and every library-private support contract -required to build and verify itself outside the Goggles repository. Public headers and library-owned -internal code SHALL NOT depend on Goggles-private `src/util/*` headers, Goggles application config -types, or Goggles source-tree include-layout assumptions. - -#### Scenario: Public surface excludes Goggles-private support headers -- **GIVEN** an external consumer compiles against the standalone library public headers -- **WHEN** header dependencies are audited from the install include root -- **THEN** the public surface SHALL depend only on boundary-owned declarations and allowed third-party headers -- **AND** no public header SHALL require Goggles-private `src/util/*` includes or Goggles app-private types - -#### Scenario: Library-owned internals build without Goggles-private support -- **GIVEN** the standalone project builds its library sources from its own `src/` tree -- **WHEN** library-owned internal sources are compiled outside the Goggles repository -- **THEN** required support code SHALL be provided by the standalone project itself -- **AND** compilation SHALL NOT depend on Goggles-private helper ownership or Goggles checkout-relative include paths - -### Requirement: Installed Public-Surface Verification Boundary - -Reusable contract verification for the extracted library SHALL run against the installed public -surface using library-owned fixtures and assets. Goggles host integration coverage MAY verify host -wiring, but SHALL NOT replace installed-surface proof for the standalone library contract. - -#### Scenario: Installed contract tests stay boundary-only -- **GIVEN** the standalone library has been installed to a test prefix -- **WHEN** contract tests compile and run against the installed headers, libraries, and package metadata -- **THEN** those tests SHALL validate boundary behavior without including Goggles-private source headers -- **AND** passing Goggles in-tree tests alone SHALL NOT satisfy standalone contract verification - -#### Scenario: Library-owned fixtures and assets back verification -- **GIVEN** reusable contract tests exercise presets, shaders, or related runtime assets -- **WHEN** those tests run against the installed public surface -- **THEN** required fixtures and assets SHALL come from the library-owned project content -- **AND** the tests SHALL NOT depend on Goggles-owned fixture directories or shader asset paths - -### Requirement: In-Repo Subdirectory Bridge During Extraction - -The standalone filter-chain project SHALL initially reside as an in-repo subdirectory -(`filter-chain/`) within the Goggles repository. During the transitional extraction period, -Goggles SHALL consume the standalone target through `add_subdirectory(filter-chain/)` before -switching to `find_package()` consumption. The bridge period SHALL NOT introduce include-path -coupling back to the Goggles source tree. - -#### Scenario: Transitional bridge keeps Goggles building - -- **GIVEN** the standalone project skeleton exists at `filter-chain/` within the Goggles repository -- **WHEN** Goggles is configured with `add_subdirectory(filter-chain/)` as the integration path -- **THEN** the Goggles build SHALL succeed with all existing tests passing -- **AND** the standalone target SHALL expose only installed-surface include paths to the Goggles consumer - -#### Scenario: Bridge does not re-introduce include-path coupling - -- **GIVEN** Goggles consumes the standalone target through the `add_subdirectory()` bridge -- **WHEN** the standalone target's include directories are inspected -- **THEN** no include directory SHALL reference `${CMAKE_SOURCE_DIR}/src` or `${CMAKE_SOURCE_DIR}/src/render` -- **AND** Goggles code that compiles against the standalone target SHALL NOT resolve includes through Goggles source-tree layout assumptions - -#### Scenario: Bridge is removed after package-first switch - -- **GIVEN** Goggles has been switched to `find_package(GogglesFilterChain)` as the primary path -- **WHEN** the `add_subdirectory(filter-chain/)` bridge is inspected -- **THEN** the bridge MAY remain as an optional local-development convenience -- **AND** the bridge SHALL NOT be required for Goggles release acceptance - -### Requirement: Standalone Source Tree Include-Path Isolation - -All source files under the standalone project tree SHALL use standalone-relative include paths -exclusively. The standalone source tree SHALL NOT contain any include directives that assume -the Goggles source-tree layout. - -#### Scenario: No Goggles util includes in standalone tree - -- **GIVEN** all source and header files under `filter-chain/src/` -- **WHEN** include directives are audited (e.g., `grep -r '#include ` paths -- **AND** zero matches SHALL be found for `#include ` paths that reference Goggles-layout modules - -#### Scenario: Standalone internal includes use project-relative paths - -- **GIVEN** implementation files under `filter-chain/src/` -- **WHEN** they include library-internal headers -- **THEN** include paths SHALL resolve through the standalone project's own include directories -- **AND** include paths SHALL NOT depend on Goggles `${CMAKE_SOURCE_DIR}/src` being on the include path - -### Requirement: Library-Owned Support Shim Contracts - -The standalone project SHALL provide library-owned support shims for logging, profiling, and -serialization that replace the Goggles `util/logging.hpp`, `util/profiling.hpp`, and -`util/serializer.hpp` dependencies. Each shim SHALL satisfy the same interface contract as the -Goggles original while depending only on the shim's own third-party dependency (spdlog for logging, -Tracy for profiling, standard library for serializer). - -#### Scenario: Logging shim preserves tag-based facade contract - -- **GIVEN** the standalone project's library-owned logging shim -- **WHEN** chain, shader, and texture implementation files use the logging facade -- **THEN** the shim SHALL support the same tagged logging macros (e.g., `GOGGLES_LOG_INFO`, `GOGGLES_LOG_WARN`) -- **AND** log tags (`render.chain`, `render.shader`, `render.texture`) SHALL be preserved -- **AND** the shim SHALL depend only on spdlog, not on Goggles `util/logging.hpp` - -#### Scenario: Profiling shim compiles without Tracy when Tracy is unavailable - -- **GIVEN** the standalone project is built without Tracy available in the build environment -- **WHEN** profiling macros are expanded in implementation files -- **THEN** profiling macros SHALL expand to no-op expressions -- **AND** compilation SHALL succeed without Tracy headers or libraries - -#### Scenario: Serializer shim is self-contained - -- **GIVEN** the standalone project's serializer shim under `filter-chain/src/support/` -- **WHEN** `shader_runtime.cpp` uses serialization utilities -- **THEN** the shim SHALL provide the required serialization interface using only the standard library -- **AND** the shim SHALL NOT include Goggles `util/serializer.hpp` diff --git a/openspec/specs/headless-mode/spec.md b/openspec/specs/headless-mode/spec.md deleted file mode 100644 index ac4bf11c..00000000 --- a/openspec/specs/headless-mode/spec.md +++ /dev/null @@ -1,104 +0,0 @@ -# headless-mode Specification - -## Purpose -TBD - created by archiving change add-headless-mode. Update Purpose after archive. -## Requirements -### Requirement: Headless CLI Flags -The application SHALL accept `--headless`, `--frames `, and `--output ` as CLI flags. When `--headless` is present, `--frames` and `--output` MUST both be provided; missing either SHALL produce a descriptive error and exit with a non-zero code. - -#### Scenario: All three flags provided -- **WHEN** the application is run with `--headless --frames 10 --output /tmp/frame.png -- ./app` -- **THEN** `CliOptions.headless` SHALL be `true`, `frames` SHALL be `10`, and `output_path` SHALL be `/tmp/frame.png` - -#### Scenario: Missing --output with --headless -- **WHEN** the application is run with `--headless --frames 10 -- ./app` without `--output` -- **THEN** the application SHALL print a descriptive error message -- **AND** SHALL exit with a non-zero code - -#### Scenario: Missing --frames with --headless -- **WHEN** the application is run with `--headless --output /tmp/frame.png -- ./app` without `--frames` -- **THEN** the application SHALL print a descriptive error message -- **AND** SHALL exit with a non-zero code - -### Requirement: Headless Initialization Skips SDL and ImGui -When `--headless` is set, the application SHALL NOT initialize SDL, create a window, or initialize the ImGui layer. The `CompositorServer` SHALL be initialized and operational. - -#### Scenario: No SDL window in headless mode -- **GIVEN** the application is launched with `--headless` -- **WHEN** initialization completes -- **THEN** no SDL window SHALL exist -- **AND** no ImGui context SHALL be created -- **AND** `CompositorServer` SHALL report a valid Wayland display name - -#### Scenario: Child app receives Wayland display -- **GIVEN** the application is launched with `--headless -- ./test_app` -- **WHEN** the child process is spawned -- **THEN** `WAYLAND_DISPLAY` SHALL be set in the child's environment to the compositor's socket - -### Requirement: Surfaceless VulkanBackend for Headless Mode -When initialized for headless mode, `VulkanBackend` SHALL NOT create a `vk::SurfaceKHR`, swapchain, or present-related semaphores. It SHALL allocate an offscreen `vk::Image` with format `eR8G8B8A8Unorm` and usage flags `eColorAttachment | eTransferSrc` as the sole render target. - -#### Scenario: Headless factory creates no surface -- **GIVEN** `VulkanBackend::create_headless()` is called -- **WHEN** initialization completes -- **THEN** no `vk::SurfaceKHR` SHALL exist -- **AND** no swapchain SHALL exist -- **AND** the offscreen image SHALL be allocated with format `eR8G8B8A8Unorm` - -#### Scenario: Physical device selection without present queue -- **GIVEN** headless mode is active -- **WHEN** a physical device is selected -- **THEN** the selection SHALL require DMA-BUF and external memory extensions -- **AND** SHALL NOT require surface present support - -### Requirement: Headless Render Loop -In headless mode, the application SHALL run a loop that consumes compositor frames via `get_presented_frame()`, imports each DMA-BUF into `VulkanBackend`, records and submits render commands into the offscreen image, and waits on a fence before the next frame. The loop SHALL exit when the configured number of frames have been rendered. - -#### Scenario: N frames rendered then exit -- **GIVEN** the application is launched with `--headless --frames 5 --output /tmp/out.png -- ./app` -- **WHEN** 5 compositor frames have been delivered and rendered -- **THEN** the application SHALL call `readback_to_png` and write the PNG -- **AND** SHALL exit with code 0 - -#### Scenario: No vkQueuePresentKHR called -- **GIVEN** headless mode is active -- **WHEN** a frame is rendered -- **THEN** `vkQueuePresentKHR` SHALL NOT be called -- **AND** the render fence SHALL be waited on synchronously before the next frame - -### Requirement: PNG Readback and Export -After rendering the final frame, `VulkanBackend` SHALL read back the offscreen image to CPU memory using a staging buffer and write a PNG file to the configured output path using `stb_image_write_png`. The operation SHALL return `tl::expected`; write failure SHALL propagate as an error. - -#### Scenario: Successful PNG write -- **GIVEN** headless rendering of N frames is complete -- **WHEN** `readback_to_png(output_path)` is called -- **THEN** a valid PNG file SHALL exist at `output_path` -- **AND** the image dimensions SHALL match the configured compositor output resolution - -#### Scenario: PNG write failure propagated -- **GIVEN** `output_path` is in a non-writable directory -- **WHEN** `readback_to_png(output_path)` is called -- **THEN** the function SHALL return an `Error` describing the failure -- **AND** the application SHALL exit with a non-zero code - -#### Scenario: Image layout transition before readback -- **WHEN** `readback_to_png` is called after rendering -- **THEN** the offscreen image SHALL be transitioned from `eColorAttachmentOptimal` to `eTransferSrcOptimal` before `vkCmdCopyImageToBuffer` -- **AND** the staging buffer memory SHALL be invalidated before CPU read if the memory type is not host-coherent - -### Requirement: Signal-Based Shutdown in Headless Mode -In headless mode, the application SHALL handle `SIGTERM` and `SIGINT` via `signalfd`. On signal receipt the run loop SHALL exit cleanly, child processes SHALL be terminated with the same SIGTERM→SIGKILL escalation used in windowed mode, and all Vulkan resources SHALL be released before process exit. - -#### Scenario: SIGTERM triggers clean shutdown -- **GIVEN** the application is running in headless mode -- **WHEN** `SIGTERM` is delivered to the goggles process -- **THEN** the run loop SHALL exit at the next tick -- **AND** the child process SHALL receive SIGTERM -- **AND** the application SHALL exit with a non-zero code indicating signal termination - -#### Scenario: Child exit triggers headless shutdown -- **GIVEN** the application is running in headless mode with a child app -- **WHEN** the child process exits before N frames are rendered -- **THEN** the run loop SHALL exit -- **AND** the application SHALL exit with a non-zero code - diff --git a/openspec/specs/input-forwarding/spec.md b/openspec/specs/input-forwarding/spec.md deleted file mode 100644 index cbedabbc..00000000 --- a/openspec/specs/input-forwarding/spec.md +++ /dev/null @@ -1,576 +0,0 @@ -# input-forwarding Specification - -## Purpose -TBD - created by archiving change add-wayland-input-forwarding. Update Purpose after archive. -## Requirements -### Requirement: Input Forwarding Infrastructure - -The system SHALL provide a compositor server that supports both XWayland (for X11 apps) and native Wayland clients using a unified `wlr_seat` input path, and SHALL additionally support `wlr-layer-shell-unstable-v1` overlay surfaces. - -The compositor server SHALL: -- Create a headless wlroots backend -- Bind a Wayland socket for client connections -- Start XWayland server for X11 application support -- Create a wlr_seat with keyboard and pointer capabilities -- Connect XWayland to the seat via `wlr_xwayland_set_seat()` -- Create a `wlr_layer_shell_v1` global (version 4) for overlay surface support -- Run the compositor event loop on a dedicated thread - -#### Scenario: Compositor initializes with unified input -- **WHEN** the input forwarding system starts -- **THEN** a Wayland socket is created (wayland-N) -- **AND** an XWayland server is started (DISPLAY :N) -- **AND** XWayland is connected to the seat for automatic input translation -- **AND** a seat with keyboard and pointer capabilities is available -- **AND** a `zwlr_layer_shell_v1` global is advertised on the display - -### Requirement: Surface Tracking - -The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients. - -The compositor server SHALL: -- Listen for `new_toplevel` signals from xdg_shell -- Listen for `new_surface` signals from XWayland -- Maintain a unified list of active surfaces (Wayland surfaces only) -- Register destroy listeners for Wayland surfaces only -- Auto-focus the most recently mapped surface when automatic selection is active -- Preserve the manually selected surface when manual override is active -- Use mutual exclusion to manage focus between Wayland and XWayland surfaces - -**Important**: XWayland surfaces SHALL NOT use destroy listeners. XWayland destroy signals fire at -unpredictable times during normal X11 operation, causing input forwarding failures. Instead, stale -XWayland pointers are cleared during focus transitions. - -#### Scenario: Wayland client connects and receives focus -- **WHEN** a Wayland client creates an xdg_toplevel -- **AND** automatic selection is active -- **THEN** the surface is tracked by the compositor -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: XWayland client connects and receives focus -- **WHEN** an X11 app creates a window via XWayland -- **AND** automatic selection is active -- **THEN** the XWayland surface is tracked by the compositor (not in m_surfaces list) -- **AND** the new surface receives keyboard and pointer focus - -#### Scenario: New surface does not steal focus during manual override -- **GIVEN** manual surface selection is active -- **AND** surface A has keyboard and pointer focus -- **WHEN** a new surface is created -- **THEN** surface A retains keyboard and pointer focus - -#### Scenario: Wayland client disconnects -- **WHEN** a tracked Wayland surface is destroyed -- **THEN** the surface is removed from tracking via destroy listener -- **AND** if it was focused, focus is cleared - -#### Scenario: XWayland client disconnects -- **WHEN** an X11 app exits -- **THEN** no destroy listener fires (by design) -- **AND** m_focused_xsurface becomes a dangling pointer -- **AND** when a new surface gains focus, stale XWayland pointers are cleared safely - -### Requirement: Unified Keyboard Input - -The system SHALL forward keyboard events to the focused surface using `wlr_seat_keyboard_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_keyboard_enter()` on surface focus -- Use `wlr_seat_keyboard_notify_key()` for key events -- Use `wlr_seat_keyboard_notify_modifiers()` for modifier state -- Marshal events from main thread to compositor thread via eventfd - -The wlr_xwm SHALL automatically translate keyboard events to X11 for XWayland surfaces. - -#### Scenario: Key event forwarded to Wayland client -- **WHEN** user presses a key in the viewer window -- **AND** a Wayland surface has keyboard focus -- **THEN** the key event is delivered via wl_keyboard.key protocol - -#### Scenario: Key event forwarded to X11 client -- **WHEN** user presses a key in the viewer window -- **AND** an XWayland surface has keyboard focus -- **THEN** wlr_xwm translates the event to X11 KeyPress/KeyRelease -- **AND** the X11 app receives the event - -### Requirement: Unified Pointer Input - -The system SHALL forward pointer events (motion, button, axis) to the focused surface using -`wlr_seat_pointer_*` APIs. - -The implementation SHALL: -- Use `wlr_seat_pointer_enter()` on surface focus with the compositor cursor position. -- Use `wlr_seat_pointer_notify_motion()` with compositor cursor coordinates for absolute motion. -- Use `wlr_seat_pointer_notify_button()` for all button events (extended set). -- Use `wlr_seat_pointer_notify_axis()` for scroll events. -- Use `wlr_seat_pointer_notify_frame()` to group related events. -- Send relative motion via `wlr_relative_pointer_manager_v1_send_relative_motion()` using raw - deltas from the viewer (no scale adjustment). -- Respect active pointer constraints when processing motion. - -The wlr_xwm SHALL automatically translate pointer events to X11 for XWayland surfaces. - -#### Scenario: Absolute motion uses software cursor -- **WHEN** the user moves the mouse with no active pointer lock -- **THEN** `wl_pointer.motion` is sent using the compositor cursor position -- **AND** the rendered software cursor matches the motion on-screen - -#### Scenario: Relative motion remains raw under scaling -- **WHEN** a client is bound to `zwp_relative_pointer_v1` -- **THEN** the client receives raw, unscaled deltas -- **AND** relative motion is delivered regardless of viewer scaling - -### Requirement: Thread-Safe Event Marshaling - -The system SHALL marshal input events from the main thread to the compositor thread safely. - -The implementation SHALL: -- Use `SPSCQueue` for lock-free event passing (per project threading policy) -- Use eventfd for wl_event_loop wakeup notification -- Process events on compositor thread via wl_event_loop integration -- Avoid blocking the main thread during event delivery - -#### Scenario: Event delivered without blocking main thread -- **WHEN** an input event is forwarded -- **THEN** the main thread pushes to SPSCQueue and writes to eventfd -- **AND** the main thread returns immediately -- **AND** the compositor thread drains the queue and dispatches via wlr_seat_* - -### Requirement: Coordinate Handling - -The system SHALL derive compositor cursor motion from raw relative deltas and SHALL NOT rely on -viewer absolute coordinates. - -The system SHALL: -- Maintain a compositor-local cursor position in surface coordinates for the focused surface. -- Apply raw relative motion deltas to the compositor cursor position. -- Clamp cursor motion to the surface bounds and any active pointer confinement region. -- Use the compositor cursor position for `wl_pointer.enter` and `wl_pointer.motion` events. - -#### Scenario: Relative-only cursor updates -- **WHEN** the user moves the mouse with the UI overlay hidden -- **THEN** the compositor cursor updates using raw relative motion deltas -- **AND** absolute viewer coordinates are ignored - -### Requirement: Virtual Keyboard Device - -The system SHALL create a virtual keyboard device for input delivery. - -The virtual keyboard SHALL: -- Be created via `wlr_keyboard_init()` from the keyboard interface -- Have an xkb keymap configured via `wlr_keyboard_set_keymap()` -- Be attached to the seat via `wlr_seat_set_keyboard()` - -#### Scenario: Virtual keyboard provides keymap to clients -- **WHEN** a Wayland client connects and binds wl_keyboard -- **THEN** the client receives the keyboard's xkb keymap -- **AND** key events use keycodes consistent with the keymap - -### Requirement: No X11/XTest Dependencies - -The input forwarding module SHALL NOT depend on X11 or XTest libraries for input injection. - -All input SHALL be delivered through the unified `wlr_seat_*` APIs, with wlr_xwm handling X11 translation for XWayland surfaces. - -#### Scenario: Input works without X11 client connection -- **WHEN** input events are forwarded -- **THEN** no X11 Display connection is opened by the input forwarder -- **AND** no XTest extension calls are made -- **AND** wlr_xwm handles all X11 protocol translation internally - -### Requirement: Popup Surface Support - -The system SHALL support Wayland `xdg_popup` surfaces associated with a mapped `xdg_toplevel`. - -The compositor server SHALL: -- Listen for `new_popup` signals from `xdg_shell` -- Track popup surfaces separately from toplevel surfaces with parent linkage and stacking order -- Send initial configure for popups and respect ack_configure before focus changes -- Render popups above their parent surface in the presented frame -- Route pointer and keyboard events to the topmost mapped popup while a popup grab is active - -#### Scenario: Wayland menu popup renders -- **GIVEN** a Wayland toplevel is mapped -- **WHEN** the client creates and maps an `xdg_popup` (menu/dropdown) -- **THEN** the popup is configured and rendered above the parent surface -- **AND** input is delivered to the popup until it is dismissed - -#### Scenario: Popup dismissed with parent -- **GIVEN** a parent toplevel with a mapped popup -- **WHEN** the parent surface is destroyed -- **THEN** all associated popups are removed from tracking - -### Requirement: XWayland Override-Redirect Popups - -The system SHALL present XWayland override-redirect surfaces (menus/tooltips) as popups. - -The compositor server SHALL: -- Track override-redirect XWayland surfaces as transient popups -- Accept map requests for override-redirect surfaces -- Render override-redirect surfaces above the focused XWayland surface -- Route pointer and keyboard events to the topmost mapped override-redirect surface while visible - -#### Scenario: X11 menu popup renders -- **GIVEN** an XWayland surface is focused -- **WHEN** the app creates an override-redirect menu window -- **THEN** the menu is mapped and rendered above the parent surface -- **AND** pointer clicks are delivered to the menu window - -### Requirement: Relative Pointer Motion - -The system SHALL support relative pointer motion via the `zwp_relative_pointer_v1` Wayland protocol extension. - -The implementation SHALL: -- Create a `wlr_relative_pointer_manager_v1` on the Wayland display -- Send relative motion events via `wlr_relative_pointer_manager_v1_send_relative_motion()` for all pointer motion -- Forward SDL's `xrel/yrel` motion deltas to the compositor -- Send both relative and absolute motion for each pointer event (gamescope pattern) - -#### Scenario: Relative motion forwarded to FPS game -- **WHEN** user moves mouse with relative delta (dx=10, dy=-5) -- **THEN** both `wl_pointer.motion` and `zwp_relative_pointer_v1.relative_motion` events are sent -- **AND** the client receives raw, unaccelerated deltas for mouselook - -#### Scenario: Relative motion works without absolute position -- **WHEN** a game uses only relative pointer protocol (mouselook mode) -- **THEN** mouse movement translates to view rotation -- **AND** no cursor position is displayed or tracked - -### Requirement: Pointer Constraints - -The system SHALL support pointer lock and confine via the `zwp_pointer_constraints_v1` Wayland -protocol extension. - -The implementation SHALL: -- Create a `wlr_pointer_constraints_v1` manager on the Wayland display. -- Listen for `new_constraint` signals from clients. -- Listen for `set_region` signals to refresh confinement bounds. -- Activate constraints on the focused surface automatically. -- Deactivate constraints when focus changes to a different surface. -- Support both `locked_pointer` (cursor disappears) and `confined_pointer` (cursor stays in region). -- Send `activated`/`deactivated` events to inform clients of constraint state. -- Apply confinement using the constraint region in surface coordinates. -- Apply cursor hints when provided for locked constraints. - -#### Scenario: Pointer lock activated by game -- **WHEN** a game requests pointer lock via `zwp_pointer_constraints_v1.lock_pointer` -- **AND** the game's surface has focus -- **THEN** the constraint is activated -- **AND** relative motion events continue to be sent -- **AND** absolute cursor position is not updated - -#### Scenario: Pointer lock uses cursor hint -- **WHEN** a locked pointer provides a cursor hint -- **THEN** the compositor cursor position is updated to the hinted location -- **AND** absolute pointer motion remains suppressed while locked - -#### Scenario: Pointer confine region updates -- **WHEN** a client updates the pointer confine region -- **THEN** the compositor clamps cursor motion to the new region - -#### Scenario: Pointer confine restricts cursor -- **WHEN** a client requests pointer confine to a region -- **AND** user moves cursor toward the region boundary -- **THEN** cursor motion is clamped to stay within the region -- **AND** motion events reflect the clamped position - -### Requirement: Extended Button Support - -The system SHALL forward all mouse button events, not limited to left/middle/right. - -The implementation SHALL: -- Map SDL button codes to Linux `input-event-codes.h` button constants -- Support side buttons: `BTN_SIDE` (X1), `BTN_EXTRA` (X2) -- Support forward/back buttons: `BTN_FORWARD`, `BTN_BACK` -- Support task button: `BTN_TASK` -- Pass through unmapped buttons using `BTN_MISC` + offset as fallback - -#### Scenario: Side button forwarded correctly -- **WHEN** user presses mouse side button (X1) -- **THEN** `BTN_SIDE` (0x113) is forwarded to the focused surface -- **AND** the client receives the button event - -#### Scenario: Unmapped button passed through -- **WHEN** user presses an uncommon button not in standard mapping -- **THEN** the button is forwarded as `BTN_MISC` + button offset -- **AND** logging indicates the fallback mapping - -### Requirement: Surface Enumeration - -The system SHALL provide an API to enumerate all connected surfaces with metadata. - -The `SurfaceInfo` struct SHALL contain: -- `id`: Unique surface identifier (assigned on creation) -- `title`: Window title (from `WM_NAME` for XWayland, empty for XDG) -- `class_name`: Window class (from `WM_CLASS` for XWayland, empty for XDG) -- `width`, `height`: Surface dimensions in pixels -- `is_xwayland`: True for X11 surfaces, false for native Wayland -- `is_input_target`: True if this surface currently receives input - -The `CompositorServer::get_surfaces()` method SHALL return a snapshot of all tracked surfaces. - -#### Scenario: Surface list includes XWayland client -- **WHEN** an X11 app connects via XWayland -- **AND** `get_surfaces()` is called -- **THEN** the returned list includes the surface with `is_xwayland = true` -- **AND** `title` contains the X11 `WM_NAME` property value -- **AND** `class_name` contains the X11 `WM_CLASS` property value - -#### Scenario: Surface list includes Wayland client -- **WHEN** a native Wayland client creates an xdg_toplevel -- **AND** `get_surfaces()` is called -- **THEN** the returned list includes the surface with `is_xwayland = false` -- **AND** `title` and `class_name` are empty strings - -#### Scenario: Surface removed from list on disconnect -- **WHEN** a client disconnects -- **AND** `get_surfaces()` is called -- **THEN** the returned list does not include the disconnected surface - -### Requirement: Manual Input Target Selection - -The system SHALL allow manual selection of which surface receives input. - -The `CompositorServer` SHALL provide: -- `set_input_target(uint32_t surface_id)`: Route input to specified surface -- `clear_input_override()`: Revert to automatic selection (first surface) - -When a manual target is set, input SHALL be routed to that surface regardless of connection order. - -When the manual target surface disconnects, the system SHALL clear the override and revert to automatic selection. - -#### Scenario: Manual selection routes input -- **WHEN** two surfaces are connected -- **AND** `set_input_target(surface_2_id)` is called -- **THEN** input events are delivered to surface 2 -- **AND** surface 1 does not receive input - -#### Scenario: Clear override reverts to auto -- **WHEN** a manual target is active -- **AND** `clear_input_override()` is called -- **THEN** input is routed to the first connected surface (auto behavior) - -#### Scenario: Manual target disconnect clears override -- **WHEN** `set_input_target(surface_id)` is active -- **AND** the target surface disconnects -- **THEN** the override is cleared automatically -- **AND** input is routed to the first remaining surface - -### Requirement: Wlroots Logging Bridge -The compositor server SHALL route wlroots log output through the project logging utilities and respect configured verbosity. - -#### Scenario: Default log level filters wlroots debug noise -- **GIVEN** the application log level is info (default) -- **WHEN** the compositor initializes wlroots logging -- **THEN** wlroots debug logs are suppressed -- **AND** wlroots info/error logs are emitted through the project logger - -#### Scenario: Debug level exposes wlroots diagnostics -- **GIVEN** the application log level is debug or trace -- **WHEN** wlroots emits a debug log message -- **THEN** the message is emitted via the project logger at debug level - -### Requirement: Stderr Suppression Does Not Hide Wlroots Logs -The compositor server SHALL NOT suppress wlroots logs when stderr suppression is active for external helper noise. - -#### Scenario: External stderr suppression enabled -- **GIVEN** stderr suppression is enabled to reduce external helper noise -- **WHEN** wlroots emits an error log -- **THEN** the error is still visible via the project logger - -### Requirement: Compositor Public API - -The system SHALL expose input forwarding and compositor-presented surface frames via the `CompositorServer` public API, without requiring a separate forwarding wrapper. - -#### Scenario: Application integrates compositor directly -- **WHEN** the application initializes input forwarding -- **THEN** it creates and owns a `CompositorServer` -- **AND** it forwards SDL input events via `CompositorServer` methods -- **AND** it can query compositor-presented surface frames from the same instance - -### Requirement: Surface Selection Requests - -The system SHALL allow requesting focus and presentation of a specific tracked surface without -disabling automatic surface selection. - -The compositor server SHALL provide `set_input_target(uint32_t surface_id)` which: -- Focuses the requested surface on the compositor thread -- Refreshes presentation to the focused surface -- Does not suppress automatic selection of newly mapped surfaces - -#### Scenario: User selects a surface to focus -- **WHEN** two surfaces are connected -- **AND** surface A currently has keyboard and pointer focus -- **AND** `set_input_target(surface_b_id)` is called -- **THEN** surface B receives keyboard and pointer focus -- **AND** presentation switches to surface B - -#### Scenario: New surface still auto-focuses after a selection -- **GIVEN** surface B was focused via `set_input_target(surface_b_id)` -- **WHEN** a new surface C is mapped -- **THEN** surface C receives keyboard and pointer focus - -### Requirement: Compositor Software Cursor - -The system SHALL render a software cursor inside compositor-presented frames for the focused -surface. - -The compositor server SHALL: -- Track cursor position in surface-local coordinates. -- Render the cursor overlay into the compositor-presented frame buffer. -- Hide the software cursor when pointer lock is active or when input forwarding is suspended. -- Source cursor imagery via runtime cursor providers without requiring bundled cursor theme assets. -- Use a deterministic fallback chain: runtime cursor image when available, then system cursor lookup, - then a built-in generated cursor image. -- Preserve hotspot-correct placement for all cursor sources. - -#### Scenario: Cursor visible for compositor surface -- **GIVEN** the compositor is presenting a surface frame -- **AND** the focused surface does not hold a pointer lock -- **WHEN** pointer forwarding is active -- **THEN** a software cursor is rendered into the presented frame -- **AND** the cursor position matches the compositor cursor coordinates used for pointer events - -#### Scenario: Cursor hidden during pointer lock -- **GIVEN** a focused surface activates `zwp_pointer_constraints_v1.lock_pointer` -- **WHEN** pointer lock is active -- **THEN** the software cursor is hidden -- **AND** only relative pointer events continue to be delivered - -#### Scenario: Runtime cursor source unavailable -- **GIVEN** no runtime cursor image is available from the active session cursor source -- **WHEN** the compositor needs a cursor image for rendering -- **THEN** the compositor attempts system cursor lookup -- **AND** if system lookup is unavailable it renders the built-in generated fallback cursor - -#### Scenario: Cursor hidden while UI overlay is visible -- **GIVEN** the viewer UI overlay is visible -- **WHEN** pointer events are suspended for forwarded clients -- **THEN** the compositor software cursor is hidden - -### Requirement: Goggles Overlay Toggle - -The viewer application SHALL use Ctrl+Alt+Shift+Q to toggle visibility of the Goggles Overlay. - -When Goggles Overlay is hidden: -- All overlay windows SHALL be invisible -- All keyboard and mouse input SHALL be forwarded to the target application without interception -- The Ctrl+Alt+Shift+Q key combination itself SHALL NOT be forwarded (consumed by toggle) - -When Goggles Overlay is visible: -- Shader Controls and Application windows appear side-by-side (dockable by user) -- Input forwarding to target application SHALL be blocked when ImGui wants keyboard/mouse capture - -#### Scenario: User toggles overlay visibility -- **WHEN** user presses Ctrl+Alt+Shift+Q -- **AND** Goggles Overlay is currently hidden -- **THEN** the tabbed overlay windows become visible - -#### Scenario: User hides overlay -- **WHEN** user presses Ctrl+Alt+Shift+Q -- **AND** Goggles Overlay is currently visible -- **THEN** all overlay windows are hidden - -#### Scenario: All function keys forwarded to application -- **WHEN** user presses F1, F2, F3, or F4 -- **THEN** the key event is forwarded to the target application -- **AND** no viewer UI action occurs - -### Requirement: Dockable Windows - -The Goggles Overlay windows SHALL be dockable via ImGui's docking feature. - -- "Shader Controls" window for shader-related settings -- "Application" window for performance and input controls - -Users MAY dock windows together as tabs if desired. The layout is saved in imgui.ini. - -#### Scenario: User docks overlay windows -- **WHEN** Goggles Overlay is visible -- **AND** user drags "Shader Controls" onto "Application" -- **THEN** both windows become docked in a shared tab stack -- **AND** the docking layout persists in imgui.ini for the next launch - -### Requirement: Application Window - -The Application window SHALL consolidate all application and view controls. - -The window SHALL include: -- A "Performance" collapsible section containing: - - Render FPS histogram showing frame-to-frame timing - - Source FPS histogram showing captured frame cadence -- An "Input" collapsible section containing: - - A checkbox labeled "Force Enable Pointer Lock" to force pointer lock regardless of app requests - - When pointer lock is enabled, a hint SHALL display: "Press Ctrl+Alt+Shift+Q to toggle overlay" - - A list of connected surfaces for input target selection - - A "Reset to Auto" button to clear manual surface selection - -#### Scenario: User views performance metrics -- **WHEN** Application window is visible -- **AND** user expands the "Performance" section -- **THEN** FPS graphs and frame timing statistics are visible - -#### Scenario: User enables forced pointer lock -- **WHEN** Application window is visible -- **AND** user checks "Force Enable Pointer Lock" -- **THEN** the viewer window enters relative mouse mode -- **AND** pointer lock is active regardless of target application requests -- **AND** a hint displays the overlay toggle shortcut - -#### Scenario: User returns to automatic pointer lock -- **WHEN** Application window is visible -- **AND** user unchecks "Force Enable Pointer Lock" -- **THEN** pointer lock follows the target application's requests - -### Requirement: Shader Controls Window - -The Shader Controls window SHALL contain only shader-related settings. - -The window SHALL include: -- Pre-Chain Stage controls (resolution profile, parameters) -- Effect Stage controls (preset selection, parameters) -- Post-Chain Stage controls (output parameters) - -No view or application controls SHALL be present in the Shader Controls window. - -#### Scenario: User adjusts shader settings -- **WHEN** Shader Controls window is visible -- **AND** user modifies a shader parameter -- **THEN** the shader pipeline reflects the change -- **AND** no application or view settings are affected - -### Requirement: Layer Surface Keyboard Interactivity - -The system SHALL temporarily transfer seat keyboard focus to a mapped layer surface that requests -`exclusive` keyboard interactivity, without changing the surface used for frame capture or surface -enumeration. - -The compositor server SHALL: -- In the layer surface `map` handler: if `keyboard_interactive` is `exclusive`, call - `wlr_seat_keyboard_enter()` for the layer surface's `wlr_surface` -- In the layer surface `unmap` and `destroy` handlers: if `focused_surface` is non-null, restore - keyboard focus via `wlr_seat_keyboard_enter()` for `focused_surface` -- NOT modify `focused_surface` or `focused_xsurface` in any layer surface handler -- NOT include layer surfaces in `get_surfaces()` or `focus_surface_by_id()` enumeration - -#### Scenario: Exclusive layer surface takes keyboard focus -- **GIVEN** a game surface (`focused_surface`) has keyboard focus -- **WHEN** a layer surface with `exclusive` keyboard interactivity maps -- **THEN** `wlr_seat_keyboard_enter()` is called for the layer surface's `wlr_surface` -- **AND** `focused_surface` remains pointing to the game surface - -#### Scenario: Exclusive layer surface unmaps, focus restored -- **GIVEN** a layer surface with `exclusive` interactivity currently has keyboard focus -- **WHEN** the layer surface unmaps or is destroyed -- **THEN** `wlr_seat_keyboard_enter()` is called to restore focus to `focused_surface` - -#### Scenario: None-interactivity layer surface does not take keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `none` keyboard interactivity maps -- **THEN** keyboard focus remains with the game surface unchanged - -#### Scenario: Layer surface not enumerated as input target -- **WHEN** `get_surfaces()` is called -- **THEN** the returned list does not include any layer surfaces -- **AND** layer surfaces cannot be selected via `set_input_target()` - diff --git a/openspec/specs/layer-shell-overlay/spec.md b/openspec/specs/layer-shell-overlay/spec.md deleted file mode 100644 index 6d8ad8d5..00000000 --- a/openspec/specs/layer-shell-overlay/spec.md +++ /dev/null @@ -1,176 +0,0 @@ -# layer-shell-overlay Specification - -## Purpose -TBD - created by archiving change add-layer-shell-support. Update Purpose after archive. - -## Requirements - -### Requirement: Layer Shell Global - -The compositor server SHALL create and advertise a `wlr_layer_shell_v1` global (version 4) on its -Wayland display during startup. - -The compositor server SHALL: -- Call `wlr_layer_shell_v1_create(display, 4)` in `setup_layer_shell()` -- Return a `Result` error if creation fails, propagated via `GOGGLES_TRY` in `start()` -- Register a `new_surface` signal listener to receive incoming layer surface connections -- Detach the `new_surface` listener and null the pointer during `stop()` - -#### Scenario: Layer shell global created on startup -- **WHEN** `CompositorServer::start()` succeeds -- **THEN** a `zwlr_layer_shell_v1` global is available on the Wayland display -- **AND** Wayland clients can bind to it - -#### Scenario: Layer shell creation failure propagates -- **WHEN** `wlr_layer_shell_v1_create()` returns null -- **THEN** `start()` returns an error -- **AND** the compositor does not reach the running state - ---- - -### Requirement: Layer Surface Lifecycle Tracking - -The compositor server SHALL track each incoming `wlr_layer_surface_v1` with a `LayerSurfaceHooks` -struct allocated on the heap, following the existing `XdgToplevelHooks` pattern. - -`LayerSurfaceHooks` SHALL contain: -- `impl`: pointer back to `Impl` -- `layer_surface`: the `wlr_layer_surface_v1*` -- `surface`: the associated `wlr_surface*` -- `id`: unique surface identifier assigned on creation -- `layer`: the `zwlr_layer_shell_v1_layer` enum value from `pending.layer` at creation time -- `configured`: bool, set to true after the first `wlr_layer_surface_v1_configure()` call -- `mapped`: bool, toggled by map/unmap signal handlers -- Listeners: `surface_commit`, `surface_map`, `surface_unmap`, `surface_destroy`, `layer_destroy`, - `new_popup` - -The `layer_hooks` vector SHALL be guarded by `hooks_mutex` (same mutex as `xdg_hooks`). - -#### Scenario: New layer surface registered -- **WHEN** a Wayland client creates a layer surface -- **THEN** a `LayerSurfaceHooks` is allocated and pushed to `layer_hooks` -- **AND** all six signal listeners are registered - -#### Scenario: Layer surface destroyed -- **WHEN** the `layer_destroy` signal fires -- **THEN** all listeners on the hooks struct are detached -- **AND** the hooks struct is removed from `layer_hooks` and deleted - ---- - -### Requirement: Layer Surface Initial Configuration - -The compositor server SHALL send a configure event to each layer surface on its first commit, -using the headless output dimensions and the surface's requested anchor, size, and margin state. - -The compositor server SHALL: -- Check `layer_surface->initial_commit` in the `surface_commit` handler -- Compute `width` and `height` using the anchor/margin/size rules (fully-anchored → output size; - partially-anchored → output dimension on anchored axis, desired size on free axis) -- Call `wlr_layer_surface_v1_configure(layer_surface, width, height)` exactly once -- Set `hooks->configured = true` after the call -- Skip the configure call on subsequent commits - -#### Scenario: Fully-anchored layer surface configured at output size -- **GIVEN** a layer surface with all four anchor edges set -- **WHEN** the surface commits for the first time (`initial_commit == true`) -- **THEN** `wlr_layer_surface_v1_configure()` is called with the headless output width and height - -#### Scenario: Partially-anchored layer surface configured with requested size -- **GIVEN** a layer surface with only top+left+right anchors and `desired_height = 40` -- **WHEN** the surface commits for the first time -- **THEN** `wlr_layer_surface_v1_configure()` is called with output width and height 40 - -#### Scenario: Subsequent commits do not re-configure -- **GIVEN** a layer surface that has already been configured -- **WHEN** it commits again -- **THEN** `wlr_layer_surface_v1_configure()` is NOT called again - ---- - -### Requirement: Layer Surface Render Integration - -The compositor server SHALL render mapped layer surfaces in the `render_surface_to_frame()` render -pass, ordered by protocol layer before and after the primary capture surface tree. - -Render order within the pass SHALL be: -1. `ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND` -2. `ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM` -3. Primary capture surface tree (existing) -4. XWayland override-redirect popups (existing) -5. `ZWLR_LAYER_SHELL_V1_LAYER_TOP` -6. `ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY` -7. Software cursor (existing) - -`render_layer_surfaces(pass, layer)` SHALL: -- Iterate `layer_hooks` under `hooks_mutex` -- Skip hooks where `mapped == false` or `layer_surface == nullptr` -- Use `wlr_layer_surface_v1_for_each_surface()` with `render_surface_iterator` to render the - surface tree -- Compute position from `current.anchor` and `current.margin` relative to the headless output - -#### Scenario: Overlay layer surface renders above game -- **GIVEN** a game surface is the primary capture target -- **AND** a layer surface with layer `overlay` is mapped -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the layer surface is rendered after the game surface and before the cursor - -#### Scenario: Background layer surface renders below game -- **GIVEN** a layer surface with layer `background` is mapped -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the background layer surface is rendered before the game surface - -#### Scenario: Unmapped layer surface not rendered -- **GIVEN** a layer surface exists but `mapped == false` -- **WHEN** `render_surface_to_frame()` runs -- **THEN** the layer surface is not included in the render pass - ---- - -### Requirement: Layer Surface Popup Children - -The compositor server SHALL handle `xdg_popup` surfaces spawned by a mapped layer surface by -routing them through the existing `handle_new_xdg_popup()` infrastructure. - -The compositor server SHALL: -- Connect `layer_surface->events.new_popup` in `handle_new_layer_surface()` -- Forward the `wlr_xdg_popup*` to `handle_new_xdg_popup()` in the signal handler - -#### Scenario: Layer surface spawns a popup -- **GIVEN** a mapped layer surface (e.g., a Steam overlay panel) -- **WHEN** the client creates an `xdg_popup` child surface -- **THEN** the popup is tracked via `XdgPopupHooks` -- **AND** the popup is rendered via the existing popup render path - ---- - -### Requirement: Layer Surface Keyboard Interactivity - -The compositor server SHALL forward keyboard focus to a mapped layer surface that requests -`exclusive` keyboard interactivity, and restore focus to the primary capture surface on unmap or -destroy. - -The compositor server SHALL: -- In the `surface_map` handler: if `layer_surface->current.keyboard_interactive == - ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE`, call - `wlr_seat_keyboard_enter(seat, surface, ...)` for the layer surface -- In the `surface_unmap` and `layer_destroy` handlers: restore keyboard focus to - `focused_surface` (the primary capture target) if it is non-null, via `wlr_seat_keyboard_enter()` -- NOT change `focused_surface` or `focused_xsurface` in any layer surface handler - -#### Scenario: Exclusive layer surface takes keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `exclusive` keyboard interactivity maps -- **THEN** the seat's keyboard focus moves to the layer surface -- **AND** `focused_surface` is unchanged - -#### Scenario: Exclusive layer surface unmaps, focus restored -- **GIVEN** a layer surface with `exclusive` interactivity has keyboard focus -- **WHEN** the layer surface unmaps -- **THEN** keyboard focus is restored to `focused_surface` -- **AND** `focused_surface` is unchanged - -#### Scenario: None-interactivity layer surface does not take keyboard focus -- **GIVEN** a game surface has keyboard focus -- **WHEN** a layer surface with `none` keyboard interactivity maps -- **THEN** keyboard focus remains with the game surface diff --git a/openspec/specs/object-lifecycle/spec.md b/openspec/specs/object-lifecycle/spec.md deleted file mode 100644 index 81bf7382..00000000 --- a/openspec/specs/object-lifecycle/spec.md +++ /dev/null @@ -1,74 +0,0 @@ -# object-lifecycle Specification - -## Purpose -Defines factory-based initialization and `ResultPtr` lifecycle rules for major Goggles subsystems. -## Requirements -### Requirement: Factory Pattern for Complex Initialization -Major subsystem classes with fallible initialization SHALL use static factory methods returning `ResultPtr` instead of two-phase initialization (constructor + `init()`). - -**Applicability**: This requirement applies to core subsystem classes (backends, chains, passes, runtimes, receivers) that must be fully initialized before use. It does NOT apply to: -- Optional/lazy-initialized components that can validly exist in inactive states -- Utility classes where default construction is semantically valid - -#### Scenario: Successful initialization -- **WHEN** a factory method is called with valid parameters -- **THEN** the method SHALL return a `ResultPtr` containing a fully initialized object -- **AND** the object SHALL be ready for immediate use without additional initialization calls - -#### Scenario: Initialization failure -- **WHEN** initialization fails for any reason -- **THEN** the factory method SHALL return an error using `make_result_ptr_error()` -- **AND** the error SHALL include a clear error code and descriptive message -- **AND** all partially allocated resources SHALL be cleaned up automatically - -#### Scenario: Constructor visibility -- **WHEN** a class uses factory pattern -- **THEN** the constructor SHALL be private -- **AND** direct construction SHALL be prevented at compile time - -### Requirement: ResultPtr Type Alias -The codebase SHALL provide a `ResultPtr` type alias for `Result>` to simplify factory method signatures. - -#### Scenario: Type alias usage -- **WHEN** declaring a factory method -- **THEN** the return type SHALL use `ResultPtr` instead of `Result>` -- **AND** the code SHALL use `make_result_ptr()` for success cases -- **AND** the code SHALL use `make_result_ptr_error()` for error cases - -### Requirement: Classes Using Factory Pattern - -The following classes SHALL use factory pattern instead of two-phase initialization: -- VulkanBackend -- FilterChain -- FilterPass -- ShaderRuntime -- Framebuffer -- OutputPass -- CaptureReceiver -- InputForwarder (already implemented) -- Application (app orchestrator) - -#### Scenario: Factory method signature -- **WHEN** implementing a factory method -- **THEN** it SHALL be declared as `[[nodiscard]] static auto create(...) -> ResultPtr;` -- **AND** it SHALL accept all necessary initialization parameters -- **AND** it SHALL use `GOGGLES_TRY()` for nested factory calls where applicable - -### Requirement: Two-Phase Initialization SHALL NOT Be Used for Major Subsystems -Major subsystem classes (backends, chains, passes, runtimes, receivers) SHALL NOT use two-phase initialization (constructor + `init()`) pattern, as it allows objects to exist in uninitialized states, violating RAII principles. - -**Exception**: Two-phase initialization MAY be used for optional/lazy-initialized components where an "inactive" or "uninitialized" state is a valid operational mode (e.g., components that can be disabled or have zero-state configurations). - -#### Scenario: Prohibited two-phase initialization -- **WHEN** implementing a major subsystem class -- **THEN** the class SHALL NOT expose a public `init()` method -- **AND** the class SHALL use factory pattern instead - -### Requirement: App Orchestrator Uses Factory Pattern - -The app orchestrator component (e.g., `goggles::app::Application`) SHALL use the factory pattern (`create(...) -> ResultPtr`) for fallible initialization and SHALL NOT expose two-phase initialization. - -#### Scenario: Orchestrator initialization failure -- **WHEN** orchestrator initialization fails at any step -- **THEN** `create(...)` SHALL return an error `ResultPtr` -- **AND** any partially acquired resources SHALL be cleaned up automatically via RAII diff --git a/openspec/specs/packaging/spec.md b/openspec/specs/packaging/spec.md deleted file mode 100644 index f33332fe..00000000 --- a/openspec/specs/packaging/spec.md +++ /dev/null @@ -1,63 +0,0 @@ -# packaging Specification - -## Purpose -Defines AppImage packaging behavior for launching Goggles, locating bundled assets, installing shader packs, and preserving Steam-compatible command passthrough. -## Requirements -### Requirement: AppImage Distribution - -The project SHALL provide an AppImage distribution artifact for the Goggles viewer that runs on arbitrary Linux distributions without requiring root installation. - -#### Scenario: AppImage starts viewer -- **GIVEN** the user has downloaded the Goggles AppImage -- **WHEN** the user executes the AppImage -- **THEN** the Goggles viewer SHALL start successfully - -### Requirement: Steam Launch UX Compatibility - -The packaging SHALL support Steam launch options of the form `goggles -- %command%`. - -#### Scenario: Launch option passthrough -- **GIVEN** Steam is configured with launch options `goggles -- %command%` -- **WHEN** Steam launches the game -- **THEN** Goggles SHALL execute the target command exactly as provided by Steam -- **AND** it SHALL NOT require any Vulkan-layer-specific activation step - -### Requirement: Packaged Assets Are Not CWD-Dependent -The packaged runtime SHALL locate shipped assets (configuration templates and shader assets) without -relying on the current working directory. - -#### Scenario: AppImage provides a stable resource root -- **GIVEN** the Goggles AppImage is executed from an arbitrary working directory -- **WHEN** the viewer loads its default configuration template and shader assets -- **THEN** the viewer SHALL locate shipped assets via a stable `resource_dir` resolution rule -- **AND** it SHALL NOT require `./config` or `./shaders` to exist in the working directory - -### Requirement: Optional Shader Pack Install Location - -The packaging SHALL provide a way to install/update the full RetroArch shader pack (slang-shaders) into a stable user location without requiring Pixi. - -#### Scenario: Shader pack is fetched into XDG data -- **WHEN** the user invokes the AppImage shader fetch/update flow -- **THEN** the shader pack SHALL be installed under `${XDG_DATA_HOME:-$HOME/.local/share}/goggles/shaders/retroarch/` - -### Requirement: Cursor Theme Asset Exclusion - -The packaging workflow SHALL NOT ship bundled cursor-theme assets as part of Goggles distribution -artifacts. - -The packaging workflow SHALL: -- Exclude `assets/cursor` from AppImage payload content. -- Keep existing required packaged assets (configuration templates and shader assets) intact. -- Preserve software cursor runtime behavior through runtime/system/fallback cursor sourcing. - -#### Scenario: AppImage payload excludes bundled cursor theme assets -- **GIVEN** AppImage staging is executed for a release build -- **WHEN** the staged payload tree is inspected -- **THEN** `usr/share/goggles/assets/cursor` is absent -- **AND** other expected asset roots remain present - -#### Scenario: Viewer still starts with software cursor after packaging change -- **GIVEN** a packaged Goggles runtime without bundled cursor theme assets -- **WHEN** the viewer starts and presents a forwarded surface -- **THEN** software cursor rendering remains available through runtime/system/fallback cursor sourcing - diff --git a/openspec/specs/profiling/spec.md b/openspec/specs/profiling/spec.md deleted file mode 100644 index 6e7edbf3..00000000 --- a/openspec/specs/profiling/spec.md +++ /dev/null @@ -1,205 +0,0 @@ -# profiling Specification - -## Purpose -Defines Tracy instrumentation and the single-process profile session workflow used by Goggles. -## Requirements -### Requirement: Profiling Infrastructure - -The system SHALL provide a compile-time toggleable profiling infrastructure via a CMake option `ENABLE_PROFILING`. - -#### Scenario: Profiling disabled (default) - -- **WHEN** `ENABLE_PROFILING` is `OFF` (default) -- **THEN** all profiling macros SHALL expand to no-ops -- **AND** no Tracy symbols or overhead SHALL be present in the binary - -#### Scenario: Profiling enabled - -- **WHEN** `ENABLE_PROFILING` is `ON` -- **THEN** profiling macros SHALL emit Tracy instrumentation -- **AND** the application SHALL be connectable to the Tracy profiler UI - -### Requirement: Profiling Macro API - -The system SHALL provide profiling macros in `src/util/profiling.hpp` that abstract the underlying profiler implementation. - -#### Scenario: Scoped zone profiling - -- **WHEN** `GOGGLES_PROFILE_SCOPE(name)` is used -- **THEN** a named profiling zone SHALL be created that ends when the scope exits - -#### Scenario: Function profiling - -- **WHEN** `GOGGLES_PROFILE_FUNCTION()` is used -- **THEN** a profiling zone named after the enclosing function SHALL be created - -#### Scenario: Frame boundary marking - -- **WHEN** `GOGGLES_PROFILE_FRAME(name)` is used -- **THEN** a frame boundary marker SHALL be emitted for frame-rate analysis - -#### Scenario: Manual zone control - -- **WHEN** `GOGGLES_PROFILE_BEGIN(name)` and `GOGGLES_PROFILE_END()` are used as a pair -- **THEN** a profiling zone SHALL span from BEGIN to END - -#### Scenario: Zone annotation - -- **WHEN** `GOGGLES_PROFILE_TAG(text)` is used within a zone -- **THEN** the zone SHALL be annotated with the provided text - -#### Scenario: Numeric value plotting - -- **WHEN** `GOGGLES_PROFILE_VALUE(name, value)` is used -- **THEN** the numeric value SHALL be recorded for time-series visualization - -### Requirement: Render Pipeline Instrumentation - -The system SHALL instrument performance-critical render pipeline functions with profiling zones. - -#### Scenario: Frame render profiling - -- **WHEN** `VulkanBackend::render_frame()` executes -- **THEN** profiling data SHALL capture the full frame render duration - -#### Scenario: Filter chain profiling - -- **WHEN** `FilterChain::record()` executes -- **THEN** profiling data SHALL capture the filter chain execution duration - -#### Scenario: Per-pass profiling - -- **WHEN** `FilterPass::record()` executes -- **THEN** profiling data SHALL capture individual pass durations with pass identification - -### Requirement: Shader Compilation Instrumentation - -The system SHALL instrument shader compilation functions with profiling zones. - -#### Scenario: Shader compilation profiling - -- **WHEN** `ShaderRuntime::compile_shader()` executes -- **THEN** profiling data SHALL capture compilation duration - -#### Scenario: Cache operation profiling - -- **WHEN** SPIR-V cache load or save operations execute -- **THEN** profiling data SHALL capture I/O duration - -### Requirement: Compositor Capture Instrumentation - -The system SHALL instrument compositor frame export with minimal profiling so frame acquisition remains visible in the viewer trace. - -#### Scenario: Compositor frame export profiling - -- **WHEN** compositor frame preparation and DMA-BUF export execute -- **THEN** profiling data SHALL capture that frame acquisition work in the viewer trace - -### Requirement: CMake Build Preset - -The system SHALL provide a CMake preset for profiling builds. - -#### Scenario: Profile preset usage - -- **WHEN** building with `cmake --preset profile` -- **THEN** a Release build with profiling enabled SHALL be produced - -### Requirement: Unified Profile Session Command - -The system SHALL provide a profile-session command that orchestrates build, launch, capture, and -artifact generation for a Goggles viewer profile session. - -#### Scenario: Start-pattern CLI for profile sessions - -- **WHEN** a user runs `pixi run profile [goggles_args...] -- [app_args...]` -- **THEN** the command SHALL parse arguments using the same split semantics as `pixi run start` -- **AND** it SHALL launch a profiling session without requiring manual Tracy command orchestration - -### Requirement: Viewer Trace Capture - -A profile session SHALL capture the Goggles viewer/compositor process and persist one raw trace. - -#### Scenario: Capture viewer trace in one run - -- **WHEN** a profile session completes successfully -- **THEN** it SHALL produce `viewer.tracy` for the viewer/compositor process - -#### Scenario: Capture worker log is preserved - -- **WHEN** a profile session starts Tracy capture for the viewer process -- **THEN** it SHALL persist the capture worker output alongside the session artifacts - -### Requirement: Session Artifact Manifest - -The system SHALL persist machine-readable metadata for every profile session. - -#### Scenario: Manifest contains reproducibility metadata - -- **WHEN** a session finishes -- **THEN** it SHALL write a manifest describing command line, timestamps, ports, client mapping, and artifact paths -- **AND** it SHALL include exit codes and warnings encountered during capture - -### Requirement: Per-Pass GPU Timestamp Queries - -The profiling system SHALL support per-pass GPU timestamp queries that measure actual GPU execution time for each filter pass, complementing existing Tracy CPU profiling. - -#### Scenario: GPU timestamps recorded per effect pass -- GIVEN Tier 1 diagnostics are active and the GPU supports timestamp queries -- WHEN effect passes are recorded for a frame -- THEN a GPU timestamp query SHALL be written before and after each pass's draw commands -- AND the timestamp pair SHALL be associated with the pass ordinal - -#### Scenario: GPU timestamps for pre-processing and final composition -- GIVEN Tier 1 diagnostics are active -- WHEN the pre-processing region and final composition region are recorded -- THEN GPU timestamp queries SHALL bracket these regions as well -- AND timestamps SHALL be distinguishable from effect-pass timestamps by region identifier - -#### Scenario: GPU timestamp readback is asynchronous -- GIVEN GPU timestamp queries have been written during frame recording -- WHEN the frame submission completes -- THEN timestamp results SHALL be read back asynchronously (after fence signal) -- AND readback SHALL NOT stall the next frame's recording - -#### Scenario: GPU timestamps unavailable -- GIVEN the physical device reports `timestampPeriod` of zero or does not support timestamp queries -- WHEN Tier 1 diagnostics are requested -- THEN GPU timestamp collection SHALL be silently disabled -- AND a diagnostic info-severity event SHALL note that GPU timestamps are unavailable -- AND other Tier 1 diagnostics SHALL continue to function - -### Requirement: GPU Timing Integration with Execution Timeline - -Per-pass GPU timestamp results SHALL be integrated into the diagnostic execution timeline to provide a unified view of CPU and GPU pass durations. - -#### Scenario: Execution timeline includes GPU timing -- GIVEN Tier 1 diagnostics are active and GPU timestamps are available -- WHEN the execution timeline is generated for a frame -- THEN each pass event in the timeline SHALL include both CPU-side timing (from Tracy or wall clock) and GPU-side timing (from timestamp queries) - -#### Scenario: GPU timing identifies bottleneck pass -- GIVEN a frame with multiple effect passes and GPU timestamps -- WHEN the execution timeline is analyzed -- THEN the pass with the longest GPU duration SHALL be identifiable from the timeline data -- AND the timeline SHALL support sorting or ranking passes by GPU duration - -### Requirement: Profiling Debug Labels - -The profiling system SHALL insert Vulkan debug labels at pass boundaries so that GPU profiling tools and validation layers can associate work with specific passes. - -#### Scenario: Debug labels inserted per pass -- GIVEN debug label support is available (VK_EXT_debug_utils) -- WHEN effect passes are recorded -- THEN a debug label SHALL be inserted before each pass's draw commands identifying the pass by ordinal and shader name -- AND the label SHALL be ended after the pass's draw commands - -#### Scenario: Debug labels for temporal operations -- GIVEN debug label support is available -- WHEN history push and feedback copy operations are recorded -- THEN debug labels SHALL bracket these operations with descriptive names - -#### Scenario: Debug labels disabled without extension -- GIVEN the Vulkan instance or device does not support VK_EXT_debug_utils -- WHEN pass recording occurs -- THEN no debug label insertion SHALL be attempted -- AND no runtime error SHALL occur diff --git a/openspec/specs/render-pipeline/spec.md b/openspec/specs/render-pipeline/spec.md deleted file mode 100644 index b1acee24..00000000 --- a/openspec/specs/render-pipeline/spec.md +++ /dev/null @@ -1,1723 +0,0 @@ -# render-pipeline Specification - -## Purpose -Define the runtime render-pipeline contract for Goggles, including swapchain/output behavior, -filter-chain lifecycle, shader processing, and presentation-facing guarantees. -## Requirements -### Requirement: Shader Runtime Compilation - -The render shader subsystem SHALL compile Slang shaders to SPIR-V at runtime using the Slang API, supporting both HLSL-style native shaders and GLSL-style RetroArch shaders. Compilation SHALL produce structured compile report artifacts consumable by the diagnostics system. - -#### Scenario: Compilation emits structured compile report -- GIVEN a shader is compiled (cache miss) -- WHEN compilation completes -- THEN the shader runtime SHALL produce a structured compile report containing per-stage success, diagnostic messages with source locations, compilation timing, and cache state -- AND the report SHALL be emittable as a diagnostic event - -#### Scenario: Cache hit records provenance -- GIVEN a cached `.spv` file exists with matching source hash -- WHEN the shader is requested and loaded from cache -- THEN the compile report SHALL indicate cache-hit status -- AND the report SHALL preserve the source hash for session identity construction - -#### Scenario: Runtime signatures match implemented diagnostics hooks -- GIVEN diagnostics-aware compilation is requested for a RetroArch pass -- WHEN the runtime compiles that pass -- THEN the compile entry point SHALL accept separate preprocessed vertex and fragment source strings plus a module name -- AND compile-report emission SHALL remain optional so existing non-diagnostic call sites can pass `nullptr` - -### Requirement: Fullscreen Blit Pipeline - -The render backend SHALL provide a graphics pipeline for blitting imported textures to the swapchain, initialized via typed config structs. - -#### Scenario: Pipeline initialization - -- **GIVEN** valid SPIR-V bytecode from `ShaderRuntime` -- **WHEN** `OutputPass` is initialized via `init(const VulkanContext&, ShaderRuntime&, const OutputPassConfig&)` -- **THEN** pipeline and descriptor layout SHALL be created -- **AND** pipeline SHALL be created with `VkPipelineRenderingCreateInfo` specifying target format from config -- **AND** all Vulkan resources SHALL use RAII (`vk::Unique*`) - -### Requirement: Texture Sampling - -The blit pipeline SHALL sample imported textures using a Vulkan sampler with linear filtering. - -#### Scenario: Sampler configuration - -- **GIVEN** `BlitPipeline` is initialized -- **WHEN** the sampler is created -- **THEN** filter mode SHALL be `VK_FILTER_LINEAR` -- **AND** address mode SHALL be `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE` - -### Requirement: Swapchain Format Matching - -The render backend SHALL match swapchain output color space to the current source image -color-space classification to preserve pixel values. When the source classification changes without -an explicit preset change request, the pipeline SHALL recreate only backend-owned swapchain and -presentation resources, SHALL retarget the filter runtime through the installed public boundary, -and SHALL preserve source-independent preset-derived state instead of forcing a full preset reload. - -#### Scenario: Source color-space change retargets output side - -- **GIVEN** a preset is already active and rendering with an SRGB-matched output path -- **WHEN** the source image classification changes to UNORM -- **THEN** the swapchain SHALL be recreated with a matching UNORM output format -- **AND** the output-side runtime resources bound to swapchain presentation SHALL be retargeted for - the new format - -#### Scenario: Output retarget preserves active preset state - -- **GIVEN** a source color-space change triggers output-format retargeting -- **WHEN** the retarget succeeds -- **THEN** the active preset selection SHALL remain unchanged -- **AND** existing parameter overrides and control layout SHALL remain unchanged - -#### Scenario: External package retarget keeps host ownership split -- **GIVEN** Goggles consumes the filter runtime as an external standalone package -- **WHEN** the source image classification changes to require a different output format -- **THEN** Goggles SHALL recreate only host-owned swapchain and presentation resources -- **AND** the external filter runtime SHALL be retargeted through its public boundary rather than by reloading the preset - -#### Scenario: External consumption preserves preset-derived state -- **GIVEN** Goggles is linked against the standalone package and a preset is already active -- **WHEN** a format-only retarget succeeds -- **THEN** the active preset selection, control layout, and parameter overrides SHALL remain unchanged -- **AND** source-independent preset-derived runtime work SHALL remain available after the transition - -### Requirement: Pipeline Extensibility - -The render architecture SHALL support future multi-pass shader processing through a modular structure with explicit host-backend and filter-library boundaries. - -#### Scenario: Module organization - -- **GIVEN** the render module structure -- **WHEN** pipeline responsibilities are assigned -- **THEN** host backend code SHALL own swapchain, external image import, synchronization, and present -- **AND** the Goggles filter library boundary SHALL own filter-chain orchestration, shader processing, and preset texture loading internals -- **AND** app-facing filter operations SHALL be accessed via backend-facing facade methods rather than exposing concrete chain types - -#### Scenario: Stage ordering invariance - -- **GIVEN** the filter runtime executes pre-chain, effect, and post-chain stages -- **WHEN** filter boundary extraction changes ownership and call paths -- **THEN** stage execution order SHALL remain pre-chain -> effect -> post-chain - -#### Scenario: Semantic binding invariance - -- **GIVEN** shaders relying on established semantic texture bindings -- **WHEN** filter processing runs after boundary extraction -- **THEN** semantic bindings SHALL remain unchanged for `Source`, `OriginalHistory#`, `PassOutput#`, `PassFeedback#`, and `Feedback` - -#### Scenario: Error handling macros - -- **GIVEN** Vulkan API calls that return `vk::Result` -- **WHEN** error checking is needed -- **THEN** `VK_TRY(call, code, msg)` macro SHALL be used for early return -- **AND** error message SHALL include the Vulkan result string - -#### Scenario: Result propagation - -- **GIVEN** internal functions that return `Result` -- **WHEN** the result needs to be propagated to the caller -- **THEN** `GOGGLES_TRY(expr)` macro SHALL be used for early return - -### Requirement: Pass Infrastructure - -The render chain subsystem SHALL provide a Pass abstraction compatible with RetroArch shader system. - -#### Scenario: Pass interface - -- **GIVEN** a Pass implementation -- **WHEN** initialized with device, target format, num_sync_indices, and shader runtime -- **THEN** the pass SHALL create its pipeline with `VkPipelineRenderingCreateInfo` -- **AND** allocate `num_sync_indices` descriptor sets from its pool - -#### Scenario: PassContext provides rendering target - -- **GIVEN** a PassContext for recording -- **WHEN** passed to `Pass::record()` -- **THEN** it SHALL contain `target_image_view` (swapchain or intermediate image view) -- **AND** it SHALL contain `target_format` (for barrier transitions) -- **AND** it SHALL contain `source_texture` (previous pass output) -- **AND** it SHALL contain `original_texture` (normalized input) -- **AND** it SHALL contain `frame_index` for descriptor set selection -- **AND** it SHALL contain `output_extent` for viewport/scissor setup - -#### Scenario: Per-frame descriptor isolation - -- **GIVEN** num_sync_indices = 2 -- **WHEN** frame N is recording while frame N-1 is still on GPU -- **THEN** the pass SHALL update `descriptor_sets[N % 2]` -- **AND** NOT touch `descriptor_sets[(N-1) % 2]` -- **AND** no validation error SHALL occur - -### Requirement: OutputPass Behavior - -The `OutputPass` SHALL serve as the final post-chain pass, rendering to the swapchain. - -#### Scenario: OutputPass in post-chain vector - -- **GIVEN** `FilterChain` is initialized -- **THEN** `OutputPass` SHALL be stored in `m_postchain_passes` vector -- **AND** there SHALL NOT be a separate `m_output_pass` member pointer -- **AND** `m_postchain_passes.back()` SHALL reference the OutputPass - -#### Scenario: Direct DMA-BUF to swapchain (no RetroArch passes) - -- **GIVEN** no RetroArch shader passes are configured -- **WHEN** OutputPass processes a frame -- **THEN** it SHALL sample `ctx.source_texture` (the pre-chain output or DMA-BUF import) -- **AND** begin dynamic rendering with `ctx.target_image_view` -- **AND** use `ctx.frame_index` for descriptor set selection - -#### Scenario: Post-RetroArch to swapchain - -- **GIVEN** RetroArch shader passes are configured -- **WHEN** OutputPass (as final post-chain pass) processes a frame -- **THEN** it SHALL sample the previous post-chain pass output (or RetroArch output if first) -- **AND** render to the swapchain image view - -### Requirement: Future RetroArch Integration - -The Pass abstraction SHALL support future RetroArch shader passes. - -#### Scenario: Multi-pass chain (Phase 2+) - -- **GIVEN** RetroArch shader passes are configured -- **WHEN** filter chain processes a frame -- **THEN** NormalizePass (first) SHALL output "Original" texture -- **AND** RetroArch passes SHALL receive Source + Original via PassContext -- **AND** OutputPass (last) SHALL convert to swapchain format - -#### Scenario: PassContext extensibility - -- **GIVEN** RetroArch semantics require OriginalHistory, PassFeedback, etc. -- **WHEN** PassContext is extended -- **THEN** existing passes SHALL NOT require modification -- **AND** new texture bindings SHALL be added to PassContext struct - -### Requirement: Vulkan Validation Layer Support - -The render backend SHALL support optional Vulkan validation layer integration for development-time error detection. - -#### Scenario: Validation enabled via config - -- **GIVEN** `goggles.toml` has `[render] enable_validation = true` -- **WHEN** `VulkanBackend::init()` is called -- **THEN** `VK_LAYER_KHRONOS_validation` SHALL be enabled if available -- **AND** `VK_EXT_debug_utils` extension SHALL be enabled -- **AND** a debug messenger SHALL be created to capture validation messages - -#### Scenario: Validation disabled via config - -- **GIVEN** `goggles.toml` has `[render] enable_validation = false` or field is absent -- **WHEN** `VulkanBackend::init()` is called -- **THEN** no validation layers SHALL be enabled -- **AND** no debug messenger SHALL be created - -#### Scenario: Validation layer unavailable - -- **GIVEN** `VK_LAYER_KHRONOS_validation` is not installed on the system -- **WHEN** validation is requested via config -- **THEN** a warning SHALL be logged via `GOGGLES_LOG_WARN` -- **AND** instance creation SHALL proceed without validation -- **AND** no error SHALL be returned - -#### Scenario: Validation message routing - -- **GIVEN** validation layer is enabled and debug messenger is active -- **WHEN** a validation message is generated -- **THEN** ERROR severity messages SHALL be logged via `GOGGLES_LOG_ERROR` -- **AND** WARNING severity messages SHALL be logged via `GOGGLES_LOG_WARN` -- **AND** INFO severity messages SHALL be logged via `GOGGLES_LOG_DEBUG` -- **AND** VERBOSE severity messages SHALL be logged via `GOGGLES_LOG_TRACE` - -### Requirement: Validation Configuration Setting - -The application config SHALL include a setting to control Vulkan validation layer enablement. - -#### Scenario: Config field definition - -- **GIVEN** the `goggles::Config` struct -- **WHEN** `Config::Render` is defined -- **THEN** it SHALL include `bool enable_validation` field -- **AND** the default value SHALL be `false` - -#### Scenario: TOML parsing - -- **GIVEN** `goggles.toml` contains `[render] enable_validation = true` -- **WHEN** `load_config()` is called -- **THEN** `config.render.enable_validation` SHALL be `true` - -#### Scenario: Missing config field - -- **GIVEN** `goggles.toml` does not contain `enable_validation` field -- **WHEN** `load_config()` is called -- **THEN** `config.render.enable_validation` SHALL default to `false` - -### Requirement: Debug Messenger RAII - -The debug messenger resource SHALL be managed via RAII wrapper class. - -#### Scenario: Messenger creation - -- **GIVEN** validation is enabled and instance is created -- **WHEN** `VulkanDebugMessenger::create(instance)` is called -- **THEN** a `Result` SHALL be returned -- **AND** on success, the messenger SHALL be active and routing messages -- **AND** on failure, an appropriate `Error` SHALL be returned - -#### Scenario: Messenger destruction order - -- **GIVEN** `VulkanBackend` owns both instance and debug messenger -- **WHEN** `VulkanBackend` is destroyed -- **THEN** debug messenger SHALL be destroyed before instance -- **AND** no use-after-free SHALL occur - -### Requirement: Dynamic Rendering - -The render backend SHALL use Vulkan 1.3 dynamic rendering instead of traditional render passes for all rendering operations. - -#### Scenario: API version requirement - -- **GIVEN** `VulkanBackend::create_instance()` is called -- **WHEN** the Vulkan instance is created -- **THEN** `VkApplicationInfo.apiVersion` SHALL be `VK_API_VERSION_1_3` - -#### Scenario: Dynamic rendering feature enablement - -- **GIVEN** `VulkanBackend::create_device()` is called -- **WHEN** the logical device is created -- **THEN** `VkPhysicalDeviceDynamicRenderingFeatures.dynamicRendering` SHALL be enabled -- **AND** the feature SHALL be verified as supported before device creation - -#### Scenario: No VkRenderPass or VkFramebuffer objects - -- **GIVEN** the render backend is initialized -- **THEN** no `VkRenderPass` objects SHALL be created -- **AND** no `VkFramebuffer` objects SHALL be created -- **AND** pipelines SHALL be created with `VkPipelineRenderingCreateInfo` instead of `renderPass` - -#### Scenario: Command buffer rendering - -- **GIVEN** a pass records rendering commands -- **WHEN** rendering to a target image -- **THEN** `vkCmdBeginRendering()` SHALL be used instead of `vkCmdBeginRenderPass()` -- **AND** `vkCmdEndRendering()` SHALL be used instead of `vkCmdEndRenderPass()` -- **AND** `VkRenderingInfo` SHALL specify the target image view and format directly - -### Requirement: RetroArch Shader Preprocessing - -The shader subsystem SHALL preprocess RetroArch `.slang` files before compilation to handle custom pragmas. - -#### Scenario: Include resolution - -- **GIVEN** a `.slang` file with `#include "path/to/file.inc"` -- **WHEN** preprocessing is performed -- **THEN** the include directive SHALL be replaced with the file contents -- **AND** paths SHALL be resolved relative to the including file -- **AND** nested includes SHALL be resolved recursively - -#### Scenario: Stage splitting - -- **GIVEN** a `.slang` file with `#pragma stage vertex` and `#pragma stage fragment` -- **WHEN** preprocessing is performed -- **THEN** source SHALL be split into separate vertex and fragment sources -- **AND** shared declarations before first pragma SHALL be included in both stages -- **AND** pragmas SHALL be removed from output source - -#### Scenario: Parameter extraction - -- **GIVEN** a `.slang` file with `#pragma parameter NAME "Description" default min max step` -- **WHEN** preprocessing is performed -- **THEN** parameter metadata SHALL be extracted (name, description, default, min, max, step) -- **AND** pragma lines SHALL be removed from output source -- **AND** parameters SHALL be available for semantic binding - -#### Scenario: Metadata extraction - -- **GIVEN** a `.slang` file with `#pragma name ALIAS` or `#pragma format FORMAT` -- **WHEN** preprocessing is performed -- **THEN** name alias and format SHALL be extracted as pass metadata -- **AND** pragma lines SHALL be removed from output source - -### Requirement: Preset Parser - -The shader subsystem SHALL parse RetroArch `.slangp` preset files to configure multi-pass shader chains. - -#### Scenario: Preset file loading - -- **GIVEN** a `.slangp` preset file with `shaders = N` and per-pass configuration -- **WHEN** `PresetParser::load()` is called -- **THEN** a `PresetConfig` struct SHALL be returned -- **AND** it SHALL contain shader paths, scale types, filter modes, and format overrides for each pass - -#### Scenario: Scale type parsing - -- **GIVEN** a preset with `scale_type0 = source` or `scale_type0 = viewport` or `scale_type0 = absolute` -- **WHEN** preset is parsed -- **THEN** scale type SHALL be stored per pass -- **AND** `scale0` or `scale0_x`/`scale0_y` SHALL be parsed as scale factors - -#### Scenario: Filter mode parsing - -- **GIVEN** a preset with `filter_linear0 = true` or `filter_linear0 = false` -- **WHEN** preset is parsed -- **THEN** sampler filter mode SHALL be stored per pass (linear or nearest) - -#### Scenario: Framebuffer format parsing - -- **GIVEN** a preset with `float_framebuffer0 = true` or `srgb_framebuffer0 = true` -- **WHEN** preset is parsed -- **THEN** framebuffer format SHALL be stored (R16G16B16A16_SFLOAT, R8G8B8A8_SRGB, or R8G8B8A8_UNORM default) - -### Requirement: FilterPass Implementation - -FilterPass SHALL create Vulkan pipeline resources based on shader reflection data from SlangReflect. - -The pipeline creation process SHALL: -1. Query push constant size from reflection and use it for VkPushConstantRange -2. Build descriptor set layout from reflected UBO and texture bindings -3. Combine stage flags when a binding is used by both vertex and fragment stages -4. Create vertex input state matching reflected vertex shader inputs -5. Allocate descriptor pool with correct type counts for all binding types - -#### Scenario: Shader with UBO and textures -- **WHEN** a shader has UBO at binding 0 (vertex+fragment) and texture at binding 2 -- **THEN** descriptor layout includes both bindings with correct stage flags -- **AND** descriptor pool has capacity for uniform buffer and combined image sampler - -#### Scenario: Shader with extended push constants -- **WHEN** a shader's push constant block is 76 bytes -- **THEN** VkPushConstantRange.size is set to 76 -- **AND** pipeline creation succeeds without validation errors - -#### Scenario: Shader with vertex inputs -- **WHEN** a shader expects Position (location 0, vec4) and TexCoord (location 1, vec2) -- **THEN** vertex input binding description specifies correct stride -- **AND** vertex attribute descriptions specify locations 0 and 1 with R32G32B32A32_SFLOAT and R32G32_SFLOAT formats - -### Requirement: Slang Native Reflection - -SlangReflect SHALL expose additional reflection data for pipeline creation: - -1. `push_constant_size()` - total size in bytes of push constant block -2. `get_ubo_bindings()` - list of UBO bindings with set, binding, size, and stage flags -3. `get_vertex_inputs()` - list of vertex inputs with location, format, and offset - -#### Scenario: Reflect push constant size -- **WHEN** shader has a push constant block with 5 vec4 members -- **THEN** push_constant_size() returns 80 - -#### Scenario: Reflect UBO with combined stages -- **WHEN** vertex and fragment shaders both reference UBO at binding 0 -- **THEN** get_ubo_bindings() returns entry with stage_flags = VERTEX | FRAGMENT - -#### Scenario: Reflect vertex inputs -- **WHEN** vertex shader has `layout(location = 0) in vec4 Position` -- **THEN** get_vertex_inputs() includes {location: 0, format: R32G32B32A32_SFLOAT} - -### Requirement: Semantic Binder - -The chain subsystem SHALL populate shader uniforms with RetroArch semantic values based on reflection data. - -#### Scenario: Size semantic population - -- **GIVEN** a shader with `SourceSize`, `OutputSize`, `OriginalSize` in push constants -- **WHEN** semantic binder populates values before draw -- **THEN** each size SHALL be written as vec4 `[width, height, 1/width, 1/height]` -- **AND** values SHALL reflect actual texture/output dimensions - -#### Scenario: Frame counter population - -- **GIVEN** a shader with `FrameCount` in push constants -- **WHEN** semantic binder populates values -- **THEN** `FrameCount` SHALL be set to current frame number (monotonically increasing) - -#### Scenario: MVP matrix population - -- **GIVEN** a shader with `MVP` in UBO -- **WHEN** semantic binder populates UBO -- **THEN** `MVP` SHALL be set to identity matrix (or orthographic projection for proper UV mapping) - -#### Scenario: Texture binding - -- **GIVEN** a FilterPass with Source and Original texture semantics -- **WHEN** descriptor set is updated before draw -- **THEN** `Source` SHALL be bound to previous pass output (or Original for pass 0) -- **AND** `Original` SHALL be bound to the normalized captured frame - -### Requirement: zfast-crt Verification - -The RetroArch shader support SHALL be verified using the zfast-crt shader as minimal test case. - -#### Scenario: zfast-crt compilation - -- **GIVEN** the zfast-crt `.slang` file from `research/slang-shaders/crt/shaders/zfast_crt/` -- **WHEN** loaded via ShaderRuntime -- **THEN** preprocessing SHALL extract parameters (BLURSCALE, LOWLUMSCAN, etc.) -- **AND** compilation SHALL succeed without errors -- **AND** SPIR-V reflection SHALL identify expected bindings - -#### Scenario: zfast-crt rendering - -- **GIVEN** a compiled zfast-crt shader in a FilterPass -- **WHEN** rendered with a test input texture -- **THEN** output SHALL exhibit CRT-style scanlines and blur -- **AND** visual output SHALL match RetroArch reference (manual verification) - -### Requirement: Aspect Ratio Display Modes - -The output pass SHALL support four display modes for scaling captured frames to the output window, controlled by configuration. - -#### Scenario: Fit mode scales image to fit within window - -- **GIVEN** scale mode is set to `fit` -- **AND** source image has aspect ratio different from window -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be calculated to show the entire image -- **AND** the image SHALL be centered in the window -- **AND** letterbox (horizontal bars) or pillarbox (vertical bars) SHALL fill unused areas with black - -#### Scenario: Fill mode scales image to cover entire window - -- **GIVEN** scale mode is set to `fill` -- **AND** source image has aspect ratio different from window -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be calculated to cover the entire window -- **AND** the image SHALL be centered -- **AND** portions of the image extending beyond window bounds SHALL be clipped by scissor - -#### Scenario: Stretch mode matches window dimensions exactly - -- **GIVEN** scale mode is set to `stretch` -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL cover the entire window -- **AND** the image SHALL be scaled to match window dimensions exactly -- **AND** aspect ratio distortion is acceptable - -#### Scenario: Integer mode with auto scale finds maximum fit - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `0` (auto) -- **AND** source is 640x480 and window is 1920x1080 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the maximum integer scale that fits SHALL be calculated (2x = 1280x960) -- **AND** the image SHALL be centered with black borders - -#### Scenario: Integer mode with fixed scale of 1 shows original size - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `1` -- **AND** source is 640x480 and window is 1920x1080 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be 640x480 (original size) -- **AND** the image SHALL be centered with black borders - -#### Scenario: Integer mode with fixed scale multiplies source dimensions - -- **GIVEN** scale mode is set to `integer` -- **AND** integer_scale is `3` -- **AND** source is 640x480 -- **WHEN** `OutputPass::record()` renders the frame -- **THEN** the viewport SHALL be 1920x1440 (3x source) -- **AND** portions exceeding window bounds SHALL be clipped by scissor - -#### Scenario: Same aspect ratio produces identical output for fit/fill/stretch - -- **GIVEN** source image and window have the same aspect ratio -- **WHEN** fit, fill, or stretch mode is used -- **THEN** the output SHALL be identical regardless of mode -- **AND** the image SHALL fill the entire window - -### Requirement: Scale Mode Configuration - -The application config SHALL include a setting to control the display scale mode. - -#### Scenario: Config field definition - -- **GIVEN** the `goggles::Config` struct -- **WHEN** `Config::Render` is defined -- **THEN** it SHALL include `ScaleMode scale_mode` field -- **AND** the default value SHALL be `ScaleMode::Stretch` - -#### Scenario: TOML parsing for fit mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "fit"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Fit` - -#### Scenario: TOML parsing for fill mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "fill"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Fill` - -#### Scenario: TOML parsing for stretch mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "stretch"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Stretch` - -#### Scenario: TOML parsing for integer mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "integer"` -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL be `ScaleMode::Integer` - -#### Scenario: Missing config field uses default - -- **GIVEN** `goggles.toml` does not contain `scale_mode` field -- **WHEN** `load_config()` is called -- **THEN** `config.render.scale_mode` SHALL default to `ScaleMode::Stretch` - -#### Scenario: Invalid config value produces error - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "invalid_value"` -- **WHEN** `load_config()` is called -- **THEN** an error SHALL be returned -- **AND** the error message SHALL indicate the invalid value - -### Requirement: Integer Scale Configuration - -The application config SHALL include a setting to control the integer scaling multiplier when scale_mode is "integer". - -#### Scenario: Integer scale field definition - -- **GIVEN** the `goggles::Config` struct -- **WHEN** `Config::Render` is defined -- **THEN** it SHALL include `uint32_t integer_scale` field -- **AND** the default value SHALL be `0` (auto) - -#### Scenario: Integer scale only applies in integer mode - -- **GIVEN** `goggles.toml` contains `[render] scale_mode = "stretch"` and `integer_scale = 2` -- **WHEN** rendering occurs -- **THEN** the `integer_scale` value SHALL be ignored -- **AND** stretch mode behavior SHALL apply - -#### Scenario: TOML parsing for auto integer scale - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 0` -- **WHEN** `load_config()` is called -- **THEN** `config.render.integer_scale` SHALL be `0` - -#### Scenario: TOML parsing for fixed integer scale - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 3` -- **WHEN** `load_config()` is called -- **THEN** `config.render.integer_scale` SHALL be `3` - -#### Scenario: Integer scale validation - -- **GIVEN** `goggles.toml` contains `[render] integer_scale = 10` -- **WHEN** `load_config()` is called -- **THEN** an error SHALL be returned -- **AND** the error message SHALL indicate valid range is 0-8 - -### Requirement: FinalViewportSize Calculation - -The filter chain SHALL calculate `FinalViewportSize` based on the scale mode to ensure correct shader behavior when `scale_type = viewport` is used. - -#### Scenario: Stretch mode uses swapchain size - -- **GIVEN** scale mode is `stretch` -- **AND** swapchain size is 1920x1080 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1920, 1080) - -#### Scenario: Fit mode uses letterboxed effective area - -- **GIVEN** scale mode is `fit` -- **AND** swapchain size is 1920x1080 (16:9) -- **AND** source aspect ratio is 4:3 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1440, 1080) representing the effective content area -- **AND** shaders using `scale_type = viewport` SHALL render at this resolution - -#### Scenario: Fill mode uses scaled area exceeding bounds - -- **GIVEN** scale mode is `fill` -- **AND** swapchain size is 1920x1080 (16:9) -- **AND** source aspect ratio is 4:3 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1920, 1440) representing the full scaled content -- **AND** the OutputPass scissor SHALL clip to swapchain bounds - -#### Scenario: Integer mode uses source multiplied by scale factor - -- **GIVEN** scale mode is `integer` -- **AND** integer_scale is `2` -- **AND** source is 640x480 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** it SHALL be (1280, 960) -- **AND** shaders using `scale_type = viewport` SHALL render at this resolution - -#### Scenario: Integer mode auto calculates max scale - -- **GIVEN** scale mode is `integer` -- **AND** integer_scale is `0` (auto) -- **AND** source is 640x480 and swapchain is 1920x1080 -- **WHEN** `FinalViewportSize` is calculated -- **THEN** max scale SHALL be min(floor(1920/640), floor(1080/480)) = min(3, 2) = 2 -- **AND** FinalViewportSize SHALL be (1280, 960) - -#### Scenario: SemanticBinder uses calculated FinalViewportSize - -- **GIVEN** a shader pass with `FinalViewportSize` semantic -- **WHEN** SemanticBinder populates push constants -- **THEN** `FinalViewportSize` SHALL reflect the calculated value based on scale mode -- **AND** NOT the raw swapchain dimensions (except in stretch mode) - -### Requirement: Viewport Calculation Utility - -The render subsystem SHALL provide a utility function to calculate scaled viewport parameters. - -#### Scenario: Calculate fit viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Fit` -- **THEN** result SHALL have width=1440, height=1080 -- **AND** offset_x=240, offset_y=0 (centered horizontally) - -#### Scenario: Calculate fill viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Fill` -- **THEN** result SHALL have width=1920, height=1440 -- **AND** offset_x=0, offset_y=-180 (centered, extends beyond bounds) - -#### Scenario: Calculate stretch viewport - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Stretch` -- **THEN** result SHALL have width=1920, height=1080 -- **AND** offset_x=0, offset_y=0 - -#### Scenario: Calculate integer viewport with auto scale - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Integer` and integer_scale=0 -- **THEN** result SHALL have width=1280, height=960 (2x) -- **AND** offset_x=320, offset_y=60 (centered) - -#### Scenario: Calculate integer viewport with fixed scale - -- **GIVEN** source extent (640, 480) and target extent (1920, 1080) -- **WHEN** `calculate_viewport()` is called with `ScaleMode::Integer` and integer_scale=1 -- **THEN** result SHALL have width=640, height=480 -- **AND** offset_x=640, offset_y=300 (centered) - -### Requirement: PassContext Source Extent - -The PassContext struct SHALL include source image dimensions to enable aspect ratio calculations. - -#### Scenario: Source extent available for aspect ratio calculation - -- **GIVEN** a captured frame with known dimensions -- **WHEN** PassContext is created for OutputPass -- **THEN** `source_extent` SHALL contain the width and height of the source image -- **AND** the values SHALL be used for aspect ratio mode calculations - -### Requirement: Preset Texture Assets - -The filter chain SHALL load external textures listed in a RetroArch preset `textures` entry and bind them by name to matching sampler uniforms. - -#### Scenario: Mask LUTs loaded and bound -- **WHEN** a preset defining `textures = "mask_a;mask_b"` with paths for each name is loaded -- **THEN** each texture SHALL be decoded and uploaded to a GPU image -- **AND** each texture SHALL be bound to the sampler with the same name in the shader - -### Requirement: Preset Texture Sampling Overrides - -The filter chain SHALL honor per-texture sampling flags from the preset (`*_linear`, `*_mipmap`, `*_wrap_mode`). - -#### Scenario: Repeat + mipmapped mask texture -- **GIVEN** a preset sets `mask_grille_texture_large_wrap_mode = "repeat"` and `mask_grille_texture_large_mipmap = true` -- **WHEN** the preset is loaded -- **THEN** the bound sampler SHALL use repeat addressing and mipmapped sampling - -### Requirement: Alias Pass Routing - -The filter chain SHALL expose aliased pass outputs as named textures for subsequent passes, and SHALL provide `ALIASSize` push constants for aliased inputs. - -#### Scenario: Vertical scanline alias -- **GIVEN** pass 1 declares `alias1 = "VERTICAL_SCANLINES"` -- **WHEN** pass 7 samples a sampler named `VERTICAL_SCANLINES` -- **THEN** the bound image SHALL be the output of pass 1 -- **AND** `VERTICAL_SCANLINESSize` SHALL reflect the aliased texture size as vec4 - -### Requirement: Parameter Override Binding - -The filter chain SHALL apply preset parameter overrides and populate shader parameters by name into push constants or UBO members. - -#### Scenario: Override applied to UBO member -- **GIVEN** a shader defines parameter `mask_type` in its UBO -- **AND** the preset includes `mask_type = 2.0` -- **WHEN** the pass is recorded -- **THEN** the UBO member named `mask_type` SHALL be written with value `2.0` - -### Requirement: Pass Input Mipmap Control - -The filter chain SHALL honor `mipmap_inputN` when selecting sampler state for a pass input. - -#### Scenario: Mipmap input enabled -- **GIVEN** a preset sets `mipmap_input11 = true` -- **WHEN** pass 11 samples `Source` -- **THEN** the sampler bound to `Source` SHALL have mipmapping enabled - -### Requirement: DMA-BUF Import Uses Exported Plane Layout - -The render backend SHALL import DMA-BUF textures using the plane layout metadata provided by the capture layer. - -#### Scenario: Import explicit modifier + offset -- **GIVEN** the capture layer provides a DMA-BUF FD with `stride`, `offset`, and `modifier` -- **WHEN** the viewer imports the DMA-BUF via `VkImageDrmFormatModifierExplicitCreateInfoEXT` -- **THEN** the render backend SHALL set `VkSubresourceLayout.rowPitch` to the provided `stride` -- **AND** it SHALL set `VkSubresourceLayout.offset` to the provided `offset` -- **AND** it SHALL set `drmFormatModifier` to the provided `modifier` - -### Requirement: Shader Caching -The system SHALL cache compiled RetroArch shaders to disk to minimize startup latency and eliminate redundant GPU work. - -#### Scenario: Persistent Cache Lookup -- **GIVEN** a shader has been compiled once -- **WHEN** the same shader is requested again (even after app restart) -- **THEN** it SHALL be loaded from disk cache and Slang compilation SHALL be bypassed. - -#### Scenario: Serialization of Reflection -- **GIVEN** a RetroArch shader requires complex bindings (UBOs, Textures, Push Constants) -- **WHEN** cached to disk -- **THEN** the cache MUST include full `ReflectionData` and it MUST be restored correctly on cache hit, including all binding offsets and stage flags. - -#### Scenario: Automatic Invalidation -- **GIVEN** a cached shader exists -- **WHEN** the source code of that shader is modified -- **THEN** the system SHALL detect the hash mismatch and it SHALL recompile and update the cache. - -#### Scenario: Type-Safe Serialization -- **GIVEN** data being serialized to disk -- **WHEN** using `write_pod` or `read_pod` -- **THEN** the system MUST enforce `std::is_standard_layout_v` to ensure memory safety for Vulkan-specific types like bitmasks and handles. - -#### Scenario: Atomic Cache Updates -- **GIVEN** the system is writing a new cache file -- **WHEN** a crash or disk-full event occurs during the write -- **THEN** the existing valid cache file MUST NOT be corrupted -- **AND** the system SHALL use a temporary file and atomic rename to ensure cache integrity. - -#### Scenario: Integrity Validation -- **GIVEN** a potentially corrupted cache file on disk -- **WHEN** the system attempts to load it -- **THEN** it SHALL validate SPIR-V alignment and header magic/version -- **AND** it SHALL discard the corrupted file and recompile the shader if validation fails. - -#### Scenario: Minimal Log Output -- **GIVEN** the system is running at default log levels -- **WHEN** a cache hit occurs -- **THEN** it SHALL NOT output detailed per-parameter or diagnostic logs -- **AND** detailed information SHALL only be available at `TRACE` or `DEBUG` levels. - -### Requirement: Present Wait Frame Pacing - -The render backend SHALL use `VK_KHR_present_wait` when supported to pace viewer presentation and -avoid uncapped mailbox behavior on high-end GPUs. - -`render.target_fps` SHALL be treated as the effective global pacing target for the current Goggles -session. - -The viewer backend SHALL reuse the existing present-wait and CPU-throttle fallback behavior as the -viewer half of that global pacing contract. - -#### Scenario: Present wait enabled -- **GIVEN** the physical device supports `VK_KHR_present_wait` -- **WHEN** the swapchain is created -- **THEN** the device SHALL enable the extension -- **AND** the present mode SHALL be `FIFO` -- **AND** the backend SHALL use present wait to pace viewer presentation to `render.target_fps` - -#### Scenario: Uncapped target fps -- **GIVEN** `render.target_fps` is set to `0` -- **WHEN** present wait is available -- **THEN** the backend SHALL skip waiting for a target interval -- **AND** viewer presentation SHALL proceed as fast as `FIFO` allows - -#### Scenario: Present wait unsupported -- **GIVEN** `VK_KHR_present_wait` is not supported -- **WHEN** the swapchain is created -- **THEN** the backend SHALL prefer `MAILBOX` present mode -- **AND** it SHALL apply CPU-side frame capping when `render.target_fps` is non-zero -- **AND** it SHALL fall back to `FIFO` if `MAILBOX` is unavailable - -#### Scenario: Target fps changes via config or runtime control -- **GIVEN** a Goggles session has an effective `render.target_fps` -- **WHEN** configuration, CLI startup, or Application-window runtime controls change that target -- **THEN** the backend SHALL update viewer pacing to the new target without requiring restart -- **AND** `render.target_fps = 0` SHALL continue to mean uncapped pacing - -### Requirement: Pass Shader Parameter Interface - -The `Pass` base class SHALL provide virtual methods for exposing tunable shader uniforms, allowing internal passes to opt-in to runtime parameter adjustment. - -#### Scenario: Default implementation returns no parameters - -- **GIVEN** a Pass subclass that does not override parameter methods -- **WHEN** `get_shader_parameters()` is called -- **THEN** an empty vector SHALL be returned - -#### Scenario: Pass exposes shader parameters - -- **GIVEN** a Pass subclass that overrides `get_shader_parameters()` -- **WHEN** the method is called -- **THEN** a vector of `ShaderParameter` metadata SHALL be returned -- **AND** each entry SHALL include name, description, default, min, max, and step - -#### Scenario: Parameter value update - -- **GIVEN** a Pass with exposed parameters -- **WHEN** `set_shader_parameter(name, value)` is called -- **THEN** the internal parameter value SHALL be updated -- **AND** the change SHALL take effect on the next frame - -#### Scenario: FilterPass implements parameter interface - -- **GIVEN** a FilterPass with shader parameters from preprocessing -- **WHEN** `get_shader_parameters()` is called -- **THEN** it SHALL return the parameters extracted from the shader -- **AND** `set_shader_parameter()` SHALL update the parameter override map - -### Requirement: Downsample Filter Type Selection - -The DownsamplePass SHALL support runtime selection of downsampling filter algorithm via the shader parameter interface. - -#### Scenario: Area filter (default) - -- **GIVEN** DownsamplePass with `filter_type = 0` -- **WHEN** downsampling is performed -- **THEN** weighted box filter SHALL be used -- **AND** each source pixel SHALL be weighted by coverage overlap - -#### Scenario: Gaussian filter - -- **GIVEN** DownsamplePass with `filter_type = 1` -- **WHEN** downsampling is performed -- **THEN** Gaussian-weighted bilinear sampling SHALL be used -- **AND** 4 bilinear taps SHALL approximate a Gaussian kernel -- **AND** effective sampling SHALL cover 16 source texels - -#### Scenario: Nearest-neighbor filter - -- **GIVEN** DownsamplePass with `filter_type = 2` -- **WHEN** downsampling is performed -- **THEN** nearest-neighbor sampling SHALL be used -- **AND** each output pixel SHALL sample a single nearest source texel without area or gaussian weighting - -#### Scenario: Filter type exposed as parameter - -- **GIVEN** a DownsamplePass instance -- **WHEN** `get_shader_parameters()` is called -- **THEN** a parameter named `filter_type` SHALL be returned -- **AND** min SHALL be 0, max SHALL be 2, step SHALL be 1 - -#### Scenario: Filter type runtime change - -- **GIVEN** DownsamplePass is actively rendering -- **WHEN** `set_shader_parameter("filter_type", 2.0)` is called -- **THEN** the next frame SHALL use nearest-neighbor filter -- **AND** no pipeline rebuild SHALL occur - -#### Scenario: Legacy filter values remain stable - -- **GIVEN** persisted runtime state or configuration stores `filter_type = 0` or `filter_type = 1` -- **WHEN** that state is loaded by a build that supports nearest-neighbor downsampling -- **THEN** `0` SHALL continue to mean area filtering -- **AND** `1` SHALL continue to mean gaussian filtering - -#### Scenario: Persisted nearest-neighbor value remains explicit - -- **GIVEN** persisted runtime state or configuration stores `filter_type = 2` -- **WHEN** that state is loaded by a build that supports nearest-neighbor downsampling -- **THEN** `2` SHALL select nearest-neighbor filtering -- **AND** the loaded runtime state SHALL NOT reinterpret `2` as area or gaussian filtering - -### Requirement: Unified Pass Parameter UI - -The ImGui layer SHALL provide a reusable helper for rendering pass parameter controls. - -#### Scenario: Parameter sliders rendered for pass with parameters - -- **GIVEN** a Pass with non-empty `get_shader_parameters()` result -- **WHEN** the parameter UI helper is invoked -- **THEN** a slider SHALL be rendered for each parameter -- **AND** slider range SHALL use min/max from parameter metadata -- **AND** slider step SHALL use step from parameter metadata - -#### Scenario: Enum-style parameter rendered as combo box - -- **GIVEN** a parameter with step = 1 and integer min/max range -- **WHEN** the parameter UI helper is invoked -- **THEN** a combo box MAY be rendered instead of a slider -- **AND** values SHALL map to descriptive labels - -#### Scenario: No UI rendered for pass without parameters - -- **GIVEN** a Pass with empty `get_shader_parameters()` result -- **WHEN** the parameter UI helper is invoked -- **THEN** no UI elements SHALL be rendered - -#### Scenario: Parameter changes propagate to pass - -- **GIVEN** a parameter control is displayed -- **WHEN** the user adjusts the value -- **THEN** `set_shader_parameter(name, value)` SHALL be called on the pass -- **AND** the change SHALL be reflected in the next rendered frame - -### Requirement: Post-Chain Infrastructure - -The filter chain subsystem SHALL provide a generic post-chain stage that executes after RetroArch passes and before final swapchain presentation. - -#### Scenario: Post-chain as vector of passes - -- **GIVEN** `FilterChain` is initialized -- **THEN** it SHALL maintain `m_postchain_passes` as a vector of `Pass` pointers -- **AND** it SHALL maintain `m_postchain_framebuffers` as a vector of `Framebuffer` pointers -- **AND** the vectors SHALL have the same count (minus one for final output) - -#### Scenario: Post-chain execution order - -- **GIVEN** post-chain contains N passes -- **WHEN** `record_postchain()` is called -- **THEN** passes SHALL execute in vector order (0 to N-1) -- **AND** each pass output SHALL become the next pass input -- **AND** the final pass SHALL render to the swapchain - -#### Scenario: OutputPass as final post-chain entry - -- **GIVEN** `FilterChain` is initialized -- **THEN** `OutputPass` SHALL be added as the last entry in `m_postchain_passes` -- **AND** it SHALL always be present (minimum post-chain size is 1) -- **AND** no framebuffer SHALL be allocated for the final pass (renders to swapchain) - -#### Scenario: Post-chain extensibility - -- **GIVEN** a post-processing effect is needed after RetroArch passes -- **WHEN** a new pass is added to `m_postchain_passes` before OutputPass -- **THEN** it SHALL receive the RetroArch chain output as input -- **AND** its output SHALL be passed to subsequent post-chain passes - -### Requirement: Pre-Chain Stage Infrastructure - -The filter chain SHALL support a generic pre-chain stage that processes captured frames before the RetroArch shader passes. The pre-chain is a vector of passes, analogous to the RetroArch pass vector, allowing multiple preprocessing steps. - -#### Scenario: Pre-chain as extensible pass vector - -- **GIVEN** `FilterChain` is initialized -- **WHEN** pre-chain passes are configured -- **THEN** `m_prechain_passes` SHALL be a vector capable of holding multiple passes -- **AND** `m_prechain_framebuffers` SHALL be a vector of corresponding framebuffers -- **AND** passes SHALL execute in vector order - -#### Scenario: Pre-chain disabled by default - -- **GIVEN** no pre-chain passes are configured -- **WHEN** `FilterChain::record()` executes -- **THEN** captured frames SHALL pass directly to RetroArch passes (or OutputPass in passthrough mode) - -#### Scenario: Pre-chain output becomes Original for RetroArch chain - -- **GIVEN** pre-chain contains one or more passes -- **WHEN** `FilterChain::record()` executes -- **THEN** pre-chain passes SHALL execute first in vector order -- **AND** the final pre-chain output SHALL be used as `original_view` for RetroArch passes -- **AND** `OriginalSize` semantic SHALL reflect final pre-chain output dimensions - -#### Scenario: Generic pre-chain recording - -- **GIVEN** pre-chain contains N passes -- **WHEN** `record_prechain()` executes -- **THEN** each pass SHALL receive the previous pass's output as input -- **AND** image barriers SHALL be inserted between passes -- **AND** the loop SHALL NOT be hardcoded to a specific pass type - -### Requirement: Downsample Pass - -The internal pass library SHALL include a configurable downsampling pass that can be added to the pre-chain. - -#### Scenario: Area filter downsampling - -- **GIVEN** source image at `1920x1080` and target resolution `640x480` -- **WHEN** downsample pass executes with `filter_type = 0` -- **THEN** each output pixel SHALL be computed as a weighted average of covered source pixels -- **AND** the result SHALL exhibit minimal aliasing compared to point sampling - -#### Scenario: Nearest-neighbor downsampling - -- **GIVEN** source image at `1920x1080` and target resolution `640x480` -- **WHEN** downsample pass executes with `filter_type = 2` -- **THEN** each output pixel SHALL be produced from nearest-neighbor sampling of the source image -- **AND** the result SHALL preserve sharp pixel edges instead of area-averaged smoothing - -#### Scenario: Downsample added to pre-chain when configured - -- **GIVEN** source resolution is configured via `--app-width` and/or `--app-height` -- **WHEN** `FilterChain` is created -- **THEN** a `DownsamplePass` SHALL be added to `m_prechain_passes` -- **AND** a framebuffer sized to target resolution SHALL be added to `m_prechain_framebuffers` - -#### Scenario: Identity passthrough at same resolution - -- **GIVEN** source and target resolution are identical -- **WHEN** downsample pass executes with any supported `filter_type` -- **THEN** output SHALL exactly match input -- **AND** no blurring or aliasing SHALL occur - -### Requirement: Source Resolution CLI Semantics - -The `--app-width` and `--app-height` CLI options SHALL configure the downsample pass in the pre-chain. Either option may be specified alone, with the other dimension calculated to preserve aspect ratio. - -#### Scenario: Both dimensions specified - -- **GIVEN** user specifies `--app-width 640 --app-height 480` -- **WHEN** Goggles starts -- **THEN** `DownsamplePass` SHALL be added to pre-chain with target 640x480 - -#### Scenario: Only width specified preserves aspect ratio - -- **GIVEN** user specifies `--app-width 640` without `--app-height` -- **AND** captured frame is 1920x1080 (16:9 aspect ratio) -- **WHEN** first frame is processed -- **THEN** height SHALL be computed as `round(640 * 1080 / 1920) = 360` -- **AND** downsample pass target SHALL be 640x360 - -#### Scenario: Only height specified preserves aspect ratio - -- **GIVEN** user specifies `--app-height 480` without `--app-width` -- **AND** captured frame is 1920x1080 (16:9 aspect ratio) -- **WHEN** first frame is processed -- **THEN** width SHALL be computed as `round(480 * 1920 / 1080) = 853` -- **AND** downsample pass target SHALL be 853x480 - -#### Scenario: Options still set environment variables - -- **GIVEN** user specifies `--app-width` and/or `--app-height` -- **WHEN** target app is launched -- **THEN** `GOGGLES_WIDTH` and `GOGGLES_HEIGHT` environment variables SHALL be set for specified dimensions -- **AND** WSI proxy (if enabled) SHALL use these values for virtual surface sizing - -### Requirement: Shader Stage UI Organization - -The shader controls window SHALL organize controls into three collapsible sections corresponding to pipeline stages: Pre-Chain, Effect, and Post-Chain. - -#### Scenario: Pre-chain section displays downsample controls - -- **GIVEN** the shader controls window is visible -- **WHEN** the Pre-Chain section is expanded -- **THEN** resolution width and height input fields SHALL be displayed -- **AND** an "Apply" button SHALL be displayed to confirm changes - -#### Scenario: Effect section displays RetroArch controls - -- **GIVEN** the shader controls window is visible -- **WHEN** the Effect section is expanded -- **THEN** the shader enable checkbox SHALL be displayed -- **AND** the current preset label SHALL be displayed -- **AND** the available presets tree SHALL be displayed -- **AND** shader parameters SHALL be displayed if a preset is loaded - -#### Scenario: Post-chain section displays placeholder - -- **GIVEN** the shader controls window is visible -- **WHEN** the Post-Chain section is expanded -- **THEN** a label indicating "Output Blit" SHALL be displayed -- **AND** no controls SHALL be displayed - -### Requirement: Pre-Chain Pipeline Configuration - -The filter chain SHALL support runtime updates to pre-chain pipeline configuration (resolution) without requiring application restart. Pipeline configuration is distinct from shader parameters - it affects resource allocation and triggers pass rebuilds. - -#### Scenario: Resolution update triggers pass rebuild - -- **GIVEN** a pre-chain downsample pass exists -- **WHEN** `FilterChain::set_prechain_resolution(width, height)` is called with new values -- **THEN** existing pre-chain passes and framebuffers SHALL be cleared -- **AND** new passes SHALL be created on the next frame with the updated resolution - -#### Scenario: Resolution query returns current state - -- **GIVEN** a pre-chain resolution is configured -- **WHEN** `FilterChain::get_prechain_resolution()` is called -- **THEN** the current target width and height SHALL be returned - -#### Scenario: Zero resolution disables pre-chain - -- **GIVEN** pre-chain passes exist -- **WHEN** `set_prechain_resolution(0, 0)` is called -- **THEN** pre-chain processing SHALL be disabled -- **AND** captured frames SHALL pass directly to effect stage - -### Requirement: Pre-Chain UI State Synchronization - -The UI layer SHALL maintain synchronized state with the filter chain pre-chain configuration. - -#### Scenario: UI initialized from backend state - -- **GIVEN** the application starts with `--app-width 640 --app-height 480` -- **WHEN** the ImGui layer is initialized -- **THEN** the pre-chain resolution inputs SHALL display 640 and 480 - -#### Scenario: UI callback propagates changes - -- **GIVEN** the pre-chain section is visible -- **WHEN** the user changes resolution and clicks Apply -- **THEN** the pre-chain change callback SHALL be invoked -- **AND** the new resolution SHALL be passed to the filter chain - -### Requirement: External Image Format Normalization -The application SHALL represent external image metadata using `VkFormat` for all render imports. -Sources that provide DRM FourCC formats (e.g., compositor surface frames) SHALL be converted to -`VkFormat` before reaching the render backend. - -#### Scenario: Compositor surface frame conversion -- **GIVEN** a compositor frame provides DRM FourCC format metadata -- **WHEN** the frame is ingested by the application -- **THEN** the metadata SHALL be converted to the equivalent `VkFormat` -- **AND** frames with unsupported formats SHALL be skipped - -#### Scenario: Capture receiver format passthrough -- **GIVEN** a capture frame provides `VkFormat` metadata over IPC -- **WHEN** the application ingests the frame -- **THEN** the metadata SHALL be forwarded unchanged to the render backend - -### Requirement: Dynamic Scale Mode - -The viewer SHALL support a dynamic scale mode that requests source resolution changes to match the viewer window. - -#### Scenario: Dynamic mode activation - -- **GIVEN** `scale_mode = "dynamic"` in configuration -- **WHEN** the viewer window is resized -- **THEN** the viewer SHALL send a resolution request to the source -- **AND** render using fit mode until source resolution changes - -#### Scenario: Dynamic mode with WSI proxy source - -- **GIVEN** `scale_mode = "dynamic"` is configured -- **AND** source is running in WSI proxy mode -- **WHEN** the viewer window is resized -- **THEN** the source SHALL recreate its swapchain with the new resolution -- **AND** subsequent frames SHALL match the viewer window resolution - -#### Scenario: Dynamic mode with non-proxy source - -- **GIVEN** `scale_mode = "dynamic"` is configured -- **AND** source is NOT running in WSI proxy mode -- **WHEN** the viewer window is resized -- **THEN** the resolution request SHALL be ignored by the source -- **AND** the viewer SHALL fall back to fit mode behavior - -### Requirement: Preset Reference Directive - -The preset parser SHALL support `#reference` directive for including other presets. - -#### Scenario: Mega-Bezel modular preset -- **GIVEN** a preset contains `#reference "Base_CRT_Presets/MBZ__3__STD__GDV.slangp"` -- **WHEN** the preset is parsed -- **THEN** the referenced preset SHALL be loaded and merged -- **AND** paths SHALL be resolved relative to the referencing file - -#### Scenario: Nested references -- **GIVEN** preset A references preset B which references preset C -- **WHEN** parsing completes -- **THEN** all references SHALL be resolved recursively -- **AND** final config SHALL contain merged settings from all presets - -#### Scenario: Reference depth limit -- **GIVEN** a reference chain exceeds 8 levels -- **WHEN** parsing is attempted -- **THEN** an error SHALL be returned with message indicating depth exceeded - -#### Scenario: Circular reference detection -- **GIVEN** preset A references preset B which references preset A -- **WHEN** parsing is attempted -- **THEN** an error SHALL be returned indicating circular reference - -### Requirement: Frame History Access - -The filter chain SHALL maintain a ring buffer of previous frame textures and expose them as OriginalHistory[0-6] samplers. - -#### Scenario: Afterglow accesses previous frame -- **GIVEN** a shader samples `OriginalHistory0` -- **WHEN** the pass is recorded -- **THEN** `OriginalHistory0` SHALL be bound to the previous frame's Original texture -- **AND** `OriginalHistory0Size` SHALL be populated as vec4 [width, height, 1/width, 1/height] - -#### Scenario: Motion interpolation accesses multiple frames -- **GIVEN** a shader samples `OriginalHistory1` and `OriginalHistory2` -- **WHEN** the pass is recorded -- **THEN** `OriginalHistory1` SHALL be bound to frame N-2 -- **AND** `OriginalHistory2` SHALL be bound to frame N-3 - -#### Scenario: History depth auto-detection -- **GIVEN** shaders reference `OriginalHistory3` as highest index -- **WHEN** filter chain initializes -- **THEN** a ring buffer of exactly 4 frames SHALL be allocated -- **AND** unused history slots SHALL NOT be allocated - -#### Scenario: First frames without history -- **GIVEN** filter chain has processed fewer frames than history depth -- **WHEN** OriginalHistory[N] is requested for unavailable frame -- **THEN** a black texture SHALL be bound as fallback - -### Requirement: Frame Count Modulo - -The filter chain SHALL apply per-pass frame_count_mod to the FrameCount semantic. - -#### Scenario: NTSC alternating lines -- **GIVEN** pass 27 sets `frame_count_mod27 = 2` -- **AND** current absolute frame is 157 -- **WHEN** FrameCount semantic is populated for pass 27 -- **THEN** FrameCount SHALL be 157 % 2 = 1 - -#### Scenario: Different modulo per pass -- **GIVEN** pass 5 sets `frame_count_mod5 = 4` -- **AND** pass 10 sets `frame_count_mod10 = 100` -- **WHEN** passes are recorded -- **THEN** each pass SHALL receive its own modulo-applied FrameCount - -#### Scenario: No modulo uses absolute count -- **GIVEN** no frame_count_mod is set for a pass -- **WHEN** FrameCount semantic is populated -- **THEN** FrameCount SHALL be the absolute frame count - -#### Scenario: Modulo value of zero -- **GIVEN** `frame_count_mod5 = 0` is set -- **WHEN** FrameCount semantic is populated for pass 5 -- **THEN** FrameCount SHALL be the absolute frame count (0 means disabled) - -### Requirement: Rotation Semantic - -The semantic binder SHALL provide Rotation push constant for display orientation. - -#### Scenario: No rotation (landscape) -- **GIVEN** display rotation is 0 degrees -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 0 - -#### Scenario: Portrait rotation (90 degrees) -- **GIVEN** display rotation is 90 degrees clockwise -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 1 - -#### Scenario: Inverted rotation (180 degrees) -- **GIVEN** display rotation is 180 degrees -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 2 - -#### Scenario: Portrait rotation (270 degrees) -- **GIVEN** display rotation is 270 degrees clockwise -- **WHEN** Rotation semantic is populated -- **THEN** Rotation SHALL be 3 - -### Requirement: Runtime Shader Preset Reload -The render pipeline SHALL support rebuilding the RetroArch filter chain at runtime when the -application explicitly requests a new `.slangp` preset or explicitly reloads the current preset. -Explicit preset reload SHALL remain a full preset/runtime rebuild and SHALL remain distinct from -output-format retargeting caused by source color-space changes. - -#### Scenario: Explicit preset reload performs full rebuild -- **GIVEN** a preset is active and the application explicitly requests a preset reload -- **WHEN** the render pipeline handles the request -- **THEN** it SHALL perform full preset reload behavior -- **AND** preset parsing, include expansion, shader compilation/reflection, preset texture loading, - and effect-pass setup SHALL be re-executed before the replacement runtime becomes active - -#### Scenario: Output retarget is not an explicit preset reload -- **GIVEN** the active preset path and runtime controls have not changed -- **WHEN** only the source color-space classification changes -- **THEN** the pipeline SHALL NOT report or execute the event as an explicit preset reload -- **AND** the next frame after a successful transition SHALL continue using the same preset-derived - effect behavior - -#### Scenario: Explicit reload failure preserves previous runtime -- **GIVEN** the application explicitly requests a preset reload -- **WHEN** the requested reload fails before replacement activation -- **THEN** the previously active runtime SHALL remain active for rendering -- **AND** the failure SHALL be reported without leaving a partially activated replacement runtime - -### Requirement: Passthrough Mode Toggle -The render pipeline SHALL provide a passthrough mode that bypasses all filter passes and blits the captured frame directly when requested, while remembering the last successful preset for restoration. - -#### Scenario: Passthrough enabled at runtime -- **GIVEN** the Shader Controls panel requests passthrough mode -- **WHEN** the render pipeline processes the request -- **THEN** it SHALL stop invoking the filter chain and route the captured texture directly into `OutputPass` -- **AND** no RetroArch preset compilation SHALL occur while passthrough is active - -#### Scenario: Passthrough disabled restores preset -- **GIVEN** passthrough mode is active and the user turns it off -- **WHEN** the render pipeline receives the request -- **THEN** it SHALL reload the last successful preset (or the default from config if none exists) -- **AND** rendering SHALL resume using the restored filter chain without requiring an application restart - -### Requirement: Runtime Parameter Access -The render pipeline SHALL expose shader parameter metadata and runtime override capabilities so the UI layer can display and modify filter chain parameters. - -#### Scenario: Parameter list query -- **GIVEN** a filter chain with one or more passes is loaded -- **WHEN** the UI queries available parameters -- **THEN** the pipeline SHALL return a list of ShaderParameter (name, description, min, max, step, default, current value) -- **AND** parameters from all passes SHALL be accessible - -#### Scenario: Parameter override -- **GIVEN** a valid parameter name and value within bounds -- **WHEN** set_parameter_override(name, value) is called -- **THEN** the filter chain SHALL apply the override to the appropriate pass -- **AND** update_ubo_parameters() SHALL be invoked before the next render - -#### Scenario: Parameter reset -- **GIVEN** one or more parameters have been overridden -- **WHEN** clear_parameter_overrides() is called -- **THEN** all overrides SHALL be removed -- **AND** parameters SHALL revert to preset defaults on the next frame - -### Requirement: Render Scale Mode Ownership - -The render backend SHALL own the active scale mode and integer scale values and expose them for application and UI synchronization. - -#### Scenario: Query returns current runtime state - -- **GIVEN** the active scale mode has been updated at runtime -- **WHEN** the application queries the render backend for the active scale mode -- **THEN** the backend SHALL return the updated mode -- **AND** the current integer scale SHALL be available alongside it - -### Requirement: Runtime Scale Mode Switching - -The viewer SHALL allow switching the render scale mode at runtime and apply changes to subsequent frames without restart. - -#### Scenario: UI change updates active scale mode - -- **GIVEN** the shader controls window is visible -- **WHEN** the user selects a new scale mode in the Pre-Chain section -- **THEN** the active render scale mode SHALL update without restart -- **AND** subsequent frames SHALL use the new mode - -#### Scenario: Dynamic mode request uses active backend state - -- **GIVEN** the capture receiver is connected -- **WHEN** the active scale mode is `dynamic` and the swapchain extent changes or dynamic mode becomes active -- **THEN** the viewer SHALL request the source resolution to match the swapchain extent -- **AND** no request SHALL be sent when the active scale mode is not `dynamic` - -### Requirement: Pre-Chain Scale Mode Controls - -The Pre-Chain stage UI SHALL expose controls for the viewer scale mode. - -#### Scenario: Scale mode selector is available - -- **GIVEN** the shader controls window is visible -- **WHEN** the Pre-Chain section is expanded -- **THEN** a scale mode selector SHALL be displayed -- **AND** it SHALL include fit, fill, stretch, integer, and dynamic options - -#### Scenario: Integer scale input visibility - -- **GIVEN** the scale mode selector is set to `integer` -- **WHEN** the Pre-Chain section is visible -- **THEN** an integer scale input SHALL be displayed -- **AND** changes SHALL update the active integer scale at runtime - -### Requirement: Per-Surface Filter Chain Routing -The render pipeline SHALL honor a per-surface filter-chain enable flag and a session-wide global -enable flag when deciding whether to execute prechain and effect processing for a frame. - -The effect stage SHALL also respect `Shader Controls -> Effect Stage (RetroArch) -> Enable Shader`. - -When the global flag is disabled, the pipeline SHALL bypass prechain and effect stages for all -surfaces and render captured surfaces using a compositor-style maximize resize so the client -re-renders at the window size (no stretch-blit). - -When the global flag is enabled but the per-surface flag is disabled, the pipeline SHALL bypass -prechain and effect stages for that surface and render it using a compositor-style maximize resize -so the client re-renders at the window size (no stretch-blit). - -The postchain output blit SHALL remain active for presentation in all modes, including global -bypass mode. - -The per-surface mode SHALL apply to the entire xdg_toplevel surface, including all popups and -subsurfaces belonging to that toplevel. - -The runtime SHALL resolve the effective stage policy once per frame and apply it atomically so -prechain/effect stage state cannot diverge during toggle transitions. - -The runtime SHALL preserve the active policy across filter-chain recreation and async chain swap so -new chain instances start with the same effective stage policy before rendering their first frame. - -The runtime SHALL avoid startup-order-dependent behavior between source capture arrival, compositor -resize requests, and prechain target initialization. - -In direct Vulkan capture sessions, the default prechain target initialization SHALL use viewer -swapchain extent unless the user/config explicitly sets a prechain resolution. - -Compositor maximize/restore requests tied to filter policy SHALL be emitted on effective policy -transitions (or surface topology changes), not as unconditional periodic requests. - -#### Scenario: Default uses filter chain -- **GIVEN** a surface has no explicit override -- **WHEN** a frame is rendered for that surface -- **THEN** prechain and effect stages SHALL execute for that frame - -#### Scenario: Bypass filter chain for a surface -- **GIVEN** a surface has filter-chain disabled -- **WHEN** a frame is rendered for that surface -- **THEN** prechain and effect stages SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit -- **AND** the postchain output blit SHALL still present the frame - -#### Scenario: Global bypass overrides per-surface -- **GIVEN** the global filter-chain flag is disabled -- **WHEN** a frame is rendered for any surface -- **THEN** prechain and effect stages SHALL be bypassed -- **AND** the surface SHALL be rendered via a maximize-style resize without stretch-blit -- **AND** the postchain output blit SHALL still present the frame - -#### Scenario: Effect stage toggle only affects effect stage -- **GIVEN** global and per-surface filter-chain flags are enabled -- **AND** `Enable Shader` is disabled -- **WHEN** a frame is rendered -- **THEN** prechain SHALL execute -- **AND** effect stage SHALL be bypassed -- **AND** postchain output blit SHALL present the frame - -#### Scenario: Popup inherits parent mode -- **GIVEN** an xdg_toplevel surface has filter-chain disabled -- **WHEN** a popup or subsurface belonging to that toplevel is rendered -- **THEN** the popup SHALL be rendered with prechain/effect bypass and maximize-style resize without stretch-blit - -#### Scenario: Async chain swap keeps active policy -- **GIVEN** the runtime has an active effective stage policy -- **WHEN** an async shader reload completes and swaps in a new chain instance -- **THEN** the new chain SHALL use the active effective stage policy on its first rendered frame -- **AND** no frame SHALL be rendered with default stage policy values - -#### Scenario: Direct Vulkan startup is deterministic -- **GIVEN** the session uses direct Vulkan capture -- **WHEN** the application starts and filter chain is enabled -- **THEN** prechain default target initialization SHALL use viewer swapchain extent -- **AND** startup SHALL not depend on first-arrival source-frame extent - -#### Scenario: Resize requests do not oscillate during startup -- **GIVEN** startup state is settling and surfaces are enumerating -- **WHEN** effective filter policy for a surface has not changed -- **THEN** the runtime SHALL NOT repeatedly emit contradictory maximize/restore resize requests - -### Requirement: Pass Initialization Interface - -All render pass classes SHALL use a consistent initialization pattern with typed config structs and shared Vulkan context. - -#### Scenario: VulkanContext sharing - -- **GIVEN** `VulkanBackend` has initialized device and physical device -- **WHEN** any pass is initialized -- **THEN** the pass SHALL receive a `VulkanContext` reference containing both handles -- **AND** the pass SHALL NOT store redundant copies of device handles - -#### Scenario: OutputPass initialization with config - -- **GIVEN** an `OutputPassConfig` with target format, sync indices, and shader directory -- **WHEN** `OutputPass::init()` is called with `VulkanContext`, `ShaderRuntime`, and config -- **THEN** the pass SHALL initialize using the provided configuration -- **AND** the signature SHALL be `init(const VulkanContext&, ShaderRuntime&, const OutputPassConfig&)` - -#### Scenario: FilterPass initialization with config - -- **GIVEN** a `FilterPassConfig` with target format, sync indices, shader sources, and filter mode -- **WHEN** `FilterPass::init()` is called with `VulkanContext`, `ShaderRuntime`, and config -- **THEN** the pass SHALL compile shaders and create pipeline from the config -- **AND** the signature SHALL be `init(const VulkanContext&, ShaderRuntime&, const FilterPassConfig&)` - -### Requirement: Surfaceless VulkanBackend Factory -`VulkanBackend` SHALL provide a `create_headless(RenderSettings) -> ResultPtr` static factory that creates a Vulkan instance, selects a physical device, and creates a logical device and queue without requiring a `vk::SurfaceKHR`. This factory SHALL NOT create a swapchain, present semaphores, or frame-pacing resources. - -#### Scenario: Headless factory succeeds without display -- **GIVEN** a GPU supporting DMA-BUF import and external memory extensions is available -- **WHEN** `VulkanBackend::create_headless(settings)` is called -- **THEN** it SHALL return a valid `VulkanBackend` instance -- **AND** the instance SHALL hold no `vk::SurfaceKHR` or swapchain - -#### Scenario: Device selection without present support -- **GIVEN** headless mode is active -- **WHEN** a physical device is selected -- **THEN** the device SHALL be required to support `VK_EXT_external_memory_dma_buf`, `VK_EXT_image_drm_format_modifier`, and `VK_KHR_external_semaphore_fd` -- **AND** surface present support SHALL NOT be a selection criterion - -### Requirement: Offscreen Render Target Allocation -When operating in headless mode, `VulkanBackend` SHALL allocate a single `vk::Image` with format `eR8G8B8A8Unorm`, tiling `eOptimal`, and usage `eColorAttachment | eTransferSrc` as the render target. This image SHALL be used as the target passed to `FilterChain::record()` in place of a swapchain image view. - -#### Scenario: Offscreen image created at initialization -- **GIVEN** `VulkanBackend::create_headless()` completes -- **WHEN** the first render call is made -- **THEN** the offscreen image SHALL exist in device-local memory -- **AND** its format SHALL be `eR8G8B8A8Unorm` -- **AND** its dimensions SHALL match the configured compositor output resolution - -#### Scenario: Filter chain writes to offscreen image -- **GIVEN** headless mode is active and a compositor frame has been imported -- **WHEN** `render()` is called -- **THEN** `FilterChain::record()` SHALL receive the offscreen image view as its render target -- **AND** no swapchain image view SHALL be passed - -### Requirement: Headless Frame Submission Without Present -In headless mode, frame submission SHALL queue render commands and wait on a fence for GPU completion. `vkQueuePresentKHR` SHALL NOT be called. Frame pacing via `throttle_present` or `vkWaitForPresentKHR` SHALL NOT be applied. - -#### Scenario: Fence-based synchronization replaces present -- **GIVEN** headless mode is active -- **WHEN** a frame's render commands are submitted -- **THEN** `vkWaitForFences` SHALL be called to synchronize before the next frame -- **AND** `vkQueuePresentKHR` SHALL NOT be called - -### Requirement: Offscreen Image Readback to PNG -`VulkanBackend` SHALL expose `readback_to_png(std::filesystem::path) -> tl::expected` that copies the offscreen image to a host-visible staging buffer and writes a PNG via `stb_image_write_png`. The image SHALL be transitioned to `eTransferSrcOptimal` before copy and back to `eColorAttachmentOptimal` after. - -#### Scenario: Successful readback and PNG write -- **GIVEN** headless mode has completed N render frames -- **WHEN** `readback_to_png("/tmp/out.png")` is called -- **THEN** a valid PNG file SHALL be written to `/tmp/out.png` -- **AND** the image dimensions SHALL match the offscreen image extent - -#### Scenario: Staging buffer invalidated before CPU read -- **GIVEN** the staging buffer memory type is not `eHostCoherent` -- **WHEN** the GPU-to-buffer copy completes -- **THEN** `vkInvalidateMappedMemoryRanges` SHALL be called before the CPU reads the buffer - -### Requirement: Async Filter Lifecycle Safety - -The render pipeline SHALL preserve async preset reload, output-format retarget, chain swap, and -resize safety behavior after introducing the `goggles-filter-chain` boundary. - -#### Scenario: Output retarget completion is observable only after activation - -- **GIVEN** an output-format retarget is performed asynchronously -- **WHEN** the retargeted runtime becomes active for rendering -- **THEN** swap-complete notification SHALL be observable only after the retargeted runtime is active -- **AND** consumers SHALL observe the retargeted output path as current state - -#### Scenario: Output retarget failure keeps prior runtime active - -- **GIVEN** an output-format retarget attempt fails before activation -- **WHEN** host code checks active runtime state and swap-complete state -- **THEN** the previously active runtime SHALL remain the active rendering runtime -- **AND** no swap-complete indication SHALL be emitted for the failed retarget - -#### Scenario: Pending reload is retargeted before swap - -- **GIVEN** an explicit preset reload is building a pending runtime -- **AND** the authoritative source color-space classification changes before that runtime becomes - active -- **WHEN** the pending runtime is prepared for swap -- **THEN** the pending runtime SHALL be retargeted to the latest output format before activation -- **AND** the system SHALL NOT swap in a runtime bound to stale output format and immediately retarget - it afterward - -#### Scenario: Retarget does not change eager preset processing semantics - -- **GIVEN** a preset has already been processed into an active runtime -- **WHEN** a later source color-space change triggers output-format retargeting -- **THEN** the system SHALL preserve eager preset processing behavior -- **AND** it SHALL NOT defer preset parsing, compilation, or preset-texture preparation until first - use after the retarget - -#### Scenario: Resize handoff with boundary split - -- **GIVEN** resize or format changes trigger swapchain recreation -- **WHEN** host backend recreates present-path resources -- **THEN** filter runtime resize/recreation SHALL occur through boundary-facing operations -- **AND** app and UI modules SHALL NOT directly recreate concrete chain resources - -### Requirement: Reflection Conformance Gate - -The render pipeline SHALL enforce a reflection conformance gate after shader compilation that validates the merged pass contract before pass resources are created. - -#### Scenario: Strict mode rejects empty reflection -- GIVEN diagnostic policy is set to strict mode -- WHEN a shader pass compiles successfully but reflection produces an empty contract -- THEN the pipeline SHALL reject the pass and emit an error-severity diagnostic event -- AND the pass SHALL NOT be installed into the compiled chain - -#### Scenario: Compatibility mode degrades on empty reflection -- GIVEN diagnostic policy is set to compatibility mode -- WHEN a shader pass compiles successfully but reflection produces an empty contract -- THEN the pipeline SHALL install the pass with a degraded marker -- AND a warning-severity diagnostic event SHALL be emitted -- AND the degradation SHALL be recorded in the degradation ledger - -#### Scenario: Binding collision detection -- GIVEN a merged reflection contract for a pass -- WHEN two reflected resources claim the same binding slot with different types or layouts -- THEN the conformance gate SHALL reject the pass in strict mode -- AND the conformance gate SHALL emit a diagnostic event identifying both conflicting resources - -### Requirement: Diagnostic Instrumentation Points in Shader Flow - -The render pipeline SHALL emit diagnostic events at each stage of the shader processing flow to support authoring analysis. - -#### Scenario: Preset parsing emits diagnostic event -- GIVEN a preset file is being loaded -- WHEN preset parsing completes (success or failure) -- THEN the pipeline SHALL emit a diagnostic event with category "authoring" containing the normalized preset structure and any parse errors - -#### Scenario: Include expansion emits diagnostic event -- GIVEN shader source undergoes include expansion -- WHEN expansion completes for a pass -- THEN the pipeline SHALL emit a diagnostic event recording the include graph depth, cycle detection result, and expansion success or failure - -#### Scenario: Stage compilation emits diagnostic event -- GIVEN vertex and fragment stages are compiled for a pass -- WHEN compilation completes for each stage -- THEN the pipeline SHALL emit a diagnostic event per stage containing compilation success, diagnostic messages, timing, and cache-hit status - -#### Scenario: Reflection emits diagnostic event -- GIVEN compiled stages undergo reflection -- WHEN reflection completes for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the reflected resource summary and any merge conflicts - -#### Scenario: Diagnostic-aware pipeline entry points use explicit optional outputs -- GIVEN diagnostic-aware authoring analysis is enabled during preset load -- WHEN the render pipeline APIs are invoked -- THEN `ChainBuilder::build(...)` SHALL accept an optional `diagnostics::DiagnosticSession* session = nullptr` -- AND `RetroArchPreprocessor::preprocess(const std::filesystem::path& shader_path, diagnostics::SourceProvenanceMap* provenance = nullptr)` SHALL expose optional provenance capture -- AND `ShaderRuntime::compile_retroarch_shader(const std::string& vertex_source, const std::string& fragment_source, const std::string& module_name, diagnostics::CompileReport* report = nullptr)` SHALL expose optional compile-report output - -### Requirement: Source Provenance Tracking in Preprocessing - -The render pipeline's shader preprocessing stage SHALL maintain a source provenance map that tracks the origin of every line through include expansion and compatibility rewrites. - -#### Scenario: Provenance survives include expansion -- GIVEN a shader with nested includes -- WHEN preprocessing completes -- THEN every line in the expanded output SHALL have a provenance entry mapping to an original file path and line number - -#### Scenario: Provenance survives compatibility rewrites -- GIVEN a shader that undergoes compatibility text rewrites -- WHEN preprocessing completes -- THEN rewritten lines SHALL retain provenance to the original source location -- AND the provenance entry SHALL indicate that a rewrite transformation was applied - -### Requirement: Pass-Level Runtime Diagnostic Events - -The render pipeline SHALL emit diagnostic events during per-pass frame recording to support runtime validation. - -#### Scenario: Binding plan event per pass -- GIVEN effect passes are being recorded for a frame -- WHEN texture bindings are rebuilt for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the resolved binding plan with resource identities, fallback status, and extents - -#### Scenario: Semantic population event per pass -- GIVEN effect passes are being recorded for a frame -- WHEN semantic values are populated for a pass -- THEN the pipeline SHALL emit a diagnostic event containing the semantic assignment summary with destination classifications - -#### Scenario: Fallback substitution event -- GIVEN a non-source texture is missing at binding time during frame recording -- WHEN the engine substitutes the source image as a fallback -- THEN the pipeline SHALL emit a diagnostic event with the expected resource identity, the substituted resource identity, and the pass ordinal diff --git a/openspec/specs/shader-testing/spec.md b/openspec/specs/shader-testing/spec.md deleted file mode 100644 index 012eca74..00000000 --- a/openspec/specs/shader-testing/spec.md +++ /dev/null @@ -1,94 +0,0 @@ -# shader-testing Specification - -## Purpose -TBD - created by archiving change add-shader-batch-test. Update Purpose after archive. -## Requirements -### Requirement: Batch shader preset testing -The system SHALL provide a batch testing tool that validates all RetroArch shader presets for parsing and compilation compatibility. - -#### Scenario: Run batch test on all presets -Given the shaders/retroarch directory contains .slangp files -When `scripts/test_shaders.sh` is executed -Then each preset is tested for parse and compile success -And results are written to build/shader_test_results.json -And exit code is 0 if no regressions, non-zero otherwise - -#### Scenario: Filter by category -Given a category argument is provided (e.g., "crt") -When `scripts/test_shaders.sh crt` is executed -Then only presets under shaders/retroarch/crt/ are tested - -### Requirement: Machine-readable results - -The batch test tool SHALL output results in JSON format containing per-preset status, summary statistics, structured compile reports, reflection summaries, authoring verdicts, and conformance findings. - -#### Scenario: JSON output format with diagnostics -- GIVEN batch tests have completed -- WHEN results are written to build/shader_test_results.json -- THEN each preset entry SHALL include: path, parse_ok, compile_ok, error (if any), authoring_verdict, compile_report (per-stage diagnostics), reflection_summary, and conformance_findings -- AND summary SHALL include: total, passed, failed, skipped, degraded counts - -### Requirement: Structured Compile Report in Batch Testing - -The batch shader testing tool SHALL produce structured compile reports per preset that include per-stage compilation diagnostics with source-mapped locations, reflection summaries, and authoring verdicts. - -#### Scenario: Compile report includes source-mapped diagnostics -- GIVEN a preset with a shader that produces compilation warnings or errors -- WHEN the batch test processes the preset -- THEN the per-preset result SHALL include a compile report with diagnostic messages mapped to original source file paths and line numbers via the source provenance map - -#### Scenario: Compile report includes reflection summary -- GIVEN a preset with shaders that compile successfully -- WHEN the batch test processes the preset -- THEN the per-preset result SHALL include a reflection summary listing reflected resources (uniform buffer members, push constant members, texture bindings) per stage per pass - -#### Scenario: Compile report includes authoring verdict -- GIVEN a preset has been processed through parsing, compilation, and reflection -- WHEN the batch test generates the per-preset result -- THEN the result SHALL include an authoring verdict (pass, degraded, or fail) based on the diagnostics system's static validation - -### Requirement: Authoring Validation Corpus - -The shader testing infrastructure SHALL maintain categorized test presets that exercise authoring validation rules, including intentionally invalid presets. - -#### Scenario: Invalid preset corpus for parse failures -- GIVEN a set of intentionally malformed preset files -- WHEN batch testing runs the authoring validation corpus -- THEN each malformed preset SHALL produce a "fail" authoring verdict -- AND the diagnostic findings SHALL identify the specific parse failure - -#### Scenario: Invalid shader corpus for compile failures -- GIVEN a set of presets referencing intentionally invalid shader sources -- WHEN batch testing runs the authoring validation corpus -- THEN each preset SHALL produce a "fail" authoring verdict with compilation-stage errors -- AND error messages SHALL include source-mapped locations - -#### Scenario: Reflection-loss corpus -- GIVEN a set of presets where compilation succeeds but reflection produces incomplete contracts -- WHEN batch testing runs the authoring validation corpus in strict mode -- THEN each preset SHALL produce a "fail" authoring verdict -- AND the findings SHALL identify the specific passes with reflection loss - -#### Scenario: Valid presets in corpus still pass -- GIVEN the existing set of valid RetroArch presets -- WHEN batch testing runs the authoring validation corpus -- THEN all previously passing presets SHALL continue to produce "pass" authoring verdicts - -### Requirement: Semantic and Parameter Conformance Reporting in Batch Testing - -The batch shader testing tool SHALL produce parameter and semantic conformance reports that identify unresolved overrides, duplicate parameter names, and unresolved semantic destinations. - -#### Scenario: Unused override detection -- GIVEN a preset with numeric overrides that do not match any reflected parameter destination -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each unused override with the override name and the preset source location - -#### Scenario: Duplicate parameter name detection -- GIVEN a preset where multiple passes declare parameters with the same name -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each duplicate parameter name with the pass ordinals involved - -#### Scenario: Unresolved semantic destination detection -- GIVEN a pass with reflected uniform buffer members that do not match any known semantic family -- WHEN batch testing processes the preset -- THEN the conformance report SHALL flag each unresolved destination with the member name, pass ordinal, and suggested near-match if one exists diff --git a/openspec/specs/surface-frame-presentation/spec.md b/openspec/specs/surface-frame-presentation/spec.md deleted file mode 100644 index 616b35a5..00000000 --- a/openspec/specs/surface-frame-presentation/spec.md +++ /dev/null @@ -1,34 +0,0 @@ -# surface-frame-presentation Specification - -## Purpose -TBD - created by archiving change update-surface-frame-retention. Update Purpose after archive. -## Requirements -### Requirement: Preserve last surface frame on target change -The compositor presentation path SHALL retain the most recently presented frame for each surface -and use it when that surface becomes the active presentation target without waiting for a new -commit. - -#### Scenario: Manual target switch -- **GIVEN** surface A and surface B have each presented at least one frame -- **AND** surface A is currently the presentation target -- **WHEN** the user sets surface B as the manual input target -- **THEN** the compositor SHALL present surface B's most recent retained frame without waiting for - a new commit - -#### Scenario: Auto focus switch -- **GIVEN** surface A and surface B have each presented at least one frame -- **AND** surface A is currently the presentation target -- **WHEN** focus changes to surface B via auto selection -- **THEN** the compositor SHALL present surface B's most recent retained frame without waiting for - a new commit - -### Requirement: Clear presentation when no retained frame exists -The compositor presentation path SHALL clear the presented frame when the new target surface has -no retained frame to display. - -#### Scenario: Switch to a surface without a retained frame -- **GIVEN** surface A is currently presenting a frame -- **AND** surface B exists but has not yet presented a frame -- **WHEN** surface B becomes the presentation target -- **THEN** the compositor SHALL clear the presented frame - diff --git a/openspec/specs/test-client-apps/spec.md b/openspec/specs/test-client-apps/spec.md deleted file mode 100644 index 53da5969..00000000 --- a/openspec/specs/test-client-apps/spec.md +++ /dev/null @@ -1,87 +0,0 @@ -# test-client-apps Specification - -## Purpose -TBD - created by archiving change test-framework-phase1. Update Purpose after archive. -## Requirements -### Requirement: Solid color test client -The system SHALL provide a `solid_color_client` binary that connects to a Wayland compositor and renders a single solid color surface via `wl_shm`. - -#### Scenario: Default color rendering -- **GIVEN** `solid_color_client` is launched with `WAYLAND_DISPLAY` set to the compositor socket -- **WHEN** the client connects and commits its first buffer -- **THEN** the surface SHALL be filled with RGBA(255, 0, 0, 255) by default - -#### Scenario: Color override via environment variable -- **GIVEN** `TEST_COLOR=0,255,0,255` is set in the environment -- **WHEN** `solid_color_client` renders its surface -- **THEN** every pixel SHALL be RGBA(0, 255, 0, 255) - -#### Scenario: Clean exit after frame count -- **GIVEN** `solid_color_client` has committed its buffers -- **WHEN** it has rendered at least 30 stable frames (default) -- **THEN** the process SHALL exit with code 0 - -### Requirement: Quadrant test client -The system SHALL provide a `quadrant_client` binary that renders four colored quadrants at deterministic pixel positions. - -#### Scenario: Quadrant color layout -- **GIVEN** `quadrant_client` is launched and connected to the compositor -- **WHEN** it commits its first buffer -- **THEN** the top-left quadrant SHALL be red (255, 0, 0, 255) -- **AND** the top-right quadrant SHALL be green (0, 255, 0, 255) -- **AND** the bottom-left quadrant SHALL be blue (0, 0, 255, 255) -- **AND** the bottom-right quadrant SHALL be white (255, 255, 255, 255) - -#### Scenario: Quadrant boundary precision -- **GIVEN** a surface of width W and height H -- **WHEN** quadrant colors are rendered -- **THEN** the pixel at (W/2 - 1, H/2 - 1) SHALL be red -- **AND** the pixel at (W/2 + 1, H/2 - 1) SHALL be green -- **AND** the pixel at (W/2 - 1, H/2 + 1) SHALL be blue -- **AND** the pixel at (W/2 + 1, H/2 + 1) SHALL be white - -#### Scenario: Clean exit after frame count -- **GIVEN** `quadrant_client` has committed its buffers -- **WHEN** it has rendered at least 30 stable frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Gradient test client -The system SHALL provide a `gradient_client` binary that renders a horizontal linear gradient from black (left) to white (right) via `wl_shm`. - -#### Scenario: Gradient pixel values -- **GIVEN** `gradient_client` is connected and has committed its buffer -- **WHEN** the output PNG is read -- **THEN** the pixel at x=0 SHALL have R=G=B=0 (black) -- **AND** the pixel at x=W-1 SHALL have R=G=B=255 (white) -- **AND** intermediate pixels SHALL increase monotonically left to right - -#### Scenario: Clean exit after frame count -- **GIVEN** `gradient_client` has rendered at least 30 stable frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Multi-surface test client -The system SHALL provide a `multi_surface_client` binary that creates a main surface and one subsurface/popup with distinct solid colors. - -#### Scenario: Two-surface layout -- **GIVEN** `multi_surface_client` is connected and both surfaces are committed -- **WHEN** the compositor presents the scene -- **THEN** the main surface SHALL render its background color (blue: 0, 0, 255, 255) -- **AND** the popup surface SHALL render its foreground color (red: 255, 0, 0, 255) -- **AND** the popup SHALL be composited on top of the main surface at a known offset - -#### Scenario: Clean exit after frame count -- **GIVEN** both surfaces have been committed for at least 30 frames -- **THEN** the process SHALL exit with code 0 - -### Requirement: Wayland protocol compliance -All test clients SHALL use `wl_shm` shared memory buffers and comply with the `xdg-wm-base` shell protocol for toplevel surface creation. - -#### Scenario: No Vulkan dependency -- **WHEN** any test client binary is executed in an environment without a GPU or Vulkan loader -- **THEN** it SHALL connect, render, and exit successfully using only `wl_shm` - -#### Scenario: WAYLAND_DISPLAY is required -- **GIVEN** `WAYLAND_DISPLAY` is not set or the socket does not exist -- **WHEN** any test client is launched -- **THEN** the process SHALL exit with a non-zero code and print an error to stderr - diff --git a/openspec/specs/visual-regression/spec.md b/openspec/specs/visual-regression/spec.md deleted file mode 100644 index a3ffb125..00000000 --- a/openspec/specs/visual-regression/spec.md +++ /dev/null @@ -1,192 +0,0 @@ -# visual-regression Specification - -## Purpose -Defines the current Goggles visual regression contract: the image-comparison library and CLI, the headless smoke check, the aspect-ratio and shader-golden tests, the golden refresh workflow, and the labeling/scope boundaries for Goggles-owned visual regression coverage. - -## Requirements -### Requirement: Image comparison library -The system MUST provide an image-comparison library with an explicit `Image` model, `load_png(...)` loader, `compare_images(...)` overloads for whole-image and ROI comparison, and `generate_diff_heatmap(...)`. `compare_images(...)` MUST compare already-loaded `Image` values rather than file paths. - -#### Scenario: Identical images pass -- **GIVEN** two loaded images with identical pixel data -- **WHEN** `compare_images(actual, reference, tolerance)` is called with any tolerance greater than or equal to `0.0` -- **THEN** `CompareResult.passed` SHALL be `true` -- **AND** `CompareResult.max_channel_diff` SHALL be `0.0` -- **AND** `CompareResult.failing_pixels` SHALL be `0` - -#### Scenario: Differing images fail at zero tolerance -- **GIVEN** two loaded images where at least one pixel differs by one channel value -- **WHEN** `compare_images(...)` is called with `tolerance = 0.0` -- **THEN** `CompareResult.passed` SHALL be `false` -- **AND** `CompareResult.failing_pixels` SHALL be at least `1` - -#### Scenario: Tolerance allows small differences -- **GIVEN** two loaded images where all channels differ by at most `2/255` -- **WHEN** `compare_images(...)` is called with `tolerance = 2.0/255.0` -- **THEN** `CompareResult.passed` SHALL be `true` - -#### Scenario: Result metrics are populated -- **GIVEN** a comparison is performed over either the whole image or an ROI -- **WHEN** `compare_images(...)` returns -- **THEN** `CompareResult` SHALL report `max_channel_diff`, `mean_diff`, `failing_pixels`, and `failing_percentage` -- **AND** `failing_percentage` SHALL be based on the compared pixel count for the selected image region - -#### Scenario: Diff image is generated on failure -- **GIVEN** a comparison fails and a diff output path is provided -- **WHEN** `compare_images(...)` returns -- **THEN** a PNG SHALL be written at the diff path -- **AND** pixels that exceeded tolerance SHALL be highlighted in red -- **AND** non-failing pixels SHALL be shown at reduced intensity - -#### Scenario: Size mismatch is reported as comparison failure -- **GIVEN** two loaded images with different dimensions -- **WHEN** `compare_images(...)` is called -- **THEN** `CompareResult.passed` SHALL be `false` -- **AND** `CompareResult.error_message` SHALL describe the dimension mismatch - -#### Scenario: Structural similarity is optional -- **GIVEN** two loaded images of the same dimensions -- **WHEN** `compare_images(...)` is called with structural similarity enabled -- **THEN** `CompareResult.structural_similarity` SHALL report a value in the range `[0.0, 1.0]` - -#### Scenario: ROI comparison scopes the evaluated region -- **GIVEN** two loaded images and a rectangular region of interest -- **WHEN** the ROI overload of `compare_images(...)` is called -- **THEN** comparison metrics SHALL be computed only over the ROI bounds -- **AND** invalid ROI inputs SHALL produce a failed result with an explanatory error message - -#### Scenario: Heatmap generation is available as a separate helper -- **GIVEN** two loaded images of the same dimensions -- **WHEN** `generate_diff_heatmap(...)` is called -- **THEN** the helper SHALL write a heatmap PNG representing per-pixel difference magnitude -- **AND** size mismatches SHALL be reported as an error result - -### Requirement: Image comparison CLI tool -The system MUST provide a `goggles_image_compare` CLI binary that loads two PNG files, compares them through the image-comparison library, accepts optional `--tolerance` and `--diff` arguments, and uses process exit codes to report success or failure. - -#### Scenario: Pass exit code -- **WHEN** `goggles_image_compare actual.png reference.png --tolerance 0.01` is run and the images match within tolerance -- **THEN** the process SHALL exit with code `0` - -#### Scenario: Fail exit code and summary output -- **WHEN** `goggles_image_compare actual.png reference.png --tolerance 0.0` is run and the images differ -- **THEN** the process SHALL exit with code `1` -- **AND** stdout SHALL include `failing_pixels`, `max_channel_diff`, and `failing_percentage` - -#### Scenario: Usage or load errors return code 2 -- **WHEN** required positional arguments are missing, an option value is invalid, or either input PNG fails to load -- **THEN** the process SHALL exit with code `2` - -#### Scenario: Diff image output -- **WHEN** `--diff diff.png` is passed and the comparison fails -- **THEN** a diff PNG SHALL be written to `diff.png` - -### Requirement: Headless pipeline smoke test -The system MUST provide a headless smoke test that runs Goggles for a small fixed frame count, writes a PNG, and verifies that the output file exists. This smoke coverage MUST remain labeled as `integration` rather than `visual`. - -#### Scenario: Smoke test produces a PNG -- **GIVEN** the project is built with the required binaries -- **WHEN** the headless smoke test runs -- **THEN** Goggles SHALL exit successfully after rendering a fixed small number of frames -- **AND** the configured smoke-test PNG output SHALL exist - -#### Scenario: Smoke test is integration-labeled -- **WHEN** CTest labels are inspected -- **THEN** the headless smoke test SHALL be in the `integration` tier -- **AND** it SHALL NOT be part of the `visual` label contract - -### Requirement: Aspect ratio visual tests -The system MUST provide eight Catch2 visual tests that validate the rendered geometry for fit, fill, stretch, integer-1x, integer-2x, integer-auto, and dynamic scale-mode scenarios using the quadrant client as the deterministic source. - -#### Scenario: Fit letterbox geometry -- **GIVEN** headless output at `1920x1080` in fit mode with the fixed `640x480` quadrant source -- **WHEN** the output PNG is captured -- **THEN** side bars SHALL be black at the expected pillarbox positions -- **AND** the content rectangle SHALL show the expected quadrant colors within the configured content tolerance - -#### Scenario: Fit perfect 4:3 geometry -- **GIVEN** headless output forced to `800x600` in fit mode with the same quadrant source -- **WHEN** the output PNG is captured -- **THEN** the full output SHALL contain the expected quadrant colors with no black bars - -#### Scenario: Fill geometry covers the viewport -- **GIVEN** headless output at `1920x1080` in fill mode -- **WHEN** the output PNG is captured -- **THEN** the viewport center SHALL contain non-black content rather than border fill - -#### Scenario: Stretch geometry fills the viewport -- **GIVEN** headless output at `1920x1080` in stretch mode -- **WHEN** the output PNG is captured -- **THEN** the full viewport SHALL contain the expected quadrant colors with no black bars - -#### Scenario: Integer scale geometries match their fixed rectangles -- **GIVEN** headless output at `1920x1080` in integer mode with scale `1`, scale `2`, or auto scale -- **WHEN** the output PNG is captured -- **THEN** the content rectangle and black-border regions SHALL match the expected centered geometry for each case -- **AND** auto scale SHALL resolve to the same geometry as `2x` for the fixed quadrant source - -#### Scenario: Dynamic mode falls back to fit for stable source size -- **GIVEN** headless output at `1920x1080` in dynamic mode with no mid-stream source-size change -- **WHEN** the output PNG is captured -- **THEN** the geometry SHALL match the fit-letterbox case - -### Requirement: Shader visual tests with golden image comparison -The system MUST provide three Catch2 shader visual tests covering bypass, zfast-crt, and a bypass-vs-zfast toggle distinction. These tests MUST compare captured output images against golden PNGs using explicit tolerance and failing-percentage thresholds. - -#### Scenario: Bypass shader matches golden -- **GIVEN** the bypass golden image is present -- **WHEN** Goggles renders the bypass shader scenario and compares the output against the golden -- **THEN** the comparison SHALL pass within the bypass tolerance and failing-percentage threshold - -#### Scenario: Zfast shader matches golden -- **GIVEN** the zfast-crt golden image is present -- **WHEN** Goggles renders the zfast scenario and compares the output against the golden -- **THEN** the comparison SHALL pass within the zfast tolerance and failing-percentage threshold - -#### Scenario: Toggle scenarios remain distinct and valid -- **GIVEN** both shader goldens are present -- **WHEN** Goggles renders the bypass and zfast scenarios separately -- **THEN** each output SHALL match its corresponding golden contract - -#### Scenario: Missing goldens skip instead of fail -- **GIVEN** one or more required shader goldens are absent -- **WHEN** the shader visual tests run -- **THEN** the affected tests SHALL emit Catch2 `SKIP` results rather than failures -- **AND** the skip message SHALL direct the user to run `pixi run update-golden` - -### Requirement: Golden image update workflow -The system MUST provide a reproducible workflow to refresh Goggles-owned golden PNGs through `pixi run update-golden`. The Goggles visual-regression contract only requires final-output shader goldens for the active visual tests. - -#### Scenario: Golden refresh updates the shader goldens -- **GIVEN** the project is built on a machine that can execute the capture workflow -- **WHEN** `pixi run update-golden` is executed -- **THEN** the bypass and zfast shader goldens SHALL be regenerated from current captures - -#### Scenario: Golden PNGs remain Git LFS tracked -- **GIVEN** golden PNGs are committed under the golden-image directory -- **WHEN** repository LFS tracking rules are applied -- **THEN** those PNGs SHALL remain tracked through Git LFS - -### Requirement: Visual test CTest label isolation -The visual regression executables registered through the visual test CMake wiring MUST carry the `visual` label and MUST NOT also be labeled `unit` or `integration`. - -#### Scenario: Visual label selects visual tests -- **WHEN** `ctest --preset test -L visual` is run -- **THEN** the aspect-ratio and shader visual tests SHALL be included - -#### Scenario: Unit label excludes visual tests -- **WHEN** `ctest --preset test -L unit` is run -- **THEN** the aspect-ratio and shader visual tests SHALL NOT be included - -#### Scenario: Integration label excludes visual tests -- **WHEN** `ctest --preset test -L integration` is run -- **THEN** the aspect-ratio and shader visual tests SHALL NOT be included - -### Requirement: Goggles visual-regression scope excludes standalone filter-chain diagnostics workflows -Goggles-owned visual regression requirements SHALL cover final-output image comparison, CLI/library helpers, headless smoke, aspect-ratio tests, shader goldens, and visual CTest labeling. Intermediate-pass golden baselines, earliest-divergence localization, temporal-sequence golden requirements, and semantic-probe preset requirements are OUT OF SCOPE for the current Goggles visual-regression contract. - -#### Scenario: Consumer reads the Goggles visual-regression contract -- **GIVEN** a reader wants to know what Goggles visual tests currently guarantee -- **WHEN** it inspects this living spec -- **THEN** it SHALL find requirements for the actively maintained Goggles visual test surface -- **AND** it SHALL NOT interpret standalone filter-chain diagnostic workflows as required Goggles visual-regression behavior diff --git a/openspec/specs/vulkan-backend-module-layout/spec.md b/openspec/specs/vulkan-backend-module-layout/spec.md deleted file mode 100644 index da9c04ca..00000000 --- a/openspec/specs/vulkan-backend-module-layout/spec.md +++ /dev/null @@ -1,204 +0,0 @@ -# vulkan-backend-module-layout Specification - -## Purpose -Define the living contract for preserving `VulkanBackend` behavior while its internal implementation is split into explicit backend subsystems. - -## Requirements - -### Requirement: VulkanBackend Facade Remains Stable - -The backend refactor SHALL preserve `VulkanBackend` as the public integration facade for app-side -render backend usage. - -The refactor SHALL: - -- keep `src/render/backend/vulkan_backend.hpp` as the public API declaration surface -- preserve the existing public backend methods and `Application` integration points unless the - change artifacts are updated first -- keep `src/render/backend/vulkan_backend.cpp` limited to public entrypoints and high-level - orchestration after the split - -#### Scenario: Public facade preserved after extraction -- **GIVEN** the backend refactor is complete -- **WHEN** app-side integration is inspected -- **THEN** `Application` still integrates through `VulkanBackend` -- **AND** the public backend declaration surface remains `src/render/backend/vulkan_backend.hpp` - -### Requirement: Behavior-Oriented Backend Subsystems - -The backend implementation SHALL organize internal code into behavior-oriented subsystems so future -edits can remain local to one backend concern. - -The split SHALL provide subsystem boundaries that match these responsibilities: - -- `VulkanContext` for instance, device, queue, surface, validation, and stable capability facts -- `RenderOutput` for swapchain/headless target lifetime and frame retirement -- `ExternalFrameImporter` for DMA-BUF import and temporary explicit-sync wait ownership -- `FilterChainController` for backend-side boundary-facing filter coordination, reload requests, - policy inputs, current preset path state, and temporary GPU-drain-safe retirement - -Long-lived filter runtime ownership, shader runtime ownership/creation, shader processing, and -preset texture loading SHALL remain inside `goggles-filter-chain`. - -The implementation SHALL NOT introduce a generic `misc`, `helpers`, or `utils` dumping-ground module -for backend extraction. - -#### Scenario: Localized edit surface for output behavior -- **GIVEN** a future change only affects swapchain recreation, headless target lifetime, or frame - retirement behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `RenderOutput` -- **AND** unrelated importer and filter-controller logic does not need to remain in the same - translation unit - -#### Scenario: Localized edit surface for import behavior -- **GIVEN** a future change only affects imported image lifetime or explicit-sync import behavior -- **WHEN** an implementer identifies the primary edit surface -- **THEN** the primary implementation surface is `ExternalFrameImporter` -- **AND** output-target and filter-runtime logic does not need to remain in the same translation unit - -### Requirement: Filter Boundary Ownership Remains Intact - -The backend refactor SHALL keep long-lived filter runtime ownership inside `goggles-filter-chain` -while using backend-side coordination only for boundary-facing sequencing and temporary retirement. - -#### Scenario: Runtime ownership stays in the filter boundary -- **GIVEN** the renderer initializes or reloads filter processing after the backend split -- **WHEN** runtime ownership is inspected -- **THEN** chain orchestration, shader runtime ownership/creation, shader processing, and preset - texture loading remain owned within `goggles-filter-chain` -- **AND** backend-side `FilterChainController` consumes boundary-facing contracts instead of owning - those long-lived runtime internals - -#### Scenario: Host-side retirement remains bounded -- **GIVEN** a filter runtime handoff replaces an active runtime after the backend split -- **WHEN** the previous runtime is retired -- **THEN** any host-side retirement ownership is temporary and limited to GPU-drain-safe destruction -- **AND** active runtime ownership remains in the filter boundary - -### Requirement: Subsystem Authority and Dependency Direction Remain Explicit - -The backend refactor SHALL preserve one explicit authority for each mutable backend state domain and -SHALL keep subsystem dependency direction acyclic. - -The split SHALL enforce these authority rules: - -- `VulkanBackend` coordinates cross-subsystem transitions -- `RenderOutput` is the authority for target extent, target format, and frame-retirement state -- `ExternalFrameImporter` is the authority for imported-image lifetime and temporary wait objects -- `FilterChainController` is the authority for backend-side reload request state, stage-policy input - state, current preset path state, and temporary host-side retirement bookkeeping - -Allowed internal dependency edges are: - -- `RenderOutput -> VulkanContext` -- `ExternalFrameImporter -> VulkanContext` -- `FilterChainController -> boundary-owned host<->filter VulkanContext contract` - -Forbidden internal dependency edges are: - -- `RenderOutput -/-> ExternalFrameImporter` -- `RenderOutput -/-> FilterChainController` -- `ExternalFrameImporter -/-> FilterChainController` - -#### Scenario: Single owner remains inspectable after extraction -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** target state, imported-source state, and filter-runtime state ownership are inspected -- **THEN** each mutable state domain still resolves to one named subsystem owner -- **AND** `VulkanBackend` does not duplicate that authoritative state without an explicitly documented reason - -#### Scenario: Cross-subsystem calls stay facade-owned -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** a frame is rendered or the swapchain is recreated -- **THEN** cross-boundary sequencing is coordinated by `VulkanBackend` -- **AND** non-context subsystems do not call each other directly to trigger rebuild or lifetime transitions - -### Requirement: Boundary-Owned VulkanContext Contract Remains Intact - -The backend refactor SHALL preserve a boundary-owned host<->filter `VulkanContext` contract so the -filter boundary does not consume backend-only context headers. - -#### Scenario: Filter boundary uses a boundary-owned context contract -- **GIVEN** host/backend code initializes `goggles-filter-chain` after the backend split -- **WHEN** include and initialization dependencies are inspected -- **THEN** the filter boundary consumes a boundary-owned host<->filter `VulkanContext` contract or adapter -- **AND** backend-only `VulkanContext` headers are not included directly from filter-boundary code - -### Requirement: Extraction Contract Is Explicit for Apply - -The change artifacts SHALL define a compile-safe extraction order and verification plan that minimize -behavior drift during apply. - -The change artifacts SHALL specify: - -- an initial declaration-seam step that adds only the narrow internal headers and types needed for a - buildable multi-file split -- updating `src/render/backend/CMakeLists.txt` as backend translation units land -- extracting `VulkanContext` before other subsystem owners -- extracting `RenderOutput` before `ExternalFrameImporter` and `FilterChainController` -- extracting `ExternalFrameImporter` before final facade cleanup so imported-source ownership is explicit -- preserving or introducing the boundary-owned host<->filter `VulkanContext` contract before - `FilterChainController` depends on it -- extracting `FilterChainController` after output/import seams are stable -- a verification contract that names baseline preset gates, environment-sensitive checks, fallback - policy, mandatory no-fallback checks, forbidden dependency-edge audits, and named module-layout - plus lifetime tests - -#### Scenario: Migration order is explicit from artifacts alone -- **GIVEN** implementation starts from the repository artifacts alone -- **WHEN** the artifacts are read before editing code -- **THEN** the OpenSpec artifacts specify the required extraction order -- **AND** the implementation does not depend on undocumented external context to determine safe sequencing - -### Requirement: Backend Behavior Is Preserved Across the Split - -The backend refactor SHALL preserve existing backend behavior while changing only the internal module -layout. - -The preserved behavior SHALL include: - -- windowed swapchain rendering and present flow -- headless surfaceless rendering and PNG readback behavior -- DMA-BUF plane-layout import and explicit-sync submission wiring -- filter-chain stage-order invariants and async preset reload behavior -- Result-based error propagation and no expected-failure exceptions in backend refactor scope -- `goggles::util::JobSystem`-owned async preset rebuild behavior -- filter-boundary ownership of long-lived runtime objects and boundary-owned initialization contracts -- swapchain recreation behavior, including output-format and filter-runtime rebuild coordination -- unchanged `Application` backend integration points - -#### Scenario: Headless and windowed behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** windowed and headless render flows are exercised through the defined verification plan -- **THEN** swapchain render/present behavior and headless offscreen/readback behavior remain unchanged - -#### Scenario: DMA-BUF and filter-chain behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** imported-frame, explicit-sync, and filter-boundary behavior are exercised through the defined verification plan -- **THEN** DMA-BUF import semantics, temporary wait handling, stage ordering, and async preset reload behavior remain unchanged - -#### Scenario: Error flow and async rebuild behavior remain unchanged -- **GIVEN** the backend implementation has been split into subsystem-oriented files -- **WHEN** backend failure paths and preset reload behavior are exercised through the defined verification plan -- **THEN** expected runtime failures still propagate through `Result`-style APIs without expected-failure exceptions -- **AND** async preset rebuild behavior remains owned by `goggles::util::JobSystem` - -### Requirement: Shutdown and Async Lifetime Ordering Remain Safe - -The backend refactor SHALL preserve auditable shutdown ordering across async filter work, imported -resources, output resources, and Vulkan context state. - -The shutdown contract SHALL require this order: - -- wait for pending async filter rebuild work and clear pending-ready state -- idle the device before subsystem teardown proceeds -- destroy filter-controller active, pending, and deferred-retire runtime state before output/context teardown -- destroy imported-image state before output/context teardown completes -- destroy output resources before destroying context-owned device, surface, debug messenger, and instance state -- treat any shutdown or async lifetime ordering drift as a proposal/spec/design reconciliation stop condition - -#### Scenario: Device-rooted resources tear down in safe order -- **GIVEN** the backend implementation is shutting down after the subsystem split -- **WHEN** teardown order is inspected or exercised through verification -- **THEN** async runtime work is resolved before subsystem destruction proceeds -- **AND** device-rooted child resources are destroyed before the owning Vulkan context state is destroyed From e3c09a1b87fefa8a3ea926997fcc1603674b0807 Mon Sep 17 00:00:00 2001 From: "kingstom.chen" Date: Wed, 25 Mar 2026 12:31:46 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20update=20pixi.lock=20(wayland=201.?= =?UTF-8?q?24.0=20=E2=86=92=201.25.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock file was out of sync with the workspace due to upstream wayland version bump in conda-forge. --- pixi.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pixi.lock b/pixi.lock index 35d94155..35662c5a 100644 --- a/pixi.lock +++ b/pixi.lock @@ -132,7 +132,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/vulkan-tools-1.4.328.1-h215f996_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/vulkan-validation-layers-1.4.328-h95c1ebc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.47-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda @@ -2654,20 +2654,20 @@ packages: purls: [] size: 6339593 timestamp: 1759804086510 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda - sha256: 3aa04ae8e9521d9b56b562376d944c3e52b69f9d2a0667f77b8953464822e125 - md5: 035da2e4f5770f036ff704fa17aace24 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda + sha256: ea374d57a8fcda281a0a89af0ee49a2c2e99cc4ac97cf2e2db7064e74e764bdb + md5: 996583ea9c796e5b915f7d7580b51ea6 depends: - __glibc >=2.17,<3.0.a0 - - libexpat >=2.7.1,<3.0a0 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - libgcc >=14 - libstdcxx >=14 license: MIT license_family: MIT purls: [] - size: 329779 - timestamp: 1761174273487 + size: 334139 + timestamp: 1773959575393 - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.47-hd8ed1ab_0.conda sha256: 9ab2c12053ea8984228dd573114ffc6d63df42c501d59fda3bf3aeb1eaa1d23e md5: 7da1571f560d4ba3343f7f4c48a79c76 @@ -2692,7 +2692,7 @@ packages: target_platform: linux-64 depends: - wayland - - wayland >=1.24.0,<2.0a0 + - wayland >=1.25.0,<2.0a0 - libxkbcommon - libxkbcommon >=1.13.1,<2.0a0 - pixman