Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/actions/rust-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ runs:
# them were reviewed rather than trusted -- runner updates, patch-release
# table entries, a cross-device-copy fix and checkout bumps.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # v1
with:
toolchain: ${{ steps.resolve.outputs.toolchain }}
targets: ${{ inputs.targets }}
Expand Down
72 changes: 32 additions & 40 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,41 @@ updates:
labels:
- "dependencies"
- "rust"
# The prefix carries no scope: `include: "scope"` appends `(deps)` (or
# `(deps-dev)`) itself, so a `chore(deps)` prefix titled every PR
# `chore(deps)(deps): ...` until v3.0.1.
commit-message:
prefix: "chore(deps)"
prefix: "chore"
include: "scope"
reviewers:
- "doublegate"
# No `reviewers:` key: GitHub retired it in 2025 in favour of CODEOWNERS
# (github.blog changelog 2025-08-08), and `.github/CODEOWNERS` assigns
# @doublegate to every path. Removed in v3.0.1.
assignees:
- "doublegate"
# HOLD the egui tier. A comment in `Cargo.toml` explains WHY the pin exists,
# but a comment does not reach Dependabot -- it would keep re-opening the
# same three PRs every Monday, and the risk is not the noise, it is that the
# twentieth identical PR gets merged on the assumption it is routine.
#
# `egui-winit` 0.36.1 does not compile for `wasm32-unknown-unknown`, and
# RustyNES ships a wasm demo:
#
# error[E0407]: method `bytes` is not a member of trait `egui::DroppedFile`
#
# egui 0.36 split `DroppedFile` by target (`bytes_async` on wasm, `bytes` on
# native) while egui-winit's `NativeFile` impl has no cfg gate, so on wasm it
# implements a method the trait does not declare and omits the one it does.
# `wgpu` is held with them only because `egui-wgpu` 0.35 pins it.
#
# REMOVE ALL FOUR ENTRIES once upstream ships the fix -- this is a hold, not
# a policy. Re-check by bumping and running both wasm clippy invocations.
ignore:
- dependency-name: "egui"
versions: [">=0.36"]
- dependency-name: "egui-wgpu"
versions: [">=0.36"]
- dependency-name: "egui-winit"
versions: [">=0.36"]
- dependency-name: "wgpu"
versions: [">=30"]
# `naga` is a DIRECT dependency of `rustynes-frontend` (WGSL validation),
# not merely a wgpu transitive, so Dependabot would propose 30 on its own
# and the hold would be broken from a direction the four entries above do
# not cover. Caught in review on the v2.6.3 refresh.
- dependency-name: "naga"
versions: [">=30"]
# Group minor and patch updates together
# From 2026-09-12 to 2026-09-28 an `ignore` block here held egui,
# egui-wgpu, egui-winit (`>=0.36`), wgpu and naga (`>=30`) at the 0.35 tier,
# because egui-winit 0.36 did not compile for wasm32. The tier then moved to
# 0.36 / wgpu 30 with egui-winit vendored (see `Cargo.toml`), and the hold
# stayed behind until v3.0.1 -- silently blocking every later egui and wgpu
# release. The coupling it protected is real, so it is now a GROUP instead.
groups:
# ONE PR FOR THE WHOLE TIER. `Cargo.toml` says these five move together:
# `egui-wgpu` pins one wgpu major, a graph holding two wgpu majors fails
# every device/queue handoff, and `naga` is a DIRECT dependency of
# `rustynes-frontend` (WGSL validation) that must match wgpu's. Listed
# FIRST, with no `update-types`, because Dependabot puts a dependency in
# the first group it matches -- below the catch-all groups, a minor bump
# of one of these would land alone in `production-dependencies`. Before
# merging one: egui-winit is vendored (`vendor/egui-winit/VENDORED.md`),
# and a bump past 0.36.2 bypasses that patch, so run both wasm clippy gates.
egui-wgpu-tier:
patterns:
- "egui"
- "egui-wgpu"
- "egui-winit"
- "wgpu"
- "naga"
# Group minor and patch updates together
development-dependencies:
dependency-type: "development"
update-types:
Expand Down Expand Up @@ -119,7 +113,7 @@ updates:
- "dependencies"
- "android"
commit-message:
prefix: "chore(deps)"
prefix: "chore"
include: "scope"

# `crates/rustynes-cosim` is EXCLUDED from the workspace on purpose (cargo
Expand All @@ -142,7 +136,7 @@ updates:
- "dependencies"
- "rust"
commit-message:
prefix: "chore(deps)"
prefix: "chore"
include: "scope"

# Maintain GitHub Actions
Expand All @@ -159,7 +153,5 @@ updates:
- "github-actions"
commit-message:
prefix: "chore(ci)"
reviewers:
- "doublegate"
assignees:
- "doublegate"
2 changes: 2 additions & 0 deletions .github/release-notes/v2.6.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ A detent is the notch that holds a mechanism at an exact position and makes that

**The emulation core is unchanged.** AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction; the work is in the sibling repository and in the release machinery.

> Correction (v3.0.1): the sentence above is wrong. The emulation core changed in one place in this release, the taken-branch NMI poll in `Cpu::handle_interrupts` (`skip_irq_sample_q`, `CPU_SNAPSHOT_VERSION` 3 to 4; see "An interrupt polled on a cycle the documentation forbids" below), and AccuracyCoin 141/141 and nestest 0-diff were re-run against it rather than holding by construction.

## The gate

| # | criterion | result |
Expand Down
52 changes: 52 additions & 0 deletions .github/release-notes/v3.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# RustyNES v3.0.1 — "Mortar"

A maintenance release on v3.0.0. It fixes one game's graphics, reaches the last untested exception of the MMC3 timing rule in the MiSTer core (and fixes the one-cycle defect that found), moves the toolchain and every dependency to its newest release, answers every bot review left unanswered since PR #1, settles two provenance questions, and writes the plan to v4.0.0. **Movies recorded with v3.0.0, and netplay peers running it, are refused**: the emulation epoch rises to 2. Save states are unaffected.

## Breaking change

- **Movies and netplay from v3.0.0 are refused.** The mapper 45 fix below changes what *Famicom Yarou Vol.1* produces, so `EMULATION_EPOCH` rises from 1 to 2 ([ADR 0045](https://github.com/doublegate/RustyNES/blob/main/docs/adr/0045-a-core-timing-epoch-guards-movies-and-netplay.md)). The refusal names both epochs. Save states load as before.

## Fixed

- ***Famicom Yarou Vol.1 7-in-1* draws its menu.** Mapper 45 (GA23C) addresses a cartridge's CHR-RAM unbanked, as the board does; the emulator had banked it like CHR-ROM and drew noise (T-GA23C-CHRRAM).
- **The MiSTer core's MMC3 timing, one cycle on odd frames.** v3.0.0's dot-0 rule for the background's A12 had an exception for the dot the odd-frame skip replaces, and nothing ever reached it. A new generated test ROM does, and found that a `$2001` write taking effect one dot late still applied the rule, so the MMC3 interrupt came one CPU cycle early. The rule now asks whether cycle 0 was rendering. The core and the emulator agree on all 2,978,055 cycles of the new test, and a new comparison of the cycle on which each interrupt rises catches the exception's mutant, which the bus comparison alone could not.
- **Every unanswered bot review, back to PR #1, answered.** 290 unanswered review threads, review-body findings and Antigravity reviews across both repositories got 473 verdicts; the 80 still valid were fixed. Among them: the Bisqwit NTSC filter kept showing the last game frame after a ROM was closed; a mapper-0 override saved from the ROM Database panel vanished on restart; the release tooling could exit 0 on a stale lockfile; three release audits could pass on prose they should fail; and Dependabot's egui holds blocked every future egui and wgpu update (now a group that moves them together).

## Provenance

- **The shared Bisqwit NTSC pass is recorded as derived.** Its documentation called it an independent implementation, but it is a generated copy of the desktop filter, whose tables were long recorded as ported from Bisqwit's C via Mesen2. It now carries the same attribution, and the audit lost the exception that hid it.
- **The TriCNES source moved out of the repository.** TriCNES (MIT, by the AccuracyCoin author) stays the one reference whose source may be consulted, for AccuracyCoin work and always attributed. The committed cross-diff evidence stays.
- **A softened comment in the Sunsoft 5B mixer says "derived from" again.**

## Toolchain and dependencies

- **Rust 1.99, everywhere.** The libretro buildbot moves too: the release first held it on 1.96 because its build image passed a flag Rust 1.97 rejects, then found the image had dropped it, and a test branch built all 15 buildbot jobs on 1.99, Apple included. CI now fails if the two toolchains ever differ.
- **Every dependency at its newest release**: crates, GitHub Actions (macOS jobs move to `macos-15`), Android (`cargo-ndk` 4, NDK r30), the web build (wasm-opt now pinned), the documentation build, and Docker images (Rust 1.99, Debian 13).

## The MiSTer core

- **Release candidate, not hardware-verified.** No hardware has run either bitstream.
- **The co-simulation ladder is 200 passed, 0 failed, 1 expected failure on-die and 201 passed, 0 failed, 1 expected failure off-die**, each from one run of a frozen tree, with nothing skipped. The one new gate is the odd-frame A12 test above.
- **New bitstreams, because the fix is RTL.** Both builds are compiled at fitter seed 2, chosen from eight seeds swept on one build date (261007), every one of which closes on both builds. Each was compiled twice to the same bytes:
- on-die `RustyNES_MiSTer-v3.0.1.rbf`, md5 `7e81a71869760b35a2fd04e6309c5c39` (timing margin +0.448 ns setup, +0.113 ns hold);
- off-die `RustyNES_MiSTer-v3.0.1-offdie.rbf`, md5 `88d1dfa53a94996a92c0f768b3be3039` (+0.401 / +0.096 ns; the SDRAM read +0.447 / +1.184 ns, assuming zero board delay).
- The bitstreams carry the sweep's build date, which may be earlier than the release date.

## The road to v4.0.0

The plan from v3.1.0 to v4.0.0 is written ([`to-dos/plans/v3.1-to-v4.0-line-plan.md`](https://github.com/doublegate/RustyNES/blob/main/to-dos/plans/v3.1-to-v4.0-line-plan.md)), from 29 maintainer decisions. v4.0.0 makes the remaining public enums `#[non_exhaustive]` and brings the MiSTer core to feature parity. The hardware-verification release (the SuperStation One board session and the mobile device run) comes at the end of the v3.9.x line, so it tests the near-final core.

## Verification

- `--features test-roms`: 3,234 passed, 0 failed, 11 ignored.
- The local commercial suites: `external_real_games` 60/0, `external_extended` 137/0, `external_coverage` 6/0 over every staged ROM. The one moved baseline is *Famicom Yarou Vol.1*, which now draws its menu.
- AccuracyCoin 144/144, nestest 0-diff.
- The libretro buildbot: all 15 jobs on Rust 1.99 before the pin was lifted.

## Install

- Download the pre-built binaries for Linux, macOS, and Windows below.
- The MiSTer core bitstreams are attached below, as a release candidate, not hardware-verified. The on-die build is `RustyNES_MiSTer-v3.0.1.rbf`, also attached under its datecoded name `RustyNES_20261007.rbf`, and the off-die build is `RustyNES_MiSTer-v3.0.1-offdie.rbf`.
- The WebAssembly build is live at [doublegate.github.io/RustyNES](https://doublegate.github.io/RustyNES/).
- The RetroArch core is in RetroArch's Online Updater on the platforms the libretro buildbot publishes to.
- Licensed under GPL-3.0-or-later.
72 changes: 58 additions & 14 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ concurrency:

env:
CARGO_TERM_COLOR: always
# v3.0.1: NDK r30, the newest stable (`ndk;30.0.16248370` in Google's SDK
# repository). The runner image ships r29 as its newest, so every job
# installs this one with `sdkmanager` in its "Resolve NDK" step. r30's
# changelog lists a newer clang, simpleperf prebuilts and sysroots up to
# API 37; nothing that touches a `--platform 26` build.
NDK_VERSION: "30.0.16248370"

jobs:
cross-build:
Expand Down Expand Up @@ -103,7 +109,7 @@ jobs:
- 'crates/rustynes-mobile/**'
- '.github/workflows/android.yml'

# The project toolchain (rust-toolchain.toml = 1.96) plus the two shipped
# The project toolchain (rust-toolchain.toml = 1.99) plus the two shipped
# Android targets (arm64 ships; x86_64 is the emulator/CI ABI), through
# the shared composite. A bare `rustup target add` made rustup
# AUTO-INSTALL the pinned toolchain first, which rustup now deprecates
Expand All @@ -122,19 +128,29 @@ jobs:
# `taiki-e/install-action` knows, which is why this is the caching form.
- uses: taiki-e/cache-cargo-install-action@v3
with:
tool: cargo-ndk@3
tool: cargo-ndk@4

# ubuntu-26.04 ships the Android SDK + an NDK; resolve its path so
# cargo-ndk finds the toolchain. (We pin a recent NDK; r27+ aligns .so to
# 16 KB by default — a Play requirement for Android 15+.)
# Both variables, to the same NDK. The runner image sets ANDROID_NDK_ROOT
# to its default NDK (27.x) while this job selects the latest (29.x) via
# to its default NDK (27.x) while this job selects its own (r30, above) via
# ANDROID_NDK_HOME, and cargo-ndk warned on every build that the two
# disagree -- a real ambiguity about which toolchain linked the library.
- name: Resolve NDK
run: |
echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
set -euo pipefail
sdk="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
# `yes` answers the licence prompt. Under `pipefail` its own exit
# status (EPIPE / SIGPIPE once sdkmanager closes the pipe, 141 here)
# failed the step even when the install succeeded, so it is absorbed;
# sdkmanager's status, and the clang check below, still decide.
(yes || true) | "$sdk/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null
ndk="$sdk/ndk/$NDK_VERSION"
test -x "$ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" \
|| { echo "::error::NDK $NDK_VERSION did not install at $ndk"; exit 1; }
echo "ANDROID_NDK_HOME=$ndk" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ndk" >> "$GITHUB_ENV"

- name: Cross-compile mobile + android (arm64 + x86_64)
run: |
Expand Down Expand Up @@ -198,15 +214,29 @@ jobs:
cache-key: android-ndk
- uses: taiki-e/cache-cargo-install-action@v3
with:
tool: cargo-ndk@3
tool: cargo-ndk@4
# v3.0.1: Temurin 25, the newest LTS (was 17). This is the JDK that RUNS
# Gradle; the bytecode target stays JVM 17 (`jvmTarget` and
# `sourceCompatibility` in app/build.gradle.kts), which is AGP 9.4's
# floor. Gradle 9.8 supports running on Java 25 (9.1.0 and later).
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '17'
java-version: '25'
- name: Resolve NDK
run: |
echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
set -euo pipefail
sdk="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
# `yes` answers the licence prompt. Under `pipefail` its own exit
# status (EPIPE / SIGPIPE once sdkmanager closes the pipe, 141 here)
# failed the step even when the install succeeded, so it is absorbed;
# sdkmanager's status, and the clang check below, still decide.
(yes || true) | "$sdk/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null
ndk="$sdk/ndk/$NDK_VERSION"
test -x "$ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" \
|| { echo "::error::NDK $NDK_VERSION did not install at $ndk"; exit 1; }
echo "ANDROID_NDK_HOME=$ndk" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ndk" >> "$GITHUB_ENV"
- uses: gradle/actions/setup-gradle@v6.4.0
with:
cache-provider: basic
Expand Down Expand Up @@ -254,19 +284,33 @@ jobs:
# `taiki-e/install-action` knows, which is why this is the caching form.
- uses: taiki-e/cache-cargo-install-action@v3
with:
tool: cargo-ndk@3
tool: cargo-ndk@4
# v3.0.1: Temurin 25, the newest LTS (was 17). This is the JDK that RUNS
# Gradle; the bytecode target stays JVM 17 (`jvmTarget` and
# `sourceCompatibility` in app/build.gradle.kts), which is AGP 9.4's
# floor. Gradle 9.8 supports running on Java 25 (9.1.0 and later).
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '17'
java-version: '25'
# Both variables, to the same NDK. The runner image sets ANDROID_NDK_ROOT
# to its default NDK (27.x) while this job selects the latest (29.x) via
# to its default NDK (27.x) while this job selects its own (r30, above) via
# ANDROID_NDK_HOME, and cargo-ndk warned on every build that the two
# disagree -- a real ambiguity about which toolchain linked the library.
- name: Resolve NDK
run: |
echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
set -euo pipefail
sdk="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
# `yes` answers the licence prompt. Under `pipefail` its own exit
# status (EPIPE / SIGPIPE once sdkmanager closes the pipe, 141 here)
# failed the step even when the install succeeded, so it is absorbed;
# sdkmanager's status, and the clang check below, still decide.
(yes || true) | "$sdk/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null
ndk="$sdk/ndk/$NDK_VERSION"
test -x "$ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" \
|| { echo "::error::NDK $NDK_VERSION did not install at $ndk"; exit 1; }
echo "ANDROID_NDK_HOME=$ndk" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ndk" >> "$GITHUB_ENV"
- uses: gradle/actions/setup-gradle@v6.4.0
with:
# v6 extracted the default ("enhanced") Gradle User Home cache into the
Expand Down
Loading