1use alloc::vec::Vec;
28use thiserror::Error;
29
30use crate::Region;
31use crate::apu::Apu;
32use crate::blip::BlipBuf;
33use crate::dmc::Dmc;
34use crate::envelope::Envelope;
35use crate::frame_counter::{FrameCounter, Mode as FcMode};
36use crate::length::LengthCounter;
37use crate::mixer::{FilterChain, OnePole};
38use crate::noise::Noise;
39use crate::pulse::Pulse;
40use crate::triangle::Triangle;
41
42pub const APU_SNAPSHOT_VERSION: u8 = 5;
87
88#[derive(Debug, Error)]
90#[non_exhaustive]
91pub enum ApuSnapshotError {
92 #[error("APU snapshot truncated at offset {0}")]
94 Truncated(usize),
95 #[error("APU snapshot has {0} trailing byte(s) after its last field")]
98 TrailingBytes(usize),
99 #[error("APU snapshot unsupported version {0}")]
101 UnsupportedVersion(u8),
102 #[error("APU snapshot has invalid region tag {0}")]
104 InvalidRegion(u8),
105 #[error("APU snapshot has invalid frame-counter mode tag {0}")]
107 InvalidMode(u8),
108 #[error("APU snapshot has invalid optional presence byte {0}")]
110 InvalidPresence(u8),
111 #[error("APU snapshot field `{field}` is {value}, above its maximum {max}")]
123 FieldOutOfRange {
124 field: &'static str,
126 value: u8,
128 max: u8,
130 },
131 #[error("APU snapshot resampler field `{0}` is out of range or not finite")]
139 InvalidResampler(&'static str),
140}
141
142fn bounded(r: &mut R<'_>, field: &'static str, max: u8) -> Result<u8, ApuSnapshotError> {
144 let value = r.u8()?;
145 if value > max {
146 return Err(ApuSnapshotError::FieldOutOfRange { field, value, max });
147 }
148 Ok(value)
149}
150
151fn finite_f32(v: f32, field: &'static str) -> Result<f32, ApuSnapshotError> {
155 if v.is_finite() {
156 Ok(v)
157 } else {
158 Err(ApuSnapshotError::InvalidResampler(field))
159 }
160}
161
162const RESAMPLER_HELD_MAX: f32 = 4.0;
207const RESAMPLER_INTEGRATOR_MAX: f32 = 16.0;
208const RESAMPLER_DRIFT_MAX: f32 = 1.0;
209const FILTER_SLACK: f32 = 1.0;
210const FILTER_INPUT_MAX: [f32; 3] = [
212 RESAMPLER_INTEGRATOR_MAX,
213 2.0 * RESAMPLER_INTEGRATOR_MAX + FILTER_SLACK,
214 2.0 * (2.0 * RESAMPLER_INTEGRATOR_MAX + FILTER_SLACK) + FILTER_SLACK,
215];
216
217fn bounded_f32(v: f32, max: f32, field: &'static str) -> Result<f32, ApuSnapshotError> {
218 let v = finite_f32(v, field)?;
219 if v.abs() <= max {
220 Ok(v)
221 } else {
222 Err(ApuSnapshotError::InvalidResampler(field))
223 }
224}
225
226fn region_to_u8(r: Region) -> u8 {
227 match r {
228 Region::Ntsc => 0,
229 Region::Pal => 1,
230 Region::Dendy => 2,
231 }
232}
233fn region_from_u8(v: u8) -> Result<Region, ApuSnapshotError> {
234 match v {
235 0 => Ok(Region::Ntsc),
236 1 => Ok(Region::Pal),
237 2 => Ok(Region::Dendy),
238 other => Err(ApuSnapshotError::InvalidRegion(other)),
239 }
240}
241fn mode_to_u8(m: FcMode) -> u8 {
242 match m {
243 FcMode::FourStep => 0,
244 FcMode::FiveStep => 1,
245 }
246}
247fn mode_from_u8(v: u8) -> Result<FcMode, ApuSnapshotError> {
248 match v {
249 0 => Ok(FcMode::FourStep),
250 1 => Ok(FcMode::FiveStep),
251 other => Err(ApuSnapshotError::InvalidMode(other)),
252 }
253}
254
255struct W {
256 buf: Vec<u8>,
257}
258impl W {
259 fn u8(&mut self, v: u8) {
260 self.buf.push(v);
261 }
262 fn u16(&mut self, v: u16) {
263 self.buf.extend_from_slice(&v.to_le_bytes());
264 }
265 fn u32(&mut self, v: u32) {
266 self.buf.extend_from_slice(&v.to_le_bytes());
267 }
268 fn u64(&mut self, v: u64) {
269 self.buf.extend_from_slice(&v.to_le_bytes());
270 }
271 fn f32(&mut self, v: f32) {
272 self.buf.extend_from_slice(&v.to_le_bytes());
273 }
274 fn f64(&mut self, v: f64) {
275 self.buf.extend_from_slice(&v.to_le_bytes());
276 }
277 fn bool(&mut self, v: bool) {
278 self.buf.push(u8::from(v));
279 }
280}
281
282struct R<'a> {
283 src: &'a [u8],
284 pos: usize,
285}
286impl R<'_> {
287 fn need(&self, n: usize) -> Result<(), ApuSnapshotError> {
288 if self.src.len() - self.pos < n {
289 return Err(ApuSnapshotError::Truncated(self.pos));
290 }
291 Ok(())
292 }
293 fn u8(&mut self) -> Result<u8, ApuSnapshotError> {
294 self.need(1)?;
295 let v = self.src[self.pos];
296 self.pos += 1;
297 Ok(v)
298 }
299 fn u16(&mut self) -> Result<u16, ApuSnapshotError> {
300 self.need(2)?;
301 let v = u16::from_le_bytes([self.src[self.pos], self.src[self.pos + 1]]);
302 self.pos += 2;
303 Ok(v)
304 }
305 fn u32(&mut self) -> Result<u32, ApuSnapshotError> {
306 self.need(4)?;
307 let mut a = [0u8; 4];
308 a.copy_from_slice(&self.src[self.pos..self.pos + 4]);
309 self.pos += 4;
310 Ok(u32::from_le_bytes(a))
311 }
312 fn u64(&mut self) -> Result<u64, ApuSnapshotError> {
313 self.need(8)?;
314 let mut a = [0u8; 8];
315 a.copy_from_slice(&self.src[self.pos..self.pos + 8]);
316 self.pos += 8;
317 Ok(u64::from_le_bytes(a))
318 }
319 fn f32(&mut self) -> Result<f32, ApuSnapshotError> {
320 self.need(4)?;
321 let mut a = [0u8; 4];
322 a.copy_from_slice(&self.src[self.pos..self.pos + 4]);
323 self.pos += 4;
324 Ok(f32::from_le_bytes(a))
325 }
326 fn f64(&mut self) -> Result<f64, ApuSnapshotError> {
327 self.need(8)?;
328 let mut a = [0u8; 8];
329 a.copy_from_slice(&self.src[self.pos..self.pos + 8]);
330 self.pos += 8;
331 Ok(f64::from_le_bytes(a))
332 }
333 fn bool(&mut self) -> Result<bool, ApuSnapshotError> {
334 Ok(self.u8()? != 0)
335 }
336}
337
338fn write_envelope(w: &mut W, e: Envelope) {
339 w.bool(e.start);
340 w.bool(e.loop_flag);
341 w.bool(e.constant);
342 w.u8(e.volume_or_period);
343 w.u8(e.divider);
344 w.u8(e.decay);
345}
346fn read_envelope(r: &mut R<'_>) -> Result<Envelope, ApuSnapshotError> {
347 Ok(Envelope {
348 start: r.bool()?,
349 loop_flag: r.bool()?,
350 constant: r.bool()?,
351 volume_or_period: bounded(r, "envelope.volume_or_period", 15)?,
354 divider: bounded(r, "envelope.divider", 15)?,
355 decay: bounded(r, "envelope.decay", 15)?,
356 })
357}
358
359fn write_length(w: &mut W, l: LengthCounter) {
360 w.u8(l.count);
361 w.bool(l.halt);
362 w.bool(l.enabled);
363}
364fn read_length(r: &mut R<'_>) -> Result<LengthCounter, ApuSnapshotError> {
365 let count = r.u8()?;
366 let halt = r.bool()?;
367 let enabled = r.bool()?;
368 Ok(LengthCounter {
377 count,
378 halt,
379 new_halt: halt,
380 enabled,
381 reload_val: 0,
382 previous_count: 0,
383 })
384}
385
386fn write_pulse(w: &mut W, p: &Pulse) {
387 w.u8(p.duty);
388 w.u8(p.step);
389 w.u16(p.timer_period);
390 w.u16(p.timer);
391 write_envelope(w, p.envelope);
392 write_length(w, p.length);
393 w.bool(p.sweep_enabled);
394 w.u8(p.sweep_period);
395 w.bool(p.sweep_negate);
396 w.u8(p.sweep_shift);
397 w.bool(p.sweep_reload);
398 w.u8(p.sweep_divider);
399 w.bool(p.is_pulse1);
400}
401fn read_pulse(r: &mut R<'_>) -> Result<Pulse, ApuSnapshotError> {
402 let duty = bounded(r, "pulse.duty", 3)?;
406 let step = bounded(r, "pulse.step", 7)?;
407 let timer_period = r.u16()?;
408 let timer = r.u16()?;
409 let envelope = read_envelope(r)?;
410 let length = read_length(r)?;
411 let sweep_enabled = r.bool()?;
412 let sweep_period = bounded(r, "pulse.sweep_period", 7)?;
413 let sweep_negate = r.bool()?;
414 let sweep_shift = bounded(r, "pulse.sweep_shift", 7)?;
415 let sweep_reload = r.bool()?;
416 let sweep_divider = bounded(r, "pulse.sweep_divider", 7)?;
417 let is_pulse1 = r.bool()?;
418 let mut p = Pulse::new(is_pulse1);
419 p.duty = duty;
420 p.step = step;
421 p.timer_period = timer_period;
422 p.timer = timer;
423 p.envelope = envelope;
424 p.length = length;
425 p.sweep_enabled = sweep_enabled;
426 p.sweep_period = sweep_period;
427 p.sweep_negate = sweep_negate;
428 p.sweep_shift = sweep_shift;
429 p.sweep_reload = sweep_reload;
430 p.sweep_divider = sweep_divider;
431 Ok(p)
432}
433
434fn write_triangle(w: &mut W, t: &Triangle) {
435 w.u16(t.timer_period);
436 w.u16(t.timer);
437 w.u8(t.step);
438 write_length(w, t.length);
439 w.u8(t.linear_reload_value);
440 w.u8(t.linear_counter);
441 w.bool(t.linear_control);
442 w.bool(t.linear_reload_flag);
443}
444fn read_triangle(r: &mut R<'_>) -> Result<Triangle, ApuSnapshotError> {
445 let mut t = Triangle::new();
446 t.timer_period = r.u16()?;
447 t.timer = r.u16()?;
448 t.step = bounded(r, "triangle.step", 31)?;
450 t.length = read_length(r)?;
451 t.linear_reload_value = r.u8()?;
452 t.linear_counter = r.u8()?;
453 t.linear_control = r.bool()?;
454 t.linear_reload_flag = r.bool()?;
455 Ok(t)
456}
457
458fn write_noise(w: &mut W, n: &Noise) {
459 w.u16(n.lfsr);
460 w.bool(n.mode);
461 w.u16(n.timer_period);
462 w.u16(n.timer);
463 write_envelope(w, n.envelope);
464 write_length(w, n.length);
465 w.u8(region_to_u8(n.region));
466}
467fn read_noise(r: &mut R<'_>) -> Result<Noise, ApuSnapshotError> {
468 let lfsr = r.u16()?;
469 let mode = r.bool()?;
470 let timer_period = r.u16()?;
471 let timer = r.u16()?;
472 let envelope = read_envelope(r)?;
473 let length = read_length(r)?;
474 let region = region_from_u8(r.u8()?)?;
475 let mut n = Noise::new(region);
476 n.lfsr = lfsr;
477 n.mode = mode;
478 n.timer_period = timer_period;
479 n.timer = timer;
480 n.envelope = envelope;
481 n.length = length;
482 Ok(n)
483}
484
485fn write_dmc(w: &mut W, d: &Dmc) {
486 w.bool(d.irq_enable);
487 w.bool(d.loop_flag);
488 w.u8(d.rate_index);
489 w.u16(d.sample_addr);
490 w.u16(d.sample_length);
491 w.u16(d.current_addr);
492 w.u16(d.bytes_remaining);
493 if let Some(b) = d.sample_buffer {
494 w.u8(1);
495 w.u8(b);
496 } else {
497 w.u8(0);
498 w.u8(0);
499 }
500 w.u8(d.shift_register);
501 w.u8(d.bits_remaining);
502 w.u8(d.dac);
503 w.bool(d.silence);
504 w.u16(d.timer_period);
505 w.u16(d.timer);
506 w.bool(d.irq_flag);
507}
508fn read_dmc(r: &mut R<'_>, region: Region) -> Result<Dmc, ApuSnapshotError> {
509 let irq_enable = r.bool()?;
510 let loop_flag = r.bool()?;
511 let rate_index = bounded(r, "dmc.rate_index", 15)?;
513 let sample_addr = r.u16()?;
514 let sample_length = r.u16()?;
515 let current_addr = r.u16()?;
516 let bytes_remaining = r.u16()?;
517 let presence = r.u8()?;
518 let buf_byte = r.u8()?;
519 let sample_buffer = match presence {
520 0 => None,
521 1 => Some(buf_byte),
522 other => return Err(ApuSnapshotError::InvalidPresence(other)),
523 };
524 let shift_register = r.u8()?;
525 let bits_remaining = bounded(r, "dmc.bits_remaining", 8)?;
528 let dac = bounded(r, "dmc.dac", 127)?;
529 let silence = r.bool()?;
530 let timer_period = r.u16()?;
531 let timer = r.u16()?;
532 let irq_flag = r.bool()?;
533 let mut d = Dmc::new(region);
534 d.irq_enable = irq_enable;
535 d.loop_flag = loop_flag;
536 d.rate_index = rate_index;
537 d.sample_addr = sample_addr;
538 d.sample_length = sample_length;
539 d.current_addr = current_addr;
540 d.bytes_remaining = bytes_remaining;
541 d.sample_buffer = sample_buffer;
542 d.shift_register = shift_register;
543 d.bits_remaining = bits_remaining;
544 d.dac = dac;
545 d.silence = silence;
546 d.timer_period = timer_period;
547 d.timer = timer;
548 d.irq_flag = irq_flag;
549 Ok(d)
550}
551
552fn write_fc(w: &mut W, fc: &FrameCounter) {
553 w.u8(mode_to_u8(fc.mode));
554 w.bool(fc.irq_inhibit);
555 w.bool(fc.irq_flag);
556 w.u32(fc.cycle);
557 w.u8(fc.reset_in);
558 w.u8(mode_to_u8(fc.pending_mode));
559 w.bool(fc.pending_inhibit);
560 w.bool(fc.apu_aligned);
561 w.u64(fc.irq_flag_clear_cycle);
565 w.bool(fc.irq_line_active);
571}
572fn read_fc(r: &mut R<'_>) -> Result<FrameCounter, ApuSnapshotError> {
573 let mode = mode_from_u8(r.u8()?)?;
574 let irq_inhibit = r.bool()?;
575 let irq_flag = r.bool()?;
576 let cycle = r.u32()?;
577 let reset_in = r.u8()?;
578 let pending_mode = mode_from_u8(r.u8()?)?;
579 let pending_inhibit = r.bool()?;
580 let apu_aligned = r.bool()?;
581 let irq_flag_clear_cycle = r.u64()?;
582 let irq_line_active = r.bool()?;
583 let mut fc = FrameCounter::new();
584 fc.mode = mode;
585 fc.irq_inhibit = irq_inhibit;
586 fc.irq_flag = irq_flag;
587 fc.irq_line_active = irq_line_active;
588 fc.cycle = cycle;
589 fc.reset_in = reset_in;
590 fc.pending_mode = pending_mode;
591 fc.pending_inhibit = pending_inhibit;
592 fc.apu_aligned = apu_aligned;
593 fc.irq_flag_clear_cycle = irq_flag_clear_cycle;
594 Ok(fc)
595}
596
597fn write_onepole(w: &mut W, o: &OnePole) {
598 w.f32(o.coeff);
599 w.f32(o.prev_in);
600 w.f32(o.prev_out);
601 w.bool(o.is_hpf);
602}
603fn read_onepole(r: &mut R<'_>, pos: usize) -> Result<OnePole, ApuSnapshotError> {
607 let coeff = finite_f32(r.f32()?, "filter.coeff")?;
608 if !(0.0..=1.0).contains(&coeff) {
614 return Err(ApuSnapshotError::InvalidResampler("filter.coeff"));
615 }
616 let x_max = FILTER_INPUT_MAX[pos];
617 let prev_in = bounded_f32(r.f32()?, x_max, "filter.prev_in")?;
618 let prev_out = finite_f32(r.f32()?, "filter.prev_out")?;
619 let is_hpf = r.bool()?;
620 if is_hpf != (pos < 2) {
621 return Err(ApuSnapshotError::InvalidResampler("filter.kind"));
622 }
623 let excess = if is_hpf {
624 (prev_out - coeff * prev_in).abs() - coeff * x_max
625 } else {
626 prev_out.abs() - x_max
627 };
628 if excess > FILTER_SLACK {
629 return Err(ApuSnapshotError::InvalidResampler("filter.prev_out"));
630 }
631 let mut o = if is_hpf {
635 OnePole::high_pass(0.0, 1.0)
636 } else {
637 OnePole::low_pass(0.0, 1.0)
638 };
639 o.coeff = coeff;
640 o.prev_in = prev_in;
641 o.prev_out = prev_out;
642 o.is_hpf = is_hpf;
643 Ok(o)
644}
645
646fn write_filter(w: &mut W, f: &FilterChain) {
647 write_onepole(w, &f.hp1);
648 write_onepole(w, &f.hp2);
649 write_onepole(w, &f.lp);
650}
651fn read_filter(r: &mut R<'_>) -> Result<FilterChain, ApuSnapshotError> {
652 let hp1 = read_onepole(r, 0)?;
653 let hp2 = read_onepole(r, 1)?;
654 let lp = read_onepole(r, 2)?;
655 Ok(FilterChain { hp1, hp2, lp })
656}
657
658fn write_blip(w: &mut W, b: &BlipBuf) {
659 w.u32(b.sample_rate);
660 w.f64(b.cpu_rate);
661 w.f64(b.phase);
662 write_filter(w, &b.filter);
663 w.f32(b.held_value);
664 let (head, primed, integrator, window) = b.live_state();
677 w.u16(head);
678 w.bool(primed);
679 w.f32(integrator);
680 for v in window {
681 w.f32(v);
682 }
683}
684fn read_blip(r: &mut R<'_>) -> Result<BlipBuf, ApuSnapshotError> {
685 let sample_rate = r.u32()?;
686 let cpu_rate = r.f64()?;
687 let phase = r.f64()?;
688 let filter = read_filter(r)?;
689 let held_value = bounded_f32(r.f32()?, RESAMPLER_HELD_MAX, "blip.held_value")?;
690 let head = r.u16()?;
691 let primed = r.bool()?;
692 let integrator = bounded_f32(r.f32()?, RESAMPLER_INTEGRATOR_MAX, "blip.integrator")?;
693 let mut window = [0.0f32; crate::blip_kernel::TAPS];
694 let mut partial = integrator;
695 for v in &mut window {
696 *v = finite_f32(r.f32()?, "blip.delta_window")?;
697 partial += *v;
698 if !partial.is_finite() || partial.abs() > RESAMPLER_INTEGRATOR_MAX {
699 return Err(ApuSnapshotError::InvalidResampler("blip.delta_window"));
700 }
701 }
702 if (partial - held_value).abs() > RESAMPLER_DRIFT_MAX {
703 return Err(ApuSnapshotError::InvalidResampler("blip.delta_window"));
704 }
705
706 if sample_rate == 0 {
707 return Err(ApuSnapshotError::InvalidResampler("blip.sample_rate"));
708 }
709 if !cpu_rate.is_finite() || cpu_rate <= 0.0 {
712 return Err(ApuSnapshotError::InvalidResampler("blip.cpu_rate"));
713 }
714 if f64::from(sample_rate) / cpu_rate > 1.0 {
720 return Err(ApuSnapshotError::InvalidResampler(
721 "blip.sample_rate/cpu_rate",
722 ));
723 }
724 if !(0.0..1.0).contains(&phase) {
725 return Err(ApuSnapshotError::InvalidResampler("blip.phase"));
726 }
727 let mut b = BlipBuf::new(sample_rate, cpu_rate);
728 b.phase = phase;
729 b.filter = filter;
730 b.held_value = held_value;
731 b.set_live_state(head, primed, integrator, &window);
732 Ok(b)
733}
734
735impl Apu {
736 #[must_use]
738 pub fn snapshot(&self) -> Vec<u8> {
739 let mut w = W {
740 buf: Vec::with_capacity(512),
741 };
742 w.u8(APU_SNAPSHOT_VERSION);
743 w.u8(region_to_u8(self.region));
744
745 write_pulse(&mut w, &self.pulse1);
746 write_pulse(&mut w, &self.pulse2);
747 write_triangle(&mut w, &self.triangle);
748 write_noise(&mut w, &self.noise);
749 write_dmc(&mut w, &self.dmc);
750 write_fc(&mut w, &self.frame_counter);
751 write_blip(&mut w, &self.blip);
752
753 w.bool(self.apu_phase);
754 w.u64(self.cpu_cycle);
755 w.bool(self.pending_dmc_dma);
756 w.u16(self.dmc_dma_addr);
757 w.u32(self.sample_rate);
758 w.u8(self.dmc_dma_delay);
759 w.bool(self.dmc_dma_is_load);
760 w.bool(self.pending_dmc_abort);
761 w.u8(self.dmc_abort_delay);
762 w.bool(self.dmc_dma_short);
763 w.bool(self.defer_dmc_reload_once);
764 w.u8(self.dmc_dma_cooldown);
765 w.u8(self.dmc_reload_suppress_outputs);
766
767 w.bool(self.put_cycle);
778 w.u64(self.parity_seed);
779 w.u8(self.cannot_run_dmc_dma);
780 w.bool(self.dmc_reenable_period_block);
781 w.u8(self.subpos_arm_countdown);
782 w.bool(self.dmc_need_halt);
783 w.bool(self.dmc_need_dummy_read);
784 w.bool(self.pending_dmc_dma_next);
785 {
786 w.u8(self.dmc_delayed_4015);
787 w.bool(self.dmc_delayed_status);
788 w.bool(self.dmc_status_applied);
789 w.bool(self.dmc_set_implicit_abort);
790 w.bool(self.dmc_implicit_abort);
791 w.bool(self.dmc_edge_arm_suppress);
792 }
793
794 w.u8(self.reset_4017_delay);
800 w.u8(self.reset_4017_value);
801
802 w.buf
803 }
804
805 pub fn restore(&mut self, data: &[u8]) -> Result<(), ApuSnapshotError> {
811 let mut r = R { src: data, pos: 0 };
812 let version = r.u8()?;
813 if version != APU_SNAPSHOT_VERSION {
816 return Err(ApuSnapshotError::UnsupportedVersion(version));
817 }
818 self.region = region_from_u8(r.u8()?)?;
819
820 self.pulse1 = read_pulse(&mut r)?;
821 self.pulse2 = read_pulse(&mut r)?;
822 self.triangle = read_triangle(&mut r)?;
823 self.noise = read_noise(&mut r)?;
824 self.dmc = read_dmc(&mut r, self.region)?;
825 self.frame_counter = read_fc(&mut r)?;
826 self.frame_counter.pal = matches!(self.region, Region::Pal);
832 self.blip = read_blip(&mut r)?;
833
834 self.apu_phase = r.bool()?;
835 self.cpu_cycle = r.u64()?;
836 self.pending_dmc_dma = r.bool()?;
837 self.dmc_dma_addr = r.u16()?;
838 self.sample_rate = r.u32()?;
839 self.dmc_dma_delay = r.u8()?;
840 self.dmc_dma_is_load = r.bool()?;
841 self.pending_dmc_abort = r.bool()?;
842 self.dmc_abort_delay = r.u8()?;
843 self.dmc_dma_short = r.bool()?;
844 self.defer_dmc_reload_once = r.bool()?;
845 self.dmc_dma_cooldown = r.u8()?;
846 self.dmc_reload_suppress_outputs = r.u8()?;
847
848 self.put_cycle = r.bool()?;
851 self.parity_seed = r.u64()?;
852 self.cannot_run_dmc_dma = r.u8()?;
853 self.dmc_reenable_period_block = r.bool()?;
854 self.subpos_arm_countdown = r.u8()?;
855 self.dmc_need_halt = r.bool()?;
856 self.dmc_need_dummy_read = r.bool()?;
857 self.pending_dmc_dma_next = r.bool()?;
858 self.dmc_delayed_4015 = r.u8()?;
859 self.dmc_delayed_status = r.bool()?;
860 self.dmc_status_applied = r.bool()?;
861 self.dmc_set_implicit_abort = r.bool()?;
862 self.dmc_implicit_abort = r.bool()?;
863 self.dmc_edge_arm_suppress = r.bool()?;
864
865 self.reset_4017_delay = r.u8()?;
868 self.reset_4017_value = r.u8()?;
869
870 if r.pos != data.len() {
872 return Err(ApuSnapshotError::TrailingBytes(data.len() - r.pos));
873 }
874 Ok(())
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881 use crate::blip::CPU_HZ_NTSC;
882
883 fn field_span(set_a: impl Fn(&mut Apu), set_b: impl Fn(&mut Apu)) -> (Vec<u8>, usize, usize) {
888 let mut a = Apu::new(Region::Ntsc, 44_100);
889 let mut b = Apu::new(Region::Ntsc, 44_100);
890 set_a(&mut a);
891 set_b(&mut b);
892 let (sa, sb) = (a.snapshot(), b.snapshot());
893 assert_eq!(sa.len(), sb.len());
894 let first = (0..sa.len())
895 .find(|&i| sa[i] != sb[i])
896 .expect("fields differ");
897 let last = (0..sa.len()).rfind(|&i| sa[i] != sb[i]).unwrap();
898 (sb, first, last + 1)
899 }
900
901 fn assert_u8_bounded(name: &'static str, max: u8, set: impl Fn(&mut Apu, u8)) {
904 let lo = max.saturating_sub(1);
905 let (blob, at, end) = field_span(|a| set(a, lo), |a| set(a, max));
906 assert_eq!(end - at, 1, "{name} is one byte");
907 Apu::new(Region::Ntsc, 44_100)
908 .restore(&blob)
909 .unwrap_or_else(|e| panic!("{name} = {max} is legal and must load: {e}"));
910 for bad in [max + 1, 0x80u8.max(max + 1), 0xFF] {
911 let mut b = blob.clone();
912 b[at] = bad;
913 match Apu::new(Region::Ntsc, 44_100).restore(&b) {
914 Err(ApuSnapshotError::FieldOutOfRange {
915 field,
916 value,
917 max: m,
918 }) => {
919 assert_eq!((field, value, m), (name, bad, max));
920 }
921 Err(e) => panic!("{name} = {bad}: wrong error {e}"),
922 Ok(()) => panic!("{name} = {bad}: restore ACCEPTED an out-of-range value"),
923 }
924 }
925 }
926
927 #[test]
928 fn every_register_width_field_is_bounded_on_restore() {
929 assert_u8_bounded("pulse.duty", 3, |a, v| a.pulse1.duty = v);
933 assert_u8_bounded("pulse.step", 7, |a, v| a.pulse1.step = v);
934 assert_u8_bounded("pulse.sweep_period", 7, |a, v| a.pulse2.sweep_period = v);
935 assert_u8_bounded("pulse.sweep_shift", 7, |a, v| a.pulse1.sweep_shift = v);
936 assert_u8_bounded("pulse.sweep_divider", 7, |a, v| a.pulse2.sweep_divider = v);
937 assert_u8_bounded("envelope.volume_or_period", 15, |a, v| {
938 a.pulse1.envelope.volume_or_period = v;
939 });
940 assert_u8_bounded("envelope.divider", 15, |a, v| a.noise.envelope.divider = v);
941 assert_u8_bounded("envelope.decay", 15, |a, v| a.pulse2.envelope.decay = v);
942 assert_u8_bounded("triangle.step", 31, |a, v| a.triangle.step = v);
943 assert_u8_bounded("dmc.rate_index", 15, |a, v| a.dmc.rate_index = v);
944 assert_u8_bounded("dmc.bits_remaining", 8, |a, v| a.dmc.bits_remaining = v);
945 assert_u8_bounded("dmc.dac", 127, |a, v| a.dmc.dac = v);
946 }
947
948 fn assert_float_rejected(name: &'static str, span: (Vec<u8>, usize, usize), bad: &[u8]) {
951 let (mut b, at, end) = span;
952 assert_eq!(
953 end - at,
954 bad.len(),
955 "{name}: located span is the value's width"
956 );
957 b[at..end].copy_from_slice(bad);
958 match Apu::new(Region::Ntsc, 44_100).restore(&b) {
959 Err(ApuSnapshotError::InvalidResampler(f)) => assert_eq!(f, name),
960 Err(e) => panic!("{name}: wrong error {e}"),
961 Ok(()) => panic!("{name}: restore ACCEPTED {bad:02x?}"),
962 }
963 }
964
965 #[test]
966 fn resampler_fields_that_would_hang_or_poison_audio_are_rejected() {
967 let rate = |a: &mut Apu, v: u32| a.blip.sample_rate = v;
974 let sr = || field_span(|a| rate(a, 44_100), |a| rate(a, !44_100));
975 assert_float_rejected("blip.sample_rate", sr(), &0u32.to_le_bytes());
976 assert_float_rejected("blip.sample_rate/cpu_rate", sr(), &u32::MAX.to_le_bytes());
979
980 let cpu = |a: &mut Apu, v: f64| a.blip.cpu_rate = v;
981 let cr = || {
982 field_span(
983 |a| cpu(a, CPU_HZ_NTSC),
984 |a| cpu(a, f64::from_bits(!CPU_HZ_NTSC.to_bits())),
985 )
986 };
987 for bad in [f64::NAN, f64::INFINITY, 0.0, -1.0] {
988 assert_float_rejected("blip.cpu_rate", cr(), &bad.to_le_bytes());
989 }
990 assert_float_rejected("blip.sample_rate/cpu_rate", cr(), &1.0e-9f64.to_le_bytes());
991
992 let ph = |a: &mut Apu, v: f64| a.blip.phase = v;
993 let pp = || field_span(|a| ph(a, 0.0), |a| ph(a, f64::from_bits(!0)));
994 for bad in [1.0, 1.0e300, -0.25, f64::NAN] {
995 assert_float_rejected("blip.phase", pp(), &bad.to_le_bytes());
996 }
997
998 let hv = |a: &mut Apu, v: f32| a.blip.held_value = v;
999 let hp = || field_span(|a| hv(a, 0.0), |a| hv(a, f32::from_bits(!0)));
1000 assert_float_rejected("blip.held_value", hp(), &f32::NAN.to_le_bytes());
1001 for bad in [f32::MAX, -2.05e38, 17.0] {
1003 assert_float_rejected("blip.held_value", hp(), &bad.to_le_bytes());
1004 }
1005 let po = |a: &mut Apu, v: f32| a.blip.filter.hp1.prev_out = v;
1006 let pop = || field_span(|a| po(a, 0.0), |a| po(a, f32::from_bits(!0)));
1007 let pi = |a: &mut Apu, v: f32| a.blip.filter.hp1.prev_in = v;
1008 let pip = || field_span(|a| pi(a, 0.0), |a| pi(a, f32::from_bits(!0)));
1009 for bad in [f32::MAX, -2.05e38, -2048.0] {
1010 assert_float_rejected("filter.prev_out", pop(), &bad.to_le_bytes());
1011 assert_float_rejected("filter.prev_in", pip(), &bad.to_le_bytes());
1012 }
1013
1014 let co = |a: &mut Apu, v: f32| a.blip.filter.lp.coeff = v;
1015 let cp = || field_span(|a| co(a, 0.5), |a| co(a, f32::from_bits(!0.5f32.to_bits())));
1016 assert_float_rejected("filter.coeff", cp(), &f32::INFINITY.to_le_bytes());
1017 for bad in [1.5f32, -0.25] {
1019 assert_float_rejected("filter.coeff", cp(), &bad.to_le_bytes());
1020 }
1021 }
1022
1023 #[test]
1028 fn a_restore_resumes_the_exact_audio_stream() {
1029 fn program(a: &mut Apu) {
1030 a.write_register(0x4015, 0x0F);
1031 a.write_register(0x4000, 0xBF);
1032 a.write_register(0x4002, 0x40);
1033 a.write_register(0x4003, 0x01);
1034 a.write_register(0x4008, 0xFF);
1035 a.write_register(0x400A, 0x80);
1036 a.write_register(0x400B, 0x02);
1037 }
1038 let mut straight = Apu::new(Region::Ntsc, 44_100);
1039 program(&mut straight);
1040 for _ in 0..20_000 {
1041 straight.tick();
1042 }
1043 let _ = straight.blip.drain_all();
1045 let blob = straight.snapshot();
1046 let mut restored = Apu::new(Region::Ntsc, 48_000);
1047 restored.restore(&blob).unwrap();
1048 for _ in 0..40_000 {
1049 straight.tick();
1050 restored.tick();
1051 }
1052 let (a, b) = (straight.blip.drain_all(), restored.blip.drain_all());
1053 assert_eq!(a.len(), b.len(), "same number of samples");
1054 assert!(
1055 a.iter().zip(&b).all(|(x, y)| x.to_bits() == y.to_bits()),
1056 "bit-identical samples"
1057 );
1058 assert_eq!(straight.snapshot(), restored.snapshot(), "same state");
1059 }
1060
1061 #[test]
1065 fn a_window_inconsistent_with_the_held_value_is_refused() {
1066 let mut a = Apu::new(Region::Ntsc, 44_100);
1067 a.write_register(0x4015, 0x01);
1068 a.write_register(0x4000, 0xBF);
1069 a.write_register(0x4002, 0x40);
1070 a.write_register(0x4003, 0x01);
1071 for _ in 0..5_000 {
1072 a.tick();
1073 }
1074 let (head, primed, integrator, mut window) = a.blip.live_state();
1075 window[0] += 3.0;
1076 a.blip.set_live_state(head, primed, integrator, &window);
1077 assert!(matches!(
1078 Apu::new(Region::Ntsc, 44_100).restore(&a.snapshot()),
1079 Err(ApuSnapshotError::InvalidResampler("blip.delta_window"))
1080 ));
1081 }
1082
1083 #[test]
1088 fn a_huge_filter_value_is_refused_so_the_next_snapshot_still_loads() {
1089 let mut a = Apu::new(Region::Ntsc, 44_100);
1090 a.write_register(0x4015, 0x0F);
1091 a.write_register(0x4000, 0xBF);
1092 a.write_register(0x4002, 0x40);
1093 a.write_register(0x4003, 0x01);
1094 a.blip.filter.hp1.prev_out = f32::MAX;
1095 let poisoned = a.snapshot();
1096 let mut b = Apu::new(Region::Ntsc, 44_100);
1097 assert!(b.restore(&poisoned).is_err(), "the huge value is refused");
1098 let mut c = Apu::new(Region::Ntsc, 44_100);
1100 c.write_register(0x4015, 0x0F);
1101 c.write_register(0x4000, 0xBF);
1102 c.write_register(0x4002, 0x40);
1103 c.write_register(0x4003, 0x01);
1104 for _ in 0..30_000 {
1105 c.tick();
1106 }
1107 let mut d = Apu::new(Region::Ntsc, 44_100);
1108 d.restore(&c.snapshot()).expect("a real state loads");
1109 }
1110
1111 #[test]
1118 fn every_accepted_filter_state_keeps_its_own_snapshot_loadable() {
1119 fn tone(a: &mut Apu) {
1120 a.write_register(0x4015, 0x0F);
1121 a.write_register(0x4000, 0xBF);
1122 a.write_register(0x4002, 0x40);
1123 a.write_register(0x4003, 0x01);
1124 a.write_register(0x4008, 0xFF);
1125 a.write_register(0x400A, 0x80);
1126 a.write_register(0x400B, 0x02);
1127 }
1128 type Stage = fn(&mut Apu) -> &mut crate::mixer::OnePole;
1129 let stages: [(&str, Stage); 3] = [
1130 ("hp1", |a| &mut a.blip.filter.hp1),
1131 ("hp2", |a| &mut a.blip.filter.hp2),
1132 ("lp", |a| &mut a.blip.filter.lp),
1133 ];
1134 let values = [
1135 -2048.0f32, -1024.0, -67.0, -33.0, -16.0, -8.0, 0.0, 8.0, 16.0, 33.0, 67.0, 1024.0,
1136 ];
1137 let mut accepted = 0;
1138 for (name, stage) in stages {
1139 for &pi in &values {
1140 for &po in &values {
1141 let mut a = Apu::new(Region::Ntsc, 44_100);
1142 tone(&mut a);
1143 for _ in 0..2_000 {
1144 a.tick();
1145 }
1146 let _ = a.blip.drain_all();
1147 {
1148 let s = stage(&mut a);
1149 s.prev_in = pi;
1150 s.prev_out = po;
1151 }
1152 let mut b = Apu::new(Region::Ntsc, 44_100);
1153 if b.restore(&a.snapshot()).is_err() {
1154 continue;
1155 }
1156 accepted += 1;
1157 for step in 0..50 {
1161 for _ in 0..41 {
1162 b.tick();
1163 }
1164 let _ = b.blip.drain_all();
1165 let mut c = Apu::new(Region::Ntsc, 44_100);
1166 c.restore(&b.snapshot()).unwrap_or_else(|e| {
1167 panic!(
1168 "{name} prev_in {pi} prev_out {po} was accepted, but the snapshot \
1169 {step} samples later is refused: {e:?}"
1170 )
1171 });
1172 }
1173 }
1174 }
1175 }
1176 assert!(accepted > 0, "the grid must include accepted states");
1177 }
1178
1179 #[test]
1183 fn a_filter_stage_of_the_wrong_kind_is_refused() {
1184 let mut a = Apu::new(Region::Ntsc, 44_100);
1185 a.blip.filter.hp1.is_hpf = false;
1186 assert!(matches!(
1187 Apu::new(Region::Ntsc, 44_100).restore(&a.snapshot()),
1188 Err(ApuSnapshotError::InvalidResampler("filter.kind"))
1189 ));
1190 }
1191
1192 #[test]
1193 fn snapshot_round_trip_on_fresh_apu() {
1194 let a = Apu::new(Region::Ntsc, 44_100);
1195 let blob = a.snapshot();
1196 let mut b = Apu::new(Region::Pal, 48_000);
1197 b.restore(&blob).unwrap();
1198 assert_eq!(b.region, Region::Ntsc);
1199 assert_eq!(b.sample_rate, 44_100);
1200 }
1201
1202 #[test]
1203 fn snapshot_after_some_ticks_round_trips() {
1204 let mut a = Apu::new(Region::Ntsc, 44_100);
1205 a.write_register(0x4000, 0xBE);
1206 a.write_register(0x4002, 0x42);
1207 a.write_register(0x4015, 0x0F);
1208 for _ in 0..100 {
1209 a.tick();
1210 }
1211 let blob = a.snapshot();
1212 let mut b = Apu::new(Region::Ntsc, 44_100);
1213 b.restore(&blob).unwrap();
1214 assert_eq!(b.cpu_cycle, a.cpu_cycle);
1216 assert_eq!(b.pulse1.timer_period, a.pulse1.timer_period);
1217 assert_eq!(b.pulse1.length.count, a.pulse1.length.count);
1218 assert_eq!(b.frame_counter.cycle, a.frame_counter.cycle);
1219 }
1220
1221 #[test]
1222 fn snapshot_rejects_bad_version() {
1223 let mut a = Apu::new(Region::Ntsc, 44_100);
1224 let err = a.restore(&[0xFF; 4]).unwrap_err();
1225 assert!(matches!(err, ApuSnapshotError::UnsupportedVersion(0xFF)));
1226 }
1227
1228 #[test]
1229 fn snapshot_is_deterministic() {
1230 let a = Apu::new(Region::Ntsc, 44_100);
1231 assert_eq!(a.snapshot(), a.snapshot());
1232 }
1233
1234 #[test]
1235 fn stage4_tail_round_trips_parity_and_dma_state() {
1236 let mut a = Apu::new(Region::Ntsc, 44_100);
1237 a.put_cycle = true;
1238 a.cannot_run_dmc_dma = 2;
1239 a.dmc_reenable_period_block = true;
1240 a.subpos_arm_countdown = 3;
1241 a.dmc_need_halt = true;
1242 a.dmc_need_dummy_read = true;
1243 {
1244 a.dmc_delayed_4015 = 4;
1245 a.dmc_delayed_status = true;
1246 a.dmc_status_applied = true;
1247 a.dmc_edge_arm_suppress = true;
1248 }
1249 let blob = a.snapshot();
1250 let mut b = Apu::new(Region::Ntsc, 44_100);
1251 b.restore(&blob).unwrap();
1252 assert!(b.put_cycle);
1253 assert_eq!(b.cannot_run_dmc_dma, 2);
1254 assert!(b.dmc_reenable_period_block);
1255 assert_eq!(b.subpos_arm_countdown, 3);
1256 assert!(b.dmc_need_halt);
1257 assert!(b.dmc_need_dummy_read);
1258 {
1259 assert_eq!(b.dmc_delayed_4015, 4);
1260 assert!(b.dmc_delayed_status);
1261 assert!(b.dmc_status_applied);
1262 assert!(b.dmc_edge_arm_suppress);
1263 }
1264 }
1265
1266 #[test]
1270 fn older_versions_and_short_blobs_are_refused() {
1271 let a = Apu::new(Region::Ntsc, 44_100);
1272 let blob = a.snapshot();
1273 for v in 1..APU_SNAPSHOT_VERSION {
1274 let mut old = blob.clone();
1275 old[0] = v;
1276 assert!(matches!(
1277 Apu::new(Region::Ntsc, 44_100).restore(&old),
1278 Err(ApuSnapshotError::UnsupportedVersion(got)) if got == v
1279 ));
1280 }
1281 let short = &blob[..blob.len() - (2 + 21)];
1284 assert!(matches!(
1285 Apu::new(Region::Ntsc, 44_100).restore(short),
1286 Err(ApuSnapshotError::Truncated(_))
1287 ));
1288 for len in 1..blob.len() {
1290 assert!(
1291 Apu::new(Region::Ntsc, 44_100)
1292 .restore(&blob[..len])
1293 .is_err(),
1294 "an APU blob cut to {len} bytes loaded"
1295 );
1296 }
1297 let mut long = blob.clone();
1298 long.push(0);
1299 assert!(matches!(
1302 Apu::new(Region::Ntsc, 44_100).restore(&long),
1303 Err(ApuSnapshotError::TrailingBytes(n)) if n == 1
1304 ));
1305 }
1306
1307 #[test]
1308 fn v4_round_trips_the_scheduled_reset_4017_rewrite() {
1309 let mut a = Apu::new(Region::Ntsc, 44_100);
1312 a.reset_4017_delay = 2;
1313 a.reset_4017_value = 0x80;
1314 let blob = a.snapshot();
1315 assert_eq!(
1316 blob[0], APU_SNAPSHOT_VERSION,
1317 "blob carries current version"
1318 );
1319
1320 let mut b = Apu::new(Region::Pal, 48_000);
1321 b.restore(&blob).unwrap();
1322 assert_eq!(b.reset_4017_delay, 2);
1323 assert_eq!(b.reset_4017_value, 0x80);
1324 }
1325
1326 #[test]
1327 fn a_reset_survives_a_snapshot_restore_taken_mid_countdown() {
1328 let mut plain = Apu::new(Region::Ntsc, 44_100);
1334 plain.write_register(0x4017, 0x80); plain.reset();
1336 assert_eq!(plain.reset_4017_delay, 2, "reset arms the countdown");
1337
1338 let mut restored = Apu::new(Region::Pal, 48_000);
1340 restored.restore(&plain.snapshot()).unwrap();
1341
1342 for _ in 0..10 {
1348 plain.tick_with_external(0.0);
1349 restored.tick_with_external(0.0);
1350 }
1351 assert_eq!(
1352 restored.reset_4017_delay, plain.reset_4017_delay,
1353 "countdown diverged across the round trip"
1354 );
1355 assert_eq!(
1361 restored.frame_counter.cycle, plain.frame_counter.cycle,
1362 "the scheduled $4017 re-write did not survive the round trip — the \
1363 restored sequencer never restarted"
1364 );
1365 assert_eq!(
1366 restored.frame_counter.reset_in, plain.frame_counter.reset_in,
1367 "frame-counter reset maturation diverged across the round trip"
1368 );
1369 }
1370
1371 #[test]
1372 fn fresh_apu_snapshot_has_zero_irq_clear_schedule() {
1373 let a = Apu::new(Region::Ntsc, 44_100);
1374 assert_eq!(a.frame_counter.irq_flag_clear_cycle, 0);
1375 let blob = a.snapshot();
1376 let mut b = Apu::new(Region::Pal, 48_000);
1377 b.restore(&blob).unwrap();
1378 assert_eq!(b.frame_counter.irq_flag_clear_cycle, 0);
1379 }
1380}