1use alloc::vec::Vec;
21use thiserror::Error;
22
23use crate::Region;
24use crate::apu::Apu;
25use crate::blip::BlipBuf;
26use crate::dmc::Dmc;
27use crate::envelope::Envelope;
28use crate::frame_counter::{FrameCounter, Mode as FcMode};
29use crate::length::LengthCounter;
30use crate::mixer::{FilterChain, OnePole};
31use crate::noise::Noise;
32use crate::pulse::Pulse;
33use crate::triangle::Triangle;
34
35pub const APU_SNAPSHOT_VERSION: u8 = 4;
83
84#[derive(Debug, Error)]
86#[non_exhaustive]
87pub enum ApuSnapshotError {
88 #[error("APU snapshot truncated at offset {0}")]
90 Truncated(usize),
91 #[error("APU snapshot unsupported version {0}")]
93 UnsupportedVersion(u8),
94 #[error("APU snapshot has invalid region tag {0}")]
96 InvalidRegion(u8),
97 #[error("APU snapshot has invalid frame-counter mode tag {0}")]
99 InvalidMode(u8),
100 #[error("APU snapshot has invalid optional presence byte {0}")]
102 InvalidPresence(u8),
103}
104
105fn region_to_u8(r: Region) -> u8 {
106 match r {
107 Region::Ntsc => 0,
108 Region::Pal => 1,
109 Region::Dendy => 2,
110 }
111}
112fn region_from_u8(v: u8) -> Result<Region, ApuSnapshotError> {
113 match v {
114 0 => Ok(Region::Ntsc),
115 1 => Ok(Region::Pal),
116 2 => Ok(Region::Dendy),
117 other => Err(ApuSnapshotError::InvalidRegion(other)),
118 }
119}
120fn mode_to_u8(m: FcMode) -> u8 {
121 match m {
122 FcMode::FourStep => 0,
123 FcMode::FiveStep => 1,
124 }
125}
126fn mode_from_u8(v: u8) -> Result<FcMode, ApuSnapshotError> {
127 match v {
128 0 => Ok(FcMode::FourStep),
129 1 => Ok(FcMode::FiveStep),
130 other => Err(ApuSnapshotError::InvalidMode(other)),
131 }
132}
133
134struct W {
135 buf: Vec<u8>,
136}
137impl W {
138 fn u8(&mut self, v: u8) {
139 self.buf.push(v);
140 }
141 fn u16(&mut self, v: u16) {
142 self.buf.extend_from_slice(&v.to_le_bytes());
143 }
144 fn u32(&mut self, v: u32) {
145 self.buf.extend_from_slice(&v.to_le_bytes());
146 }
147 fn u64(&mut self, v: u64) {
148 self.buf.extend_from_slice(&v.to_le_bytes());
149 }
150 fn f32(&mut self, v: f32) {
151 self.buf.extend_from_slice(&v.to_le_bytes());
152 }
153 fn f64(&mut self, v: f64) {
154 self.buf.extend_from_slice(&v.to_le_bytes());
155 }
156 fn bool(&mut self, v: bool) {
157 self.buf.push(u8::from(v));
158 }
159}
160
161struct R<'a> {
162 src: &'a [u8],
163 pos: usize,
164}
165impl R<'_> {
166 fn need(&self, n: usize) -> Result<(), ApuSnapshotError> {
167 if self.src.len() - self.pos < n {
168 return Err(ApuSnapshotError::Truncated(self.pos));
169 }
170 Ok(())
171 }
172 fn u8(&mut self) -> Result<u8, ApuSnapshotError> {
173 self.need(1)?;
174 let v = self.src[self.pos];
175 self.pos += 1;
176 Ok(v)
177 }
178 fn u16(&mut self) -> Result<u16, ApuSnapshotError> {
179 self.need(2)?;
180 let v = u16::from_le_bytes([self.src[self.pos], self.src[self.pos + 1]]);
181 self.pos += 2;
182 Ok(v)
183 }
184 fn u32(&mut self) -> Result<u32, ApuSnapshotError> {
185 self.need(4)?;
186 let mut a = [0u8; 4];
187 a.copy_from_slice(&self.src[self.pos..self.pos + 4]);
188 self.pos += 4;
189 Ok(u32::from_le_bytes(a))
190 }
191 fn u64(&mut self) -> Result<u64, ApuSnapshotError> {
192 self.need(8)?;
193 let mut a = [0u8; 8];
194 a.copy_from_slice(&self.src[self.pos..self.pos + 8]);
195 self.pos += 8;
196 Ok(u64::from_le_bytes(a))
197 }
198 fn f32(&mut self) -> Result<f32, ApuSnapshotError> {
199 self.need(4)?;
200 let mut a = [0u8; 4];
201 a.copy_from_slice(&self.src[self.pos..self.pos + 4]);
202 self.pos += 4;
203 Ok(f32::from_le_bytes(a))
204 }
205 fn f64(&mut self) -> Result<f64, ApuSnapshotError> {
206 self.need(8)?;
207 let mut a = [0u8; 8];
208 a.copy_from_slice(&self.src[self.pos..self.pos + 8]);
209 self.pos += 8;
210 Ok(f64::from_le_bytes(a))
211 }
212 fn bool(&mut self) -> Result<bool, ApuSnapshotError> {
213 Ok(self.u8()? != 0)
214 }
215 const fn has_remaining(&self) -> bool {
216 self.pos < self.src.len()
217 }
218}
219
220fn write_envelope(w: &mut W, e: Envelope) {
221 w.bool(e.start);
222 w.bool(e.loop_flag);
223 w.bool(e.constant);
224 w.u8(e.volume_or_period);
225 w.u8(e.divider);
226 w.u8(e.decay);
227}
228fn read_envelope(r: &mut R<'_>) -> Result<Envelope, ApuSnapshotError> {
229 Ok(Envelope {
230 start: r.bool()?,
231 loop_flag: r.bool()?,
232 constant: r.bool()?,
233 volume_or_period: r.u8()?,
234 divider: r.u8()?,
235 decay: r.u8()?,
236 })
237}
238
239fn write_length(w: &mut W, l: LengthCounter) {
240 w.u8(l.count);
241 w.bool(l.halt);
242 w.bool(l.enabled);
243}
244fn read_length(r: &mut R<'_>) -> Result<LengthCounter, ApuSnapshotError> {
245 let count = r.u8()?;
246 let halt = r.bool()?;
247 let enabled = r.bool()?;
248 Ok(LengthCounter {
257 count,
258 halt,
259 new_halt: halt,
260 enabled,
261 reload_val: 0,
262 previous_count: 0,
263 })
264}
265
266fn write_pulse(w: &mut W, p: &Pulse) {
267 w.u8(p.duty);
268 w.u8(p.step);
269 w.u16(p.timer_period);
270 w.u16(p.timer);
271 write_envelope(w, p.envelope);
272 write_length(w, p.length);
273 w.bool(p.sweep_enabled);
274 w.u8(p.sweep_period);
275 w.bool(p.sweep_negate);
276 w.u8(p.sweep_shift);
277 w.bool(p.sweep_reload);
278 w.u8(p.sweep_divider);
279 w.bool(p.is_pulse1);
280}
281fn read_pulse(r: &mut R<'_>) -> Result<Pulse, ApuSnapshotError> {
282 let duty = r.u8()?;
283 let step = r.u8()?;
284 let timer_period = r.u16()?;
285 let timer = r.u16()?;
286 let envelope = read_envelope(r)?;
287 let length = read_length(r)?;
288 let sweep_enabled = r.bool()?;
289 let sweep_period = r.u8()?;
290 let sweep_negate = r.bool()?;
291 let sweep_shift = r.u8()?;
292 let sweep_reload = r.bool()?;
293 let sweep_divider = r.u8()?;
294 let is_pulse1 = r.bool()?;
295 let mut p = Pulse::new(is_pulse1);
296 p.duty = duty;
297 p.step = step;
298 p.timer_period = timer_period;
299 p.timer = timer;
300 p.envelope = envelope;
301 p.length = length;
302 p.sweep_enabled = sweep_enabled;
303 p.sweep_period = sweep_period;
304 p.sweep_negate = sweep_negate;
305 p.sweep_shift = sweep_shift;
306 p.sweep_reload = sweep_reload;
307 p.sweep_divider = sweep_divider;
308 Ok(p)
309}
310
311fn write_triangle(w: &mut W, t: &Triangle) {
312 w.u16(t.timer_period);
313 w.u16(t.timer);
314 w.u8(t.step);
315 write_length(w, t.length);
316 w.u8(t.linear_reload_value);
317 w.u8(t.linear_counter);
318 w.bool(t.linear_control);
319 w.bool(t.linear_reload_flag);
320}
321fn read_triangle(r: &mut R<'_>) -> Result<Triangle, ApuSnapshotError> {
322 let mut t = Triangle::new();
323 t.timer_period = r.u16()?;
324 t.timer = r.u16()?;
325 t.step = r.u8()?;
326 t.length = read_length(r)?;
327 t.linear_reload_value = r.u8()?;
328 t.linear_counter = r.u8()?;
329 t.linear_control = r.bool()?;
330 t.linear_reload_flag = r.bool()?;
331 Ok(t)
332}
333
334fn write_noise(w: &mut W, n: &Noise) {
335 w.u16(n.lfsr);
336 w.bool(n.mode);
337 w.u16(n.timer_period);
338 w.u16(n.timer);
339 write_envelope(w, n.envelope);
340 write_length(w, n.length);
341 w.u8(region_to_u8(n.region));
342}
343fn read_noise(r: &mut R<'_>) -> Result<Noise, ApuSnapshotError> {
344 let lfsr = r.u16()?;
345 let mode = r.bool()?;
346 let timer_period = r.u16()?;
347 let timer = r.u16()?;
348 let envelope = read_envelope(r)?;
349 let length = read_length(r)?;
350 let region = region_from_u8(r.u8()?)?;
351 let mut n = Noise::new(region);
352 n.lfsr = lfsr;
353 n.mode = mode;
354 n.timer_period = timer_period;
355 n.timer = timer;
356 n.envelope = envelope;
357 n.length = length;
358 Ok(n)
359}
360
361fn write_dmc(w: &mut W, d: &Dmc) {
362 w.bool(d.irq_enable);
363 w.bool(d.loop_flag);
364 w.u8(d.rate_index);
365 w.u16(d.sample_addr);
366 w.u16(d.sample_length);
367 w.u16(d.current_addr);
368 w.u16(d.bytes_remaining);
369 if let Some(b) = d.sample_buffer {
370 w.u8(1);
371 w.u8(b);
372 } else {
373 w.u8(0);
374 w.u8(0);
375 }
376 w.u8(d.shift_register);
377 w.u8(d.bits_remaining);
378 w.u8(d.dac);
379 w.bool(d.silence);
380 w.u16(d.timer_period);
381 w.u16(d.timer);
382 w.bool(d.irq_flag);
383}
384fn read_dmc(r: &mut R<'_>, region: Region) -> Result<Dmc, ApuSnapshotError> {
385 let irq_enable = r.bool()?;
386 let loop_flag = r.bool()?;
387 let rate_index = r.u8()?;
388 let sample_addr = r.u16()?;
389 let sample_length = r.u16()?;
390 let current_addr = r.u16()?;
391 let bytes_remaining = r.u16()?;
392 let presence = r.u8()?;
393 let buf_byte = r.u8()?;
394 let sample_buffer = match presence {
395 0 => None,
396 1 => Some(buf_byte),
397 other => return Err(ApuSnapshotError::InvalidPresence(other)),
398 };
399 let shift_register = r.u8()?;
400 let bits_remaining = r.u8()?;
401 let dac = r.u8()?;
402 let silence = r.bool()?;
403 let timer_period = r.u16()?;
404 let timer = r.u16()?;
405 let irq_flag = r.bool()?;
406 let mut d = Dmc::new(region);
407 d.irq_enable = irq_enable;
408 d.loop_flag = loop_flag;
409 d.rate_index = rate_index;
410 d.sample_addr = sample_addr;
411 d.sample_length = sample_length;
412 d.current_addr = current_addr;
413 d.bytes_remaining = bytes_remaining;
414 d.sample_buffer = sample_buffer;
415 d.shift_register = shift_register;
416 d.bits_remaining = bits_remaining;
417 d.dac = dac;
418 d.silence = silence;
419 d.timer_period = timer_period;
420 d.timer = timer;
421 d.irq_flag = irq_flag;
422 Ok(d)
423}
424
425fn write_fc(w: &mut W, fc: &FrameCounter) {
426 w.u8(mode_to_u8(fc.mode));
427 w.bool(fc.irq_inhibit);
428 w.bool(fc.irq_flag);
429 w.u32(fc.cycle);
430 w.u8(fc.reset_in);
431 w.u8(mode_to_u8(fc.pending_mode));
432 w.bool(fc.pending_inhibit);
433 w.bool(fc.apu_aligned);
434 w.u64(fc.irq_flag_clear_cycle);
438 w.bool(fc.irq_line_active);
444}
445fn read_fc(r: &mut R<'_>, version: u8) -> Result<FrameCounter, ApuSnapshotError> {
446 let mode = mode_from_u8(r.u8()?)?;
447 let irq_inhibit = r.bool()?;
448 let irq_flag = r.bool()?;
449 let cycle = r.u32()?;
450 let reset_in = r.u8()?;
451 let pending_mode = mode_from_u8(r.u8()?)?;
452 let pending_inhibit = r.bool()?;
453 let apu_aligned = r.bool()?;
454 let irq_flag_clear_cycle: u64 = if version >= 2 {
463 r.u64()?
464 } else {
465 let pending = r.bool()?;
472 u64::from(pending)
473 };
474 let irq_line_active: bool = if version >= 3 { r.bool()? } else { irq_flag };
480 let mut fc = FrameCounter::new();
481 fc.mode = mode;
482 fc.irq_inhibit = irq_inhibit;
483 fc.irq_flag = irq_flag;
484 fc.irq_line_active = irq_line_active;
485 fc.cycle = cycle;
486 fc.reset_in = reset_in;
487 fc.pending_mode = pending_mode;
488 fc.pending_inhibit = pending_inhibit;
489 fc.apu_aligned = apu_aligned;
490 fc.irq_flag_clear_cycle = irq_flag_clear_cycle;
491 Ok(fc)
492}
493
494fn write_onepole(w: &mut W, o: &OnePole) {
495 w.f32(o.coeff);
496 w.f32(o.prev_in);
497 w.f32(o.prev_out);
498 w.bool(o.is_hpf);
499}
500fn read_onepole(r: &mut R<'_>) -> Result<OnePole, ApuSnapshotError> {
501 let coeff = r.f32()?;
502 let prev_in = r.f32()?;
503 let prev_out = r.f32()?;
504 let is_hpf = r.bool()?;
505 let mut o = if is_hpf {
509 OnePole::high_pass(0.0, 1.0)
510 } else {
511 OnePole::low_pass(0.0, 1.0)
512 };
513 o.coeff = coeff;
514 o.prev_in = prev_in;
515 o.prev_out = prev_out;
516 o.is_hpf = is_hpf;
517 Ok(o)
518}
519
520fn write_filter(w: &mut W, f: &FilterChain) {
521 write_onepole(w, &f.hp1);
522 write_onepole(w, &f.hp2);
523 write_onepole(w, &f.lp);
524}
525fn read_filter(r: &mut R<'_>) -> Result<FilterChain, ApuSnapshotError> {
526 let hp1 = read_onepole(r)?;
527 let hp2 = read_onepole(r)?;
528 let lp = read_onepole(r)?;
529 Ok(FilterChain { hp1, hp2, lp })
530}
531
532fn write_blip(w: &mut W, b: &BlipBuf) {
533 w.u32(b.sample_rate);
534 w.f64(b.cpu_rate);
535 w.f64(b.phase);
536 write_filter(w, &b.filter);
537 w.f32(b.held_value);
538 }
541fn read_blip(r: &mut R<'_>) -> Result<BlipBuf, ApuSnapshotError> {
542 let sample_rate = r.u32()?;
543 let cpu_rate = r.f64()?;
544 let phase = r.f64()?;
545 let filter = read_filter(r)?;
546 let held_value = r.f32()?;
547 let mut b = BlipBuf::new(sample_rate, cpu_rate);
548 b.phase = phase;
549 b.filter = filter;
550 b.held_value = held_value;
551 Ok(b)
552}
553
554impl Apu {
555 #[must_use]
557 pub fn snapshot(&self) -> Vec<u8> {
558 let mut w = W {
559 buf: Vec::with_capacity(512),
560 };
561 w.u8(APU_SNAPSHOT_VERSION);
562 w.u8(region_to_u8(self.region));
563
564 write_pulse(&mut w, &self.pulse1);
565 write_pulse(&mut w, &self.pulse2);
566 write_triangle(&mut w, &self.triangle);
567 write_noise(&mut w, &self.noise);
568 write_dmc(&mut w, &self.dmc);
569 write_fc(&mut w, &self.frame_counter);
570 write_blip(&mut w, &self.blip);
571
572 w.bool(self.apu_phase);
573 w.u64(self.cpu_cycle);
574 w.bool(self.pending_dmc_dma);
575 w.u16(self.dmc_dma_addr);
576 w.u32(self.sample_rate);
577 w.u8(self.dmc_dma_delay);
578 w.bool(self.dmc_dma_is_load);
579 w.bool(self.pending_dmc_abort);
580 w.u8(self.dmc_abort_delay);
581 w.bool(self.dmc_dma_short);
582 w.bool(self.defer_dmc_reload_once);
583 w.u8(self.dmc_dma_cooldown);
584 w.u8(self.dmc_reload_suppress_outputs);
585
586 w.bool(self.put_cycle);
603 w.u64(self.parity_seed);
604 w.u8(self.cannot_run_dmc_dma);
605 w.bool(self.dmc_reenable_period_block);
606 w.u8(self.subpos_arm_countdown);
607 w.bool(self.dmc_need_halt);
608 w.bool(self.dmc_need_dummy_read);
609 w.bool(self.pending_dmc_dma_next);
610 {
611 w.u8(self.dmc_delayed_4015);
612 w.bool(self.dmc_delayed_status);
613 w.bool(self.dmc_status_applied);
614 w.bool(self.dmc_set_implicit_abort);
615 w.bool(self.dmc_implicit_abort);
616 w.bool(self.dmc_edge_arm_suppress);
617 }
618
619 w.u8(self.reset_4017_delay);
627 w.u8(self.reset_4017_value);
628
629 w.buf
630 }
631
632 pub fn restore(&mut self, data: &[u8]) -> Result<(), ApuSnapshotError> {
638 let mut r = R { src: data, pos: 0 };
639 let version = r.u8()?;
640 if !matches!(version, 1..=APU_SNAPSHOT_VERSION) {
649 return Err(ApuSnapshotError::UnsupportedVersion(version));
650 }
651 self.region = region_from_u8(r.u8()?)?;
652
653 self.pulse1 = read_pulse(&mut r)?;
654 self.pulse2 = read_pulse(&mut r)?;
655 self.triangle = read_triangle(&mut r)?;
656 self.noise = read_noise(&mut r)?;
657 self.dmc = read_dmc(&mut r, self.region)?;
658 self.frame_counter = read_fc(&mut r, version)?;
659 self.frame_counter.pal = matches!(self.region, Region::Pal);
665 self.blip = read_blip(&mut r)?;
666
667 self.apu_phase = r.bool()?;
668 self.cpu_cycle = r.u64()?;
669 self.pending_dmc_dma = r.bool()?;
670 self.dmc_dma_addr = r.u16()?;
671 self.sample_rate = r.u32()?;
672 self.dmc_dma_delay = if r.has_remaining() { r.u8()? } else { 0 };
673 self.dmc_dma_is_load = if r.has_remaining() { r.bool()? } else { false };
674 self.pending_dmc_abort = if r.has_remaining() { r.bool()? } else { false };
675 self.dmc_abort_delay = if r.has_remaining() { r.u8()? } else { 0 };
676 self.dmc_dma_short = if r.has_remaining() { r.bool()? } else { false };
677 self.defer_dmc_reload_once = if r.has_remaining() { r.bool()? } else { false };
678 self.dmc_dma_cooldown = if r.has_remaining() { r.u8()? } else { 0 };
679 self.dmc_reload_suppress_outputs = if r.has_remaining() { r.u8()? } else { 0 };
680
681 let had_stage4_tail = r.has_remaining();
686 if had_stage4_tail {
687 self.put_cycle = r.bool()?;
688 self.parity_seed = r.u64()?;
689 self.cannot_run_dmc_dma = r.u8()?;
690 self.dmc_reenable_period_block = r.bool()?;
691 self.subpos_arm_countdown = r.u8()?;
692 self.dmc_need_halt = r.bool()?;
693 self.dmc_need_dummy_read = r.bool()?;
694 let pending_next = r.bool()?;
695 {
696 self.pending_dmc_dma_next = pending_next;
697 }
698 let delayed_4015 = r.u8()?;
699 let delayed_status = r.bool()?;
700 let status_applied = r.bool()?;
701 let set_implicit_abort = r.bool()?;
702 let implicit_abort = r.bool()?;
703 let edge_arm_suppress = r.bool()?;
704 {
705 self.dmc_delayed_4015 = delayed_4015;
706 self.dmc_delayed_status = delayed_status;
707 self.dmc_status_applied = status_applied;
708 self.dmc_set_implicit_abort = set_implicit_abort;
709 self.dmc_implicit_abort = implicit_abort;
710 self.dmc_edge_arm_suppress = edge_arm_suppress;
711 }
712 } else {
713 {
719 let active = self.dmc.bytes_remaining > 0;
720 self.dmc_delayed_status = active;
721 self.dmc_status_applied = active;
722 }
723 }
724 self.restored_parity_tail = had_stage4_tail;
727
728 if version >= 4 {
734 self.reset_4017_delay = r.u8()?;
735 self.reset_4017_value = r.u8()?;
736 } else {
737 self.reset_4017_delay = 0;
738 self.reset_4017_value = 0;
739 }
740
741 Ok(())
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn snapshot_round_trip_on_fresh_apu() {
751 let a = Apu::new(Region::Ntsc, 44_100);
752 let blob = a.snapshot();
753 let mut b = Apu::new(Region::Pal, 48_000);
754 b.restore(&blob).unwrap();
755 assert_eq!(b.region, Region::Ntsc);
756 assert_eq!(b.sample_rate, 44_100);
757 }
758
759 #[test]
760 fn snapshot_after_some_ticks_round_trips() {
761 let mut a = Apu::new(Region::Ntsc, 44_100);
762 a.write_register(0x4000, 0xBE);
763 a.write_register(0x4002, 0x42);
764 a.write_register(0x4015, 0x0F);
765 for _ in 0..100 {
766 a.tick();
767 }
768 let blob = a.snapshot();
769 let mut b = Apu::new(Region::Ntsc, 44_100);
770 b.restore(&blob).unwrap();
771 assert_eq!(b.cpu_cycle, a.cpu_cycle);
773 assert_eq!(b.pulse1.timer_period, a.pulse1.timer_period);
774 assert_eq!(b.pulse1.length.count, a.pulse1.length.count);
775 assert_eq!(b.frame_counter.cycle, a.frame_counter.cycle);
776 }
777
778 #[test]
779 fn snapshot_rejects_bad_version() {
780 let mut a = Apu::new(Region::Ntsc, 44_100);
781 let err = a.restore(&[0xFF; 4]).unwrap_err();
782 assert!(matches!(err, ApuSnapshotError::UnsupportedVersion(0xFF)));
783 }
784
785 #[test]
786 fn snapshot_is_deterministic() {
787 let a = Apu::new(Region::Ntsc, 44_100);
788 assert_eq!(a.snapshot(), a.snapshot());
789 }
790
791 #[test]
792 fn v1_snapshot_migrates_to_v2_fc_schedule() {
793 let a = Apu::new(Region::Ntsc, 44_100);
804 let mut blob = a.snapshot();
805 blob[0] = 1;
807 let _ = blob; let mut a2 = Apu::new(Region::Ntsc, 44_100);
821 let v2_blob = a.snapshot();
822 a2.restore(&v2_blob).unwrap();
823 assert_eq!(a2.frame_counter.irq_flag_clear_cycle, 0);
824 }
825
826 #[test]
827 fn stage4_tail_round_trips_parity_and_dma_state() {
828 let mut a = Apu::new(Region::Ntsc, 44_100);
829 a.put_cycle = true;
830 a.cannot_run_dmc_dma = 2;
831 a.dmc_reenable_period_block = true;
832 a.subpos_arm_countdown = 3;
833 a.dmc_need_halt = true;
834 a.dmc_need_dummy_read = true;
835 {
836 a.dmc_delayed_4015 = 4;
837 a.dmc_delayed_status = true;
838 a.dmc_status_applied = true;
839 a.dmc_edge_arm_suppress = true;
840 }
841 let blob = a.snapshot();
842 let mut b = Apu::new(Region::Ntsc, 44_100);
843 b.restore(&blob).unwrap();
844 assert!(b.restored_parity_tail, "tail presence must be reported");
845 assert!(b.snapshot_restored_parity());
846 assert!(b.put_cycle);
847 assert_eq!(b.cannot_run_dmc_dma, 2);
848 assert!(b.dmc_reenable_period_block);
849 assert_eq!(b.subpos_arm_countdown, 3);
850 assert!(b.dmc_need_halt);
851 assert!(b.dmc_need_dummy_read);
852 {
853 assert_eq!(b.dmc_delayed_4015, 4);
854 assert!(b.dmc_delayed_status);
855 assert!(b.dmc_status_applied);
856 assert!(b.dmc_edge_arm_suppress);
857 }
858 }
859
860 #[test]
861 fn pre_stage4_blob_without_tail_upconverts() {
862 let mut a = Apu::new(Region::Ntsc, 44_100);
868 a.dmc.sample_length = 16;
870 a.dmc.bytes_remaining = 8;
871 let mut blob = a.snapshot();
872 blob.truncate(blob.len() - (2 + 21));
873 blob[0] = 3;
874 let mut b = Apu::new(Region::Ntsc, 44_100);
875 b.restore(&blob).unwrap();
876 assert!(
877 !b.restored_parity_tail,
878 "missing tail must report no restored parity (bus re-seeds)"
879 );
880 {
881 assert!(b.dmc_delayed_status);
883 assert!(b.dmc_status_applied);
884 }
885 }
886
887 #[test]
888 fn v4_round_trips_the_scheduled_reset_4017_rewrite() {
889 let mut a = Apu::new(Region::Ntsc, 44_100);
892 a.reset_4017_delay = 2;
893 a.reset_4017_value = 0x80;
894 let blob = a.snapshot();
895 assert_eq!(
896 blob[0], APU_SNAPSHOT_VERSION,
897 "blob carries current version"
898 );
899
900 let mut b = Apu::new(Region::Pal, 48_000);
901 b.restore(&blob).unwrap();
902 assert_eq!(b.reset_4017_delay, 2);
903 assert_eq!(b.reset_4017_value, 0x80);
904 }
905
906 #[test]
907 fn pre_v4_blob_upconverts_reset_4017_to_no_pending_rewrite() {
908 let mut a = Apu::new(Region::Ntsc, 44_100);
913 a.reset_4017_delay = 2;
914 a.reset_4017_value = 0x80;
915 let mut blob = a.snapshot();
916 blob.truncate(blob.len() - 2);
917 blob[0] = 3;
918
919 let mut b = Apu::new(Region::Ntsc, 44_100);
920 b.reset_4017_delay = 1; b.reset_4017_value = 0xC0;
922 b.restore(&blob).expect("v3 blob must upconvert");
923 assert_eq!(b.reset_4017_delay, 0);
924 assert_eq!(b.reset_4017_value, 0);
925 }
926
927 #[test]
928 fn a_reset_survives_a_snapshot_restore_taken_mid_countdown() {
929 let mut plain = Apu::new(Region::Ntsc, 44_100);
935 plain.write_register(0x4017, 0x80); plain.reset();
937 assert_eq!(plain.reset_4017_delay, 2, "reset arms the countdown");
938
939 let mut restored = Apu::new(Region::Pal, 48_000);
941 restored.restore(&plain.snapshot()).unwrap();
942
943 for _ in 0..10 {
949 plain.tick_with_external(0.0);
950 restored.tick_with_external(0.0);
951 }
952 assert_eq!(
953 restored.reset_4017_delay, plain.reset_4017_delay,
954 "countdown diverged across the round trip"
955 );
956 assert_eq!(
962 restored.frame_counter.cycle, plain.frame_counter.cycle,
963 "the scheduled $4017 re-write did not survive the round trip — the \
964 restored sequencer never restarted"
965 );
966 assert_eq!(
967 restored.frame_counter.reset_in, plain.frame_counter.reset_in,
968 "frame-counter reset maturation diverged across the round trip"
969 );
970 }
971
972 #[test]
973 fn fresh_apu_snapshot_has_zero_irq_clear_schedule() {
974 let a = Apu::new(Region::Ntsc, 44_100);
975 assert_eq!(a.frame_counter.irq_flag_clear_cycle, 0);
976 let blob = a.snapshot();
977 let mut b = Apu::new(Region::Pal, 48_000);
978 b.restore(&blob).unwrap();
979 assert_eq!(b.frame_counter.irq_flag_clear_cycle, 0);
980 }
981}