feat(accuracysnes): pitch scales the sample rate, and $2000 is an octave up (E6.02) #659
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 | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| schedule: | |
| # Weekly drift net (v1.5.0 "Bedrock"): catches a regression in a dependency/toolchain/ROM-corpus | |
| # change that landed without a matching code diff to trigger the push/pull_request triggers | |
| # above. Offset from security.yml's own Monday-00:00-UTC cron to spread runner load. | |
| - cron: "0 7 * * 1" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # Cancel a superseded run (a new push to the same PR/branch) instead of letting stale, already- | |
| # obsolete runs burn minutes to completion — the single biggest per-PR cost lever besides scoping | |
| # the full battery to tags/main/schedule (below). | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # v1.5.0 "Bedrock": detect doc-only changes so the expensive jobs below can skip cleanly on a | |
| # pure-docs PR/push instead of burning minutes proving nothing changed. NOT a strict inversion of | |
| # security.yml's paths-ignore list (caught in PR review) — this is a curated, narrower positive | |
| # list of the specific paths that can affect a Rust build/lint/test/CI outcome, so it stays | |
| # correct even if security.yml's own ignore list grows for unrelated (e.g. asset) reasons. | |
| changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| code: | |
| - "crates/**" | |
| - "tests/**" | |
| - "scripts/**" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - "rust-toolchain.toml" | |
| - ".cargo/**" | |
| - ".github/workflows/**" | |
| - ".github/actions/**" | |
| # v1.5.0 "Bedrock": one place that decides "light" (PR / feature-branch push: fast gate only) vs | |
| # "full" (push to main, a release tag, the weekly cron, or a manual dispatch: the complete | |
| # battery) — mirrors RustyNES's own `changes`/`setup` split. Always-scheduled/dispatched/tag/main | |
| # runs are treated as "full" even on a doc-only diff (`always_full`), since a manual/scheduled | |
| # run's whole point is a from-scratch verification, not an incremental one. | |
| setup: | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mode: ${{ steps.mode.outputs.mode }} | |
| steps: | |
| - id: mode | |
| run: | | |
| always_full=false | |
| if [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| always_full=true | |
| elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| always_full=true | |
| elif [[ "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then | |
| always_full=true | |
| fi | |
| if [[ "$always_full" == "true" ]]; then | |
| echo "mode=full" >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ needs.changes.outputs.code }}" == "true" ]]; then | |
| echo "mode=light" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "mode=skip" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Fast gate: every PR event and every push to `main` with code changes. Formatting + lint | |
| # (Linux only) — the 3-OS matrix, the doc build's cross-platform legs, and the no_std/bench gates | |
| # stay reserved for `full` mode (below) so PR iteration stays quick. The tradeoff (a broken PR is | |
| # only caught by the FULL battery at merge-to-main/tag, not on every single push) is deliberate; | |
| # `test-light` below closes the specific gap that tradeoff used to leave (PR-time `cargo test` | |
| # never running at all, relying solely on CONTRIBUTING.md's manual pre-push checklist). | |
| lint: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # fmt/clippy only ever check the HOST target here (no `--target` is ever passed to either | |
| # command in this job) — installing the wasm32/thumbv7em cross targets bought nothing but | |
| # setup time on every single PR push. Only `no_std` (which actually cross-compiles) needs | |
| # thumbv7em-none-eabihf; nothing in ci.yml builds for wasm32 (that's `pages.yml`'s job). | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| components: rustfmt, clippy | |
| linux-frontend-deps: "true" | |
| - run: cargo fmt --all --check | |
| # `--exclude rustysnes-android` (`v1.15.0 "Sideload"`): that crate's `ndk-sys` dependency | |
| # hard-fails (`compile_error!`) on every non-Android target, unconditionally -- it's not | |
| # feature-gated, so no `--features`/`--no-default-features` combination avoids it. Its own | |
| # cross-compiled CI coverage is `android.yml`'s job (a later rung), not this host-target | |
| # workspace gate. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets -- -D warnings | |
| # Explicit flags-off byte-identical gate (`v0.8.0` T-81-004): `default` is currently | |
| # exactly `["wasm-winit", "help-tui"]`, so this is redundant with the line above TODAY — | |
| # its value is as a named regression guard. If a future change ever folds `debug-hooks`/ | |
| # `scripting`/`cheats`/`emu-thread` into `default` without updating this line too, this still | |
| # locks a true flags-off build/lint as its own explicit, protected CI step, independent of | |
| # whatever `default` becomes. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets --no-default-features --features wasm-winit,help-tui -- -D warnings | |
| # Per-feature-combo clippy (`v0.8.0` T-81-004, extended `v0.9.0` T-82-004) — each Phase 8 | |
| # `rustysnes-frontend` flag individually, then combined; NEVER `--all-features` (the | |
| # workspace-wide rule — `wasm-winit`/`wasm-canvas` are mutually exclusive, so an | |
| # all-features build doesn't even make sense). | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features debug-hooks -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features scripting -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features cheats -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features netplay -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features retroachievements -- -D warnings | |
| # `emu-thread` itself was never actually gated here despite being referenced in the comment | |
| # above (found in review, post-`v1.3.0`) — added now, plus its one functionally meaningful | |
| # combo: `netplay` (post-`v1.3.0`, `EmuControl::netplay_paused` + `NetplayState::drive` now | |
| # runs under `emu-thread` too, previously silently unreachable there). `emu-thread,scripting` | |
| # is NOT added — see `full`'s own comment below for why that combo doesn't compile. | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features emu-thread -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features emu-thread,netplay -- -D warnings | |
| # `full` (`v1.0.0`, `cargo full-build`/`full-run`) IS this same combo (plus the no-op | |
| # `hd-pack` placeholder) — testing it directly instead of re-listing the flags keeps this | |
| # step and `full`'s own Cargo.toml definition from silently drifting apart. | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features full -- -D warnings | |
| # `rustysnes-libretro` (`v1.2.0`): already covered by the `--workspace` clippy line above (a | |
| # regular workspace member, no non-default features to combo over), but clippy doesn't | |
| # necessarily exercise the FFI-crate-specific bit that matters most for a `cdylib`/ | |
| # `staticlib`: does it actually LINK. A build-only step (not `cargo test` -- this crate has | |
| # no tests of its own; its logic lives in and is tested by `rustysnes-core`) is cheap and | |
| # catches a broken libretro C-ABI export/link before it reaches a release artifact. | |
| - run: cargo build -p rustysnes-libretro | |
| # Cheap locally (~4s) so it belongs on every PR, not just tag pushes -- catches broken | |
| # intra-doc links and rustdoc-specific warnings clippy's own lints don't cover. | |
| # `--exclude rustysnes-android`: see the clippy step above -- `cargo doc` still compiles the | |
| # crate, so it hits the same `ndk-sys` host-target failure. | |
| - run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --exclude rustysnes-android --no-deps | |
| # v1.5.0 "Bedrock": the actual gap this release closes. Previously `cargo test --workspace` ran | |
| # ONLY on a tagged release (`full-test` below) — every PR/push-to-main merged on `lint`'s | |
| # fmt+clippy alone, trusting CONTRIBUTING.md's manual pre-push "Quality gate" checklist to have | |
| # been run by hand. This job makes that check programmatic without the cost of `full-test`'s 3-OS | |
| # release-mode matrix: one fast, cached, debug-mode, Linux-only `cargo test` pass, every PR/push. | |
| test-light: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: test-light | |
| linux-frontend-deps: "true" | |
| # `--exclude rustysnes-android`: see `lint`'s own clippy step comment above. | |
| - run: cargo test --workspace --exclude rustysnes-android | |
| # Full gate: release-tag pushes, a push to `main`, the weekly cron, or a manual dispatch — see | |
| # `setup` above. The complete verification battery — the 3-OS matrix, both test invocations, and | |
| # the doc-warnings gate. | |
| full-test: | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # Same rationale as `lint`: neither fmt nor clippy nor `cargo test` cross-compiles here, so | |
| # no extra targets are installed. | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| components: rustfmt, clippy | |
| cache-key-suffix: full-test-${{ matrix.os }} | |
| linux-frontend-deps: "true" | |
| # Formatting is platform-independent (pure source-text parsing) — one leg is enough. | |
| - run: cargo fmt --all --check | |
| if: runner.os == 'Linux' | |
| # `--exclude rustysnes-android`: see `lint`'s own clippy step comment above. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets -- -D warnings | |
| - run: cargo test --workspace --exclude rustysnes-android | |
| - run: cargo test --workspace --exclude rustysnes-android --features test-roms | |
| # Exhaustive behavioral coverage (not just clippy) of every Phase 8 flag together, ahead of | |
| # a tagged release (`v0.8.0` T-81-004, extended `v0.9.0` T-82-004). Linux-only, matching | |
| # `lint`'s per-combo clippy: `scripting` vendors and compiles Lua 5.4 via `mlua`'s C source, | |
| # and `retroachievements` vendors and compiles `rcheevos` via `cc` -- both real | |
| # cross-platform build surface `lint` never exercises (Linux only, host target) — verifying | |
| # that specifically on macOS/Windows here too is out of this ticket's scope (it's a genuine | |
| # question of its own, not "is the byte-identical-off gate wired up"). | |
| - run: cargo test -p rustysnes-frontend --features full | |
| if: runner.os == 'Linux' | |
| # `emu-thread` was clippy-checked in `lint` (compiled, never run) but its own unit tests | |
| # (`EmuControl`/`PresentBuffer`, post-`v1.3.0`) never actually executed anywhere in CI until | |
| # now (found in review). `netplay` is its one functionally meaningful combo (see `lint`'s | |
| # own comment). | |
| - run: cargo test -p rustysnes-frontend --features emu-thread,netplay | |
| if: runner.os == 'Linux' | |
| - run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --exclude rustysnes-android --no-deps | |
| if: runner.os == 'Linux' | |
| # Full-gate cadence only (see `setup` above) — cross-compiling the whole chip stack is cheap but | |
| # doesn't need to happen on every single PR push. | |
| # | |
| # `v1.14.0 "Foundry"`: expanded from a single `rustysnes-core --no-default-features` build into | |
| # a per-crate matrix. The old single job only proved the no_std posture TRANSITIVELY (every | |
| # chip crate is already `default-features = false` in `rustysnes-core`'s own `Cargo.toml`, and | |
| # `#![no_std]` is unconditional -- not feature-gated -- in each chip crate's `lib.rs`), never as | |
| # its own standalone build target. This matrix builds each chip crate directly, so a future | |
| # accidental `std`-only dependency added to just one of them fails on ITS OWN row instead of | |
| # only surfacing (if at all) through `rustysnes-core`'s aggregate build. | |
| no_std: | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| crate: | |
| - rustysnes-cpu | |
| - rustysnes-ppu | |
| - rustysnes-apu | |
| - rustysnes-cart | |
| - rustysnes-core | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| targets: thumbv7em-none-eabihf | |
| cache-key-suffix: no_std-${{ matrix.crate }} | |
| - run: cargo build -p ${{ matrix.crate }} --target thumbv7em-none-eabihf --no-default-features | |
| # Full-gate cadence only (`v1.0.0`): 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 | |
| # release/main/cron cadence, not on every iteration. Ported from RustyNES's own `bench` job | |
| # (`scripts/bench_regression_check.sh` + `docs/performance.md`). | |
| bench: | |
| name: frame-time regression gate | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # The `rustysnes-core` `headless_frame` bench is chip-stack-only (no wgpu/winit/egui), so | |
| # no Linux frontend system deps are needed here (unlike `lint`/`full-test`). | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: bench | |
| - run: ./scripts/bench_regression_check.sh | |
| # AccuracySNES reproducibility gate. The cart's `.sfc` is committed so contributors can run the | |
| # battery without an assembler; this job proves the committed image is exactly what the source | |
| # produces, so the binary can never silently drift from `gen/`. | |
| accuracysnes: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: accuracysnes | |
| - name: Install cc65 (ca65/ld65) | |
| run: | | |
| dpkg -s cc65 >/dev/null 2>&1 || { | |
| sudo apt-get update | |
| sudo apt-get install -y cc65 | |
| } | |
| - name: Rebuild the cartridge from source | |
| run: cargo run -p accuracysnes-gen | |
| - name: Assert the committed artifacts are byte-identical | |
| run: | | |
| if ! git diff --exit-code -- tests/roms/AccuracySNES; then | |
| echo "::error::AccuracySNES build output differs from the committed artifacts." | |
| echo "::error::Run 'cargo run -p accuracysnes-gen' and commit the result." | |
| exit 1 | |
| fi | |
| - name: Run the battery | |
| run: | | |
| cargo test -p rustysnes-test-harness --features test-roms \ | |
| --test accuracysnes -- --nocapture | |
| # v1.5.0 "Bedrock": the one stable required-check name branch protection points at | |
| # (`docs/adr/0011`). Individual jobs above are conditionally skipped depending on `setup`'s mode, | |
| # which makes them unsuitable to require directly (a skipped required check blocks merging | |
| # forever). This job succeeds iff every job it depends on either succeeded or was cleanly | |
| # skipped, and fails if any of them failed or were cancelled. | |
| ci-success: | |
| if: always() | |
| needs: [changes, setup, lint, test-light, full-test, no_std, bench, accuracysnes] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check all required jobs | |
| run: | | |
| echo '${{ toJSON(needs) }}' | |
| for result in $(echo '${{ toJSON(needs.*.result) }}' | jq -r '.[]'); do | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| echo "::error::A required job did not succeed (result: $result)" | |
| exit 1 | |
| fi | |
| done |