spectrusty_formats/
sna.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/*! **SNA** snapshot format utilities.
9
1048k `SNA` file (LSB first):
11
12| offset | size  | description                              |
13|--------|-------|------------------------------------------|
14|      0 |     1 | register: I                              |
15|      1 |     6 | registers: HL', DE', BC'                 |
16|      7 |     2 | registers: AF'                           |
17|      9 |     6 | registers: HL, DE, BC                    |
18|     15 |     4 | registers: IY, IX                        |
19|     19 |     1 | interrupt flags: bit 1=IFF1, bit 2=IFF1) |
20|     20 |     1 | register: R                              |
21|     21 |     4 | registers: AF                            |
22|     23 |     4 | register: SP                             |
23|     25 |     1 | interrupt mode: 0=IM0, 1=IM1, 2=IM2      |
24|     26 |     1 | border color: 0..=7                      |
25|     27 | 49152 | bytes: RAM 16384..=65535                 |
26
27Total size: `49179` bytes
28
29128k extended `SNA` file:
30
31| offset | size  | description                                   |
32|--------|-------|-----------------------------------------------|
33|      0 |    27 | 48k `SNA` header                              |
34|     27 | 16384 | bytes: RAM page 1, bank 5                     |
35|  16411 | 16384 | bytes: RAM page 2, bank 2                     |
36|  32795 | 16384 | bytes: RAM page 3, currently paged bank       |
37|  49179 |     2 | register:  PC                                 |
38|  49181 |     1 | byte: last OUT to 0x7ffd                      |
39|  49182 |     1 | byte: TR-DOS ROM 1=paged, 0=not paged         |
40|  49183 |*16384 | bytes: remaining RAM banks in ascending order |
41
42Total size:
43* `131103` bytes if RAM page 3 is not one of bank 5 or bank 2
44* `147487` bytes if RAM page 3 is bank 5 or bank 2 (included twice)
45
46*/
47use core::mem::size_of;
48use std::convert::TryInto;
49use std::io::{self, ErrorKind, Error, Read, Write, Seek, SeekFrom, Result};
50
51use spectrusty_core::{
52    chip::{Ula128MemFlags, ReadEarMode},
53    memory::ZxMemory,
54    video::BorderColor,
55    z80emu::{Cpu, Prefix, StkReg16, CpuFlags, InterruptMode, Z80NMOS}
56};
57
58use crate::{StructRead, StructWrite};
59use super::snapshot::*;
60
61#[derive(Clone, Copy, Debug, Default)]
62#[repr(C)]
63#[repr(packed)]
64struct SnaHeader {
65    i: u8,
66    hl_alt: [u8;2],
67    de_alt: [u8;2],
68    bc_alt: [u8;2],
69    f_alt: u8,
70    a_alt: u8,
71    hl: [u8;2],
72    de: [u8;2],
73    bc: [u8;2],
74    iy: [u8;2],
75    ix: [u8;2],
76    iffs: u8,
77    r: u8,
78    f: u8,
79    a: u8,
80    sp: [u8;2],
81    im: u8,
82    border: u8
83}
84
85#[derive(Clone, Copy, Debug, Default)]
86#[repr(C)]
87#[repr(packed)]
88struct SnaHeader128 {
89    pc: [u8;2],
90    port_data: u8,
91    trdos_rom: u8
92}
93
94// Structs must be packed and consist of `u8` or/and arrays of `u8` primitives only.
95unsafe impl StructRead for SnaHeader {}
96unsafe impl StructRead for SnaHeader128 {}
97unsafe impl StructWrite for SnaHeader {}
98unsafe impl StructWrite for SnaHeader128 {}
99
100/// The length in bytes of the 48k **SNA** file.
101pub const SNA_LENGTH: u64 = 49179;
102
103const PAGE_SIZE: usize = 0x4000;
104
105fn read_header<R: Read, C: Cpu>(rd: R, cpu: &mut C) -> Result<BorderColor> {
106    let sna = SnaHeader::read_new_struct(rd)?;
107    cpu.reset();
108    cpu.set_i(sna.i);
109    cpu.set_reg16(StkReg16::HL, u16::from_le_bytes(sna.hl_alt));
110    cpu.set_reg16(StkReg16::DE, u16::from_le_bytes(sna.de_alt));
111    cpu.set_reg16(StkReg16::BC, u16::from_le_bytes(sna.bc_alt));
112    cpu.exx();
113    cpu.set_acc(sna.a_alt);
114    cpu.set_flags(CpuFlags::from_bits_truncate(sna.f_alt));
115    cpu.ex_af_af();
116    cpu.set_reg16(StkReg16::HL, u16::from_le_bytes(sna.hl));
117    cpu.set_reg16(StkReg16::DE, u16::from_le_bytes(sna.de));
118    cpu.set_reg16(StkReg16::BC, u16::from_le_bytes(sna.bc));
119    cpu.set_index16(Prefix::Yfd, u16::from_le_bytes(sna.iy));
120    cpu.set_index16(Prefix::Xdd, u16::from_le_bytes(sna.ix));
121    let iff = sna.iffs & (1<<2) != 0;
122    cpu.set_iffs(iff, iff);
123    cpu.set_r(sna.r);
124    cpu.set_acc(sna.a);
125    cpu.set_flags(CpuFlags::from_bits_truncate(sna.f));
126    cpu.set_sp(u16::from_le_bytes(sna.sp));
127    cpu.set_im(sna.im.try_into().map_err(|_| {
128       Error::new(ErrorKind::InvalidData, "Not a proper SNA block: invalid interrupt mode")
129    })?);
130    sna.border.try_into().map_err(|e| Error::new(ErrorKind::InvalidData, e))
131}
132
133/// Reads a 48k **SNA** file and inserts its content into provided memory and configures the `Cpu`.
134/// Returns a border color on success.
135///
136/// # Note
137/// This function handles only the 48k **SNA** files.
138///
139/// # Errors
140/// This function will return an error if the file is too small.
141/// Other errors may also be returned from attempts to read the file.
142pub fn read_sna48<R: Read, M: ZxMemory, C: Cpu>(
143        mut rd: R,
144        cpu: &mut C,
145        mem: &mut M
146    ) -> Result<BorderColor>
147{
148    let border = read_header(rd.by_ref(), cpu)?;
149    let sp = cpu.get_sp();
150    cpu.set_sp(sp.wrapping_add(2));
151    cpu.inc_r();
152    cpu.inc_r(); // RETN would increase this 2 times
153    mem.load_into_mem(0x4000..=0xFFFF, rd).map_err(|_| {
154       Error::new(ErrorKind::InvalidData, "SNA: needs at least 48k RAM memory")
155    })?;
156    cpu.set_pc(mem.read16(sp));
157    Ok(border)
158}
159
160/// Loads an **SNA** file from `rd` into the provided snapshot `loader` implementing [SnapshotLoader].
161///
162/// Requires both [Read] and [Seek] implementations to determine the file version.
163///
164/// # Errors
165/// This function will return an error if the file size is incorrect or there is something wrong
166/// with the format.
167/// Other errors may also be returned from attempts to read the file.
168pub fn load_sna<R: Read + Seek, S: SnapshotLoader>(
169        mut rd: R,
170        loader: &mut S
171    ) -> Result<()>
172{
173    let cur_pos = rd.seek(SeekFrom::Current(0))?;
174    let end_pos = rd.seek(SeekFrom::Current(SNA_LENGTH as i64))?;
175    if end_pos - cur_pos != SNA_LENGTH {
176        return Err(Error::new(ErrorKind::InvalidData, "SNA: wrong size of the supplied stream"));
177    }
178
179    let mut sna_ext = SnaHeader128::default();
180    let ext_read = sna_ext.read_struct_or_nothing(rd.by_ref())?;
181
182    rd.seek(SeekFrom::Start(cur_pos))?;
183
184    if !ext_read {
185        return load_sna48(rd, loader)
186    }
187
188    let mut cpu = Z80NMOS::default();
189    let border = read_header(rd.by_ref(), &mut cpu)?;
190    cpu.set_pc(u16::from_le_bytes(sna_ext.pc));
191
192    let extensions = if sna_ext.trdos_rom != 0 {
193        Extensions::TR_DOS
194    }
195    else {
196        Extensions::NONE
197    };
198    let model = ComputerModel::Spectrum128;
199    loader.select_model(model, extensions, border, ReadEarMode::Issue3)
200          .map_err(|e| Error::new(ErrorKind::Other, e))?;
201
202    let index48 = [5, 2];
203    let last_page = Ula128MemFlags::from_bits_truncate(sna_ext.port_data)
204                    .last_ram_page_bank();
205    for page in index48.iter().chain(
206                    Some(&last_page).filter(|n| !index48.contains(n))
207                ) {
208        loader.read_into_memory(
209            MemoryRange::Ram(page * PAGE_SIZE..(page + 1) * PAGE_SIZE), rd.by_ref()
210        )?;
211    }
212
213    rd.seek(SeekFrom::Current(size_of::<SnaHeader128>() as i64))?;
214
215    for page in (0..8).filter(|n| !index48.contains(n) && *n != last_page) {
216        loader.read_into_memory(
217            MemoryRange::Ram(page * PAGE_SIZE..(page + 1) * PAGE_SIZE), rd.by_ref()
218        )?;
219    }
220    loader.assign_cpu(CpuModel::NMOS(cpu));
221    loader.write_port(0x7ffd, sna_ext.port_data);
222    if sna_ext.trdos_rom == 1 {
223        loader.tr_dos_rom_paged_in();
224    }
225    Ok(())
226}
227
228/// Loads a 48k **SNA** file from `rd` into the provided snapshot `loader` implementing [SnapshotLoader].
229///
230/// # Note
231/// This function handles only the 48k **SNA** files.
232///
233/// # Errors
234/// This function will return an error if the file is too small or the `SP` register points into the ROM page.
235/// Other errors may also be returned from attempts to read the file.
236pub fn load_sna48<R: Read, S: SnapshotLoader>(
237        mut rd: R,
238        loader: &mut S
239    ) -> Result<()>
240{
241    let mut cpu = Z80NMOS::default();
242    let border = read_header(rd.by_ref(), &mut cpu)?;
243    let sp = cpu.get_sp();
244    if sp < 0x4000 || sp == 0xFFFF  {
245        return Err(Error::new(ErrorKind::InvalidData, "SNA: can't determine the PC address"))
246    }
247    cpu.set_sp(sp.wrapping_add(2));
248    cpu.inc_r();
249    cpu.inc_r(); // RETN would increase this 2 times
250
251    let model = ComputerModel::Spectrum48;
252    loader.select_model(model, Default::default(), border, ReadEarMode::Issue3)
253          .map_err(|e| Error::new(ErrorKind::Other, e))?;
254
255    let pc_offset = sp as usize - 0x4000;
256    loader.read_into_memory(MemoryRange::Ram(0..pc_offset), rd.by_ref())?;
257    let mut pc = [0u8;2];
258    rd.read_exact(&mut pc)?;
259    cpu.set_pc(u16::from_le_bytes(pc));
260    let rest_offset = pc_offset + 2;
261    if rest_offset < 0xC000 {
262        loader.read_into_memory(MemoryRange::Ram(rest_offset..0xC000), rd)?;
263    }
264    loader.assign_cpu(CpuModel::NMOS(cpu));
265    Ok(())
266}
267
268fn make_header<C: Cpu>(cpu: &C) -> SnaHeader {
269    let mut sna = SnaHeader {
270        i: cpu.get_i(),
271        hl_alt: cpu.get_alt_reg16(StkReg16::HL).to_le_bytes(),
272        de_alt: cpu.get_alt_reg16(StkReg16::DE).to_le_bytes(),
273        bc_alt: cpu.get_alt_reg16(StkReg16::BC).to_le_bytes(),
274        hl: cpu.get_reg16(StkReg16::HL).to_le_bytes(),
275        de: cpu.get_reg16(StkReg16::DE).to_le_bytes(),
276        bc: cpu.get_reg16(StkReg16::BC).to_le_bytes(),
277        iy: cpu.get_index16(Prefix::Yfd).to_le_bytes(),
278        ix: cpu.get_index16(Prefix::Xdd).to_le_bytes(),
279        r: cpu.get_r(),
280        im: match cpu.get_im() {
281            InterruptMode::Mode0 => 0,
282            InterruptMode::Mode1 => 1,
283            InterruptMode::Mode2 => 2,
284        },
285        sp: cpu.get_sp().to_le_bytes(),
286        ..Default::default()
287    };
288    (sna.a_alt, sna.f_alt) = cpu.get_alt_reg2(StkReg16::AF);
289    (sna.a, sna.f) = cpu.get_reg2(StkReg16::AF);
290    let (iff1, _) = cpu.get_iffs();
291    sna.iffs = (iff1 as u8) << 2;
292    sna
293}
294
295/// Saves an **SNA** file into `wr` from the provided reference to a `snapshot` struct
296/// implementing [SnapshotCreator].
297///
298/// # Errors
299/// This function may return an error from attempts to write the file or if for some reason
300/// a snapshot could not be created.
301pub fn save_sna<C: SnapshotCreator, W: Write>(
302        snapshot: &C,
303        mut wr: W
304    ) -> Result<SnapshotResult>
305{
306    use ComputerModel::*;
307    let mut result = SnapshotResult::KEYB_ISSUE_NSUP;
308    let model = snapshot.model();
309    let is_128 = match model {
310        Spectrum48 => false,
311        Spectrum128 => true,
312        SpectrumPlus2|SpectrumPlus2A|SpectrumPlus3|SpectrumPlus3e|SpectrumSE => {
313            result.insert(SnapshotResult::MODEL_NSUP);
314            true
315        }
316        Spectrum16|SpectrumNTSC|TimexTC2048|TimexTC2068|TimexTS2068 => {
317            result.insert(SnapshotResult::MODEL_NSUP);
318            false
319        }
320    };
321
322    let extensions = snapshot.extensions();
323    if extensions.intersects(Extensions::IF1) && snapshot.is_interface1_rom_paged_in()
324       || extensions.intersects(Extensions::PLUS_D) && snapshot.is_plus_d_rom_paged_in()
325    {
326        return Err(Error::new(ErrorKind::InvalidInput,
327                "SNA: can't create a snapshot with the external ROM paged in"))
328    }
329    if extensions != Extensions::NONE && extensions != Extensions::TR_DOS {
330        result.insert(SnapshotResult::EXTENSTION_NSUP);
331    }
332
333    let cpu = match snapshot.cpu() {
334        CpuModel::NMOS(cpu) => cpu,
335        CpuModel::CMOS(cpu) => {
336            result.insert(SnapshotResult::CPU_MODEL_NSUP);
337            cpu.into_flavour()
338        },
339        CpuModel::BM1(cpu) => {
340            result.insert(SnapshotResult::CPU_MODEL_NSUP);
341            cpu.into_flavour()
342        }
343    };
344
345    if !is_cpu_safe_for_snapshot(&cpu) {
346        return Err(Error::new(ErrorKind::InvalidInput, "SNA: can't safely snapshot the CPU state"))
347    }
348
349    let mut sna = make_header(&cpu);
350    sna.border = snapshot.border_color().into();
351
352    if snapshot.joystick().is_some() {
353        result.insert(SnapshotResult::JOYSTICK_NSUP);
354    }
355
356    if is_128 || snapshot.ay_state(Ay3_891xDevice::Melodik).is_some()
357              || snapshot.ay_state(Ay3_891xDevice::FullerBox).is_some()
358              || snapshot.ay_state(Ay3_891xDevice::Timex).is_some() {
359        result.insert(SnapshotResult::SOUND_CHIP_NSUP);
360    }
361
362    if !is_128 {
363        return save_sna48(snapshot, cpu, model == ComputerModel::Spectrum16, sna, wr, result)
364    }
365
366    let memflags = snapshot.ula128_flags();
367    let mut sna_ext = SnaHeader128 {
368        pc: cpu.get_pc().to_le_bytes(),
369        port_data: memflags.bits(),
370        ..Default::default()
371    };
372
373    if extensions.intersects(Extensions::TR_DOS) {
374        sna_ext.trdos_rom = snapshot.is_tr_dos_rom_paged_in().into();
375    }
376
377    sna.write_struct(wr.by_ref())?;
378
379    let last_page: usize = memflags.last_ram_page_bank();
380    let index48 = [5,2,last_page];
381    for page in index48.iter() {
382        wr.write_all(
383            snapshot.memory_ref(MemoryRange::Ram(page * PAGE_SIZE..(page + 1) * PAGE_SIZE))?
384        )?;
385    }
386
387    sna_ext.write_struct(wr.by_ref())?;
388
389    for page in (0..8).filter(|n| !index48.contains(n) && *n != last_page) {
390        wr.write_all(
391            snapshot.memory_ref(MemoryRange::Ram(page * PAGE_SIZE..(page + 1) * PAGE_SIZE))?
392        )?;
393    }
394
395    wr.flush()?;
396    Ok(result)
397}
398
399fn save_sna48<C: SnapshotCreator, W: Write>(
400        snapshot: &C,
401        cpu: Z80NMOS,
402        is_mem16k: bool,
403        mut sna: SnaHeader,
404        mut wr: W,
405        result: SnapshotResult
406    ) -> Result<SnapshotResult>
407{
408    const ROMSIZE: usize = 0x4000;
409    let ramtop = if is_mem16k { 0x7FFF } else { 0xFFFF };
410    let sp = cpu.get_sp().wrapping_sub(2);
411    if (sp as usize) < ROMSIZE || sp >= ramtop  {
412        return Err(Error::new(ErrorKind::InvalidData, "SNA: can't store the PC address"))
413    }
414    sna.sp = sp.to_le_bytes();
415    sna.write_struct(wr.by_ref())?;
416    let pc = cpu.get_pc().to_le_bytes();
417    let pc_offset = sp as usize - ROMSIZE;
418    let mem_slice = snapshot.memory_ref(MemoryRange::Ram(0..pc_offset))?;
419    wr.write_all(mem_slice)?;
420    wr.write_all(&pc)?;
421    let mem_slice = snapshot.memory_ref(MemoryRange::Ram(pc_offset + 2..(ramtop as usize + 1) - ROMSIZE))?;
422    wr.write_all(mem_slice)?;
423    if is_mem16k {
424        io::copy(&mut io::repeat(!0).take(0x8000), &mut wr)?;
425    }
426    wr.flush()?;
427    Ok(result)
428}