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:
264
src/native_core/apu/api.rs
Normal file
264
src/native_core/apu/api.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use super::types::{Apu, ApuStateTail};
|
||||
|
||||
impl Apu {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
io: [0; 0x20],
|
||||
frame_cycle: 0,
|
||||
frame_mode_5step: false,
|
||||
frame_irq_inhibit: false,
|
||||
frame_irq_pending: false,
|
||||
channel_enable_mask: 0,
|
||||
length_counters: [0; 4],
|
||||
dmc_bytes_remaining: 0,
|
||||
dmc_irq_enabled: false,
|
||||
dmc_irq_pending: false,
|
||||
dmc_cycle_counter: 0,
|
||||
dmc_current_addr: 0xC000,
|
||||
dmc_sample_buffer: 0,
|
||||
dmc_sample_buffer_valid: false,
|
||||
dmc_shift_reg: 0,
|
||||
dmc_bits_remaining: 8,
|
||||
dmc_silence: true,
|
||||
dmc_output_level: 0,
|
||||
dmc_dma_request: false,
|
||||
envelope_divider: [0; 3],
|
||||
envelope_decay: [0; 3],
|
||||
envelope_start_flags: 0,
|
||||
triangle_linear_counter: 0,
|
||||
triangle_linear_reload_flag: false,
|
||||
sweep_divider: [0; 2],
|
||||
sweep_reload_flags: 0,
|
||||
cpu_cycle_parity: false,
|
||||
frame_reset_pending: false,
|
||||
frame_reset_delay: 0,
|
||||
pending_frame_mode_5step: false,
|
||||
pending_frame_irq_inhibit: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registers(&self) -> &[u8; 0x20] {
|
||||
&self.io
|
||||
}
|
||||
|
||||
pub fn set_registers(&mut self, regs: [u8; 0x20]) {
|
||||
self.io = regs;
|
||||
}
|
||||
|
||||
pub fn read(&mut self, addr: u16) -> u8 {
|
||||
match addr {
|
||||
0x4000..=0x4013 => self.io[(addr as usize) - 0x4000],
|
||||
0x4015 => self.read_status(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x4000..=0x4013 => {
|
||||
self.io[(addr as usize) - 0x4000] = value;
|
||||
match addr {
|
||||
0x4003 => {
|
||||
self.reload_length_counter(0, value >> 3);
|
||||
self.envelope_start_flags |= 1 << 0;
|
||||
}
|
||||
0x4001 => {
|
||||
self.sweep_reload_flags |= 1 << 0;
|
||||
}
|
||||
0x4007 => {
|
||||
self.reload_length_counter(1, value >> 3);
|
||||
self.envelope_start_flags |= 1 << 1;
|
||||
}
|
||||
0x4005 => {
|
||||
self.sweep_reload_flags |= 1 << 1;
|
||||
}
|
||||
0x400B => {
|
||||
self.reload_length_counter(2, value >> 3);
|
||||
self.triangle_linear_reload_flag = true;
|
||||
}
|
||||
0x400F => {
|
||||
self.reload_length_counter(3, value >> 3);
|
||||
self.envelope_start_flags |= 1 << 2;
|
||||
}
|
||||
0x4010 => {
|
||||
self.dmc_irq_enabled = (value & 0x80) != 0;
|
||||
if !self.dmc_irq_enabled {
|
||||
self.dmc_irq_pending = false;
|
||||
}
|
||||
}
|
||||
0x4011 => {
|
||||
self.dmc_output_level = value & 0x7F;
|
||||
}
|
||||
0x4012 => {
|
||||
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
0x4014 => {
|
||||
self.io[0x14] = value;
|
||||
}
|
||||
0x4015 => {
|
||||
self.io[(addr as usize) - 0x4000] = value;
|
||||
self.channel_enable_mask = value & 0x1F;
|
||||
self.dmc_irq_pending = false;
|
||||
for chan in 0..4usize {
|
||||
if (self.channel_enable_mask & (1 << chan)) == 0 {
|
||||
self.length_counters[chan] = 0;
|
||||
}
|
||||
}
|
||||
if (self.channel_enable_mask & 0x10) == 0 {
|
||||
self.dmc_bytes_remaining = 0;
|
||||
self.dmc_cycle_counter = 0;
|
||||
self.dmc_dma_request = false;
|
||||
} else if self.dmc_bytes_remaining == 0 {
|
||||
self.dmc_bytes_remaining = self.dmc_sample_length_bytes();
|
||||
self.dmc_cycle_counter = self.dmc_byte_period();
|
||||
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||
if !self.dmc_sample_buffer_valid {
|
||||
self.dmc_dma_request = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
0x4017 => {
|
||||
self.io[(addr as usize) - 0x4000] = value;
|
||||
self.pending_frame_mode_5step = (value & 0x80) != 0;
|
||||
self.pending_frame_irq_inhibit = (value & 0x40) != 0;
|
||||
self.frame_reset_delay = if self.cpu_cycle_parity { 4 } else { 3 };
|
||||
self.frame_reset_pending = true;
|
||||
if self.pending_frame_irq_inhibit {
|
||||
self.frame_irq_pending = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clock_cpu_cycle(&mut self) {
|
||||
let mut skip_frame_counter_clock = false;
|
||||
if self.frame_reset_pending {
|
||||
self.frame_reset_delay = self.frame_reset_delay.saturating_sub(1);
|
||||
if self.frame_reset_delay == 0 {
|
||||
self.frame_reset_pending = false;
|
||||
self.frame_mode_5step = self.pending_frame_mode_5step;
|
||||
self.frame_irq_inhibit = self.pending_frame_irq_inhibit;
|
||||
self.frame_cycle = 0;
|
||||
if self.frame_mode_5step {
|
||||
self.clock_quarter_frame();
|
||||
self.clock_half_frame();
|
||||
}
|
||||
skip_frame_counter_clock = true;
|
||||
}
|
||||
}
|
||||
if !skip_frame_counter_clock {
|
||||
self.clock_frame_counter();
|
||||
}
|
||||
self.clock_dmc();
|
||||
self.cpu_cycle_parity = !self.cpu_cycle_parity;
|
||||
}
|
||||
|
||||
pub fn poll_irq(&self) -> bool {
|
||||
self.dmc_irq_pending || (self.frame_irq_pending && !self.frame_irq_inhibit)
|
||||
}
|
||||
|
||||
pub fn take_dmc_dma_request(&mut self) -> Option<u16> {
|
||||
if self.dmc_dma_request {
|
||||
Some(self.dmc_current_addr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provide_dmc_dma_byte(&mut self, byte: u8) {
|
||||
if !self.dmc_dma_request {
|
||||
return;
|
||||
}
|
||||
self.dmc_dma_request = false;
|
||||
self.dmc_sample_buffer = byte;
|
||||
self.dmc_sample_buffer_valid = true;
|
||||
self.dmc_current_addr = if self.dmc_current_addr == 0xFFFF {
|
||||
0x8000
|
||||
} else {
|
||||
self.dmc_current_addr.wrapping_add(1)
|
||||
};
|
||||
|
||||
if self.dmc_bytes_remaining > 0 {
|
||||
self.dmc_bytes_remaining -= 1;
|
||||
}
|
||||
if self.dmc_bytes_remaining == 0 {
|
||||
if (self.io[0x10] & 0x40) != 0 {
|
||||
self.dmc_bytes_remaining = self.dmc_sample_length_bytes();
|
||||
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||
self.dmc_dma_request = true;
|
||||
} else if self.dmc_irq_enabled {
|
||||
self.dmc_irq_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_state_tail(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.frame_cycle.to_le_bytes());
|
||||
out.push(u8::from(self.frame_mode_5step));
|
||||
out.push(u8::from(self.frame_irq_inhibit));
|
||||
out.push(u8::from(self.frame_irq_pending));
|
||||
out.push(self.channel_enable_mask);
|
||||
out.extend_from_slice(&self.length_counters);
|
||||
out.extend_from_slice(&self.dmc_bytes_remaining.to_le_bytes());
|
||||
out.push(u8::from(self.dmc_irq_enabled));
|
||||
out.push(u8::from(self.dmc_irq_pending));
|
||||
out.extend_from_slice(&self.dmc_cycle_counter.to_le_bytes());
|
||||
out.extend_from_slice(&self.dmc_current_addr.to_le_bytes());
|
||||
out.push(self.dmc_sample_buffer);
|
||||
out.push(u8::from(self.dmc_sample_buffer_valid));
|
||||
out.push(self.dmc_shift_reg);
|
||||
out.push(self.dmc_bits_remaining);
|
||||
out.push(u8::from(self.dmc_silence));
|
||||
out.push(self.dmc_output_level);
|
||||
out.push(u8::from(self.dmc_dma_request));
|
||||
out.extend_from_slice(&self.envelope_divider);
|
||||
out.extend_from_slice(&self.envelope_decay);
|
||||
out.push(self.envelope_start_flags);
|
||||
out.push(self.triangle_linear_counter);
|
||||
out.push(u8::from(self.triangle_linear_reload_flag));
|
||||
out.extend_from_slice(&self.sweep_divider);
|
||||
out.push(self.sweep_reload_flags);
|
||||
out.push(u8::from(self.cpu_cycle_parity));
|
||||
out.push(u8::from(self.frame_reset_pending));
|
||||
out.push(self.frame_reset_delay);
|
||||
out.push(u8::from(self.pending_frame_mode_5step));
|
||||
out.push(u8::from(self.pending_frame_irq_inhibit));
|
||||
}
|
||||
|
||||
pub fn load_state_tail(&mut self, state: ApuStateTail) {
|
||||
self.frame_cycle = state.frame_cycle;
|
||||
self.frame_mode_5step = state.frame_mode_5step;
|
||||
self.frame_irq_inhibit = state.frame_irq_inhibit;
|
||||
self.frame_irq_pending = state.frame_irq_pending;
|
||||
self.channel_enable_mask = state.channel_enable_mask;
|
||||
self.length_counters = state.length_counters;
|
||||
self.dmc_bytes_remaining = state.dmc_bytes_remaining;
|
||||
self.dmc_irq_enabled = state.dmc_irq_enabled;
|
||||
self.dmc_irq_pending = state.dmc_irq_pending;
|
||||
self.dmc_cycle_counter = state.dmc_cycle_counter;
|
||||
self.dmc_current_addr = state.dmc_current_addr;
|
||||
self.dmc_sample_buffer = state.dmc_sample_buffer;
|
||||
self.dmc_sample_buffer_valid = state.dmc_sample_buffer_valid;
|
||||
self.dmc_shift_reg = state.dmc_shift_reg;
|
||||
self.dmc_bits_remaining = state.dmc_bits_remaining.max(1);
|
||||
self.dmc_silence = state.dmc_silence;
|
||||
self.dmc_output_level = state.dmc_output_level & 0x7F;
|
||||
self.dmc_dma_request = state.dmc_dma_request;
|
||||
self.envelope_divider = state.envelope_divider;
|
||||
self.envelope_decay = state.envelope_decay;
|
||||
self.envelope_start_flags = state.envelope_start_flags & 0x07;
|
||||
self.triangle_linear_counter = state.triangle_linear_counter & 0x7F;
|
||||
self.triangle_linear_reload_flag = state.triangle_linear_reload_flag;
|
||||
self.sweep_divider = state.sweep_divider;
|
||||
self.sweep_reload_flags = state.sweep_reload_flags & 0x03;
|
||||
self.cpu_cycle_parity = state.cpu_cycle_parity;
|
||||
self.frame_reset_pending = state.frame_reset_pending;
|
||||
self.frame_reset_delay = state.frame_reset_delay;
|
||||
self.pending_frame_mode_5step = state.pending_frame_mode_5step;
|
||||
self.pending_frame_irq_inhibit = state.pending_frame_irq_inhibit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user