Skip to content

Releases: doublegate/Rusty2600

v2.11.0 "Field Trip"

Choose a tag to compare

@doublegate doublegate released this 03 Jul 02:36

Rusty2600 v2.11.0 "Field Trip"

Eighth release of the RustyNES gap-closure arc — wires
rusty2600-mobile's already-real save_state()/load_state() UniFFI
methods (present since v1.11.0, never exposed in either mobile UI)
into real Android and iOS save-state slot UIs, researches cloud
save-state sync, and checks physical Android hardware availability.

Shipped through PR #21, reviewed by GitHub Copilot and Gemini Code
Assist — 11 findings, all genuine, all fixed, including a real
Android thread-safety bug.

Android Save State / Load State UI

8 numbered slots, matching desktop's v2.4.0 "Save Point" convention
exactly — same slot count, .r26s extension, and label wording. Keyed
by the same CRC32 ROM tag MainActivity already computes for
loadRom, stored under app-private storage (Context.filesDir), so a
slot can never even be written under the wrong ROM's key.

Genuinely verified end-to-end, not just "it compiles": rebuilt,
reinstalled on the real Pixel_8_API_34 emulator (KVM-accelerated),
loaded a real vendored homebrew ROM through the actual Storage Access
Framework file picker, saved to a slot, confirmed the on-disk file's
directory name against an independently-computed CRC32 of the source
ROM (an exact match), reopened the dialog to see a live timestamp, and
loaded it back — a real "Loaded slot N" toast confirming the full
round trip. Zero crashes across the entire session's logcat.

iOS Save State / Load State UI

The same convention over FileManager's Application Support directory
(a genuinely private, non-user-visible location — see Fixed below).
Also fixed a latent bug while wiring this up: the ROM tag was
previously derived only from byte count (two same-size ROMs would
silently collide) — replaced with a real FNV-1a-64 content hash, since
this is the first release to actually build per-tag slot directories
on iOS. Ships source-complete-but-unverified by compilation, the same
standing, accepted limitation as the rest of this project's iOS work
(no Xcode toolchain in this sandbox).

Cloud save-state sync — researched, deliberately deferred

Checked the sibling RustyNES project's actual shipped implementations
rather than assuming a target: iOS uses CloudKit; Android uses
Google Play Games Services v2 Snapshots, not Google Drive as the
roadmap assumed. Both need live backend credentials (a real Google
Play Console project, an Apple Developer account) this sandbox has
none of — documented as the concrete reference implementations to
port later, once those credentials exist.

Physical hardware verification — checked, none available

adb devices -l showed only the emulator, both before and during the
session. No device-farm tooling configured. Verification stays at the
v1.11.0 "real emulator" bar — checked for real this release per the
roadmap's own framing, not assumed unavailable.

PR review, for real

PR #21 was reviewed by GitHub Copilot and Gemini Code Assist — 11
findings, all genuine, all fixed:

  • A real Android thread-safety bug (Gemini Code Assist):
    saveState()/loadState() ran directly on the UI thread — but
    emulator is the same instance the ~60Hz frame loop mutates on a
    background handler, so this wasn't just a jank risk, it raced the
    frame loop over the same emulator state. Fixed by posting the bridge
    call + file I/O onto that same handler.
  • File I/O failures could crash the app (Copilot): widened
    catch (e: MobileException) to catch (e: Exception), covering the
    IOException the slot store's file writes/reads can throw.
  • iOS SaveSlots used .documentDirectory, which is user-visible
    via the Files app / iTunes-style sharing (Copilot): switched to the
    genuinely private .applicationSupportDirectory.
  • iOS EmulatorViewModel's save/load/probe ran synchronously on the
    @MainActor
    (Gemini Code Assist): offloaded to Task.detached.
  • Two smaller findings (Gemini Code Assist): ULong.toString(16) +
    padStart instead of a Long-cast %016x format (avoids
    JVM-specific negative-number hex formatting); url.resourceValues (forKeys:) instead of the deprecated FileManager.attributesOfItem (atPath:).

All Android fixes independently re-verified end-to-end on the live
emulator: rebuilt, reinstalled, saved to a new slot through the fixed
threaded code path, confirmed the file on disk, confirmed a
pre-existing slot (written by the old code) still loads correctly with
the new hex formatting, and scanned the full re-verification session's
logcat for any exception/error/crash: zero.

Test count

374 tests passing on default features (378 with --features test-roms) — unchanged from v2.10.0, since this release touches
zero Rust code (purely Kotlin/Swift/docs work). Full CI green —
Linux/macOS/Windows, the perf regression gate, and the no_std gate.

Honest verification boundary

iOS remains unverified by compilation (no Mac in this sandbox — a
permanent, accepted limitation, not a regression). Cloud save-state
sync is researched and scoped but not implemented, since both
platforms' real implementations need live backend credentials this
sandbox cannot provide. Physical Android hardware verification was
genuinely checked this release and confirmed unavailable.

What's next

v2.12.0 "Open Book" — Repo Content & Debugger Depth: real authored
GitHub Wiki content (mirroring RustyNES's own 20-page structure), a
trace logger debugger panel, an inline 6507 assembler, and cart
header/info + perf-monitor panels. See to-dos/ROADMAP.md for the
complete v2.4.0 -> v3.0.0 roadmap — this is the last feature release
before v3.0.0 "Convergence"'s beta/rc cycle begins.

v2.10.0 "Prism"

Choose a tag to compare

@doublegate doublegate released this 03 Jul 01:23

Rusty2600 v2.10.0 "Prism"

Seventh release of the RustyNES gap-closure arc — grows
rusty2600-gfx-shaders beyond its prior CRT scanline + composite-artifact
approximation with a genuine NTSC composite YIQ decode, hqNx/xBRZ
upscaling, a generalized arbitrary-length shader stack, and a constrained
RetroArch .slangp/.cgp preset importer.

Shipped through PR #20, reviewed by GitHub Copilot and Gemini Code
Assist — 6 findings, all genuine, all fixed, including a real bug both
bots independently caught.

A genuine NTSC composite YIQ decode

PassKind::NtscComposite is adapted — not ported — from the
Bisqwit/Mesen signal-decode technique used in the sibling RustyNES
project, re-derived for the TIA's own 4-bit-hue + 3-bit-luma color
model. The TIA's dot clock runs at exactly the NTSC color-subcarrier
frequency, so unlike the NES there's no per-dot phase drift to
synthesize: a given (hue, luma) pair decodes in isolation to one fixed
point in YIQ space, already captured by the existing measured palette.

The genuinely new physics this pass models is the real, named cause of
2600 "artifact colors": a real composite decoder's chroma (I/Q)
bandwidth is much narrower than its luma (Y) bandwidth (~0.6-1.5 MHz vs
~4.2 MHz). The pass takes the center tap's luma unblended and a
5-tap weighted average of the neighborhood's chroma, then converts back
to RGB — sharp luma detail (a single-dot sprite edge), blended chroma
(the hue-dithering artifact colors 2600 programmers deliberately
exploited).

Verification, stated plainly: a pure-Rust test proves the
forward/inverse YIQ matrix pair is a true mathematical inverse for all
128 real NTSC palette entries — the standard published FCC/SMPTE
coefficients, not fabricated numbers. A second test guards the WGSL's
duplicated palette table against transcription drift from the real
frontend table. NTSC-only: PAL's phase-alternating modulation and
SECAM's absent chroma aren't modeled, so the Settings checkbox is
disabled outside the NTSC region.

Landing this needed threading the raw TIA palette-index byte through
the presentation path (present_buffer::Frame::index,
EmuCore::index_buffer()) purely additively, alongside — never
replacing — the existing RGB conversion every other build path still
uses unchanged.

hqNx and xBRZ upscaling

PassKind::HqNx/Xbrz — independently re-derived WGSL adaptations of
the published edge-directed pixel-art smoothing techniques (Maxim
Stepin's hqx, Hyllian/Zenju's xBR/xBRZ), not ports of any existing
implementation.

A generalized shader stack

ShaderStack no longer assumes exactly 2 built-in passes — the
previous fixed-2-slot design's own doc comment incorrectly claimed a
3rd intermediate texture would be needed for a 3rd pass; a linear chain
only ever needs 2 ping-pong textures regardless of length. render now
loops over an arbitrary pass list.

A constrained RetroArch preset importer

slang_preset.rs matches the sibling RustyNES project's own ADR 0013
scope exactly: parses a .slangp/.cgp preset, maps known community
shader filename stems onto Rusty2600's 5 built-in passes, and reports
anything unrecognized as explicitly unsupported — never silently
dropped, and never any attempt at real GLSL/Slang-to-WGSL translation
(that stays permanently out of scope, same reasoning RustyNES's own ADR
gives). Deliberately never maps a preset entry onto the
position-constrained NtscComposite, since a preset could place it
anywhere in the stack.

PR review, for real

PR #20 was reviewed by GitHub Copilot and Gemini Code Assist — 6
findings, all genuine, all fixed:

  • A real ShaderStack::render bug (both bots, independently): the
    ping-pong parity (i % 2) and is_last check were computed off the
    original pass-list index, even though a position-constrained pass
    found anywhere but index 0 gets defensively skipped. If that skipped
    pass were last in the list, no executed pass would ever see is_last = true — nothing would write the swapchain, a blank frame. If it
    were in the middle, the parity of subsequent original indices would
    no longer match which ping-pong texture actually held the latest
    data — a wrong-texture read. Fixed by filtering to the
    actually-executed pass list first and computing both from that
    list's own indices, with direct unit tests for both failure shapes.
  • Inline comments in a preset value (Gemini Code Assist): shader0 = "path" # note or // note would corrupt parsing. Fixed — a quoted
    value is now kept verbatim, an unquoted value is truncated at the
    first comment marker.
  • Duplicate shaderN keys weren't deduplicated (Gemini Code
    Assist): a repeated key produced duplicate/redundant passes instead
    of the RetroArch-standard last-write-wins override. Fixed via a
    BTreeMap.
  • stock/passthrough stem matching was exact-match only (Gemini
    Code Assist): now a substring match, consistent with every other
    stem check.
  • "(see log)" logged nothing (Copilot): unsupported preset passes
    are now actually printed to stderr.

Test count

374 tests passing on default features (378 with --features test-roms) — up from 349/353, 25 new tests covering the YIQ
round-trip/palette parity, the preset importer, and the
ShaderStack::render bug fixes. Full CI green — Linux/macOS/Windows,
the perf regression gate, and the no_std gate. Independently
re-verified beyond the standard gate: cargo clippy --target wasm32-unknown-unknown --features wasm-winit[,debug-hooks] clean;
--features wasm-canvas (the other wasm build) unaffected.

Honest verification boundary

Live in-browser verification (the new passes actually rendering, the
preset importer actually driving a real Settings dialog) remains
blocked by the same sandboxed-headless-Chromium GPU limitation
documented since v2.5.0 — this sandbox can't render wasm-winit at
all. Everything verifiable without live browser GPU access (unit
tests, WGSL naga validation, wasm32 compile + clippy checks) has been
confirmed.

What's next

v2.11.0 "Field Trip" — Mobile Parity: save-state UI on Android and
iOS (reusing rusty2600-mobile's already-real bridge methods, never
exposed in either UI), a research pass on cloud save-state sync, and
real physical Android hardware verification if available. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.

v2.9.0 "Full Circle"

Choose a tag to compare

@doublegate doublegate released this 03 Jul 00:00

Rusty2600 v2.9.0 "Full Circle"

Sixth release of the RustyNES gap-closure arc — closes the remaining
wasm-winit capability gap against RustyNES's own wasm demo: a
share-link feature, PWA install, and a wasm32-safe debugger overlay —
plus a real, not re-deferred, investigation into in-browser Lua
scripting that concluded honestly deferred.

Shipped through PR #19, reviewed by GitHub Copilot and Gemini Code
Assist — 2 findings, both fixed.

?settings= share-link

Round-trips the whole Config (region, video, audio, both players' key
bindings) through a hand-rolled, URL-safe base64 codec — pure and
target-agnostic, so the encode/decode logic is exercised by 7 new native
unit tests without touching a single browser API. Applied on boot,
overriding Config::load()'s persisted localStorage value when a
?settings= parameter is present in the URL. Deliberately settings-only
(no ROM reference) — neither the native rfd file dialog nor the wasm
<input type=file> picker has a URL a share link could carry, so a
recipient still picks their own ROM file.

A wasm32-safe debugger overlay

debug-hooks is now confirmed wasm32-safe for wasm-winit: the CPU/
TIA/RIOT/Memory panels plus nearly every other native debugger panel
(Watch, Callstack, Events, Player-Missile-Ball, Access, Compare,
TAStudio's jump-to-frame) work identically in-browser. The one
exception — TAStudio's "Save branch" action, which needs rfd's
native-only save-file dialog — is scoped out specifically
(MenuAction::TastudioSaveBranch is not(target_arch = "wasm32")-gated)
rather than excluding all of debug-hooks from the wasm build.

PWA install

A web app manifest plus a service worker (web/manifest.json,
web/sw.js) make the deployed build installable ("Add to Home Screen" /
desktop install prompt) and usable offline after a first visit. Since
Trunk hashes build filenames per release, a static precache manifest
would go stale every rebuild — instead the service worker runtime-caches
same-origin GETs: cache-first-then-revalidate for sub-resources (wasm,
JS glue, CSS, icons, manifest), network-first for the app-shell
navigation request (see the PR review fix below for why that split
matters).

In-browser Lua scripting — investigated for real, deferred with a confirmed reason

This crate's own module doc has flagged a piccolo-backed wasm fallback
as a "genuine, scoped follow-up" since it was first built. This release
actually did that follow-up investigation rather than re-deferring on
the same old reasoning:

  • mlua on wasm32 is confirmed, again, a hard wall. A direct
    build attempt (cargo check --target wasm32-unknown-unknown -p rusty2600-script) fails inside lua-src's build script: "don't know
    how to build Lua for wasm32-unknown-unknown." The vendored C build
    genuinely cannot target that toolchain — not a bug to work around.
  • piccolo is not yet a workable substrate either, for two
    compounding reasons: its only crates.io-published release (0.3.3)
    implements almost none of Lua's string/table stdlib — per its own
    README, ruling out exactly the kind of ordinary script this crate's
    emu API invites (string.format for a debug label, table.insert
    for per-frame state tracking); and its Executor-based "stackless"
    VM architecture (a gc-arena-rooted, Send-incompatible design) has
    no equivalent to engine.rs's "keep the VM around, call into it once
    per real frame" pattern without building real gc-arena/Rootable!
    plumbing from scratch — a genuine engine rewrite, not a parallel
    engine_piccolo.rs sibling. Its more-complete unpublished master
    branch is explicitly labeled pre-1.0-unstable by its own maintainer.

Native's mlua backend is completely unchanged — same crate, same
version pin, same feature gating. Documented in crates/rusty2600- script/src/lib.rs's module doc and docs/scripting.md; revisit once
piccolo publishes a crates.io release with real string/table
coverage — an upstream milestone, not one this project controls or can
schedule against.

PR review, for real

PR #19 was reviewed by GitHub Copilot and Gemini Code Assist — 2
findings, both genuine, both fixed:

  • A real cache-safety bug (Copilot): the service worker's
    activate handler deleted every cache except its own CACHE_NAME
    but Cache Storage is origin-wide, not scoped to one service worker, so
    this could have wiped a sibling GitHub Pages project's caches sharing
    the same origin. Fixed by filtering on a CACHE_PREFIX instead, so
    only a previous Rusty2600 shell cache is ever evicted.
  • A real offline-breaking race condition (Gemini Code Assist, high
    priority): stale-while-revalidate on the index.html navigation
    request, combined with Trunk's hashed sub-resource filenames, could
    leave a later offline visit with HTML referencing hashed assets that
    were never actually fetched (if the background revalidate silently
    updated the cached HTML to a newer version before those new hashed
    assets were ever cached). Fixed by switching navigation requests to
    network-first; sub-resources keep cache-first-then-revalidate, since
    they're effectively immutable-by-hash.

Test count

349 tests passing on default features (353 with --features test-roms)
— up from 342/346, the 7 new tests covering the share-link base64 codec
and its encode/decode round-trip. Full CI green — Linux/macOS/Windows,
the perf regression gate, and the no_std gate. Independently
re-verified beyond the standard gate: cargo check/cargo clippy --target wasm32-unknown-unknown --features wasm-winit and the new
--features wasm-winit,debug-hooks combination (the wasm debugger
overlay) both clean; --features wasm-canvas (the other wasm build)
unaffected.

Honest verification boundary

Live in-browser verification (the share-link actually round-tripping
via a real browser URL bar, the service worker actually caching
offline, the debugger overlay actually rendering) remains blocked by
the same sandboxed-headless-Chromium GPU limitation documented since
v2.5.0 — this sandbox can't render wasm-winit at all. Everything
verifiable without live browser GPU access (unit tests, wasm32 compile +
clippy checks) has been confirmed.

What's next

v2.10.0 "Prism" — Shader Stack Expansion: a higher-quality NTSC
composite demodulation pass, hqNx/xBRZ upscaling, and a constrained
RetroArch .slangp/.cgp preset importer, growing
rusty2600-gfx-shaders beyond its current CRT scanline + composite-
artifact approximation. See to-dos/ROADMAP.md for the complete
v2.4.0 -> v3.0.0 roadmap.

v2.8.0 "Touchpoint"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 22:46

Rusty2600 v2.8.0 "Touchpoint"

Fifth release of the RustyNES gap-closure arc — the first wave of
wasm-winit web parity, building real interactivity on top of v2.5.0's
bare wasm render.

Shipped through PR #18, reviewed by GitHub Copilot and Gemini Code
Assist — 6 findings, 5 fixed, 1 dismissed with documented evidence.

On-screen touch controls (the headline)

A D-pad + fire button + console-switch row for the wasm-winit build
(ShellState::render_touch_overlay, wasm32-only), since a browser user
on mobile Safari/Chrome has no physical keyboard. All plain egui
widgets, feeding the exact same InputState the keyboard path
populates, via a new input::TouchButton/TouchOverlayState pair:
pure, target-agnostic press/hold/release + latch-edge tracking, unit-
tested under the ordinary native cargo test --workspace run (not
gated to wasm32-only, so this logic is exercised on every CI platform).

Latching switches (Color/Difficulty) toggle exactly once per press
rather than re-toggling 60 times a second while held; momentary buttons
(joystick directions/fire, Select/Reset) emit a press/release edge
matching how a real key's keydown/keyup already drives them. Defaults
visible (a touch-only device has no other way to drive the emulator);
View -> Touch overlay toggles it off for a desktop-browser user who
does have a keyboard.

Honestly documented limitation: egui-winit's default touch handling
tracks one synthesized pointer, so true simultaneous multi-finger
combos (holding a direction while also tapping Fire) aren't guaranteed
to both register — a real egui/egui-winit constraint, unverifiable
either way in this project's sandbox (no real touchscreen), not
something this release introduces or works around.

Settings panel + console-switch buttons — verified, not built

Reviewed the Settings window tab-by-tab for wasm32-safety: the Video/
Audio/Input/System tabs are plain egui widgets with no native-only API
in the path (no rfd dialogs, no blocking file I/O), so the panel
already worked correctly on wasm32 structurally — no code needed there.
Console-switch buttons were likewise already reachable "for free" via
the shared Emulation -> Console switches menu on both native and
wasm32. The real gap this release closes is persistence (below) and the
touch-friendly on-screen row, for a user who can't easily reach a
dropdown menu with a finger.

Real localStorage config persistence

Config::save()/Config::load() now genuinely persist on wasm32 via
web_sys::window().local_storage(), replacing a previous no-op stub
(and a previously-nonexistent load()). Shares TOML (de)serialization
helpers with the native config.toml file path, so both backends are
covered by one set of native unit tests. Falls back to defaults on any
missing/corrupt/foreign stored value, never blocking launch.

Save-state SLOT persistence (v2.4.0's manual save slots) is
deliberately deferred — it needs per-slot localStorage keys, an
mtime substitute (localStorage has none), and real size budgeting
across 8 slots within the ~5-10MB origin quota, a distinctly bigger
lift than the single-key Settings value above. Documented in
docs/frontend.md rather than rushed alongside this release's headline
items.

PR review, for real

PR #18 was reviewed by GitHub Copilot and Gemini Code Assist — 6
findings:

  • Draggable touch-overlay Areas (Gemini Code Assist): egui::Area
    is movable by default, so the D-pad/fire/console-switch overlays
    could be accidentally dragged by a touch. Fixed with .movable(false)
    on all 3.
  • A wasted per-frame audio-sample allocation on wasm32 (Gemini Code
    Assist): the Vec collecting DC-blocked samples was allocated and
    populated every frame (60 times/sec) even on wasm32, where nothing
    reads it (audio_tx is native-only, wasm-winit audio being an
    explicitly deferred stretch goal). Fixed — now gated to native only;
    the DC-blocker filter state still advances every sample on both
    targets, since that state must stay correct for whenever wasm32 audio
    eventually gets wired up.
  • A misleading doc comment (Copilot): TouchOverlayState's doc
    comment claimed momentary buttons "emit their live level every
    frame" — the code was already correct (edge-based, matching
    keydown/keyup), only the comment was wrong. Corrected.
  • A false-positive claim (Gemini Code Assist, dismissed with
    evidence): that this project's && let chain syntax "will cause
    compilation to fail on stable Rust toolchains." This project's
    edition is 2024 (let-chains stabilized since Rust 1.88), the exact
    same pattern is already used in emu_thread.rs (merged in v2.7.0),
    and CI for this very PR had already passed on Linux/macOS/Windows
    with this exact syntax on stable rustc 1.96. Dismissed rather than
    "fixed" into a stylistic regression.

Test count

342 tests passing on default features (346 with --features test-roms) — up from 333/337, the 9 new tests covering
TouchButton/TouchOverlayState and the shared TOML (de)serialization
helpers. Full CI green — Linux/macOS/Windows, the perf regression gate,
and the no_std gate. Independently re-verified beyond the standard
gate: cargo check/cargo clippy --target wasm32-unknown-unknown --features wasm-winit and cargo check --target wasm32-unknown-unknown --features wasm-canvas (the other wasm build, confirming no
regression) both clean.

Honest verification boundary

Live in-browser interactive verification (touch input actually working
on a real touchscreen) remains blocked by the same sandboxed-headless-
Chromium GPU limitation v2.5.0 already documented — this sandbox
can't render wasm-winit at all, let alone drive it with touch.
Everything verifiable without live browser GPU access (unit tests,
wasm32 compile + clippy checks) has been confirmed.

What's next

v2.9.0 "Full Circle" — Web Parity Wave 2: in-browser Lua scripting, a
share-link feature, PWA install, and a wasm debugger overlay, closing
the remaining wasm-demo capability gap against RustyNES. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.

v2.7.0 "True Colors"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 21:38

Rusty2600 v2.7.0 "True Colors"

Fourth release of the RustyNES gap-closure arc — builds the TIA object-ID
mask rusty2600-frontend::sprite_pack's own doc comment has flagged as
the missing prerequisite since v1.4.0, then wires the live HD-pack
rendering splice that mask enables.

Shipped through PR #17, reviewed by GitHub Copilot and Gemini Code
Assist — 3 findings, all genuine, all fixed and re-verified before merge.

TIA object-ID mask (the real prerequisite)

rusty2600-tia gains a new, parallel per-pixel output channel behind the
off-by-default hd-pack feature: Tia::object_mask: Vec<ObjectTag>,
indexed identically to video_buffer (scanline * 160 + x). Each
ObjectTag names which object (Background/Playfield/Ball/
Missile0/Missile1/Player0/Player1) won color-priority resolution
at that dot, plus — for player pixels — the exact live GRPx
(VDELP-resolved)/NUSIZx at the moment that pixel was rendered.

Capturing GRPx/NUSIZx fresh per-pixel (not once per frame or
scanline) is what makes sprite multiplexing — a game rewriting GRP0/
GRP1 mid-scanline to draw more than 2 player objects per line, a real
historical 2600 programming technique — resolve correctly.

Honest verification, stated plainly: this touches rusty2600-tia,
the crate carrying this project's cycle-exact accuracy guarantees, so it
got the same review scrutiny as CDF/CDFJ/CDFJ+ in v2.3.0. Confirmed by
direct line-number inspection that self.current_object_tag is assigned
strictly after self.current_color is already resolved — the new code
cannot possibly alter the existing, accuracy-critical color computation.
Independently re-verified (not just the implementing work's own report):
333 tests passing on default features and 337 with --features test-roms, both exactly unchanged from v2.6.0's own baseline, proving
zero regression with hd-pack off.

Live HD-pack rendering splice

rusty2600-frontend::emu_thread::EmuCore gains a sprite_pack field and
set_sprite_pack setter. When a SpritePack is installed,
step_frame consults the object mask: a player pixel whose captured
(GRPx, NUSIZx) matches a loaded replacement bitmap gets that bitmap's
pixel instead of the flat resolved TIA color, nearest-neighbor scaled
onto the object's on-screen footprint (honoring NUSIZx size bits) and
alpha-blended against the underlying color. Missile/ball/playfield/
background pixels are untouched — sprite_pack's data model stays
player-only by design, unchanged since v1.4.0.

Proof-of-mechanism replacement-art pack

No replacement-art content existed anywhere in this project before this
release. A small fixture (tests/fixtures/hd_pack_demo/) plus a
hand-assembled synthetic 2600 ROM with known, static player-0 graphics
prove the splice mechanism works end-to-end: the test ROM runs a real
VSYNC(3 lines)/VBLANK(37 lines)/active-picture(192 lines) kernel
paced by WSYNC, and the integration test asserts the produced frame
actually shows the replacement bitmap's placeholder color — with a
control run (same ROM, no pack loaded) confirming that color never
appears without the splice active.

PR review, for real

PR #17 was reviewed by GitHub Copilot and Gemini Code Assist — 3
findings, all genuine, all fixed:

  • A real rendering bug (Gemini Code Assist): the splice's contiguous
    sprite-footprint run reset on every GRPx 0-bit (a transparent gap
    inside real sprite art, as opposed to this release's fully-opaque
    proof-of-mechanism bitmap), distorting the coordinate mapping onto the
    replacement bitmap. The same finding also caught that the replacement
    bitmap's alpha channel was ignored entirely, so any non-opaque
    replacement pixel would render as solid black. Both fixed: the run
    now survives transparent gaps within its own footprint width, and the
    alpha channel is alpha-tested/blended against the original TIA color.
  • A stale-state bug (Copilot): EmuCore::load_rom never cleared a
    previously installed sprite_pack, so a prior cartridge's HD-pack
    replacements could bleed into a newly loaded ROM's graphics. Fixed —
    load_rom now clears it.
  • A test-robustness finding (Copilot): the integration test's
    synthetic ROM never asserted VSYNC, so step_frame ended the
    captured frame via its 200,000-instruction safety timeout rather than
    a real frame boundary. Rewrote the ROM with a genuine NTSC kernel
    (VSYNC/VBLANK/active-picture, WSYNC-paced) so the test now ends
    on an actual VSYNC 1->0 transition.

Test count

333 tests passing on default features (337 with --features test-roms)
— unchanged from v2.6.0, since the object-ID mask and HD-pack splice
are both gated behind the off-by-default hd-pack feature. 147 tests
passing across rusty2600-{tia,core,frontend} with --features hd-pack enabled (independently re-run and confirmed, not just the
implementing work's own report). Full CI green — Linux/macOS/Windows,
the perf regression gate, and the no_std gate.

What's next

v2.8.0 "Touchpoint" — Web Parity Wave 1: on-screen touch controls, a
working Settings panel in wasm32, console-switch on-screen buttons,
and localStorage/IndexedDB persistence, building on v2.5.0's bare
wasm render. See to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0
roadmap.

v2.6.0 "Rollback Bridge"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 20:17

Rusty2600 v2.6.0 "Rollback Bridge"

Third release of the RustyNES gap-closure arc — closes the browser WebRTC
netplay gap v2.3.0 deliberately deferred, and folds in a master
dependency-upgrade sweep consolidating 12 open Dependabot PRs into one
reviewed, tested pass.

Shipped through two PRs: #15 (browser WebRTC netplay transport) and #16 (the
dependency-upgrade sweep, merged first). Both went through this arc's
standing CI + automated review-bot adjudication process (GitHub Copilot,
Gemini Code Assist) before merging.

Browser WebRTC netplay transport (the headline gap)

Native UDP + STUN (v2.3.0) only ever reached native peers — browser-to-
browser netplay was impossible. This release closes that gap.

ADR 0008 (written first, before any implementation code, per this
release's own plan) answers the question v2.3.0's STUN work deliberately
deferred: can WebRTC's async surface stay contained to one-time connection
setup, without pulling an async runtime into this project's otherwise
100%-synchronous core? Yes — using the exact wasm_bindgen_futures:: spawn_local pattern v2.5.0's Gfx::new_async already proved in this
codebase. The per-frame send/poll hot path stays fully synchronous.

WebRtcSocket (rusty2600-netplay::webrtc, wasm32-only) implements
ggrs::NonBlockingSocket<SocketAddr> over an already-open RtcDataChannel,
mirroring the existing PunchedUdpSocket and reusing its exact wire format
— so RollbackSession::with_webrtc_socket (the one new public entry point)
needed zero changes to the session-driving code GGRS itself owns. Since this
project is deliberately 2-player-only, a fixed sentinel SocketAddr stands
in for "the one WebRTC peer" (never a real IP). WebRtcPeer handles the
one-time async connection-establishment dance (SDP offer/answer, ICE
gathering). A minimal manual/copy-paste SDP exchange (no signaling server,
per the ADR) is provided via a standalone rusty2600-netplay/web/ test
page.

Honest verification, stated plainly: the entire connection-establishment
code path — createOffer/createAnswer/acceptAnswer — was driven
end-to-end in a real Chromium instance across two independent
RTCPeerConnections (CDP-scripted). Every step succeeded with real data: a
real SDP offer generated, a real SDP answer generated in response,
set_remote_description accepting it without error. The data channel itself
never reached "open" in this sandbox — diagnosed to zero ICE candidates
being gathered on either side (ruled out mDNS candidate obfuscation as the
cause; a genuine sandbox/Chromium network restriction, not a bug in the Rust
code). A native (non-browser) WebRTC path stays explicitly deferred per the
ADR — browser-to-browser was always the primary, more valuable, and more
testable target.

PR review, for real

PR #15 was reviewed by GitHub Copilot and Gemini Code Assist — 12 findings,
all genuine, all fixed:

  • A real memory-safety bug (both bots independently found this): WebRtcSocket
    had no Drop impl, so a message arriving after the socket was dropped
    would have the browser invoke a JS reference into deallocated memory — a
    wasm-bindgen trap/panic, not a benign no-op. Fixed with impl Drop.
  • Two more leaked JS closures, both in the connection-establishment code:
    the ICE-gathering-completion handler and the joiner-side ondatachannel
    handler were both .forget()-ten unconditionally. Both are now held and
    cleared once their one-time job finishes.
  • A standalone test-harness page (web/index.html) wasn't actually testing
    the binary data-channel path production code depends on — it was sending
    plain strings. Now sends real Uint8Array bytes with binaryType = "arraybuffer", exercising the same path WebRtcSocket does.
  • Several doc-comment inaccuracies (a stale claim about a "JSON-serialized
    blob" return value that's actually a plain SDP string; a verification
    script's docstring that overstated what it proved) — all corrected.

Master dependency-upgrade sweep (PR #16)

Consolidated all 12 open Dependabot PRs into one reviewed, tested PR rather
than merging each in isolation:

Applied: egui/egui-wgpu/egui-winit 0.34.3 → 0.35.0, zip 2.4.2 →
8.6.0, mlua 0.10.5 → 0.11.6, uniffi 0.28.3 → 0.32.0 (Kotlin/Swift
bindings regenerated — 0.31 changed method checksums, making the old
bindings incompatible with the new Rust scaffolding), actions/checkout v4
→ v7, clap_complete 4.6.5 → 4.6.7.

Declined, each with a documented reason and a dependabot.yml ignore
rule
: bincode 1.3.3 → 3.0.0 (bincode is now unmaintained — RUSTSEC-2025-
0141 — and 3.0.0 is a deliberately broken placeholder release, not real
code; rusty2600-netplay needs byte-identical wire compatibility with
GGRS's own internal bincode 1.3.3 usage, so staying pinned is correct);
dtolnay/rust-toolchain@1.100 (Rust 1.100 does not exist yet — confirmed
via a direct install attempt against the real release server); wgpu/
naga 29 → 30 (egui-wgpu 0.35.0, the latest available release, still
requires wgpu 29.x internally — bumping would create two incompatible
wgpu versions in the dependency graph).

Also found and fixed a real, pre-existing clippy::large_stack_frames
failure in App::dispatch_actions under --features netplay — confirmed
present on main before any of these bumps, never caught before since this
project's CI only clippy-checks the default feature set.

Test count

333 tests passing on default features (337 with --features test-roms) —
unchanged from v2.5.0, since the new WebRTC netplay code is wasm32-only
and doesn't touch the native default test suite. Full CI green —
Linux/macOS/Windows, the perf regression gate, and the no_std gate.

What's next

v2.7.0 "True Colors" — the TIA object-ID mask sprite_pack's own doc
comment has flagged as the missing prerequisite since v1.4.0, plus the
HD-pack live rendering splice it's been waiting on ever since. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.

v2.5.0 "Web Awakens"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 18:01

Rusty2600 v2.5.0 "Web Awakens"

Second release of the RustyNES gap-closure arc — the headline item here is
the single largest capability gap the whole gap analysis found: a real
winit+wgpu+egui rendering build on the web, replacing the bare
canvas-2D bootstrap that's carried the GH-Pages demo since it first shipped.

Shipped through PR #14, reviewed by GitHub Copilot and Gemini Code Assist —
7 findings, all genuine, all fixed (including a real memory leak in the new
wasm ROM-loading path and a latent wasm32 feature-gate compile-error risk).

Real winit+wgpu+egui rendering on wasm32 (the headline gap)

The GH-Pages demo has always been a bare canvas-2D requestAnimationFrame
bootstrap — a real winit+wgpu+egui build matching the native binary was
explicitly never attempted, honestly documented as such throughout this
project's history. This release makes the real attempt.

The SAME App/Gfx/shader_pass/emu_thread shell the native build uses
now compiles for wasm32-unknown-unknown behind a real wasm-winit
feature (previously an empty placeholder). The hard architectural problem —
Gfx::new_async's adapter/device acquisition can't block the browser's
single JS thread the way native's pollster::block_on does — is solved via
wasm_bindgen_futures::spawn_local, populating a shared
Rc<RefCell<Option<Active>>> cell once ready; every ApplicationHandler
callback already had to treat a not-yet-resumed Active as a no-op, so
this needed no further changes elsewhere. ROM loading routes through a
hidden <input type=file> instead of rfd (a native-only dependency).

Honest status, stated plainly: it compiles cleanly, and a real trunk build produces a working bundle — confirmed loading in headless Chromium,
with run_winit() executing and logging correctly. But the wgpu
adapter-request step itself could not be verified actually rendering a
frame: this sandbox's headless-Chromium-with-SwiftShader environment has no
navigator.gpu and, in this specific setup, wgpu 29's adapter request also
failed to fall back to the compiled-in GL/WebGL2 backend (gl support not compiled in / webgpu found no adapters) — independently reproduced
twice, once during implementation and once during coordinator verification
on a fresh build. This is a likely wgpu-internal webgpu-vs-wgpu_core
backend-dispatch limitation for this target, not something fixable from
this project's own Cargo.toml alone (egui-wgpu's own unconditional
wgpu dependency reintroduces the webgpu feature via Cargo's per-crate
feature unification regardless of what this crate's own Cargo.toml
requests). Real desktop browsers with genuine GPU access — real WebGPU
support, or a real hardware-accelerated GL driver instead of SwiftShader —
may well work where this sandboxed environment could not; that's a real,
open question for a future session with real browser access to confirm.

wasm-canvas (the proven, complete canvas-2D bootstrap) stays the actual
GH-Pages-deployed build for now. wasm-winit is real, committed, and
available to build — just not promoted to the live demo until rendering is
confirmed in a real browser.

GH Pages /api/ rustdoc hosting — already done

Checked before starting any new work: this was already live (confirmed via
a direct HTTP check against the deployed site). No code needed.

Debugger Lua console panel

Lua's print() previously went nowhere useful — real stdout, invisible in
a GUI app. crates/rusty2600-script's Lua VM now overrides the print
global to route into a new capped ScriptLog ring buffer (500 lines,
oldest dropped first), matching real Lua semantics (arguments tab-joined,
re-resolving the global tostring on every call so a script that
overrides tostring still sees that reflected). onFrame runtime errors
are captured into the same log automatically. A new Debug -> Lua Console
panel renders the captured history live, errors in red, with a Clear
button. Output-only — not an interactive REPL (would need to respect the
same WritesLocked determinism gate the normal onFrame tick already
enforces; real additional design work deliberately out of scope here).

Keyboard Controller / Trak-Ball — researched, not modeled

A documentation-only decision, no code change. Checked Stella's own
implementation and properties database rather than assumption: the
Trak-Ball has zero official Atari 2600 releases (only 2 homebrew ROMs in
Stella's database); the Keyboard/Keypad Controller has 40 ROMs including
one real official release (Brain Games, 1978) but remains niche relative
to the whole catalogue. Neither is modeled this arc — deliberately
deprioritized, not a permanent "never."

PR review, for real

7 findings from GitHub Copilot and Gemini Code Assist, all genuine, all
fixed — none dismissed:

  • A real memory leak (Gemini, high priority): the wasm ROM FileReader's
    on_load callback leaked its heap allocation on every single ROM load
    (Closure::<dyn FnMut()>::new + .forget() instead of the
    once-per-read-appropriate Closure::once_into_js).
  • A latent wasm32 feature-gate mismatch (Gemini): app.rs's shell.render
    call site gated cheevos/script arguments on bare feature flags while
    shell.rs gates the matching parameters on all(not(wasm32), feature = "...") — a wasm32 build with either feature enabled would have failed
    to compile.
  • Lua's print() now re-resolves tostring on every call instead of
    capturing it once at install time, matching real Lua semantics.
  • The wasm ROM-picker <input> now clears its value before .click(), so
    re-selecting the same file reliably fires change again.
  • A stale doc comment (log_error no longer needs to be called by a host
    after tick_frame — it already calls it internally) and two doc/comment
    drift fixes (claiming web/index.html builds wasm-winit when it
    actually builds wasm-canvas).

Test count

333 tests passing on default features (337 with --features test-roms),
up from 327/331 at v2.4.0. Full CI green — Linux/macOS/Windows, the perf
regression gate, and the no_std gate.

What's next

v2.6.0 "Rollback Bridge" — netplay's WebRTC transport, starting with an
explicit ADR on how far the async-runtime surface it needs can stay
contained to the netplay-feature-gated, wasm-target code path. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.

v2.4.0 "Save Point"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 16:27

Rusty2600 v2.4.0 "Save Point"

The first release of the RustyNES gap-closure arc (v2.4.0 -> v3.0.0) — a
comprehensive gap analysis against Rusty2600's mature sibling NES emulator,
RustyNES, found a handful of real, closeable gaps despite the two projects'
otherwise-complete v1.1.0 -> v2.3.0 parity line. This release closes the
single most player-visible one, plus three smaller but genuine gaps
surfaced along the way.

This is also the first Rusty2600 release shipped through a real GitHub
PR with CI + automated review-bot adjudication
(PR #1), rather than a
direct-to-main push — every release through v3.0.0 will follow this same
flow.

Manual save-state slots (the headline gap)

Rusty2600 had no player-facing "save my game" feature at all — despite
rusty2600-core::SaveState being a real, versioned, battle-tested format
(ADR 0007) since v1.10.0, consumed only by the rewind ring, run-ahead,
netplay rollback, and Lua's saveState()/loadState(). Every other modern
emulator lets a player save progress to a slot; Rusty2600 couldn't.

File -> Save State / Load State now offer 8 numbered slots per ROM,
each keyed by an FNV-1a hash of the loaded ROM's raw bytes
(crate::config::save_slot_path) — a slot can never silently load against
the wrong cartridge, since SaveState::restore's existing rom_tag check
enforces this. The File menu shows each slot's live status (empty, or its
last-saved timestamp). Loading a slot now also clears the rewind ring — a
real bug caught by automated PR review: without this, pressing Rewind
right after a slot load would jump back to the pre-load timeline, a
confusing discontinuity. Native-only for now; wasm-side persistence is a
later release's scope.

CI-gated performance-regression check

A new rusty2600-core::system_full_ntsc_frame Criterion bench drives one
full NTSC frame (262 lines x 228 color clocks) through the real
CPU+TIA+RIOT+cart System — not a per-chip proxy. scripts/ bench_regression_check.sh runs it in a new CI perf job and fails on a
measured mean above a fixed absolute ceiling (3.75 ms, ~3x the
measured ~1.25 ms baseline — deliberately not relative/percentage-based,
since CI-runner timing noise makes relative comparisons unreliable).
Validated for real: a regression was deliberately injected, confirmed to
fail the gate, then reverted.

Paddle-timing Stella-oracle differential test

The real RC-circuit analog paddle simulation (v2.1.0) had never been
cross-checked against an independent oracle with an executable test — only
by manual transcription at port time. A from-scratch re-derivation of
Stella's AnalogReadout formula, kept test-local (never shared with
production code), now confirms every RC constant and formula in
rusty2600-tia::paddle matches Stella's source exactly, across a full
position sweep and multiple charge/discharge scenarios. A real
commercial-game cross-check (Breakout/Warlords/Kaboom!) stays honestly
documented as blocked — no such ROM is legally obtainable in this
development environment.

ROM loading from .zip archives

Neither the native File -> Open ROM dialog nor the wasm GH-Pages demo
could previously load a ROM packaged inside a .zip — a real, current gap
given zipped ROM redistribution is the norm. A new shared rom_archive
module extracts the first .a26/.bin/.rom entry from an in-memory
zip, with reads bounded to a 1 MiB ceiling regardless of what the zip's
central directory claims (a decompression-bomb guard, not just a
declared-size check) and never panics on malformed input.

GitHub repo hygiene

CODE_OF_CONDUCT.md, SECURITY.md (with a Rusty2600-specific threat
model — ROM/zip parsing, the Thumb coprocessor interpreter, save-state
deserialization, netplay, Lua scripting), .github/CODEOWNERS,
.github/dependabot.yml, an issue-template config.yml plus a
scheme_request.md template scoped to the now-closed 26/26 bankswitch
catalogue, and GitHub Discussions enabled. The repo description's stale
"23-of-25 bankswitch schemes" claim (26/26 since v2.3.0) was also
corrected.

PR review, for real

PR #1 was reviewed by GitHub Copilot and Gemini Code Assist. All 9 inline
findings were genuine and fixed — none dismissed:

  • Save-slot status is now cached instead of re-probed via 8 filesystem
    stat calls every single frame at 60+ FPS.
  • Loading a state slot now clears the rewind ring (see above).
  • RetroAchievements ROM identification is only attempted when the ROM
    actually loaded successfully (a pre-existing issue, surfaced because
    this release's zip-loading work touched both call sites anyway).
  • The zip-entry extraction buffer is now pre-allocated using the entry's
    own (capped) declared size.
  • bench_regression_check.sh now uses mktemp instead of a fixed /tmp
    path.
  • Two documentation fixes (an algorithm-name mismatch, a typo).

Test count

327 tests passing on default features (331 with --features test-roms),
up from 313/317 at v2.3.0. Full CI green — Linux/macOS/Windows, the new
perf regression gate, and the no_std gate.

What's next

v2.5.0 "Web Awakens" — the largest capability gap this whole analysis
found: a real winit+wgpu+egui rendering build on wasm32, replacing the
current bare canvas-2D GH-Pages demo bootstrap. See
to-dos/ROADMAP.md and the gap-closure arc's full plan for the complete
v2.4.0 -> v3.0.0 roadmap.

v2.3.0 "Full Catalogue"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 14:15

Rusty2600 v2.3.0 "Full Catalogue"

Closes the cart bankswitch catalogue to 26 of 26 schemes and lands three
more /goal follow-up items: DPC+ music-mode audio, script overlay
compositing, and a live-tested netplay STUN client.

CDF/CDFJ/CDFJ+ (T-0401-006, BestEffort tier)

BankCdf wires the last unimplemented scheme into rusty2600-cart::detect()
— one struct covering all four sub-versions (CDF0/CDF1/CDFJ/CDFJ+) via
a CdfVersion const table, ported from Gopher2600's Go cdf package.

Reuses BankDpcPlus's synchronous CALLFN-to-ProgramEnded ARM entry shape,
plus genuinely new mechanics DPC+ never needed:

  • FastJMP — redirects a JMP absolute instruction's own opcode fetch
    (not just an operand) through a data-fetcher stream, guarded by a countdown
    state machine ported to close a real, documented false-positive hazard in
    the reference.
  • A real ARMinterrupt fault-servicing dispatch — unlike DPC+'s no-op
    stub equivalent (DPC+'s driver never triggers it), CDF's driver ROM makes
    genuine host-serviced calls via a BX to a fixed non-Thumb address. Caught
    through rusty2600-thumb's existing Fault::UnimplementedPeripheral
    path — zero changes needed to that crate: Arm7Tdmi::instruction_pc()
    already reported the correct call-site address, and set_register's
    existing PC-storage convention already produced the correct resume target.
    Verified with a real hand-assembled Thumb-1 program that plants an
    actual BX instruction at the documented call-site offset and asserts the
    dispatch loop set the targeted music fetcher's frequency field — not just
    that a fault was caught.
  • CDFJ+'s version constants are derived via a runtime byte-pattern scan of
    the driver ROM (not a fixed table, per the reference).

DPC+ music-mode audio

BankDpcPlus gains a Board::tick() override advancing its 3 music
fetchers' phase accumulators every 59th call. Turned out to be a fully
self-contained rusty2600-cart fix — the hook it needed already existed and
was already called at the right rate, contrary to v2.2.0's own speculation
that this would need a rusty2600-tia change.

Script overlay compositing (scripting feature)

take_overlay()'s accumulated drawText/drawRect/drawPixel output now
actually reaches the screen — piggybacked on the frontend's existing egui
pass rather than a new render pipeline, reusing egui's own font
rasterization for text. Closes the v1.9.0 overlay-compositing gap.

Netplay STUN client (netplay feature)

A real RFC 5389 STUN client (via stun_codec, sans-IO, fitting this
codebase's 100%-synchronous convention) discovers this machine's public
NAT-mapped address, plus a best-effort UDP hole-punch and a "Connect via
STUN" dialog button. Live-verified — a genuine round trip against a real
public STUN server, confirmed passing and independently re-run during
release verification.

WebRTC (browser or native) remains explicitly deferred — it would need
either browser-only web-sys bindings or a native Rust WebRTC stack that
would pull an async runtime into an otherwise 100% synchronous codebase.

Documentation correction (no code change)

docs/cart.md carried a stale "E7... not yet implemented" claim this entire
session — BankE7 has actually been implemented and wired into detect()
since commit 94ca3a4 (2026-07-01), before the v1.1.0 release line even
began. This bug was silently copied into v2.1.0's and v2.2.0's own
CHANGELOG/STATUS entries without independently verifying it against the
code. Both prior entries stay published as-is (this project never rewrites
CHANGELOG history) — corrected here.

Test count

313 tests passing on default features (317 with --features test-roms), up
from 295 at v2.2.0. Full CI green — Linux/macOS/Windows + the no_std
gate.

What's next

Open follow-up work: real Xcode-verified iOS build/run (no Mac in this
sandbox), netplay's WebRTC transport and real cross-NAT verification, and
per-player console-switch/paddle modeling for DPC+/CDF-family games. See
CHANGELOG.md's [2.3.0] entry and to-dos/ROADMAP.md's "Beyond v2.0.0"
section for full detail.

v2.2.0 "Coprocessor Online"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 03:38

Rusty2600 v2.2.0 "Coprocessor Online"

Closes the final open item from the /goal directive that produced v2.1.0
"Follow-Through": ARM coprocessor cart-scheme wiring.

DPC+ (T-0401-006, BestEffort tier)

BankDpcPlus wires DPC+ — the first of the Harmony/Melody ARM-coprocessor
cart families — into rusty2600-cart::detect(), using the rusty2600-thumb
ARM7TDMI Thumb-1 interpreter that has existed unconsumed since v1.6.0.

A full port of Gopher2600's Go dpcplus package (not Stella's C++, matching
this project's established precedent for ARM-adjacent code):

  • The complete $00..=$7F register window: RNG, 8 plain + 8 windowed + 8
    fractional data fetchers, FastFetch LDA #immediate redirection, and
    the $5A CALLFUNCTION register.
  • DpcPlusArmMemory implements rusty2600_thumb::ThumbMemory over the
    board's driver/custom/data/freq ROM+RAM segments at Gopher2600's own
    Harmony-architecture addresses (Flash 0x0000_0000, SRAM 0x4000_0000).
  • The ARM entry point ($5A write of 254/255) runs Arm7Tdmi::step()
    in a loop, synchronously, from within cpu_write — this needed no
    Bus/scheduler change at all, since DPC+'s CALLFUNCTION is
    call-and-run-to-completion, not a per-clock tick. A generous, documented
    step-count safety cap guards against a runaway/buggy ROM.
  • Detected via content signature (the ASCII string "DPC+" occurring
    twice, matching Stella's own isProbablyDPCplus), not size alone.

Verified with a real hand-assembled Thumb-1 program (MOV/LDR
PC-relative/STRB/BX LR, opcodes hand-derived and cross-checked against
rusty2600-thumb's own format encoders) that actually executes via the
interpreter and writes a byte into data RAM through a genuine STRB
instruction — proving the ARM coprocessor actually runs, not just that
registers decode.

Honestly deferred, not silently dropped

  • DPC+'s music-mode continuous-time audio (the reference's Step(clock)-
    driven phase accumulator) is not implemented — register plumbing
    round-trips correctly, but waveform sampling always reads index 0, so
    DPC+ music-mode audio is silent/incorrect on that one channel. A
    rusty2600-tia audio-timing follow-up, not a cart-catalogue one.
  • Function-call service 2 ("copy value to fetcher, N times") is ported
    with Gopher2600's own address formula verbatim, including what looks
    like a copy-paste artifact (Hi is also advanced by the loop index, so
    it does not fill a contiguous block) — Stella can't cross-check this
    specific service, so it's ported exactly rather than "corrected" on a
    guess.
  • CDF/CDFJ/CDFJ+ (the other three Harmony/Melody families) remain their own
    future, separately-scoped follow-up.

Doc fix found during this release's reconciliation

docs/cart.md's scheme-catalogue tally said "15 BestEffort (25 schemes),"
but the table itself has always had 16 BestEffort rows (26 total) — a stale
count predating F0/3F/3E being split into three distinct rows from the
source research report's combined "F0 / 3F-variants" draft entry. Corrected
the tally, not the catalogue — no scheme was added or removed by this fix.

24 of 26 schemes are now implemented and wired into detect(), leaving
E7 (T-0401-002, a pre-existing, unrelated gap) and CDF/CDFJ/CDFJ+ (this
release's own deliberately-deferred scope) as the two remaining entries.

Test count

295 tests passing on default features (299 with --features test-roms), up
from 283 at v2.1.0. Full CI green — Linux/macOS/Windows + the no_std
gate (confirming integer-only ARM emulation has no std-only dependencies).

The /goal directive, now complete

This release closes the last of four items requested via /goal for a
follow-up release after v2.0.0 "Parity": AR/Supercharger, real TIA paddle
timing, and Lua/netplay frontend wiring shipped in v2.1.0 "Follow-Through";
ARM coprocessor cart-scheme wiring ships here.

See CHANGELOG.md's [2.2.0] entry and to-dos/ROADMAP.md's "Beyond
v2.0.0" section for the full remaining open-work list.