Repository navigation
Commit fd5ad88
release: cut v2.0.3 "Harbor" (2-cycle-ALE promoted to default — AccuracyCoin 141/141 + Android monetization glue) (#240)
* docs: 2-cycle-ALE campaign plan + ADR 0030 update (first-principles needs the refactor)
* feat(ppu): Phase 1 — 2-cycle-ALE fetch-split scaffold behind mc-ppu-2cycle-ale [behavior-neutral]
Phase 1 of the v2.0.3->v2.0.4 2-cycle-ALE fetch refactor (ADR 0030
"Option 1", the Timebase-scale campaign the maintainer chose after the
first-principles whole-dot refine was proven infeasible). Introduces the
genuine two-dot VRAM-access STRUCTURE behind a new default-off cargo
feature `mc-ppu-2cycle-ale` (declared in rustynes-ppu, forwarded through
rustynes-core and rustynes-test-harness exactly like the v2.0.2
`mc-ppu-bus-addr-hybrid` stand-in). No corruptions yet — those are Phase 3.
Model (ported from TriCNES `Emulator.cs` FetchPPU:150-177, commit 9199870):
each background fetch is split across two PPU dots. The EVEN (ALE) dot drives
the full 14-bit fetch address onto a new first-class `address_bus: u16` and
captures its low byte into the `octal_latch` (the 74LS373); the ODD (read)
dot resolves the effective address through the multiplexed-bus splice
`(address_bus & 0x3F00) | octal_latch`, reads VRAM, and drives the DATA byte
back onto the low 8 bits (AD7-0). For a coherent fetch the ALE-driven address
equals what the read would compute (`v` is constant across the 8-dot group's
phases 0..=6; the coarse-X increment is at phase 7 AFTER the read), so the
splice returns the intended address and observable behavior is UNCHANGED —
the split alone regresses nothing. Attribute + both pattern fetches are TRUE
two-dot ALEs (ALE at phases 2/4/6, read at 3/5/7).
The NAMETABLE fetch is a documented Phase-1 exception. Its MMC5 vertical-split
query (`bg_split_state`) has a mapper-observable side effect
(`split_chr_bank_latch`, which `nametable_fetch`/`chr_offset` read), and
moving that call one dot earlier to a phase-0 ALE shifts the Uchuu Keibitai
SDF split rendering (framebuffer-hash-only; cycle count and audio stay
byte-identical). Empirically confirmed both single- and double-query at
phase 0 diverge. So the NT split query stays at the read dot (phase 1) and
the NT octal-latch load co-locates there via `ale_splice`'s not-armed path
(no phase-0 NT ALE). Making NT a true two-dot fetch needs a non-mutating
split PEEK at the ALE dot, deferred to Phase 3 alongside the corruptions.
Implementation (all `#[cfg(feature = "mc-ppu-2cycle-ale")]`-gated; the
shipped/default build compiles NONE of it and is byte-identical):
- New PPU fields: `address_bus: u16`, `ale_armed: bool`; `octal_latch` is now
shared with the hybrid model via `any(..)` (the two flags are alternative
fetch models, never both-on in CI). None are snapshotted — they self-heal on
the next fetch ALE, so PPU_SNAPSHOT_VERSION stays at 4 (no format bump).
- New ALE dispatch in `tick` (phases 2/4/6) + helpers `ale_drive_at`,
`ale_drive_bg_lo`, `ale_drive_bg_hi`, `drive_bus`, `ale_splice`,
`ale_drive_data`. `ale_armed` distinguishes a read that followed a real ALE
(main dispatch) from one with none (the dot-337-340 garbage NT fetches), so
the latter stays transparent (drive+latch in place) and never splices a
stale bus.
- A12/mapper notification (`observe_a12_addr`) stays on the INTENDED
(un-spliced) address at every call site, so fetch-address/A12 timing is
unchanged — the top silent-regression risk the plan flagged.
STOP-OR-GO gate — flag-OFF byte-identical AND flag-ON full battery green,
both verified (release, sccache-safe flock):
- flag-off baseline: AccuracyCoin 139/141 (98.58%), nestest 0-diff,
mmc3 18/18 (A12 IRQ clocking), ppu_sprites 19/19 (sprite-zero), mmc1_a12,
nes_blargg 8/8, 60-ROM commercial oracle 60/60.
- flag-on (`mc-ppu-2cycle-ale`): IDENTICAL — AccuracyCoin 139/141 (same two
expected PPU failures; corruptions are Phase 3), nestest 0-diff, mmc3 18/18,
ppu_sprites 19/19, mmc1_a12, nes_blargg 8/8, and the 60-ROM commercial
byte-identity oracle 60/60 (byte-identical to the flag-off baseline,
including the MMC5 titles Castlevania 3 / Uchuu Keibitai SDF / Bandit Kings).
- `cargo fmt --all --check` clean; `cargo clippy -D warnings` clean for the
workspace default AND the `mc-ppu-2cycle-ale` flag-on config.
Phase 1 of a multi-phase campaign: no corruptions, no promotion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ppu): Phases 2+3 — 2-cycle-ALE corruptions arise NATURALLY, AccuracyCoin 141/141 flag-on
Phases 2 (NT true two-dot fetch) + 3 (both corruptions, naturally) of the
v2.0.3->v2.0.4 2-cycle-ALE fetch refactor (ADR 0030 "Option 1"), behind the
default-off `mc-ppu-2cycle-ale` feature. Both new AccuracyCoin PPU tests now
pass flag-on with NO reconstruction: the octal latch carries the stale bytes
on its own, per the TriCNES transistor-level model. The v2.0.2 `+1 coarse-X`
whole-dot stand-in is not used here.
Result (release, sccache-safe flock):
- flag-ON (`mc-ppu-2cycle-ale`): AccuracyCoin 141/141 (100.00%) — both
"ALE + Read" ($0491) AND "Hybrid Addresses" ($0492) = $01.
- flag-OFF (shipped/default): byte-identical — AccuracyCoin 139/141,
nestest 0-diff (none of this code compiles into the default build).
Model (ported from TriCNES `Emulator.cs`, MIT, commit 9199870):
Phase 2 — NT true two-dot fetch. A new phase-0 nametable ALE (`ale_drive_nt`)
drives the PLAIN `0x2000 | (v & 0x0FFF)` address and loads `octal_latch` with
its low byte one dot BEFORE the phase-1 read; the read (`fetch_nt`) resolves
through the armed `ale_splice` = `(address_bus & 0x3F00) | octal_latch`
(TriCNES `PPU_Render_ShiftRegistersAndBitPlanes` case 0/1). The MMC5
vertical-split query keeps living at the read dot (its `split_chr_bank_latch`
side effect is mapper-observable); when it returns split-active, `fetch_nt`
disarms the phase-0 ALE and reads the synthesized `split.nt_addr` co-located
— so split rendering is byte-identical WITHOUT needing a new non-mutating
peek trait method (deferring the correction to the read dot achieves the same
end). This resolves the Phase-1 "NT not yet a true 2-dot ALE" finding.
Phase 3a — "Hybrid Addresses" via the delayed-`CopyV` countdown
(`copy_v_delay`, TriCNES `PPU_Update2006Delay` :1684-1704). A `$2006` second
write in the active BG-fetch window no longer copies `t -> v` immediately; it
stages a `COPY_V_DELAY = 4`-dot countdown (RustyNES's fixed CPU/PPU alignment
== TriCNES's delay-4 case; the ROM's `.word` retries cover its two answer-key
alignments and the fixed one hits). While the countdown runs the fetch
cadence advances coarse-X (`inc_hori_v` at phase 7) and the per-group phase-0
NT ALE keeps loading the CURRENT (pre-copy) `v`'s NT-low — so by the landing
`octal_latch` NATURALLY holds the one-tile-ahead `$19`; the landing (top of
`tick`, before the fetch dispatch) sets `address_bus = v` (new `t`, high
`$2F00`) and the next NT read splices `$2F00 | $19 = $2F19`. No `(v&0xE0)|
(coarse_x+1)` reconstruction — the +1 coarse-X stand-in is deleted from this
path. The deferral is gated to the active-fetch window (dots 1..=256 + the
321..=336 prefetch), the only region a BG fetch can consume the stale latch,
so HBlank-window scroll writes are untouched. Verified natural by the octal
trace: `W2006 @ sl4 dot182 -> HYBRID $2F19 @ dot186` (delay exactly 4).
Phase 3b — "ALE + Read" reuses the existing `render_data_bus` /
`ppudata_sm_countdown` machinery: at the PPUDATA state-machine landing during
render the latch freezes on the read's DATA byte (`pattern_latch_stale`), and
`drive_bus` suppresses the next pattern ALE's latch reload so the pattern read
splices `(PAR high 6):(stale $FF) = $0FFF` — the natural multiplexed-bus
splice, no explicit reconstruction. Verified: `SMLAND $FF @ sl3 dot228 ->
STALE $0FFF @ dot230`.
A12/mapper notify stays on the INTENDED (un-spliced) address at every call
site; the only A12-timing change is the `$2006`-write edge, delayed 4 dots
during render (rare, verified inert on the MMC3/MMC1 A12 suites). No snapshot
format bump — `address_bus`/`ale_armed`/`octal_latch`/`copy_v_delay`/
`pattern_latch_stale` all self-heal within a scanline; `PPU_SNAPSHOT_VERSION`
stays at 4.
Implementation (all `#[cfg(feature = "mc-ppu-2cycle-ale")]`-gated):
- new field `copy_v_delay: u8`; `pattern_latch_stale` widened to `any(hybrid,
2cycle)`; `octal_trace` module + re-export widened to `any(...)` so the
2cycle build can cross-diff against TriCNES via `RUSTYNES_OCTAL_TRACE=1`.
- new `ale_drive_nt`; `drive_bus` gains an `is_pattern` arg + the stale-freeze
suppression; `ale_splice` gains a diagnostic HYBRID/STALE trace push;
`fetch_nt` armed-splice / split-disarm; the `$2006` case-6 delayed copy; the
countdown landing at the top of `tick`; the SM-landing freeze widened.
Regression battery (flag-ON): nestest 0-diff; mmc3 A12-IRQ 18/18; ppu_sprites
(sprite-zero) all pass; mmc1_a12 pass; nes_blargg all pass. `cargo fmt` +
`clippy -D warnings` clean for the default, `mc-ppu-bus-addr-hybrid`, and
`mc-ppu-2cycle-ale` configs; `no_std` cross-compile clean.
60-ROM commercial byte-identity oracle (flag-ON): 58/60 byte-identical. The
two changes are Super Mario Bros. 3 (8 px, scanline 194 cols 0-7) and Uchuu
Keibitai SDF (7 px, scanline 15 cols 152-158) — both legitimately do
`$2006`-during-active-render, both differ ONLY in the framebuffer (audio FNV +
CPU-cycle count byte-identical), each a single-tile sub-scanline shift at the
exact scanline of a mid-render `$2006` scroll write: the precise 2-cycle-ALE
artifact, MORE TriCNES-faithful than the flag-off immediate-`v=t`
approximation. The committed `insta` snapshots are LEFT at the flag-off values
so the shipped build stays byte-identical; the two flag-on diffs are
documented exceptions to re-bless (or make feature-conditional) at the
Phase-4 promotion gate. Full analysis in ADR 0030's final update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ppu)!: promote 2-cycle-ALE PPU model to default — shipped AccuracyCoin 141/141, retire experimental flags (ADR 0030)
Promote the first-principles 2-cycle-ALE PPU fetch model from the experimental
`mc-ppu-2cycle-ale` flag to the shipped default and the ONLY PPU fetch path,
following the v2.0.0 precedent that promoted the one-clock scheduler by deleting
its flag. The full machinery is now unconditional: the phase-0/2/4/6 ALE-drive
dots (`ale_drive_nt/at/bg_lo/bg_hi` → `drive_bus`), the first-class 74LS373
`octal_latch`, the multiplexed-bus read-half splice `(address_bus & 0x3F00) |
octal_latch` (`ale_splice`), the delayed-`CopyV` "Hybrid Addresses" countdown
(`copy_v_delay`, TriCNES `PPU_Update2006Delay`), and the `$2007`-ALE-overlap
"ALE + Read" freeze (`pattern_latch_stale`).
This is a shipped-behavior change (hence `!`): two commercial titles now render
more TriCNES-faithfully at a mid-render `$2006` scroll write — Super Mario Bros. 3
(one leftmost tile, scanline 194) and Uchuu Keibitai SDF (one tile at the MMC5
split boundary, scanline 15). Their `insta` snapshots are re-blessed; each changes
ONLY its `checkpoint f600 fb_fnv1a64` line (cycle count + audio hash byte-identical
— a pure PPU-internal timing shift). No other title moved.
Retire BOTH experimental flags. `mc-ppu-2cycle-ale` (the now-default model) and
`mc-ppu-bus-addr-hybrid` (the superseded v2.0.2 whole-dot `+1 coarse-X` stand-in)
are deleted from rustynes-ppu / rustynes-core / rustynes-test-harness Cargo.toml
and all forwards. The v2.0.2 stand-in code is removed: the `octal_effective`
splice function, the `OctalFetch` enum, the `copy_v` one-shot, and the
`(v & 0xE0)|(coarse_x+1)` coarse-X reconstruction. Zero references to either flag
remain in crates/. The `octal_trace` calibration ring is kept behind a new
default-off dev feature `ppu-octal-trace` (it does NOT gate emulation behavior):
with the feature off, `push` is a zero-cost no-op stub and the ring's 64-bit-atomic
storage is absent, so the shipped hot path is untouched and the `#![no_std]` chip
stack — whose thumbv7em target lacks 64-bit atomics — still cross-compiles.
Save-state: add a `PPU_SNAPSHOT_VERSION` v5 tail serializing the five in-flight
fetch fields (`octal_latch`, `address_bus`, `ale_armed`, `pattern_latch_stale`,
`copy_v_delay`). This corrects the pre-promotion assumption that no persisted PPU
fields were needed ("stays at 4"): making the model the default path made it the
substrate for netplay rollback, whose save/restore round-trip revealed that a
mid-render checkpoint can land with these fields live — desyncing the re-simulated
framebuffer. Three rustynes-netplay rollback-determinism tests regressed on
promotion and are fixed by the v5 tail (all 16 now pass). The change is ADDITIVE
and NOT a load-break: pre-v5 .rns blobs upconvert with the fields at their inactive
rest defaults (0/false), the state a save taken at a fetch boundary always holds —
the same pattern the v3 ($2007 state machine) and v4 (overclock countdown) tails
used.
Performance: the two-dot fetch transaction costs ~10% over the R1 baseline
(nestest headless ~4.15 ms/frame, ~4x realtime, vs ~3.77 ms), well inside the
16.639 ms NTSC wall. Accepted for the accuracy gain.
Shipped-default verification (no feature flags): AccuracyCoin 141/141 (100.00%,
RAM-authoritative, zero failing tests — $0491 and $0492 both pass); nestest 0-diff;
mmc3 A12-IRQ 18/18 (+5 pre-existing by-design ignores); ppu_sprites 19/19;
mmc1_a12; nes_blargg 8/8; 60-ROM commercial oracle 60/60 (2 re-blessed titles
only); full `cargo test --workspace` green; fmt / clippy -D warnings / rustdoc
-D warnings / thumbv7em no_std all clean.
Docs: ADR 0030 gains a "Promoted (v2.0.3)" section (including the honest correction
of the "no snapshot bump" note); docs/performance.md records the ~4.15 ms baseline;
CHANGELOG [Unreleased] documents the promotion, flag retirement, and v5 tail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(android): v2.0.3 "Harbor" foss/play monetization glue + host-localizable bridge warnings
Wire the freemium / ad-supported monetization glue into the Android build behind the
foss/play flavor split (ADR 0025), and make the rustynes-mobile host warnings
localizable. Every change is additive and off-by-default: the FOSS (F-Droid /
GitHub-Releases) artifact links no ads / store SDK and stays behaviourally identical to
the pre-monetization build, and the desktop/wasm frontends are untouched.
Monetization facade (ADR 0025 step 5)
-------------------------------------
Follows the repo's established twin-facade idiom (LicenseManager/Cast/Integrity): a
single MonetizationGate class exists in both source sets — a byte-compatible no-op in
src/foss (every feature free, no ads, no gate, never even constructs the AdPolicy core
or loads its JNA library) and the real implementation in src/play. MainActivity (a
src/main file) touches only MonetizationGate, so it links against either flavor
unchanged. This side-steps the #235 flavor gotcha: Kotlin type-checks inside
`if (BuildConfig.PLAY_BUILD)` regardless of the flag, so any AppLovin/RevenueCat symbol
referenced from shared code would break the default foss compile — confining all
com.applovin.* / com.revenuecat.* references to src/play (AdGate, RewardedGate,
RcBilling) and the play MonetizationGate twin is what keeps foss clean.
The play twin owns the shared Rust AdPolicy core (all policy — ad cadence, feature
gating, the free-tier play budget, offline grace — lives in Rust so Android and iOS
cannot diverge), initializes AppLovin MAX + RevenueCat off the cold-start path
(onActivityCreated, mirroring the deferred Billing connect), routes
featureEnabled(PremiumFeature) / begin_session / start_play / export_progress /
restore_progress through the core, and draws the run-out paywall + live mm:ss countdown
(RunOutOverlay, a Compose overlay layered over EmulatorScreen). The FOSS twin's
RunOutOverlay draws nothing, so the shared Box in MainActivity is visually and
behaviourally inert in the clean build. AdGate/RewardedGate/RcBilling are adapted from
crates/rustynes-monetization/shells/android; RcBilling was corrected to the RevenueCat
8.10.0 API (UpdatedCustomerInfoListener moved to .interfaces, restorePurchasesWith KTX
helper, nullable StoreTransaction? in the purchase callback) since the shell was written
against an older API shape.
Gradle / manifest / R8 (steps 1-2, 4, 6)
----------------------------------------
- productFlavors.play gains APPLOVIN_SDK_KEY / REVENUECAT_API_KEY /
MAX_INTERSTITIAL_AD_UNIT_ID / MAX_REWARDED_AD_UNIT_ID buildConfig fields (from gradle
properties, empty default so a keyless dormant build links), plus TESTER_UNLOCK per
buildType (debug=true/release=false) so the RevenueCat local-QA unlock is compiled out
of every store build.
- AppLovin MAX 13 + RevenueCat 8 added as playImplementation ONLY; AppLovin's Maven repo
added in settings.gradle.kts scoped to the com.applovin group. The foss variant links
neither.
- src/play/AndroidManifest.xml carries the AD_ID permission + AdMob APPLICATION_ID (the
official Google test id placeholder) so these never merge into the foss manifest.
- Paywall strings (en + es) live in src/play/res so they merge into the play variant
only, keeping the foss resource table clean.
- proguard-rules.pro: strict-full-mode keeps for the second UniFFI package
(com.doublegate.rustynes.monetization.ffi.**), the AppLovin SDK + mediation adapters,
and the RevenueCat surface — documented per rule; no-op keeps in the foss release.
- app versionCode 20001 -> 20003, versionName "2.0.1" -> "2.0.3" (the v2.0.2 cut was
desktop-only, so the Android host advances directly to 2.0.3).
Baseline Profile / vitals (steps 3-4)
-------------------------------------
The :baselineprofile module (StartupBenchmark + BaselineProfileGenerator, already flavor-
pinned to foss) is refreshed with MAINTAINER (v2.0.9) notes that the profile must be
regenerated on-device against the v2.0.0 "Timebase" core (the one-clock rewrite reshaped
the launch/UniFFI hot-class set); only config/plumbing is touched here — the on-device
capture, Play Console vitals dashboard, foldable WYSIWYG, and live monetization runtime
verification are device-gated and deferred to v2.0.9.
Host-localizable bridge warnings (PR #235 follow-up, step 7)
------------------------------------------------------------
rustynes-mobile: introduce an additive, non-breaking HostWarning enum (UniFFI-exported)
so hosts can localize the drainable warnings that were previously pre-baked English
Strings. HostWarning::PreTimebaseMovie::message() returns the exact prior English
verbatim; a new drain_warning_codes() -> Vec<HostWarning> exposes the codes, while
drain_warnings() -> Vec<String> is kept working (it now maps codes through message()), so
existing Kotlin/Swift hosts are unbroken and the default output is byte-identical. Added a
test asserting the code maps to the exact legacy string; the two existing movie-warning
tests stay green. The chip stack is untouched (no accuracy impact); the desktop/wasm
frontends carry their own identical warning and are deliberately left unchanged (parity
unchanged).
Verification: cargo test/clippy/fmt -p rustynes-mobile green (27 tests);
./gradlew :app:assembleFossDebug and :app:assemblePlayDebug both BUILD SUCCESSFUL
(both flavors link + assemble). On-device runtime verification is out of scope (no device).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* release: cut v2.0.3 "Harbor" (2-cycle-ALE promoted to default — AccuracyCoin 141/141 + Android monetization glue)
The third release of the v2.0.x "Harbor" mobile-finalization train, and the one
that makes the octal-latch accuracy work real at the shipped default. It bundles
the two workstreams developed in parallel on this cut:
1. PPU: promote the 2-cycle-ALE fetch model to the unconditional default (ADR 0030).
The genuine two-dot VRAM transaction (even-dot ALE-drive + octal_latch load;
odd-dot `(address & 0x3F00) | octal_latch` splice + read) becomes the only PPU
fetch path, retiring BOTH experiment flags (`mc-ppu-2cycle-ale` and the superseded
v2.0.2 whole-dot `mc-ppu-bus-addr-hybrid` stand-in) and deleting the v2.0.2
stand-in code (octal_effective, the OctalFetch enum, copy_v, the +1 coarse-X
reconstruction). The latch now NATURALLY carries the stale byte: the delayed CopyV
(copy_v_delay = 4, RustyNES's fixed alignment) makes the nametable read splice
$2F19 for "Hybrid Addresses" ($0492), and the $2007-ALE overlap freezes the latch
on $FF -> $0FFF for "ALE + Read" ($0491). Shipped AccuracyCoin is now 141/141
(100.00%, RAM-authoritative) -- both new upstream PPU tests pass out of the box
(previously an honest 139/141). Option 1 is fully landed.
Save-state: promotion made the model the netplay-rollback substrate, whose
save/restore round-trip exposed a determinism gap in the five in-flight fetch
fields (octal_latch, address_bus, ale_armed, pattern_latch_stale, copy_v_delay).
They are now serialized in an ADDITIVE PPU_SNAPSHOT_VERSION 4 -> 5 tail (pre-v5
.rns still load, upconverting to inactive rest defaults -- the same pattern the v3
$2007-state-machine and v4 overclock-countdown tails used). A v2.0.3 save-state is
forward-incompatible with <= v2.0.2 but this is an in-train sub-version bump, NOT
an ADR-0028 .rns format-epoch break.
2. Android + mobile bridge (Harbor v2.0.3 polish + foss/play step 5).
- foss/play monetization glue: the play flavor gains a real MonetizationGate
(AppLovin MAX ad glue via AdGate/RewardedGate + a RevenueCat 8.10.0 entitlement
wrapper RcBilling, driving the Rust AdPolicy core from rustynes-monetization),
feature_enabled gating, the run-out paywall + mm:ss countdown Compose overlay,
begin_session at launch, export/restore_progress persistence, and the
offline-grace path. The foss flavor keeps a no-op MonetizationGate twin (every
feature free, no ads, never touches the FFI ad core), so the default F-Droid
artifact stays behaviour-identical. Ad SDKs + AD_ID/AdMob manifest + paywall
strings (en/es) live in src/play/ only; R8 full-mode keeps cover the
monetization UniFFI package + AppLovin + RevenueCat. Both flavors assemble;
monetization is behaviourally dormant pending on-device runtime verify (v2.0.9).
Android versionCode 20001 -> 20003, versionName -> "2.0.3".
- Host-localizable mobile bridge warnings: rustynes-mobile gains a UniFFI-exported
HostWarning enum (first variant PreTimebaseMovie, the ADR 0028 .rnm warning) +
an additive drain_warning_codes() + an exported host_warning_message(). The
existing drain_warnings() -> Vec<String> is preserved verbatim (maps each code
to its English message()), so current Kotlin/Swift hosts are unbroken;
desktop/wasm parity is unchanged.
Verification (all green, independently re-verified on the merged tree):
- AccuracyCoin 141/141 (100.00%, RAM-authoritative) on the default build.
- 60-ROM commercial byte-identity oracle 60/60, with exactly two documented
re-blesses -- Super Mario Bros. 3 (one leftmost tile on scanline 194) and Uchuu
Keibitai SDF (one tile at the MMC5 split boundary on scanline 15) now render more
TriCNES-faithfully at a mid-render $2006 scroll write; audio + cycle counts stay
byte-identical, no other title changed.
- nestest 0-diff; mmc3 A12-IRQ 18/18; ppu_sprites 19/19; all 16 netplay
rollback-determinism tests pass (the v5 fix).
- cargo fmt/clippy (workspace)/RUSTDOCFLAGS doc all clean; Rust rustynes-mobile
27 tests pass; both Android flavors assemble (foss + play).
- Perf: nestest ~4.15 ms/frame (~4x realtime, ~10% over the R1 baseline), recorded
in docs/performance.md; accepted for the accuracy gain.
Sequencing note: the campaign plan nominally slotted the flag flip at v2.0.4; per
the maintainer's firmed mapping (Option 1 fully complete within v2.0.2->v2.0.4) it
landed at v2.0.3, leaving v2.0.4 for the Android RC + foss/play finalization-window
closeout. Docs reconciled (STATUS.md single source of truth, ROADMAP, the plan doc).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ppu): re-bless scanline.nes visual-regression golden for the 2-cycle-ALE promotion
The v2.0.3 promotion of the 2-cycle-ALE PPU fetch model to the default changes the
framebuffer of `nes-test-roms/scanline/scanline.nes` — the one public test ROM in
the `visual_regression` suite that drives mid-scanline scroll/timing via mid-render
$2006 writes, exactly the mechanism the octal-latch / delayed-CopyV model makes more
accurate. The `scanline_frame_180` FNV-1a golden shifts eb8260a4cf3a1f25 ->
39e8052eedc7f4d5.
This is the expected, surgical consequence of the accuracy improvement, verified:
- Deterministic: the locally reproduced hash is byte-identical to CI's, so this is
genuine 2-cycle-ALE behavior, not platform noise (the determinism contract holds).
- Localized: only this one golden shifted. The other eight visual goldens — including
the NMI-drift-sensitive `nmi_sync/demo_ntsc.nes` demo that "glitches visibly if NMI
timing drifts" — stayed byte-identical, confirming no collateral timing drift. A
full --no-fail-fast sweep of every public-ROM snapshot binary (visual_regression,
extra_scanlines, ppu_read_buffer, p240_test_suite, m22, mmc1_a12, dmc_dma,
audio_expansion, volume_tests, f2_accuracy_audit, blargg_nes_cpu_test5,
vs_dualsystem, read_joy3, input_devices, movie) surfaced no other shift.
- Model-correct: the 2-cycle-ALE fetch is per-dot verified against the TriCNES oracle;
AccuracyCoin is 141/141 and the 60-ROM commercial oracle 60/60.
The promotion commit re-blessed the two commercial oracle snapshots (SMB3, Uchuu
Keibitai SDF) but missed this one because the harness ran `cargo test --workspace`
(no test-roms feature) plus targeted suites, so the test-roms-gated visual_regression
golden was not exercised in full. CHANGELOG [2.0.3] updated to record the re-bless.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(v2.0.3): address gemini + copilot PR #240 review — monetization lifecycle, stale docs, README
Resolves all 15 bot review threads on PR #240 (gemini-code-assist x8, copilot x7).
Android monetization glue (play flavor) — real fixes, both flavors still compile:
- AdGate / RewardedGate: add destroy() that detaches the MaxAd listener, so the
long-lived interstitial/rewarded ad instances no longer retain a destroyed Activity
(leak on rotation). MonetizationGate.onDestroy() now calls both + clears the refs.
- Rewarded-ad resume bridge: the RewardedGate reward callback was an empty lambda, so a
free user who watched a rewarded ad would never have the emulator unpaused (show() is
non-blocking, so the resume can't be returned inline). Added an onResumeCallback that
RunOutOverlay sets and the gate invokes on grant.
- RunOutOverlay LaunchedEffect keyed on `premium` (was Unit) + short-circuits for premium
users, eliminating the always-on 1 Hz wakeup / battery drain when nothing is gated.
- formatMmSs forces Locale.US so the countdown uses ASCII digits (a non-ASCII-numeral
device locale could render digits the label font lacks).
- Corrected the "SDK init deferred off cold-start" comments (MainActivity + the class
doc): onActivityCreated is invoked from onCreate, so init is on the launch path (guarded
to run once); posting it off the first-frame path is flagged as a v2.0.9 refinement.
- build.gradle.kts: versionCode/versionName rationale is now a self-contained block
comment instead of a parenthetical split across the two assignments.
PPU (stale save-state comments now that v5 serializes the fields):
- The in-flight-fetch-fields block comment + the octal_latch / address_bus doc comments
said "Not snapshotted / PPU_SNAPSHOT_VERSION unchanged". They ARE serialized in the v5
tail (v2.0.3) for netplay-rollback determinism; comments corrected to say so (still
additive, still not an ADR-0028 epoch break; they self-heal only in continuous play).
README (missed spots from the 141/141 promotion):
- The dedicated AccuracyCoin badge SVG still read 98.58% (139/141) -> 100% (141/141).
- The overview parenthetical still said "two remaining failures ... known gaps" -> now
states every assigned test passes as of v2.0.3 via the promoted 2-cycle-ALE model.
Verified: cargo fmt/clippy/rustdoc on rustynes-ppu clean; ./gradlew compilePlayDebugKotlin
+ compileFossDebugKotlin BUILD SUCCESSFUL (only the pre-existing AppLovin showAd()
deprecation warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 5b4effb commit fd5ad88
34 files changed
Lines changed: 2142 additions & 257 deletions
File tree
- .github/release-notes
- android
- app
- src
- foss/java/com/doublegate/rustynes
- main/java/com/doublegate/rustynes
- play
- java/com/doublegate/rustynes
- monetization
- res
- values-es
- values
- baselineprofile/src/main/java/com/doublegate/rustynes/baselineprofile
- crates
- rustynes-core
- rustynes-mobile/src
- rustynes-ppu
- src
- rustynes-test-harness
- tests
- snapshots
- docs
- adr
- to-dos
- plans
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
23 | 92 | | |
24 | 93 | | |
25 | 94 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
0 commit comments