Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
This commit is contained in:
158
src/native_core/bus/state.rs
Normal file
158
src/native_core/bus/state.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use super::*;
|
||||
use crate::native_core::apu::ApuStateTail;
|
||||
use crate::native_core::state_io as sio;
|
||||
|
||||
const BUS_STATE_CTX: &str = "bus state";
|
||||
|
||||
impl NativeBus {
|
||||
pub fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.cpu_ram);
|
||||
self.ppu.save_state(out);
|
||||
out.extend_from_slice(self.apu.registers());
|
||||
out.push(self.cpu_open_bus);
|
||||
out.push(self.joypad_state);
|
||||
out.push(self.joypad_shift);
|
||||
out.push(self.joypad2_state);
|
||||
out.push(self.joypad2_shift);
|
||||
out.push(u8::from(self.joypad_strobe));
|
||||
out.push(u8::from(self.nmi_pending));
|
||||
out.extend_from_slice(&self.ppu_dot.to_le_bytes());
|
||||
out.push(u8::from(self.odd_frame));
|
||||
out.push(u8::from(self.in_vblank));
|
||||
out.push(u8::from(self.mmc3_a12_prev_high));
|
||||
out.extend_from_slice(&self.mmc3_a12_low_dots.to_le_bytes());
|
||||
out.extend_from_slice(&self.mmc3_last_irq_scanline.to_le_bytes());
|
||||
self.apu.save_state_tail(out);
|
||||
|
||||
let mut mapper_state = Vec::new();
|
||||
self.mapper.save_state(&mut mapper_state);
|
||||
out.extend_from_slice(&(mapper_state.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(&mapper_state);
|
||||
}
|
||||
|
||||
pub fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
let mut cursor = 0usize;
|
||||
|
||||
self.cpu_ram.copy_from_slice(sio::take_exact(
|
||||
data,
|
||||
&mut cursor,
|
||||
CPU_RAM_SIZE,
|
||||
BUS_STATE_CTX,
|
||||
)?);
|
||||
let ppu_consumed = self.ppu.load_state(&data[cursor..])?;
|
||||
cursor = cursor.saturating_add(ppu_consumed);
|
||||
let mut apu_regs = [0u8; 0x20];
|
||||
apu_regs.copy_from_slice(sio::take_exact(data, &mut cursor, 0x20, BUS_STATE_CTX)?);
|
||||
self.apu.set_registers(apu_regs);
|
||||
self.frame_complete = false;
|
||||
|
||||
self.cpu_open_bus = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad2_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad2_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_strobe = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.nmi_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.suppress_vblank_this_frame = false;
|
||||
self.ppu_dot = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.ppu_dot %= PPU_DOTS_PER_FRAME;
|
||||
self.odd_frame = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.in_vblank = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.mmc3_a12_prev_high = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.mmc3_a12_low_dots = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
self.mmc3_last_irq_scanline = u32::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let frame_cycle = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let channel_enable_mask = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let mut length_counters = [0u8; 4];
|
||||
length_counters.copy_from_slice(sio::take_exact(data, &mut cursor, 4, BUS_STATE_CTX)?);
|
||||
let dmc_bytes_remaining = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_irq_enabled = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_cycle_counter = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_current_addr = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_sample_buffer = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_sample_buffer_valid = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_shift_reg = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_bits_remaining = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_silence = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_output_level = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_dma_request = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let mut envelope_divider = [0u8; 3];
|
||||
envelope_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||
let mut envelope_decay = [0u8; 3];
|
||||
envelope_decay.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||
let envelope_start_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let triangle_linear_counter = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let triangle_linear_reload_flag = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let mut sweep_divider = [0u8; 2];
|
||||
sweep_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 2, BUS_STATE_CTX)?);
|
||||
let sweep_reload_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let cpu_cycle_parity = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_reset_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_reset_delay = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let pending_frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let pending_frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.apu.load_state_tail(ApuStateTail {
|
||||
frame_cycle,
|
||||
frame_mode_5step,
|
||||
frame_irq_inhibit,
|
||||
frame_irq_pending,
|
||||
channel_enable_mask,
|
||||
length_counters,
|
||||
dmc_bytes_remaining,
|
||||
dmc_irq_enabled,
|
||||
dmc_irq_pending,
|
||||
dmc_cycle_counter,
|
||||
dmc_current_addr,
|
||||
dmc_sample_buffer,
|
||||
dmc_sample_buffer_valid,
|
||||
dmc_shift_reg,
|
||||
dmc_bits_remaining,
|
||||
dmc_silence,
|
||||
dmc_output_level,
|
||||
dmc_dma_request,
|
||||
envelope_divider,
|
||||
envelope_decay,
|
||||
envelope_start_flags,
|
||||
triangle_linear_counter,
|
||||
triangle_linear_reload_flag,
|
||||
sweep_divider,
|
||||
sweep_reload_flags,
|
||||
cpu_cycle_parity,
|
||||
frame_reset_pending,
|
||||
frame_reset_delay,
|
||||
pending_frame_mode_5step,
|
||||
pending_frame_irq_inhibit,
|
||||
});
|
||||
let mapper_len = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)? as usize;
|
||||
let mapper_state = sio::take_exact(data, &mut cursor, mapper_len, BUS_STATE_CTX)?;
|
||||
|
||||
self.ppu.set_vblank(self.in_vblank);
|
||||
self.mapper.load_state(mapper_state)?;
|
||||
|
||||
if cursor != data.len() {
|
||||
return Err("bus state: trailing bytes in payload".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user