Skip to main content

spectrusty_formats/
snapshot.rs

1/*
2    Copyright (C) 2020-2022  Rafal Michalski
3
4    This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6    For the full copyright notice, see the lib.rs file.
7*/
8//! Common snapshot formats utilities.
9use core::fmt;
10use core::ops::Range;
11use std::io::Read;
12use bitflags::bitflags;
13
14use spectrusty_core::z80emu::{*, z80::*};
15use spectrusty_core::clock::{VideoTs, FTs};
16use spectrusty_core::chip::{
17    ControlUnit, MemoryAccess, ReadEarMode,
18    Ula128MemFlags, Ula3CtrlFlags, ScldCtrlFlags
19};
20use spectrusty_core::video::{Video, BorderColor};
21use spectrusty_core::memory::{ZxMemory, ZxMemoryError};
22use spectrusty_peripherals::ay::AyRegister;
23
24#[non_exhaustive]
25#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash)]
26pub enum ComputerModel {
27    Spectrum16,
28    Spectrum48,
29    SpectrumNTSC,
30    Spectrum128,
31    SpectrumPlus2,
32    SpectrumPlus2A,
33    SpectrumPlus3,
34    SpectrumPlus3e,
35    SpectrumSE,
36    TimexTC2048,
37    TimexTC2068,
38    TimexTS2068,
39}
40
41bitflags! {
42    #[derive(Default)]
43    pub struct Extensions: u64 {
44        const NONE       = 0x0000_0000_0000_0000;
45        const IF1        = 0x0000_0000_0000_0001;
46        const PLUS_D     = 0x0000_0000_0000_0002;
47        const DISCIPLE   = 0x0000_0000_0000_0004;
48        const SAM_RAM    = 0x0000_0000_0000_0008;
49        const ULA_PLUS   = 0x0000_0000_0000_0010;
50        const TR_DOS     = 0x0000_0000_0000_0020;
51        const RESERVED   = 0xFFFF_FFFF_FFFF_FFC0;
52    }
53}
54
55#[non_exhaustive]
56#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash)]
57pub enum JoystickModel {
58    Kempston,
59    Sinclair1,
60    Sinclair2,
61    Cursor,
62    Fuller,
63}
64
65#[non_exhaustive]
66#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash)]
67pub enum Ay3_891xDevice {
68    /// The device attached to one of the 128k/+2/+3/... models.
69    Ay128k,
70    /// The device attached to one of the 16k/48k models with the same port mappings as 128k version.
71    Melodik,
72    /// The Fuller Box port mapped AY chipset.
73    FullerBox,
74    /// The Tx2068 port mapped AY chipset.
75    Timex,
76}
77
78#[non_exhaustive]
79#[derive(Debug,Clone,PartialEq,Eq)]
80pub enum CpuModel {
81    NMOS(Z80NMOS),
82    CMOS(Z80CMOS),
83    BM1(Z80BM1),
84}
85
86impl<F: Flavour> From<CpuModel> for Z80<F>
87    where F: From<NMOS> + From<CMOS> + From<BM1>
88{
89    fn from(cpu: CpuModel) -> Self {
90        match cpu {
91            CpuModel::NMOS(z80) => z80.into_flavour(),
92            CpuModel::CMOS(z80) => z80.into_flavour(),
93            CpuModel::BM1(z80)   => z80.into_flavour(),
94        }
95    }
96}
97
98impl From<CpuModel> for Z80Any {
99    fn from(cpu: CpuModel) -> Self {
100        match cpu {
101            CpuModel::NMOS(z80) => Z80Any::NMOS(z80),
102            CpuModel::CMOS(z80) => Z80Any::CMOS(z80),
103            CpuModel::BM1(z80)  => Z80Any::BM1(z80),
104        }
105    }
106}
107
108impl From<Z80Any> for CpuModel {
109    fn from(cpu: Z80Any) -> Self {
110        match cpu {
111            Z80Any::NMOS(z80) => CpuModel::NMOS(z80),
112            Z80Any::CMOS(z80) => CpuModel::CMOS(z80),
113            Z80Any::BM1(z80)  => CpuModel::BM1(z80),
114        }
115    }
116}
117
118/// The memory range specifies which part of the emulated hardware the data should be loaded to.
119#[non_exhaustive]
120#[derive(Debug,Clone,PartialEq,Eq,Hash)]
121pub enum MemoryRange {
122    /// Load into the main ROM.
123    /// The address space starts from the top of the first ROM bank and extends towards the last ROM bank.
124    Rom(Range<usize>),
125    /// Load into the main RAM.
126    /// The address space starts from the top of the first RAM bank and extends towards the last RAM bank.
127    Ram(Range<usize>),
128    /// Load into the Interface1 ROM.
129    Interface1Rom,
130    /// Load into the MGT +D ROM.
131    PlusDRom,
132    /// Load into the MGT DISCiPLE ROM.
133    DiscipleRom,
134    /// Load into the Multiface ROM.
135    MultifaceRom,
136    /// Load into the SamRam ROM.
137    /// The address space starts from the top of the first ROM bank and extends towards the last ROM bank.
138    SamRamRom(Range<usize>),
139}
140
141/// The methods can be called more than one time.
142pub trait SnapshotCreator {
143    fn model(&self) -> ComputerModel;
144    fn extensions(&self) -> Extensions;
145    fn cpu(&self) -> CpuModel;
146    fn current_clock(&self) -> FTs;
147    fn border_color(&self) -> BorderColor;
148    fn issue(&self) -> ReadEarMode;
149    fn memory_ref(&self, range: MemoryRange) -> Result<&[u8], ZxMemoryError>;
150    fn joystick(&self) -> Option<JoystickModel> { None }
151    fn ay_state(&self, _choice: Ay3_891xDevice) -> Option<(AyRegister, &[u8;16])> { None }
152    fn ula128_flags(&self) -> Ula128MemFlags { unimplemented!() }
153    fn ula3_flags(&self) -> Ula3CtrlFlags { unimplemented!() }
154    fn timex_flags(&self) -> ScldCtrlFlags { unimplemented!() }
155    fn timex_memory_banks(&self) -> u8 { unimplemented!() }
156    // fn ulaplus_flags(&self) -> UlaPlusRegFlags;
157    fn is_interface1_rom_paged_in(&self) -> bool { unimplemented!() }
158    fn is_plus_d_rom_paged_in(&self) -> bool { unimplemented!() }
159    fn is_disciple_rom_paged_in(&self) -> bool { unimplemented!() }
160    fn is_tr_dos_rom_paged_in(&self) -> bool { unimplemented!() }
161}
162
163bitflags! {
164    #[derive(Default)]
165    pub struct SnapshotResult: u64 {
166        const OK              = 0x0000_0000_0000_0000;
167        const MODEL_NSUP      = 0x0000_0000_0000_0001;
168        const EXTENSTION_NSUP = 0x0000_0000_0000_0010;
169        const CPU_MODEL_NSUP  = 0x0000_0000_0000_0100;
170        const JOYSTICK_NSUP   = 0x0000_0000_0000_1000;
171        const SOUND_CHIP_NSUP = 0x0000_0000_0001_0000;
172        const KEYB_ISSUE_NSUP = 0x0000_0000_0010_0000;
173    }
174}
175
176/// Implement this trait to be able to load snapshot from files supported by this crate.
177///
178/// The method [SnapshotLoader::select_model] is always being called first.
179/// Then the other methods are being called in unspecified order.
180/// Not every method is always being called though.
181/// Some methods are not called when it makes no sense in the context of the loaded snapshot.
182pub trait SnapshotLoader {
183    /// The error type returned by the [SnapshotLoader::select_model] method.
184    type Error: Into<Box<(dyn std::error::Error + Send + Sync + 'static)>>;
185    /// Should create an instance of an emulated model from the given `model` and other arguments.
186    ///
187    /// If the model can not be emulated this method should return an `Err(Self::Error)`.
188    ///
189    /// This method is always being called first, before any other methods in this trait.
190    fn select_model(
191        &mut self,
192        model: ComputerModel,
193        extensions: Extensions,
194        border: BorderColor,
195        issue: ReadEarMode,
196    ) -> Result<(), Self::Error>;
197    /// Should read a memory chunk from the given `reader` source according to the specified `range`.
198    ///
199    /// This method should not fail.
200    fn read_into_memory<R: Read>(&mut self, range: MemoryRange, reader: R) -> Result<(), ZxMemoryError>;
201    /// Should attach an instance of the `cpu`.
202    ///
203    /// This method should not fail.
204    fn assign_cpu(&mut self, cpu: CpuModel);
205    /// Should set the frame T-states clock to the value given in `tstates`.
206    ///
207    /// This method should not fail.
208    fn set_clock(&mut self, tstates: FTs);
209    /// Should emulate sending the `data` to the given `port` of the main chipset.
210    ///
211    /// This method should not fail.
212    fn write_port(&mut self, port: u16, data: u8);
213    /// Should select the given joystick model if one is available.
214    ///
215    /// This method should not fail. Default implementation does nothing.
216    fn select_joystick(&mut self, _joystick: JoystickModel) {}
217    /// Should attach the amulated instance of `AY-3-891x` sound processor and initialize it from
218    /// the given arguments if one is available.
219    ///
220    /// This method is called n-times if more than one AY chipset is attached and there
221    /// are different register values for each of them.
222    ///
223    /// This method should not fail. Default implementation does nothing.
224    fn setup_ay(&mut self, _choice: Ay3_891xDevice, _reg_selected: AyRegister, _reg_values: &[u8;16]) {}
225    /// Should page in the Interface 1 ROM if one is available.
226    ///
227    /// This method should not fail. If an Interface 1 is not supported [SnapshotLoader::select_model]
228    /// may return with an error if [Extensions] would indicate such support is being requested.
229    ///
230    /// # Panics
231    /// The default implementation always panics.
232    fn interface1_rom_paged_in(&mut self) { unimplemented!() }
233    /// Should page in the MGT +D ROM if one is available.
234    ///
235    /// This method should not fail. If an MGT +D is not supported [SnapshotLoader::select_model]
236    /// may return with an error if [Extensions] would indicate such support is being requested.
237    ///
238    /// # Panics
239    /// The default implementation always panics.
240    fn plus_d_rom_paged_in(&mut self) { unimplemented!() }
241    /// Should page in the TR-DOS ROM if one is available.
242    ///
243    /// This method should not fail. If an TR-DOS is not supported [SnapshotLoader::select_model]
244    /// may return with an error if [Extensions] would indicate such support is being requested.
245    ///
246    /// # Panics
247    /// The default implementation always panics.
248    fn tr_dos_rom_paged_in(&mut self) { unimplemented!() }
249}
250
251/// Returns `true` if a `cpu` is safe for a snapshot using lossy formats.
252pub fn is_cpu_safe_for_snapshot<C: Cpu>(cpu: &C) -> bool {
253    !cpu.is_after_prefix()
254}
255
256/// Makes sure CPU is in a state that is safe for a snapshot, otherwise executes instructions until it's safe.
257///
258/// In some rare cases, a CPU will be in a state that most snapshot formats are unable to preserve it correctly.
259///
260/// That is while CPU is in a state after: 
261/// * Executing one of the `0xFD` or `0xDD` prefixes before executing the prefixed instruction,
262///   in this instance the "after the prefix" state is being lost, which will end up executing the next
263///   instruction in the wrong way.
264/// * Executing the `EI` instruction which temporarily prevents interrupts until the next instruction is executed,
265///   in this instance the "after EI" state is lost and an interrupt may be accepted while it shouldn't be just yet.
266pub fn ensure_cpu_is_safe_for_snapshot<C: Cpu, M: ControlUnit + Video + MemoryAccess>(
267        cpu: &mut C,
268        chip: &mut M
269    )
270{
271    loop {
272        if cpu.is_halt() {
273            return
274        }
275        else if cpu.is_after_prefix() {
276            match chip.memory_ref().read(cpu.get_pc()) {
277                0xFD|0xDD => return,
278                _ => {}
279            }
280        }
281        else if cpu.is_after_ei() {
282            let VideoTs { vc, hc } = chip.current_video_ts();
283            if vc != 0 || !(-1..=31).contains(&hc) {
284                return
285            }
286        }
287        else {
288            return
289        }
290        let _ = chip.execute_single_step::<_,CpuDebugFn>(cpu, None);
291    }
292}
293
294impl ComputerModel {
295    /// Returns the number of T-states per single frame.
296    pub fn frame_tstates(self) -> FTs {
297        use ComputerModel::*;
298        match self {
299            SpectrumNTSC => {
300                59136
301            }
302            Spectrum16|Spectrum48|
303            TimexTC2048|TimexTC2068|TimexTS2068 => {
304                69888
305            }
306            Spectrum128|SpectrumPlus2|
307            SpectrumPlus2A|SpectrumPlus3|SpectrumPlus3e|
308            SpectrumSE=> {
309                70908
310            }
311        }
312    }
313    /// Returns `issue` or [ReadEarMode::Clear] when it does not apply to the model.
314    pub fn applicable_issue(self, issue: ReadEarMode) -> ReadEarMode {
315        use ComputerModel::*;
316        match self {
317            Spectrum16|Spectrum48|SpectrumNTSC|
318            Spectrum128|SpectrumPlus2
319              => issue,
320            _ => ReadEarMode::Clear
321        }
322    }
323
324    pub fn validate_extensions(self, ext: Extensions) -> Result<(), Extensions> {
325        use ComputerModel::*;
326        match self {
327            Spectrum128|SpectrumPlus2|SpectrumSE if ext.intersects(Extensions::SAM_RAM) => Err(Extensions::SAM_RAM),
328            SpectrumPlus2A|SpectrumPlus3|SpectrumPlus3e
329            if ext.intersects(Extensions::SAM_RAM|Extensions::IF1|Extensions::PLUS_D|Extensions::DISCIPLE) => {
330                    Err(ext & (Extensions::SAM_RAM|Extensions::IF1|Extensions::PLUS_D|Extensions::DISCIPLE))
331            }
332            TimexTC2068|TimexTS2068 if ext.intersects(Extensions::SAM_RAM) => Err(Extensions::SAM_RAM),
333            _ => Ok(())
334        }
335    }
336}
337
338impl From<ComputerModel> for &str {
339    fn from(model: ComputerModel) -> Self {
340        use ComputerModel::*;
341        match model {
342            Spectrum16     => "ZX Spectrum 16k",
343            Spectrum48     => "ZX Spectrum 48k",
344            SpectrumNTSC   => "ZX Spectrum NTSC",
345            Spectrum128    => "ZX Spectrum 128k",
346            SpectrumPlus2  => "ZX Spectrum +2",
347            SpectrumPlus2A => "ZX Spectrum +2A",
348            SpectrumPlus3  => "ZX Spectrum +3",
349            SpectrumPlus3e => "ZX Spectrum +3e",
350            SpectrumSE     => "ZX Spectrum SE",
351            TimexTC2048    => "Timex TC2048",
352            TimexTC2068    => "Timex TC2068",
353            TimexTS2068    => "Timex TS2068",
354        }
355    }
356}
357
358impl From<&'_ ComputerModel> for &'static str {
359    fn from(model: &ComputerModel) -> Self {
360        (*model).into()
361    }
362}
363
364impl fmt::Display for ComputerModel {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        <&str>::from(self).fmt(f)
367    }
368}
369
370impl fmt::Display for Extensions {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        if self.intersects(Extensions::IF1) {
373            f.write_str(" + IF1")?;
374        }
375        if self.intersects(Extensions::ULA_PLUS) {
376            f.write_str(" + ULAPlus")?;
377        }
378        if self.intersects(Extensions::PLUS_D) {
379            f.write_str(" + MGT+D")?;
380        }
381        if self.intersects(Extensions::DISCIPLE) {
382            f.write_str(" + DISCiPLE")?;
383        }
384        if self.intersects(Extensions::SAM_RAM) {
385            f.write_str(" + SamRam")?;
386        }
387        Ok(())
388    }
389}