@@ -46,14 +46,39 @@ use dsp::{ARAM_SIZE, Dsp};
4646use rustysnes_savestate:: { SaveReader , SaveStateError , SaveWriter } ;
4747use 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 .
5555const 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
260285impl 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
714769impl 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