Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6a5a84c
fix(ppu)!: serialize the sprite-evaluation FSM in a v8 snapshot tail
doublegate Jul 23, 2026
244e6c1
test(save-state): audit chip-struct fields against the snapshot schema
doublegate Jul 23, 2026
77dc87e
fix(apu): serialize the scheduled warm-reset $4017 re-write (v4 tail)
doublegate Jul 23, 2026
96b20a9
perf(ppu): promote the fast dot path to default and expose it (~11%)
doublegate Jul 23, 2026
a38e8b8
perf(ppu): idle-line dot path behind a default-off feature (below the…
doublegate Jul 23, 2026
a019fdd
ci(release): ship the PGO binary as the linux release asset
doublegate Jul 23, 2026
6a3b13f
ci(bench): add a same-runner relative frame-time regression gate
doublegate Jul 23, 2026
819b1bd
fix(apu): calibrate the Sunsoft 5B level; widen mix_audio to i32
doublegate Jul 23, 2026
67d9544
refactor(mappers): name mapper modules for their board, not their sprint
doublegate Jul 23, 2026
1c0546a
refactor(mappers): prefix the five single-board multi-ID modules; ren…
doublegate Jul 23, 2026
669fee3
fix(mappers): FME-7 open bus on the RAM-selected-but-disabled window …
doublegate Jul 23, 2026
2ba83cd
fix(mappers): MMC1 WRAM write-protect; bless the stale commercial-ora…
doublegate Jul 23, 2026
e8030e9
feat(input): Zapper beam-relative light model (A3), default off
doublegate Jul 23, 2026
9ad4899
chore(release): v2.2.3 "Datum"
doublegate Jul 23, 2026
03c3956
fix(ci): agy reviewer skips cleanly when the diff exceeds GitHub's AP…
doublegate Jul 23, 2026
f8974f0
ci(review): hand large diffs to agy on disk instead of truncating them
doublegate Jul 23, 2026
2581ad8
fix(mappers,ppu,docs): adjudicate the 44-comment CodeRabbit pass
doublegate Jul 23, 2026
d2bdccd
fix(core,tooling,docs): address CodeRabbit's four follow-up threads
doublegate Jul 23, 2026
839eafc
test(core): add temporal-Zapper peek/read parity test; correct the pr…
doublegate Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# actionlint configuration.
#
# actionlint validates `runs-on:` against GitHub's list of hosted-runner labels
# and reports anything else as unknown, because a typo'd label is a job that
# queues forever rather than failing. Self-hosted labels therefore have to be
# declared here — that is the mechanism actionlint's own diagnostic points at.
#
# Run it over the whole tree with:
#
# actionlint
#
# (no arguments — it discovers `.github/workflows/*.yml` and reads this file).

self-hosted-runner:
labels:
# The maintainer's self-hosted runner for the Antigravity PR reviewer
# (`.github/workflows/antigravity-review.yml`, `runs-on: [self-hosted, agy]`).
# It is a personal machine holding the `agy` CLI's Google AI Ultra OAuth
# session, which is why that workflow cannot run on a hosted runner. See
# `Local_Only-Projects/antigravity-pr-review/README.md` in the workspace.
- agy
141 changes: 141 additions & 0 deletions .github/release-notes/v2.2.3.md

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions .github/workflows/antigravity-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,24 @@ jobs:
# Check out the DEFAULT BRANCH, never the PR head. This job runs the checked-out
# `scripts/agy-review.sh` on a self-hosted runner with a token in the environment,
# so taking those scripts from the PR would let the reviewed change rewrite its own
# reviewer. The review content is unaffected: the diff is fetched from the API by
# `gh pr diff`, not from the working tree. Consequence worth knowing: a PR that
# edits the reviewer or the style guide is reviewed by the version already on main.
# reviewer. The review content is unaffected: the diff comes from `gh pr diff` (or,
# for a diff over GitHub's 20,000-line API limit, from a local `git diff` of fetched
# objects) — never from the checked-out working tree, which stays on the default
# branch throughout. Consequence worth knowing: a PR that edits the reviewer or the
# style guide is reviewed by the version already on main.
- name: Check out repo (for the style guide + scripts)
uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
# Not `fetch-depth: 0`: the large-diff fallback fetches exactly the two refs it
# needs on demand, so a full history clone would be paid on every run for a path
# most runs never take.
fetch-depth: 1
# Repo-wide rule (PR #319): no checkout persists credentials. Nothing here
# performs an authenticated git network operation — `gh` uses GH_TOKEN.
# Repo-wide rule (PR #319): no checkout persists credentials. The large-diff
# fallback does perform an authenticated fetch, but supplies the token per
# command via `http.extraheader` rather than persisting it into .git/config —
# which is the point of the rule, since agy executes with the repo as its
# workspace.
persist-credentials: false

- name: Run Antigravity review
Expand All @@ -69,10 +77,12 @@ jobs:
# --- optional overrides (uncomment to change) ---
# AGY_MODEL: gemini-3-pro # default: agy's configured model
AGY_EFFORT: high # low|medium|high
# Diff budget. The prompt is passed to agy as a SINGLE argv string, and Linux
# caps one argument at MAX_ARG_STRLEN = 32 * PAGE_SIZE = 128 KiB (this is a
# separate, much lower ceiling than ARG_MAX). 90 KB leaves room for the
# instruction boilerplate + style guide; the script enforces the total cap.
# INLINE diff budget — not a cap on what gets reviewed. The prompt reaches agy
# as a SINGLE argv string, and Linux caps one argument at
# MAX_ARG_STRLEN = 32 * PAGE_SIZE = 128 KiB (a separate, much lower ceiling than
# ARG_MAX). 90 KB leaves room for the instruction boilerplate + style guide.
# A diff over this is NOT truncated: it is written into agy's workspace in parts
# and read with agy's file tools, so PR size no longer bounds review coverage.
MAX_DIFF_BYTES: "90000"
# STYLE_GUIDE: .github/agy-review.md # style guide, loaded if present
run: |
Expand Down
50 changes: 45 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ jobs:
# 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
Expand Down Expand Up @@ -418,18 +432,44 @@ jobs:
- 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. The gate asserts headless frame production
# stays well under the 16.67 ms NTSC real-time deadline — a deliberately
# non-flaky absolute ceiling (a tight %-regression gate would flake on
# shared runners; that's a local criterion-baseline workflow instead).
# See scripts/bench_regression_check.sh + docs/performance.md.
# 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
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,21 @@ jobs:
# toolchain with a POSIX-safe pipeline: prefer the canonical `Xcode_26.app`,
# else the lexically-highest `Xcode_26.x.app` (26.0–26.9 order correctly under
# plain `sort`). Using `sort -V` here would wrongly fall back on a real runner.
xc=$(ls -d /Applications/Xcode_26.app /Applications/Xcode_26.*.app 2>/dev/null | sort | tail -1 || true)
#
# Shell globbing rather than `ls -d` (shellcheck SC2012). The ordering is
# load-bearing and unchanged: both patterns are emitted together, filtered
# to real directories, then `sort | tail -1` picks the last. That still
# prefers `Xcode_26.app` over `Xcode_26.<n>.app`, because after the common
# `Xcode_26.` prefix `a` (0x61) sorts above any digit — the canonical app
# is the lexically-highest name, not the first. A non-matching glob expands
# to the literal pattern, which the `-d` test discards.
xc=$(
printf '%s\n' /Applications/Xcode_26.app /Applications/Xcode_26.*.app \
| while IFS= read -r d; do
[ -d "$d" ] && printf '%s\n' "$d"
done \
| sort | tail -1
)
if [ -n "$xc" ]; then
sudo xcode-select -s "$xc/Contents/Developer"
echo "Selected $xc"
Expand Down
87 changes: 77 additions & 10 deletions .github/workflows/pgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ name: PGO
# the workspace twice plus a multi-ROM training sweep — far too slow for the PR
# gate (that's the fast `bench` job in ci.yml). It runs ONLY on:
# * workflow_dispatch — manual, from the Actions tab; and
# * push of a release tag (v*) — alongside release.yml, so a release can
# consider shipping the PGO binary.
# * workflow_call — invoked BY `release.yml`, which ships the promoted
# binary as the Linux release asset (see below).
#
# WHY NOT `push: tags` ANY MORE (v2.2.3). It used to also trigger on a `v*` tag
# push, "alongside release.yml, so a release can consider shipping the PGO
# binary" — except nothing ever consumed the result, so the gate computed a
# promotable artifact that was then thrown away. `release.yml` now calls this
# workflow directly and uses its output. Keeping the tag trigger as well would
# start a SECOND, redundant 90-minute run for every manually-pushed tag (the
# auto-release path pushes tags with GITHUB_TOKEN, which does not re-trigger
# `on: push`, so that path would have been fine — but a hand-pushed tag would
# double up). One entry point per tag is the whole point.
#
# PROMOTION GATE (both conditions, AND):
# 1. FASTER — the PGO `full_frame` criterion mean must beat the plain-release
Expand Down Expand Up @@ -42,9 +52,26 @@ on:
required: false
default: false
type: boolean
push:
tags:
- "v*"
# Invoked by `release.yml` on a version tag. The caller consumes
# `outputs.promotable` + `outputs.artifact` to ship the PGO binary as the
# Linux release asset when the gate passes.
workflow_call:
inputs:
frames:
description: "Training frames per ROM (default 3600 = 60 s NTSC each)."
required: false
default: "3600"
type: string
outputs:
promotable:
description: "'true' when the PGO build cleared BOTH the >3% speedup bar and the byte-identical determinism oracle."
value: ${{ jobs.pgo.outputs.promotable }}
speedup_pct:
description: "Measured full_frame speedup of PGO vs plain release, in percent."
value: ${{ jobs.pgo.outputs.speedup_pct }}
artifact:
description: "Name of the uploaded PGO-binary artifact (empty unless promotable)."
value: ${{ jobs.pgo.outputs.artifact }}

# Never cancel an in-flight PGO run; let each finish so its A/B numbers + the
# determinism proof land in the log.
Expand Down Expand Up @@ -72,6 +99,10 @@ jobs:
outputs:
promotable: ${{ steps.gate.outputs.promotable }}
speedup_pct: ${{ steps.gate.outputs.speedup_pct }}
# Empty unless promotable — the upload step below is gated on the same
# condition, so a caller that downloads this name only ever does so when
# the artifact actually exists.
artifact: ${{ steps.triple.outputs.artifact }}
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -110,8 +141,15 @@ jobs:
# runs `pgo_trainer` over the committed corpus, then
# `cargo pgo optimize build -- -p rustynes-frontend`. The optimized
# `rustynes` binary lands at target/<triple>/release/rustynes.
# `inputs.frames` (NOT `github.event.inputs.frames`): the latter is
# populated only for `workflow_dispatch` and is empty on `workflow_call`,
# which would silently drop the caller's value. The `inputs.` context
# covers both triggers. Bound through `env:` rather than interpolated
# into the command string.
- name: PGO instrument + train + optimized rebuild
run: scripts/pgo/run.sh "${{ github.event.inputs.frames || '3600' }}"
env:
PGO_FRAMES: ${{ inputs.frames || '3600' }}
run: scripts/pgo/run.sh "$PGO_FRAMES"

# ---- Stage 3: PGO full_frame bench (A/B vs the `plain` baseline) ---
- name: PGO full_frame bench
Expand All @@ -125,8 +163,15 @@ jobs:
# the golden-framebuffer visual_regression suite + the APU mixer/volume
# audio suites all assert byte-exact values — if PGO codegen perturbed any
# framebuffer/audio/cycle hash, this fails and blocks promotion.
# `continue-on-error` so a determinism DIVERGENCE is a gate verdict, not a
# job crash: the gate step below already reads `oracle_ok` and treats an
# unset value as FAIL, which is what the workflow header has always
# claimed ("a failure of either is informational"). Without this the job
# died here and the gate never ran — and now that `release.yml` calls this
# workflow, that would redden a release whose assets are perfectly fine.
- name: Determinism oracle (test-roms, PGO-optimized codegen)
id: oracle
continue-on-error: true
run: |
set -o pipefail
cargo pgo optimize test -- --workspace --release --features test-roms \
Expand Down Expand Up @@ -167,8 +212,19 @@ jobs:
# ---- Stage 6: upload the PGO binary ONLY when promotable ----------
- name: Resolve target triple
id: triple
env:
PROMOTABLE: ${{ steps.gate.outputs.promotable }}
run: |
echo "triple=$(rustc -vV | sed -n 's/host: //p')" >> "$GITHUB_OUTPUT"
triple="$(rustc -vV | sed -n 's/host: //p')"
echo "triple=${triple}" >> "$GITHUB_OUTPUT"
# Publish the artifact NAME only when the gate promoted, so the
# caller's `if:` and its `download-artifact` name agree by
# construction rather than by two places staying in sync.
if [ "$PROMOTABLE" = "true" ]; then
echo "artifact=rustynes-pgo-${triple}" >> "$GITHUB_OUTPUT"
else
echo "artifact=" >> "$GITHUB_OUTPUT"
fi

- name: Upload promoted PGO binary
if: steps.gate.outputs.promotable == 'true'
Expand All @@ -185,9 +241,16 @@ jobs:
bolt:
name: BOLT post-link (linux, best-effort)
needs: pgo
# Explicit opt-in ONLY. Previously this ran whenever the PGO stage promoted
# and the event was not a workflow_dispatch-without-run_bolt — which, now
# that `release.yml` calls this workflow, would fire BOLT on every release
# and add ~90 minutes for an artifact nothing consumes (the release ships
# the PGO binary, not the BOLT one). BOLT stays the opt-in experiment it
# has always been in practice.
if: >-
needs.pgo.outputs.promotable == 'true' &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.run_bolt == 'true')
github.event_name == 'workflow_dispatch' &&
inputs.run_bolt == true
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
Expand Down Expand Up @@ -219,7 +282,9 @@ jobs:
# pgo job are intentionally not BOLT-instrumentable post-strip).
- name: PGO instrument + train + optimized rebuild
if: steps.bolt_probe.outputs.have_bolt == 'true'
run: scripts/pgo/run.sh "${{ github.event.inputs.frames || '3600' }}"
env:
PGO_FRAMES: ${{ inputs.frames || '3600' }}
run: scripts/pgo/run.sh "$PGO_FRAMES"

- name: Plain baseline bench
if: steps.bolt_probe.outputs.have_bolt == 'true'
Expand All @@ -231,9 +296,11 @@ jobs:

- name: BOLT instrument + optimize
if: steps.bolt_probe.outputs.have_bolt == 'true'
env:
PGO_FRAMES: ${{ inputs.frames || '3600' }}
run: |
cargo pgo bolt build -- -p rustynes-frontend
scripts/pgo/run.sh "${{ github.event.inputs.frames || '3600' }}"
scripts/pgo/run.sh "$PGO_FRAMES"
cargo pgo bolt optimize -- -p rustynes-frontend

- name: BOLT full_frame bench + gate
Expand Down
Loading