@@ -171,6 +171,7 @@ pub fn spawn_player(
171171
172172 thread:: Builder :: new ( )
173173 . name ( "sid-player" . into ( ) )
174+ . stack_size ( 4 * 1024 * 1024 ) // 4MB — shadow CPU needs space for C64 memory
174175 . spawn ( move || {
175176 player_loop ( cmd_rx, status_tx, engine_name, u64_address, u64_password) ;
176177 } )
@@ -279,9 +280,7 @@ fn player_loop(
279280 }
280281 }
281282 PlayEngine :: Native { shadow } => {
282- // ── U64 native — real hardware plays the SID ─
283- // Run a shadow CPU locally for visualization only.
284- // Writes are NOT sent to the device.
283+ // Run shadow CPU for visualization only.
285284 match shadow {
286285 NativeShadow :: Psid {
287286 cpu,
@@ -310,15 +309,20 @@ fn player_loop(
310309 }
311310
312311 // ── Absolute-timeline frame pacing ───────────────────
313- // Advance deadline by exactly one frame period.
314- // This prevents inter-frame overhead from accumulating
315- // as drift (was ~8% slow with per-frame Instant::now).
316312 ctx. next_frame += frame_dur;
317313
318- // If we've fallen behind (e.g. after a pause or load),
319- // snap the deadline to now rather than fast-forwarding.
320314 let now = Instant :: now ( ) ;
321315 if ctx. next_frame < now {
316+ // Frame overrun — log it periodically
317+ let overrun = now - ctx. next_frame ;
318+ if ctx. frame_count % 250 == 0 {
319+ eprintln ! (
320+ "[phosphor] frame {} overrun by {}µs (sids={})" ,
321+ ctx. frame_count,
322+ overrun. as_micros( ) ,
323+ ctx. track_info. num_sids,
324+ ) ;
325+ }
322326 ctx. next_frame = now;
323327 }
324328
@@ -474,8 +478,6 @@ fn handle_cmd(
474478
475479 if native {
476480 // Build context with shadow CPU for visualization.
477- // The U64 handles actual SID output — the shadow CPU runs
478- // locally so we can show voice levels in the visualizer.
479481 let header = & sid_file. header ;
480482 let num_sids = 1
481483 + ( header. extra_sid_addrs [ 0 ] != 0 ) as usize
@@ -507,8 +509,7 @@ fn handle_cmd(
507509 md5,
508510 } ;
509511
510- // Build a shadow emulation engine (same as normal setup,
511- // but we never send its writes to the device).
512+ // Build shadow emulation for visualization
512513 let mut sid_bases: Vec < u16 > = vec ! [ 0xD400 ] ;
513514 if header. extra_sid_addrs [ 0 ] != 0 {
514515 sid_bases. push ( header. extra_sid_addrs [ 0 ] ) ;
@@ -558,8 +559,6 @@ fn handle_cmd(
558559 }
559560 } ;
560561
561- eprintln ! ( "[phosphor] Native + shadow CPU for visualization" ) ;
562-
563562 * play_ctx = Some ( PlayContext {
564563 engine : PlayEngine :: Native { shadow } ,
565564 trampoline,
@@ -615,8 +614,6 @@ fn handle_cmd(
615614 stop_playback ( play_ctx, bridge) ;
616615
617616 if was_native {
618- // For native playback, re-send the SID file with new song number
619- // and rebuild the shadow CPU for visualization.
620617 if let Ok ( data) = std:: fs:: read ( & path) {
621618 if let Some ( ref mut br) = bridge {
622619 match br. play_sid_native ( & data, song) {
@@ -811,15 +808,14 @@ enum PlayEngine {
811808 cpu : CPU < RsidBus , Nmos6502 > ,
812809 prev_nmi : bool ,
813810 } ,
814- /// U64 native playback — the real C64 handles SID output.
815- /// A shadow CPU runs locally (no output to device) for visualization.
811+ /// U64 native playback — real C64 handles SID output.
812+ /// Shadow CPU runs locally for visualization only .
816813 Native {
817814 shadow : NativeShadow ,
818815 } ,
819816}
820817
821818/// Shadow CPU for native playback visualization.
822- /// Runs the same emulation as PSID/RSID but never sends writes to hardware.
823819enum NativeShadow {
824820 Psid {
825821 cpu : CPU < C64Memory , Nmos6502 > ,
@@ -899,24 +895,21 @@ fn setup_playback(
899895
900896 let num_sids = sid_bases. len ( ) ;
901897 let is_multi = num_sids > 1 ;
902- // Always use stereo mode — mono tunes get mirrored to both channels
903- // so sound comes from both speakers.
904898 let use_stereo = true ;
905- // force_stereo: treat 2SID tunes as mono (mirror SID1 to both channels,
906- // ignoring the second SID). Only applies to exactly 2-SID tunes;
907- // 3SID/4SID tunes always use their native multi-SID layout.
899+ // force_stereo: treat 2SID tunes as mono (mirror SID1 to both channels).
900+ // Only applies to exactly 2-SID tunes.
908901 let force_mono_2sid = force_stereo && num_sids == 2 ;
909902 let mono_mode = !is_multi || force_mono_2sid;
910- let mirror_mono = mono_mode; // mirror single-SID tunes (and forced-stereo 2SID)
903+ let mirror_mono = mono_mode;
911904
912905 // When forcing stereo on a 2SID tune, use only the base SID for mapping
913906 let mapper = if force_mono_2sid {
914907 SidMapper :: new ( & sid_bases[ ..1 ] )
915908 } else {
916909 SidMapper :: new ( & sid_bases)
917910 } ;
918- let frame_us = header. frame_us ( ) ;
919- let cycles_per_frame = if header. is_pal {
911+ let mut frame_us = header. frame_us ( ) ;
912+ let mut cycles_per_frame = if header. is_pal {
920913 PAL_CYCLES_PER_FRAME
921914 } else {
922915 NTSC_CYCLES_PER_FRAME
@@ -955,15 +948,17 @@ fn setup_playback(
955948 br. reset ( ) ;
956949 thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
957950
958- // Tell the device how many SIDs are active:
959- // mono tunes in stereo mode → 2 (mirror to both speakers)
960- // multi-SID tunes → num_sids (2, 3, or 4)
961- let active_sids = if use_stereo && mono_mode { 2 } else { num_sids } ;
962- if active_sids > 1 {
951+ if use_stereo {
952+ // Tell the device how many SIDs are active:
953+ // mono tunes in stereo mode → set_stereo(1) for mirror
954+ // multi-SID tunes → set_stereo( num_sids - 1)
955+ let active_sids = if mono_mode { 2 } else { num_sids } ;
963956 br. set_stereo ( ( active_sids - 1 ) as i32 ) ;
964957 } else {
965958 br. set_stereo ( 0 ) ;
966959 }
960+
961+ let active_sids = if use_stereo && mono_mode { 2 } else { num_sids } ;
967962 for i in 0 ..active_sids {
968963 let vol_reg = ( i as u8 ) * SID_REG_SIZE + SID_VOL_REG ;
969964 br. write ( vol_reg, 0x0F ) ;
@@ -1017,8 +1012,42 @@ fn setup_playback(
10171012 }
10181013
10191014 // Clear writes and install play trampoline for PSID
1015+ //
1016+ // For PSID with speed bit set (CIA timing), read the CIA1 Timer A latch
1017+ // that the tune programmed during INIT. This tells us the intended
1018+ // play call rate. Many 2SID tunes use ~100Hz (2× frame rate).
10201019 let engine = match engine {
10211020 PlayEngine :: Psid ( mut cpu) => {
1021+ // Check if this song uses CIA timing
1022+ let song_idx = song. saturating_sub ( 1 ) as u32 ;
1023+ let speed_bit = if song_idx < 32 {
1024+ ( header. speed >> song_idx) & 1
1025+ } else {
1026+ ( header. speed >> 31 ) & 1
1027+ } ;
1028+
1029+ if speed_bit == 1 && !header. is_rsid {
1030+ let cia_latch = cpu. memory . cia1 . timer_a . latch as u64 ;
1031+ if cia_latch > 0 && cia_latch < 0xFFFF {
1032+ let clock = if header. is_pal { 985248u64 } else { 1022727u64 } ;
1033+ let cia_us = ( cia_latch * 1_000_000 ) / clock;
1034+ let cia_hz = clock as f64 / cia_latch as f64 ;
1035+ eprintln ! (
1036+ "[phosphor] CIA timing: latch={} cycles = {}µs ({:.1}Hz)" ,
1037+ cia_latch, cia_us, cia_hz,
1038+ ) ;
1039+ // Override frame timing to match CIA rate
1040+ if cia_us > 0 && cia_us < frame_us {
1041+ frame_us = cia_us;
1042+ cycles_per_frame = cia_latch as u32 ;
1043+ eprintln ! (
1044+ "[phosphor] Frame rate adjusted to {}µs ({:.1}Hz) from CIA timer" ,
1045+ frame_us, cia_hz,
1046+ ) ;
1047+ }
1048+ }
1049+ }
1050+
10221051 cpu. memory . clear_writes ( ) ;
10231052 if header. play_address != 0 {
10241053 cpu. memory
@@ -1033,6 +1062,12 @@ fn setup_playback(
10331062 n @ PlayEngine :: Native { .. } => n,
10341063 } ;
10351064
1065+ // If CIA timing adjusted cycles_per_frame, tell the device
1066+ // so flush() generates the correct amount of audio.
1067+ if let Some ( ref mut br) = bridge {
1068+ br. set_cycles_per_frame ( cycles_per_frame) ;
1069+ }
1070+
10361071 PlayContext {
10371072 engine,
10381073 trampoline,
0 commit comments