ci(native-golden): focus shader parity matrix on native OS targets (m… #36
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Native Golden — Shader Logic, Cross-Platform & WASM Parity" | |
| # Runs on every PR and push that touches the compositor, shaders, contracts, | |
| # CLI, or the WASM crate. | |
| # | |
| # What this job proves: | |
| # ✅ WGSL shaders compile and run on Metal, DX12, Vulkan, and WebGPU. | |
| # ✅ Compositor math is self-consistent (same-machine determinism). | |
| # ✅ Any shader regression produces a measurable pixel delta and fails CI. | |
| # ⚠️ Windows and Linux runners have NO dedicated GPU — wgpu uses software | |
| # rasterizers (WARP / Lavapipe / SwiftShader). The adapter name in each | |
| # job summary confirms what actually ran. Green = shader portability, | |
| # NOT real hardware GPU parity. | |
| # | |
| # Hardware validation (what this does NOT replace): | |
| # See docs/native-architecture.md §Hardware Validation Log. | |
| # | |
| # Tolerances: | |
| # Native cross-backend (software vs Metal reference): 4 per-channel | |
| # WASM vs native (same runner, SwiftShader vs Lavapipe): 8 per-channel | |
| on: | |
| push: | |
| branches: [main, develop, master, "feat/**"] | |
| paths: | |
| - "src-tauri/src/wgpu_compositor/**" | |
| - "src-tauri/src/shaders/**" | |
| - "src-tauri/src/native_core/**" | |
| - "crates/clypra-native-cli/**" | |
| - "crates/clypra-native-core/**" | |
| - "crates/clypra-render-wasm/**" | |
| - ".github/workflows/native-golden.yml" | |
| pull_request: | |
| paths: | |
| - "src-tauri/src/wgpu_compositor/**" | |
| - "src-tauri/src/shaders/**" | |
| - "src-tauri/src/native_core/**" | |
| - "crates/clypra-native-cli/**" | |
| - "crates/clypra-native-core/**" | |
| - "crates/clypra-render-wasm/**" | |
| - ".github/workflows/native-golden.yml" | |
| permissions: | |
| contents: read | |
| env: | |
| CLYPRA_ALLOW_FALLBACK_GPU: "1" | |
| WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER: "1" | |
| jobs: | |
| golden: | |
| name: "Golden (${{ matrix.platform.name }})" | |
| runs-on: ${{ matrix.platform.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: | |
| - name: "macOS Metal" | |
| os: macos-latest | |
| backend: Metal | |
| - name: "Windows DX12" | |
| os: windows-latest | |
| backend: DX12 | |
| - name: "Linux Vulkan" | |
| os: ubuntu-24.04 | |
| backend: Vulkan | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| # ── Rust toolchain ──────────────────────────────────────────────────── | |
| - name: Setup Rust (native) | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| src-tauri | |
| crates/clypra-native-cli | |
| crates/clypra-native-core | |
| crates/clypra-render-wasm | |
| # ── Platform-specific GPU setup (Linux) ─────────────────────────────── | |
| - name: Install Vulkan + Mesa Lavapipe (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends \ | |
| libvulkan1 mesa-vulkan-drivers vulkan-tools libasound2-dev \ | |
| libgl1-mesa-dri libegl1-mesa-dev | |
| echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV" | |
| echo "VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV" | |
| echo "LIBGL_ALWAYS_SOFTWARE=1" >> "$GITHUB_ENV" | |
| # ── Build clypra-native-cli ─────────────────────────────────────────── | |
| - name: Build clypra-native-cli | |
| run: cargo build --release | |
| working-directory: crates/clypra-native-cli | |
| # ── Render fixtures ─────────────────────────────────────────────────── | |
| - name: Render minimal fixture | |
| run: | | |
| mkdir -p ../../artifacts | |
| cargo run --release -- render \ | |
| fixtures/minimal-request.json \ | |
| ../../artifacts/minimal-${{ matrix.platform.backend }}.png \ | |
| 2>&1 | tee /tmp/render-minimal.log | |
| GPU_LINE=$(grep "^GPU:" /tmp/render-minimal.log || echo "GPU: unknown") | |
| echo "GPU_INFO=${GPU_LINE}" >> "$GITHUB_ENV" | |
| working-directory: crates/clypra-native-cli | |
| shell: bash | |
| - name: Render raster fixture | |
| run: | | |
| cargo run --release -- render \ | |
| fixtures/raster-request.json \ | |
| ../../artifacts/raster-${{ matrix.platform.backend }}.png \ | |
| 2>/dev/null | |
| working-directory: crates/clypra-native-cli | |
| shell: bash | |
| # ── Diff — native platforms vs Metal reference ───────────────────────── | |
| - name: Diff minimal (vs Metal reference, tol=4) | |
| id: diff_minimal | |
| run: | | |
| cargo run --release -- diff \ | |
| ../../artifacts/minimal-${{ matrix.platform.backend }}.png \ | |
| fixtures/minimal-expected.png \ | |
| 4 | tee ../../artifacts/diff-minimal-${{ matrix.platform.backend }}.json | |
| working-directory: crates/clypra-native-cli | |
| shell: bash | |
| continue-on-error: true | |
| - name: Diff raster (vs Metal reference, tol=4) | |
| id: diff_raster | |
| run: | | |
| cargo run --release -- diff \ | |
| ../../artifacts/raster-${{ matrix.platform.backend }}.png \ | |
| fixtures/raster-expected.png \ | |
| 4 | tee ../../artifacts/diff-raster-${{ matrix.platform.backend }}.json | |
| working-directory: crates/clypra-native-cli | |
| shell: bash | |
| continue-on-error: true | |
| # ── Job summary ─────────────────────────────────────────────────────── | |
| - name: Write job summary | |
| if: always() | |
| run: | | |
| echo "## Golden — ${{ matrix.platform.name }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Adapter:** \`${GPU_INFO:-unknown}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| if echo "${GPU_INFO}" | grep -qiE "llvmpipe|software|warp|basic render|lavapipe|cpu"; then | |
| echo "> ⚠️ **Software rasterizer.** Validates shader portability only — not real hardware parity." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "> ✅ **Hardware GPU detected.**" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| DIFF_SUFFIX="${{ matrix.platform.backend }}" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Fixture | maxChannelError | differing_pixels | mean_error | result |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|---------|----------------|-----------------|------------|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| TOL="4" | |
| for fixture in minimal raster; do | |
| diff_file="artifacts/diff-${fixture}-${DIFF_SUFFIX}.json" | |
| if [ -f "$diff_file" ]; then | |
| max=$(python3 -c "import json; d=json.load(open('$diff_file')); print(d['maxChannelError'])" 2>/dev/null || echo "?") | |
| dp=$(python3 -c "import json; d=json.load(open('$diff_file')); print(d['differingPixels'])" 2>/dev/null || echo "?") | |
| mean=$(python3 -c "import json; d=json.load(open('$diff_file')); print(round(d['meanChannelError'],4))" 2>/dev/null || echo "?") | |
| result="✅" | |
| if [ "$max" != "?" ] && [ "$max" -gt "$TOL" ] 2>/dev/null; then result="❌"; fi | |
| else | |
| max="N/A"; dp="N/A"; mean="N/A"; result="⚠️ render failed" | |
| fi | |
| echo "| $fixture | $max | $dp | $mean | $result |" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| shell: bash | |
| # ── Enforce tolerance ───────────────────────────────────────────────── | |
| - name: Enforce minimal diff | |
| if: ${{ steps.diff_minimal.outcome == 'failure' }} | |
| run: | | |
| echo "::error::minimal fixture exceeds tolerance for ${{ matrix.platform.name }}" | |
| exit 1 | |
| shell: bash | |
| - name: Enforce raster diff | |
| if: ${{ steps.diff_raster.outcome == 'failure' }} | |
| run: | | |
| echo "::error::raster fixture exceeds tolerance for ${{ matrix.platform.name }}" | |
| exit 1 | |
| shell: bash | |
| # ── Upload artifacts ────────────────────────────────────────────────── | |
| - name: Upload rendered PNGs and diff reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: golden-${{ matrix.platform.backend }} | |
| path: artifacts/ | |
| retention-days: 30 |