docs: specs and a user guide for the v2.3.6 analysis tools #1228
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: CI | |
| # The workflow ALWAYS triggers (no `paths-ignore`), so the required `CI success` | |
| # check is always reported. A required status check that never runs leaves a PR's | |
| # merge gate stuck "pending" forever — exactly what a docs-only PR would hit if | |
| # `paths-ignore` skipped the whole run. Instead the `changes` job (dorny/paths- | |
| # filter) decides whether any BUILD-affecting file changed; if a change touches | |
| # only `**/*.md` / `**/*.txt` (the bulk of the docs/ + to-dos/ + ref-docs/ trees), | |
| # screenshots/, LICENSE*, NOTICE, .gitignore, or .codegraph/, every heavy job is | |
| # skipped and `CI success` passes trivially, so the doc-only PR can still merge. | |
| # (This mirrors the old `paths-ignore` set exactly — a non-md/txt asset placed | |
| # under docs/ etc., e.g. a tarball, still runs full CI, which is intentional.) | |
| # This is the branch-protection-safe replacement for the old `paths-ignore` | |
| # (which was fine only while `main` had no required checks). | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| # GitHub merge queue. When the queue is enabled in repo settings + the | |
| # `CI success` check is required by branch protection, a PR entering the queue | |
| # triggers this with the FULL cross-platform matrix (see the `setup` job) — so | |
| # nothing merges without macOS+Windows green, even though per-PR runs are | |
| # ubuntu-only. Inert until the queue is enabled (no `merge_group` events fire). | |
| merge_group: | |
| # Weekly drift safety-net. The cost optimisation below runs the FULL suite | |
| # (release-mode `test-roms`, the macOS/Windows matrix, `bench`) only on | |
| # release-class events — not on every intermediate feature PR — so a | |
| # regression merged between releases could otherwise sit on `main` unproven. | |
| # This Monday 07:00 UTC cron re-runs the full suite on `main` so any drift is | |
| # caught within a week regardless of release cadence. `workflow_dispatch` | |
| # covers an on-demand full run. | |
| schedule: | |
| - cron: "0 7 * * 1" | |
| # Cancel superseded runs on a PR branch (rapid pushes), but let every `main` | |
| # commit + merge-queue run finish so the default branch's history (and the | |
| # queue's gate) stays fully checked. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| # CI builds clean, so full debuginfo (the dev/test default `debug = 2`) only | |
| # slows linking and bloats `target/` (worse cache hits). `line-tables-only` | |
| # keeps panic/backtrace `file:line` — so failing-test diagnostics stay | |
| # readable with RUST_BACKTRACE=1 — while cutting debug compile time ~20-40%. | |
| # Set via `CARGO_PROFILE_*` (a stable, per-profile cache-key contributor) NOT | |
| # `RUSTFLAGS=-Cdebuginfo=0`, which would bust the rust-cache key + force a | |
| # full dependency rebuild. The release profile (test-roms / bench) already | |
| # defaults to `debug = 0`, so this only touches the debug-compiling jobs. | |
| CARGO_PROFILE_DEV_DEBUG: line-tables-only | |
| CARGO_PROFILE_TEST_DEBUG: line-tables-only | |
| # Least-privilege default for the whole workflow: every job here only builds / | |
| # tests / lints and reads the checked-out tree, so the default GITHUB_TOKEN is | |
| # narrowed to read-only. Resolves the CodeQL `actions/missing-workflow-permissions` | |
| # alerts (one was raised per job for the absent permissions block); a job needing | |
| # more would override with its own `permissions:` key. | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Decide whether any build-affecting file changed. Because the workflow always | |
| # runs (so `CI success` always reports for branch protection), this is what | |
| # actually skips the heavy jobs on a docs-only change: they each gate on | |
| # `needs.changes.outputs.code == 'true'`. The `code` filter is the inverse of | |
| # the doc set the old `paths-ignore` listed (match everything, then exclude | |
| # docs — picomatch/gitignore precedence). For events with no diff base (manual | |
| # dispatch, merge queue) paths-filter reports every filter as changed, so they | |
| # run the full suite. | |
| changes: | |
| name: detect code changes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| # Full history: on `push`, dorny/paths-filter diffs against the before-SHA | |
| # using local git, so the default shallow clone (fetch-depth: 1) can miss | |
| # the base commit. (On `pull_request` it uses the GitHub API and needs no | |
| # history.) Without this the push-event filter falls back to "everything | |
| # changed" — safe, but it loses the docs-only skip on direct main pushes. | |
| # | |
| # This is the one job in the credential-hardening sweep (#318) where the | |
| # change touches the git layer at all: paths-filter reads local git on | |
| # `push` events. It needs no authentication — `fetch-depth: 0` means the | |
| # base commit is already local, and the `pull_request` path goes through | |
| # the GitHub API with its own token — so dropping the persisted credential | |
| # is expected to be inert. Failure mode if that is ever wrong is benign | |
| # (the filter reports "everything changed" and the full suite runs), but | |
| # it would silently cost the docs-only skip the comment above describes. | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| code: | |
| - '**' | |
| - '!**/*.md' | |
| - '!**/*.txt' | |
| - '!screenshots/**' | |
| - '!LICENSE*' | |
| - '!NOTICE' | |
| - '!.gitignore' | |
| - '!.codegraph/**' | |
| # fmt + clippy + rustdoc share one runner + one compile of the workspace's | |
| # dependency graph (clippy and rustdoc differ only in the final pass), so | |
| # merging them removes two redundant dependency compiles and two runner | |
| # spin-ups vs three separate jobs. fmt runs first (it compiles nothing), so | |
| # a formatting slip still fails in seconds. | |
| # | |
| # Pinned to the project toolchain (rust-toolchain.toml = 1.96.0), NOT a | |
| # floating `stable`: clippy adds lints every release, so `stable` makes the | |
| # lint gate drift and break on a toolchain bump. Pinning ALSO makes the | |
| # rustdoc gate match local `cargo doc` exactly (rust-toolchain.toml pins the | |
| # local compiler to 1.96 too) — the stable-vs-1.96 mismatch previously let a | |
| # broken-intra-doc-link slip past local and only fail in CI. | |
| # | |
| # Compute ONE "full-run" flag that gates both the OS matrix and the expensive | |
| # jobs, then reuse it everywhere. The cost model: a regular (non-release) PR is | |
| # a LIGHT run — fast, essential feedback only (fmt/clippy/rustdoc, the ubuntu | |
| # `test`, no_std, wasm-clippy); every other event is a FULL run that adds the | |
| # macOS/Windows matrix legs, the release-mode `test-roms` accuracy battery, and | |
| # the `bench` gate. | |
| # | |
| # FULL when: push-to-`main` (validates every merge), the merge queue, manual | |
| # dispatch, the weekly `schedule` drift-net, OR a `release/*` PR (so a release | |
| # is fully proven before it is cut). LIGHT only on a regular feature PR — that | |
| # is where the iteration (and thus the runner-minute burn) happens: each PR gets | |
| # many pushes, so keeping the ~20-min release `test-roms` + the 10x-billed macOS | |
| # / 2x Windows legs off every PR push is the bulk of the savings. The free arm64 | |
| # leg still runs on every PR (aarch64 alignment/SIMD/atomics/fp coverage), and | |
| # the post-merge push + release PR + weekly cron still give full cross-platform | |
| # + accuracy coverage before anything ships. | |
| setup: | |
| name: select matrix | |
| needs: changes | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| os: ${{ steps.pick.outputs.os }} | |
| full: ${{ steps.pick.outputs.full }} | |
| steps: | |
| - id: pick | |
| shell: bash | |
| # Pass the event/ref through `env` (NOT direct `${{ }}` interpolation into | |
| # the script) so an attacker-chosen branch name can't inject shell — the | |
| # `actions/script-injection` pattern CodeQL flags. | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| HEAD_REF: ${{ github.head_ref }} | |
| run: | | |
| # LIGHT only for a regular PR (head branch not release/*); FULL otherwise. | |
| if [ "$EVENT_NAME" = "pull_request" ] && [[ "$HEAD_REF" != release/* ]]; then | |
| echo 'full=false' >> "$GITHUB_OUTPUT" | |
| echo 'os=["ubuntu-latest", "ubuntu-24.04-arm"]' >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'full=true' >> "$GITHUB_OUTPUT" | |
| echo 'os=["ubuntu-latest", "ubuntu-24.04-arm", "macos-latest", "windows-latest"]' >> "$GITHUB_OUTPUT" | |
| fi | |
| lint: | |
| name: fmt + clippy + rustdoc | |
| needs: changes | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ubuntu-latest | |
| env: | |
| RUSTDOCFLAGS: -D warnings | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| components: rustfmt,clippy | |
| - name: rustfmt | |
| run: cargo fmt --all -- --check | |
| - name: clippy (-D warnings) | |
| run: cargo clippy --workspace --all-targets -- -D warnings | |
| - name: rustdoc (-D warnings) | |
| run: cargo doc --workspace --no-deps | |
| # Feature-gated clippy combos CLAUDE.md mandates. These previously ran | |
| # ONLY locally, so a feature-gated lint regression (e.g. a `cfg`-stripped | |
| # autofix breaking the `retroachievements` build) could land CI-green. | |
| # Run them serially on this one already-warm runner (shared dependency | |
| # compile) rather than a matrix (which multiplies cache storage). NOT | |
| # `--all-features`: `scripting` (mlua) + `script-wasm` (piccolo) are | |
| # mutually-exclusive backends that cannot co-resolve. | |
| - name: clippy (scripting — native Lua engine) | |
| run: cargo clippy -p rustynes-frontend --all-targets --features scripting -- -D warnings | |
| - name: clippy (scripting + hd-pack) | |
| run: cargo clippy -p rustynes-frontend --all-targets --features scripting,hd-pack -- -D warnings | |
| - name: clippy (retroachievements — rcheevos FFI) | |
| run: cargo clippy -p rustynes-frontend --all-targets --features retroachievements -- -D warnings | |
| # The `full` umbrella (retroachievements + scripting + script-ipc + hd-pack + | |
| # debug-hooks + av-record) is the catch-all that lints the native features the | |
| # three subset combos above DON'T reach on their own — notably `script-ipc` and | |
| # `av-record`. One extra compile on the already-warm runner closes the remaining | |
| # cfg-gated-code coverage gap (#11a) without a per-feature matrix. | |
| - name: clippy (full — every native feature) | |
| run: cargo clippy -p rustynes-frontend --all-targets --features full -- -D warnings | |
| # `rustynes-mappers` with `mapper-audio` OFF. Every combo above turns | |
| # features ON; this is the only gate that compiles a feature OUT, and the | |
| # subtraction is where the rot happens: this configuration was outright | |
| # BROKEN — a hard `E0599`, because `Namco163Audio` was missing the | |
| # feature-off `clock()` shim that the NSF expansion router calls | |
| # unconditionally — and nothing noticed, because nothing built it. The | |
| # `no_std` job below is `-p rustynes-core`, which keeps `mapper-audio` on. | |
| # | |
| # The feature's contract (see the crate manifest) is that with audio off | |
| # the register decoders stay live — so save states from an audio-enabled | |
| # build still load — while the oscillators freeze. That asymmetry is | |
| # exactly what a compile-only gate protects. | |
| - name: clippy (mappers — mapper-audio compiled OUT) | |
| run: cargo clippy -p rustynes-mappers --no-default-features --all-targets -- -D warnings | |
| # Cross-platform behaviour: full `cargo test` on stable across the three | |
| # shipped OSes. The previous ubuntu / 1.96 MSRV *test* entry is dropped — the | |
| # `lint` job above already compiles the whole workspace (`clippy | |
| # --all-targets`) on 1.96, which IS the MSRV compile gate, and the core's | |
| # determinism makes "tests pass on 1.96" equivalent to "tests pass on stable". | |
| test: | |
| name: test (${{ matrix.os }}) | |
| # Gate the matrix on the fast fmt+clippy+rustdoc job (a lint slip fails in | |
| # ~seconds — no point spinning up OS runners until it's green) and take the | |
| # OS list from `setup` (ubuntu-only on PRs; full matrix on push / merge | |
| # queue / dispatch). `changes` keeps the whole chain off a docs-only run. | |
| needs: [changes, lint, setup] | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: ${{ fromJSON(needs.setup.outputs.os) }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key: test | |
| # NOTE: cargo-nextest is deliberately NOT adopted (§11e closed WONT-FIX). | |
| # nextest builds its test graph by running `cargo metadata --all-features`, | |
| # but this workspace cannot resolve `--all-features`: `scripting` (mlua) and | |
| # `script-wasm` (piccolo) are mutually-exclusive rustynes-script backends | |
| # (the same reason CLAUDE.md forbids `--all-features` everywhere). nextest | |
| # offers no override for that internal metadata call, so it errors before | |
| # running a single test (`cargo metadata --all-features` exits 101). Plain | |
| # `cargo test` with explicit features stays the runner. | |
| - run: cargo test --workspace | |
| test-roms: | |
| name: test (test-roms feature) | |
| # The SLOWEST job (release-mode compile + the heavy CPU / AccuracyCoin ROM | |
| # batteries, ~20 min). FULL-run only: it is skipped on a regular feature PR | |
| # and runs on push-to-`main` (every merge is accuracy-validated), the merge | |
| # queue, dispatch, the weekly cron, and `release/*` PRs (so a release is | |
| # proven before it is cut). This is the single biggest per-PR time/cost | |
| # saving. Also gated on the fast lint job — don't pay the release compile | |
| # when fmt/clippy already failed. | |
| needs: [changes, lint, setup] | |
| if: ${{ needs.changes.outputs.code == 'true' && needs.setup.outputs.full == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key: test-roms | |
| # A failure here is at the TEST stage (a ROM assertion), so keep the | |
| # expensive release-mode dependency compile cached rather than | |
| # discarding it. | |
| cache-on-failure: "true" | |
| # Built in `--release`: the heavy CPU test ROMs (instr_test-v3, | |
| # blargg_nes_cpu_test5, the nes_instr_test singles) + the AccuracyCoin | |
| # 7200-frame battery are ~10x slower in debug. The emulation core is | |
| # deterministic, so release vs debug produce byte-identical | |
| # framebuffer/audio/cycle hashes — the ROM suites (AccuracyCoin 100%, | |
| # the 60-ROM oracle) assert the same values either way. Debug-assertion | |
| # coverage of the core is NOT lost: the OS-matrix `test` job runs the | |
| # workspace tests in debug. | |
| - run: cargo test --workspace --release --features test-roms | |
| no_std: | |
| name: no_std build (thumbv7em-none-eabihf) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| targets: thumbv7em-none-eabihf | |
| apt: "false" | |
| cache-key: no_std | |
| # Proves the chip stack (rustynes-cpu / rustynes-ppu / rustynes-apu / rustynes-mappers / | |
| # rustynes-core) is genuinely no_std-capable. `rustynes-frontend` uses wgpu / | |
| # winit / cpal and stays std-only; we deliberately don't build the | |
| # whole workspace here. | |
| - run: cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features | |
| wasm: | |
| name: wasm32 build + clippy | |
| needs: changes | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| targets: wasm32-unknown-unknown | |
| components: clippy | |
| apt: "false" | |
| cache-key: wasm32 | |
| # The wasm32 frontend modules (wasm.rs / wasm_winit.rs / wasm_audio.rs) | |
| # only compile on `cfg(target_arch = "wasm32")`, so the native `lint` | |
| # job never sees them — this job is the only PR-time signal for that | |
| # code. Both `#[wasm_bindgen(start)]` flavours are checked (exactly one | |
| # is compiled per build). | |
| - name: clippy (wasm-winit — default, unified winit/wgpu/egui) | |
| run: cargo clippy -p rustynes-frontend --target wasm32-unknown-unknown --lib --bins -- -D warnings | |
| - name: clippy (wasm-canvas — lightweight embed mode) | |
| run: cargo clippy -p rustynes-frontend --target wasm32-unknown-unknown --no-default-features --features wasm-canvas --lib --bins -- -D warnings | |
| # `browser-cheevos` (casual-mode RetroAchievements wasm scaffolding, ADR 0015) | |
| # is wasm-only, so the native lint job never sees it — this is its only PR-time | |
| # clippy signal (#11a's remaining named gap; `script-ipc` is covered by the | |
| # native `full` combo in the lint job). | |
| - name: clippy (browser-cheevos — casual-mode RA wasm scaffolding) | |
| run: cargo clippy -p rustynes-frontend --target wasm32-unknown-unknown --features browser-cheevos --lib --bins -- -D warnings | |
| libretro-cross: | |
| name: libretro cross-compile (buildbot targets) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.code == 'true' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Only the buildbot triples a LINUX runner can model faithfully. | |
| # | |
| # `rust-libretro-sys` runs bindgen, and bindgen needs headers for the | |
| # target it parses `libretro.h` under. Four of the buildbot ABI | |
| # families can supply them here; the Apple family cannot: | |
| # * MinGW-Windows — no glibc involved at all (and `.cargo/config.toml` | |
| # points its bindgen at the MSVC triple), so it parses cleanly. | |
| # * Linux-aarch64 (glibc) — needs the aarch64 glibc HEADERS (not a | |
| # linker). bindgen parses `libretro.h` under `--target aarch64`, and | |
| # glibc's `stdint.h` pulls in `bits/libc-header-start.h` — an | |
| # arch-split header a stock x86_64 runner ships only for x86_64, so | |
| # clang dies exactly like the Apple note below (`bits/libc-header- | |
| # start.h` not found). The step below installs `gcc-aarch64-linux-gnu` | |
| # (for its `libc6-dev-arm64-cross` headers under | |
| # `/usr/aarch64-linux-gnu/include`) and points bindgen there via | |
| # `--sysroot`. Still check-only: that package's cross linker / `STRIP` | |
| # are unused here — they matter only at the buildbot's link/package | |
| # step. This is the PR-time rehearsal of the `.gitlab-ci.yml` | |
| # `libretro-build-linux-aarch64` job (added in #331). (An Arch dev host | |
| # masks the need — its glibc headers are not arch-split, so a local | |
| # `cargo check` passes without the sysroot; the Debian-multiarch runner | |
| # does not, which is exactly the blind spot this gate exists to catch.) | |
| # * Linux-armhf (glibc, 32-bit ARM) — identical situation to aarch64, | |
| # wired up the same way via `gcc-arm-linux-gnueabihf` and its | |
| # `libc6-dev-armhf-cross` headers under `/usr/arm-linux-gnueabihf`. | |
| # | |
| # UNLIKE the other three, this leg rehearses NO buildbot job, and | |
| # deliberately so. The buildbot's `linux/armhf` and | |
| # `linux/armv7-neon-hf` trees are abandoned — every artifact in them | |
| # is dated 2020-11-17, against 2026 for `linux/aarch64` and | |
| # `linux/x86_64` — and libretro has no active ARM32 publishing, so a | |
| # `.gitlab-ci.yml` job would build correctly and deliver to a | |
| # directory no user can reach through RetroArch's updater. It was | |
| # written, then removed for exactly that reason. | |
| # | |
| # The leg is kept because its value is independent of libretro: | |
| # 32-bit ARM is the only pointer-width-32 non-Windows target this | |
| # workspace compiles, so it is the cheapest guard against a | |
| # dependency or `usize`-assumption regression that the 64-bit legs | |
| # cannot see. It also stays ready if ARM32 publishing ever returns. | |
| # The Arch-host blind spot noted above applies here too, so a clean | |
| # local `cargo check` proves nothing about this triple — only this | |
| # runner does. | |
| # * Android — ubuntu-latest ships an NDK, whose sysroot is wired up | |
| # below exactly as `cargo ndk` does it on the buildbot. | |
| # * Apple (macOS/iOS) — DELIBERATELY EXCLUDED. There is no Apple SDK | |
| # on a Linux runner, so clang falls back to host glibc headers and | |
| # dies on Debian multiarch (`bits/libc-header-start.h` not found); | |
| # "fixing" that by feeding it `/usr/include/x86_64-linux-gnu` would | |
| # generate Apple bindings from Linux headers — a lookalike, not a | |
| # rehearsal. Doing it properly needs a macOS runner, which this repo | |
| # reserves for full (non-PR) runs; see the `test` job's `setup`-driven | |
| # OS matrix. The Apple legs share this crate's entire dependency | |
| # graph with the three legs below, so the marginal coverage is small. | |
| target: | |
| - x86_64-pc-windows-gnu | |
| - aarch64-unknown-linux-gnu | |
| - armv7-unknown-linux-gnueabihf | |
| - aarch64-linux-android | |
| steps: | |
| # `cargo check` runs build scripts and proc macros from the checkout — on | |
| # a PR that is untrusted code — and `actions/checkout` otherwise leaves | |
| # the workflow token in `.git/config` where that code could read it. No | |
| # step here needs Git auth afterwards, so drop the credentials. | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| # No `toolchain:` input anywhere in this repo: `rust-setup` resolves it | |
| # from `rust-toolchain.toml`, so this job rehearses the buildbot on the | |
| # exact channel the buildbot uses. | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| apt: "false" | |
| cache-key: libretro-${{ matrix.target }} | |
| # Point bindgen at the NDK sysroot, which is what `cargo ndk` does for us | |
| # on the buildbot. Without it clang has no Android `stdint.h` and the | |
| # build script panics before a single Rust line is compiled. The NDK path | |
| # prefix matches `android.yml`'s (`$ANDROID_NDK_LATEST_HOME`, then | |
| # `toolchains/llvm/prebuilt/linux-x86_64/...`). | |
| - name: Point bindgen at the NDK sysroot | |
| if: matrix.target == 'aarch64-linux-android' | |
| run: | | |
| echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--sysroot=$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" >> "$GITHUB_ENV" | |
| # Provision the aarch64 glibc headers for bindgen (see the matrix note). | |
| # `gcc-aarch64-linux-gnu` pulls `libc6-dev-arm64-cross`, which lands the | |
| # aarch64 glibc headers under `/usr/aarch64-linux-gnu/include`; `--sysroot` | |
| # sends clang there instead of the host's x86_64-only `/usr/include`, and | |
| # the explicit `-isystem` guarantees the include dir is on the search path | |
| # regardless of how the sysroot lays out `usr/include`. The cross linker in | |
| # that package is unused — this gate is `cargo check` only. | |
| - name: Provision the aarch64 glibc headers for bindgen | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -yq gcc-aarch64-linux-gnu | |
| echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_unknown_linux_gnu=--sysroot=/usr/aarch64-linux-gnu -isystem /usr/aarch64-linux-gnu/include" >> "$GITHUB_ENV" | |
| # Provision the armhf glibc headers for bindgen, exactly as for aarch64 | |
| # above. `gcc-arm-linux-gnueabihf` pulls `libc6-dev-armhf-cross`, landing | |
| # the 32-bit ARM glibc headers under `/usr/arm-linux-gnueabihf/include`. | |
| # The cross linker in that package is unused here — this gate is | |
| # `cargo check` only; the buildbot remains the authority on linking. | |
| - name: Provision the armhf glibc headers for bindgen | |
| if: matrix.target == 'armv7-unknown-linux-gnueabihf' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -yq gcc-arm-linux-gnueabihf | |
| echo "BINDGEN_EXTRA_CLANG_ARGS_armv7_unknown_linux_gnueabihf=--sysroot=/usr/arm-linux-gnueabihf -isystem /usr/arm-linux-gnueabihf/include" >> "$GITHUB_ENV" | |
| # Deliberately NOT the composite action's `targets:` input. That routes | |
| # through `dtolnay/rust-toolchain`, which installs the target for the | |
| # channel IT pins (stable) — while `rust-toolchain.toml` then switches | |
| # cargo to 1.96.0, leaving the target missing on the toolchain that | |
| # actually builds. `no_std` / `wasm` escape this only because their two | |
| # targets are listed in `rust-toolchain.toml`. Running `rustup target | |
| # add` from inside the checkout resolves that file first, so the target | |
| # lands on 1.96.0 — the identical one-liner `.gitlab-ci.yml` runs, which | |
| # keeps this job an honest rehearsal of the buildbot rather than a | |
| # differently-configured lookalike. | |
| - run: rustup target add ${{ matrix.target }} | |
| # Until this job existed there was ZERO libretro coverage in GitHub | |
| # Actions, so breakage only surfaced on libretro's own GitLab buildbot — | |
| # where we can't push, can't re-run, and turnaround is days. Pipeline | |
| # #91899 failed 9 of 10 jobs on problems this job reproduces in seconds: | |
| # a missing cross target (see `rust-toolchain.toml`) and an upstream | |
| # `rust-libretro` MinGW-ABI bug (see `.cargo/config.toml`). | |
| # | |
| # `check`, not `build`: neither target can LINK on a Linux runner without | |
| # its platform SDK, and every #91899 failure was compile-time. The | |
| # buildbot remains the authority on linking; this is the early-warning | |
| # gate for target availability, dependency portability, and MSRV drift. | |
| - run: cargo check --release -p rustynes-libretro --target ${{ matrix.target }} | |
| bench: | |
| name: frame-time regression gate | |
| # FULL-run only (like `test-roms`): a release-mode criterion build + run is | |
| # too costly for every PR push, and the frame-time gate is an absolute | |
| # ceiling that only needs to hold at merge / release, not on every iteration. | |
| needs: [changes, setup] | |
| if: ${{ needs.changes.outputs.code == 'true' && needs.setup.outputs.full == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| # fetch-depth: 0 — the RELATIVE gate below benches the base commit in | |
| # a worktree, which needs real history. The default depth-1 checkout | |
| # cannot resolve it and the gate would skip on every run. | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| apt: "false" | |
| cache-key: bench | |
| # The `rustynes-core` full_frame bench is chip-stack-only (no wgpu/winit), | |
| # so no apt deps are needed. | |
| # | |
| # TWO gates, deliberately different in kind: | |
| # | |
| # 1. ABSOLUTE ceiling — headless frame production stays well under the | |
| # 16.67 ms NTSC deadline. Never flakes, but on the ~4 ms/frame the | |
| # core actually runs at, a change could get 2.5x slower and still | |
| # pass. It answers "is the emulator still real-time?", not "did this | |
| # PR make it worse". | |
| # 2. RELATIVE A/B — builds and benches the base commit and HEAD back to | |
| # back on THIS runner and compares them to each other. Runner-to- | |
| # runner variance is common-mode and cancels, which is why a | |
| # percentage gate is sound here where a cross-run one would flake | |
| # (same technique as pgo.yml's >3% promotion bar). It answers the | |
| # question the ceiling cannot. | |
| # | |
| # See scripts/bench_{regression,relative}_check.sh + docs/performance.md. | |
| - run: ./scripts/bench_regression_check.sh | |
| # Base ref: the PR's base-branch tip, else the push's previous head. | |
| # Neither is present on a workflow_dispatch, and `github.event.before` is | |
| # all-zeros for a brand-new branch — in both cases the script resolves | |
| # nothing and SKIPs with exit 0 rather than inventing a verdict. | |
| - name: Relative frame-time A/B (base vs HEAD, same runner) | |
| env: | |
| BENCH_BASE_REF: >- | |
| ${{ github.event.pull_request.base.sha || github.event.before }} | |
| run: ./scripts/bench_relative_check.sh | |
| # Single summary status — the ONE check to require in branch protection / the | |
| # merge queue. It runs always (even when a needed job fails or is skipped) and | |
| # fails iff any job it depends on failed or was cancelled. A *skipped* job | |
| # counts as success — so an event-conditional leg (the macOS/Windows matrix on | |
| # a PR) never blocks, AND a docs-only change (every heavy job skipped via the | |
| # `changes` gate) still reports this check green so the PR can merge. KEEP the | |
| # `needs:` list exhaustive: a job missing from it silently stops being gated. | |
| ci-success: | |
| name: CI success | |
| if: ${{ always() }} | |
| needs: [changes, setup, lint, test, test-roms, no_std, wasm, libretro-cross, bench] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fail if any required job failed or was cancelled | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| run: | | |
| echo "A required CI job failed or was cancelled:" | |
| echo '${{ toJSON(needs) }}' | |
| exit 1 | |
| - name: All required jobs succeeded | |
| run: echo "CI success." |