Skip to content

Commit 0ce5aea

Browse files
doublegateclaude
andcommitted
feat(apu): implement the SMP wait states; AccuracySNES E3.09 measures them
$F0 bits 4-5 and 6-7 select a clock divider for the SMP, nominally {2, 4, 8, 16} -- but 8 and 16 are glitchy on real silicon and the CPU consumes 10 and 20 clocks per opcode cycle while the timers still advance by 8 and 16. ares and bsnes carry the same comment (sfc/smp/timing.cpp): "the timers are not affected by this and advance by their expected values." Two tables, and the gap between them is what the new row measures. RustySNES parsed both selectors into Io::external_wait / Io::internal_wait, saved them, restored them -- and nothing downstream ever read either one. The eighth instance of the dead-config defect class, and the first the cart found rather than grep. rustysnes-apu now carries SMP_CYCLE_WAIT = [2,4,10,20] for the CPU (and so for the recorded micro-op plan and the S-DSP catch-up, which are real base clocks) against SMP_TIMER_WAIT = [2,4,8,16] for the timers alone, with ares' SMP::wait address classification: idle cycles, $00F0-$00FF and a mapped IPL ROM take the internal selector, everything else the external one. At the reset selector both tables read SMP_WAIT, so every program that leaves $F0 alone -- which is every commercial driver -- is byte-identical to before. E3.09 reads the gap as a ratio the program can see: a selector changes clocks-per-cycle, not an instruction's cycle count, so the same loop is the same number of opcode cycles either way and only the timer-per-cycle rate moves. Over a fixed 48-pass poll loop timer 0 ticks 4x as often at selector 2 as at selector 0. Both wrong models were injected and both fail on code 2: no wait states reads 1x, charging the CPU's glitchy 10 to the timers as well reads 5x. The row separates the two ways of having the feature, not merely its absence. Mesen2 and ares both pass it; snes9x fails it off the same missing `case 0xf0` that already costs it E3.08 and E3.10, so SNES9X_KNOWN_FAILURES goes 14 -> 15 with that citation. Coverage 352 -> 353 of 443 (299 on-cart + 54 scenes), battery 340 tests at 100% on-cart, three references agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fe3ea97 commit 0ce5aea

14 files changed

Lines changed: 613 additions & 119 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- **`E3.09`, and the SMP wait states it found unimplemented.** `$F0` bits 4-5 and 6-7 select a clock
15+
divider for the SMP, nominally `{2, 4, 8, 16}` — but 8 and 16 are glitchy on real silicon and
16+
**the CPU consumes 10 and 20 clocks per opcode cycle while the timers still advance by 8 and 16.**
17+
ares and bsnes carry the same comment (`sfc/smp/timing.cpp`): *"the timers are not affected by
18+
this and advance by their expected values."* Two tables, and the gap between them is the row.
19+
20+
RustySNES parsed both selectors into `Io::external_wait` / `Io::internal_wait`, saved them,
21+
restored them — and **nothing downstream ever read either one**. The eighth instance of the
22+
dead-config defect class, and the first found by the cart rather than by grep.
23+
24+
`rustysnes-apu` now carries `SMP_CYCLE_WAIT = [2,4,10,20]` for the CPU (and hence for the recorded
25+
micro-op plan and the S-DSP catch-up, which are real base clocks) against
26+
`SMP_TIMER_WAIT = [2,4,8,16]` for the timers alone, with ares' `SMP::wait` address classification:
27+
idle cycles, `$00F0-$00FF` and a mapped IPL ROM take the *internal* selector, everything else the
28+
external one. **At the reset selector both tables read `SMP_WAIT`, so every program that leaves
29+
`$F0` alone — which is every commercial driver — is byte-identical to before.**
30+
31+
The row reads the gap as a ratio the program can see: a wait selector changes clocks-per-cycle,
32+
not an instruction's cycle count, so the same loop is the same number of opcode cycles either way
33+
and only the timer-per-cycle rate moves. Over a fixed 48-pass poll loop, timer 0 ticks **4x** as
34+
often at selector 2 as at selector 0. Both wrong models were injected and both fail on code 2:
35+
no wait states at all reads **1x**, and charging the CPU's glitchy 10 to the timers as well reads
36+
**5x**. So the row separates the two ways of having the feature, not merely its absence.
37+
38+
Coverage `352 → 353 of 443` (299 on-cart + 54 scenes); battery 340 tests, 100% on-cart.
39+
`docs/apu.md` gains the selector model.
40+
1441
- **`E9.09` — the echo write pointer wraps at the 16-bit boundary, over page zero `[ERRATA]`.**
1542
`ESA` names a page, `EDL` a length, and the address is computed in sixteen bits with nothing
1643
clamping it at `$FFFF`. A driver that sets `ESA` too high does not get a short buffer or a dropped

‎crates/rustysnes-apu/src/lib.rs‎

Lines changed: 140 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,39 @@ use dsp::{ARAM_SIZE, Dsp};
4646
use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
4747
use spc700::{Spc700, Spc700Bus};
4848

49-
/// SMP base clocks one bus access consumes — ares `cycleWaitStates[0]` (the reset wait state).
49+
/// SMP base clocks one bus access consumes at the **reset** wait state — `SMP_CYCLE_WAIT[0]`.
5050
///
5151
/// The SMP base clock is `apuFrequency / 12` (ares `SMP::create(apuFrequency()/12, …)`); a normal
52-
/// access is 2 of those base ticks, giving the ~1.024 MHz effective opcode-cycle rate. The
53-
/// per-region external/internal wait-state divider (the glitchy `{2,4,10,20}` table) collapses to
54-
/// this reset default — no committed program reprograms `$F0`'s wait selectors.
52+
/// access is 2 of those base ticks, giving the ~1.024 MHz effective opcode-cycle rate. Kept as a
53+
/// named constant because it is the value every unit test and every shipped program actually sees:
54+
/// `$F0`'s wait selectors reset to 0 and no commercial driver reprograms them.
5555
const SMP_WAIT: u32 = 2;
5656

57+
/// SMP base clocks the **CPU** consumes per opcode cycle, indexed by a `$F0` wait selector.
58+
///
59+
/// The selector is nominally a clock divider of `{2, 4, 8, 16}`, but dividers of 8 and 16 are
60+
/// glitchy on real silicon and the CPU ends up consuming **10 and 20**. ares and bsnes document it
61+
/// identically (`sfc/smp/timing.cpp`): *"sometimes the SMP will run far slower than expected, other
62+
/// times … the SMP will deadlock until the system is reset. The timers are not affected by this and
63+
/// advance by their expected values."*
64+
///
65+
/// That last sentence is why there are two tables and not one — see [`SMP_TIMER_WAIT`]. The gap
66+
/// between them is directly measurable from a cart, and `E3.09` measures it.
67+
const SMP_CYCLE_WAIT: [u32; 4] = [2, 4, 10, 20];
68+
69+
/// SMP base clocks the **timers** advance per opcode cycle, indexed by a `$F0` wait selector.
70+
///
71+
/// The un-glitched divider, ares `timerWaitStates`. At selectors 2 and 3 the timers advance 8 and
72+
/// 16 where the CPU pays 10 and 20, so over a fixed instruction sequence the timers accumulate
73+
/// `8/10` of the elapsed real time — which reads from a cart as the timers running **faster
74+
/// relative to the program** than at selector 0, by exactly `8/2 = 4`. See [`SMP_CYCLE_WAIT`].
75+
const SMP_TIMER_WAIT: [u32; 4] = [2, 4, 8, 16];
76+
77+
/// The largest `base_clocks` a recorded micro-op can legitimately carry: the slowest wait selector,
78+
/// un-halved. A save-state claiming more never came from real execution, and a value `plan_sub`
79+
/// could never reach would wedge `advance_smp_cycle`.
80+
const MAX_PLAN_BASE_CLOCKS: u32 = SMP_CYCLE_WAIT[3];
81+
5782
/// SMP base clocks per S-DSP **micro-tick**. The S-DSP runs its 32-step voice sequence one
5883
/// [`Dsp::tick`] at a time; 32 ticks = one 32 kHz stereo sample. `apuFrequency = 32040 × 768`, the
5984
/// SMP/DSP base is `apuFrequency / 12`, so one sample is `(32040 × 768 / 12) / 32040 = 64` base
@@ -258,6 +283,35 @@ impl Default for Io {
258283
}
259284

260285
impl Io {
286+
/// Which `$F0` wait selector governs an access — ares `SMP::wait`'s address classification.
287+
///
288+
/// Idle cycles (no address), the `$00F0-$00FF` register block, and the IPL ROM **while it is
289+
/// mapped** are *internal*; everything else is *external*. The IPL clause reads oddly until you
290+
/// note it is conditional on the mapping: once `$F1` bit 7 is clear those addresses are ordinary
291+
/// ARAM and take the external selector like any other.
292+
const fn wait_index(&self, address: Option<u16>) -> usize {
293+
let internal = match address {
294+
None => true,
295+
Some(a) => a & 0xFFF0 == 0x00F0 || (a >= 0xFFC0 && self.iplrom_enable),
296+
};
297+
(if internal {
298+
self.internal_wait
299+
} else {
300+
self.external_wait
301+
}) as usize
302+
}
303+
304+
/// The `(cpu, timer)` base-clock pair one access costs, halved for the split `$F4-$F7` reads.
305+
///
306+
/// Returned as a pair rather than resolved separately at each call site because the whole point
307+
/// of the glitch is that the two numbers differ; computing them apart invites one of them to be
308+
/// derived from the wrong table.
309+
const fn wait_clocks(&self, halve: bool, address: Option<u16>) -> (u32, u32) {
310+
let idx = self.wait_index(address);
311+
let shift = if halve { 1 } else { 0 };
312+
(SMP_CYCLE_WAIT[idx] >> shift, SMP_TIMER_WAIT[idx] >> shift)
313+
}
314+
261315
fn save_state(&self, s: &mut SaveWriter) {
262316
s.write_bool(self.timers_disable);
263317
s.write_bool(self.ram_writable);
@@ -619,8 +673,8 @@ impl Apu {
619673
/// `MAX_SAVED_PLAN_LEN` (mirroring the GSU's `pending_clocks` validation in
620674
/// `rustysnes-cart` — an in-flight instruction has at most a handful of micro-ops, so a
621675
/// larger claimed length could never have come from real execution); a step's `base_clocks`
622-
/// is neither `1` nor `2` (`record`/`record_next_instruction` only ever push `SMP_WAIT` or
623-
/// `SMP_WAIT >> 1`; an out-of-range value risks `plan_sub` never reaching it, wedging
676+
/// is zero or above `MAX_PLAN_BASE_CLOCKS` (`record` only ever pushes a `SMP_CYCLE_WAIT`
677+
/// entry, optionally halved; an out-of-range value risks `plan_sub` never reaching it, wedging
624678
/// `advance_smp_cycle`, or an unbounded drain); `plan_pos` exceeds the restored plan's
625679
/// length; or `plan_sub` is inconsistent with `plan_pos` (nonzero while `plan_pos` is already
626680
/// past the end of the plan, or `>=` the step at `plan_pos`'s own `base_clocks` — either
@@ -646,9 +700,10 @@ impl Apu {
646700
self.plan.clear();
647701
for _ in 0..plan_len {
648702
let base_clocks = s.read_u32()?;
649-
if base_clocks == 0 || base_clocks > SMP_WAIT {
703+
if base_clocks == 0 || base_clocks > MAX_PLAN_BASE_CLOCKS {
650704
return Err(SaveStateError::Invalid(alloc::format!(
651-
"APU instruction plan step base_clocks {base_clocks} is not 1 or {SMP_WAIT}"
705+
"APU instruction plan step base_clocks {base_clocks} exceeds the largest a \
706+
wait selector can produce ({MAX_PLAN_BASE_CLOCKS})"
652707
)));
653708
}
654709
let port_write = if s.read_bool()? {
@@ -712,15 +767,19 @@ struct SmpBus<'a> {
712767
}
713768

714769
impl SmpBus<'_> {
715-
/// Advance the SMP base clock + the timers by `clocks` base ticks (ares `SMP::step` +
716-
/// `stepTimers`). One normal bus access is [`SMP_WAIT`] (= ares `cycleWaitStates[0]`) ticks.
717-
fn step(&mut self, clocks: u32) {
718-
self.cycles += clocks;
770+
/// Advance the SMP base clock + the timers for one access — ares `SMP::wait`.
771+
///
772+
/// The CPU and the timers are advanced by **different** amounts whenever a `$F0` wait selector
773+
/// is 2 or 3: the CPU pays the glitchy `{10, 20}` while the timers advance the expected
774+
/// `{8, 16}`. At the reset selector both are [`SMP_WAIT`], which is why this reduces to the
775+
/// previous single-number behaviour for every program that leaves `$F0` alone.
776+
fn wait(&mut self, halve: bool, address: Option<u16>) {
777+
let (cpu_clocks, timer_clocks) = self.io.wait_clocks(halve, address);
778+
self.cycles += cpu_clocks;
719779
let te = self.io.timers_enable;
720780
let td = self.io.timers_disable;
721-
// Timers advance on the same SMP base timebase as the CPU (ares `timerWaitStates[0]` = 2).
722781
for t in self.timers.iter_mut() {
723-
t.step(clocks as u16, te, td);
782+
t.step(timer_clocks as u16, te, td);
724783
}
725784
}
726785

@@ -816,22 +875,22 @@ impl Spc700Bus for SmpBus<'_> {
816875
// steps around the data fetch (`wait(1)` twice). At the reset wait state the total is the
817876
// same SMP_WAIT base clocks as any other access, but the split is preserved for fidelity.
818877
if address & 0xFFFC == 0x00F4 {
819-
self.step(SMP_WAIT >> 1);
878+
self.wait(true, Some(address));
820879
let v = self
821880
.read_io(address)
822881
.unwrap_or_else(|| self.read_ram(address));
823-
self.step(SMP_WAIT >> 1);
882+
self.wait(true, Some(address));
824883
return v;
825884
}
826-
self.step(SMP_WAIT);
885+
self.wait(false, Some(address));
827886
if let Some(io) = self.read_io(address) {
828887
return io;
829888
}
830889
self.read_ram(address)
831890
}
832891

833892
fn write(&mut self, address: u16, data: u8) {
834-
self.step(SMP_WAIT);
893+
self.wait(false, Some(address));
835894
// Writes to $FFC0-$FFFF always reach ARAM even with the IPL ROM mapped in.
836895
if self.io.ram_writable && !self.io.ram_disable {
837896
self.aram[address as usize] = data;
@@ -840,7 +899,9 @@ impl Spc700Bus for SmpBus<'_> {
840899
}
841900

842901
fn idle(&mut self) {
843-
self.step(SMP_WAIT);
902+
// ares `SMP::idle` is `wait(0)`: NOT halved, and with no address, so an idle cycle takes
903+
// the *internal* selector. At the reset selector that is [`SMP_WAIT`], unchanged.
904+
self.wait(false, None);
844905
}
845906
}
846907

@@ -874,11 +935,15 @@ impl RecordingSmpBus<'_> {
874935
/// register (`$F3`) mid-execution the **cycle-correct** value: the DSP has advanced exactly the
875936
/// ticks up to that base clock and no further. blargg's `spc_dsp6` / `spc_mem_access_times` use
876937
/// the DSP as a sub-cycle reference, so this granularity is required for them to resolve.
877-
fn record(&mut self, clocks: u32) {
938+
fn record(&mut self, halve: bool, address: Option<u16>) {
939+
// The CPU's cost and the timers' are two different numbers whenever a `$F0` wait selector
940+
// is 2 or 3 — see `SMP_CYCLE_WAIT`. The plan, the DSP catch-up and `consumed` all follow
941+
// the CPU's, because those are real base clocks; only the timers take the other table.
942+
let (clocks, timer_clocks) = self.io.wait_clocks(halve, address);
878943
let te = self.io.timers_enable;
879944
let td = self.io.timers_disable;
880945
for t in self.timers.iter_mut() {
881-
t.step(clocks as u16, te, td);
946+
t.step(timer_clocks as u16, te, td);
882947
}
883948
*self.consumed += clocks;
884949
*self.dsp_counter += clocks;
@@ -996,14 +1061,14 @@ impl Spc700Bus for RecordingSmpBus<'_> {
9961061
fn read(&mut self, address: u16) -> u8 {
9971062
if address & 0xFFFC == 0x00F4 {
9981063
// $F4-$F7 read: ares splits the wait into two halved steps around the fetch.
999-
self.record(SMP_WAIT >> 1);
1064+
self.record(true, Some(address));
10001065
let v = self
10011066
.read_io(address)
10021067
.unwrap_or_else(|| self.read_ram(address));
1003-
self.record(SMP_WAIT >> 1);
1068+
self.record(true, Some(address));
10041069
return v;
10051070
}
1006-
self.record(SMP_WAIT);
1071+
self.record(false, Some(address));
10071072
if let Some(io) = self.read_io(address) {
10081073
return io;
10091074
}
@@ -1018,7 +1083,10 @@ impl Spc700Bus for RecordingSmpBus<'_> {
10181083
// already-happened — the one-access phase the blargg `spc_timer` / `spc_smp` /
10191084
// `spc_mem_access_times` suites pin. (Matches [`SmpBus::write`], which already steps first;
10201085
// the recording bus previously stored first, shifting the timer phase by one access.)
1021-
self.record(SMP_WAIT);
1086+
//
1087+
// Stepping first also means a write that CHANGES a wait selector pays the OLD one, which is
1088+
// ares' ordering too — `wait()` runs before `writeIO`.
1089+
self.record(false, Some(address));
10221090
// Writes to $FFC0-$FFFF always reach ARAM even with the IPL ROM mapped in.
10231091
if self.io.ram_writable && !self.io.ram_disable {
10241092
self.aram[address as usize] = data;
@@ -1031,7 +1099,7 @@ impl Spc700Bus for RecordingSmpBus<'_> {
10311099
}
10321100

10331101
fn idle(&mut self) {
1034-
self.record(SMP_WAIT);
1102+
self.record(false, None); // ares `SMP::idle` = `wait(0)`: internal selector, not halved
10351103
}
10361104
}
10371105

@@ -1079,6 +1147,52 @@ mod tests {
10791147
apu.tick(&mut bus);
10801148
}
10811149

1150+
/// The reset wait selector must reproduce the old single-number behaviour exactly, or every
1151+
/// timing golden in the tree moves for a feature no shipped program uses.
1152+
#[test]
1153+
fn the_reset_wait_selector_costs_what_it_always_did() {
1154+
let io = Io::default();
1155+
assert_eq!(io.wait_clocks(false, Some(0x0200)), (SMP_WAIT, SMP_WAIT));
1156+
assert_eq!(io.wait_clocks(false, None), (SMP_WAIT, SMP_WAIT));
1157+
assert_eq!(io.wait_clocks(true, Some(0x00F4)), (1, 1));
1158+
}
1159+
1160+
/// The whole point of `E3.09`: at selectors 2 and 3 the CPU pays 10/20 while the timers advance
1161+
/// 8/16. A core using one table for both is what this pins against.
1162+
#[test]
1163+
fn the_glitchy_wait_selectors_charge_the_cpu_more_than_the_timers() {
1164+
let mut io = Io::default();
1165+
for (selector, cpu, timer) in [(1u8, 4, 4), (2, 10, 8), (3, 20, 16)] {
1166+
io.external_wait = selector;
1167+
io.internal_wait = selector;
1168+
assert_eq!(
1169+
io.wait_clocks(false, Some(0x0200)),
1170+
(cpu, timer),
1171+
"selector {selector}"
1172+
);
1173+
}
1174+
}
1175+
1176+
/// The address classification, which decides *which* selector an access takes.
1177+
#[test]
1178+
fn the_register_block_and_a_mapped_ipl_take_the_internal_selector() {
1179+
let mut io = Io {
1180+
external_wait: 1, // 4 clocks
1181+
internal_wait: 3, // 20 / 16
1182+
..Io::default()
1183+
};
1184+
assert_eq!(io.wait_clocks(false, Some(0x0200)), (4, 4), "plain ARAM");
1185+
assert_eq!(io.wait_clocks(false, Some(0x00F3)), (20, 16), "$F3 is IO");
1186+
assert_eq!(io.wait_clocks(false, None), (20, 16), "an idle cycle");
1187+
assert_eq!(io.wait_clocks(false, Some(0xFFC0)), (20, 16), "IPL mapped");
1188+
io.iplrom_enable = false;
1189+
assert_eq!(
1190+
io.wait_clocks(false, Some(0xFFC0)),
1191+
(4, 4),
1192+
"unmapped, $FFC0 is ordinary ARAM and takes the external selector"
1193+
);
1194+
}
1195+
10821196
#[test]
10831197
fn power_on_state_matches_hardware() {
10841198
let cpu = Spc700::new();

0 commit comments

Comments
 (0)