diff --git a/gb-vst/src/lib.rs b/gb-vst/src/lib.rs index 1d5b0e3..753ba47 100644 --- a/gb-vst/src/lib.rs +++ b/gb-vst/src/lib.rs @@ -1,13 +1,16 @@ use async_ringbuf::AsyncHeapConsumer; use futures::executor; use gb_emu_lib::{ - connect::{AudioOutput, CpuSaveState, DownsampleType, EmulatorMessage, JoypadButtons, RomFile}, + connect::{ + AudioOutput, CpuSaveState, DownsampleType, EmulatorMessage, JoypadButtons, RomFile, + SerialTarget, + }, EmulatorCore, }; use nih_plug::prelude::*; use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField}; use std::sync::{ - mpsc::{channel, Receiver, Sender}, + mpsc::{self, channel, Receiver, Sender}, Arc, Mutex, }; use ui::{Emulator, EmulatorRenderer}; @@ -42,6 +45,7 @@ struct EmuVars { rx: AsyncHeapConsumer<[f32; 2]>, sender: Sender, emulator_core: EmulatorCore<[u8; 4]>, + serial_tx: Sender, } #[derive(Default)] @@ -101,12 +105,84 @@ impl Plugin for GameboyEmu { &mut self, buffer: &mut Buffer, _: &mut AuxiliaryBuffers, - _c: &mut impl ProcessContext, + context: &mut impl ProcessContext, ) -> ProcessStatus { - while let Some(event) = _c.next_event() { - if let Some(Basic(_midi)) = event.as_midi() {} - } if let Some(ref mut vars) = self.vars { + while let Some(event) = context.next_event() { + if let Some(Basic(as_bytes)) = event.as_midi() { + // nih_log!( + // "bytes: {:#X}, {:#X}, {:#X}", + // as_bytes[0], + // as_bytes[1], + // as_bytes[2] + // ); + match event { + NoteEvent::NoteOn { + timing: _, + voice_id: _, + channel, + note: _, + velocity: _, + } => { + if channel < 5 { + println!("noteon: {as_bytes:#?}"); + vars.serial_tx.send(0x90 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::NoteOff { + timing: _, + voice_id: _, + channel, + note: _, + velocity: _, + } => { + if channel < 5 { + println!("noteoff: {as_bytes:#?}"); + vars.serial_tx.send(0x80 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiPitchBend { + timing: _, + channel, + value: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xE0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiCC { + timing: _, + channel, + cc: _, + value: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xB0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiProgramChange { + timing: _, + channel, + program: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xC0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + _ => {} + } + } + } if buffer.channels() != 2 { panic!() } @@ -121,6 +197,8 @@ impl Plugin for GameboyEmu { } } vars.emulator_core.run_until_buffer_full(); + } else { + while context.next_event().is_some() {} } self.update_save_state(); ProcessStatus::KeepAlive @@ -174,22 +252,31 @@ impl Plugin for GameboyEmu { let window = Box::new(renderer); - let mut emulator_core = - if let Some(state) = self.params.last_save_state.state.lock().unwrap().take() { - EmulatorCore::from_save_state(state, rom, receiver, window, output) - } else { - let options = gb_emu_lib::Options::new(rom) - .with_bootrom(bootrom) - .force_no_save(); + let (serial_tx, gb_serial_rx) = mpsc::channel::(); + let serial_target = SerialTarget::Custom { + rx: Some(gb_serial_rx), + tx: None, + }; - EmulatorCore::init(receiver, options, window, output, None) - }; + let mut emulator_core = if let Some(state) = + self.params.last_save_state.state.lock().unwrap().take() + { + EmulatorCore::from_save_state(state, rom, receiver, window, output, serial_target) + } else { + let options = gb_emu_lib::Options::new(rom) + .with_bootrom(bootrom) + .with_serial_target(serial_target) + .force_no_save(); + + EmulatorCore::init(receiver, options, window, output, None) + }; emulator_core.run_until_buffer_full(); self.vars = Some(EmuVars { rx, sender, emulator_core, + serial_tx, }); self.update_save_state(); } diff --git a/lib/src/connect/mod.rs b/lib/src/connect/mod.rs index d89e41f..12a73a3 100644 --- a/lib/src/connect/mod.rs +++ b/lib/src/connect/mod.rs @@ -1,5 +1,6 @@ use crate::processor::memory::mmio::gpu::Colour; pub use crate::processor::memory::mmio::joypad::{JoypadButtons, JoypadState}; +pub use crate::processor::memory::mmio::serial::SerialTarget; pub use crate::processor::CpuSaveState; pub use crate::{HEIGHT, WIDTH}; use async_ringbuf::{AsyncHeapConsumer, AsyncHeapProducer, AsyncHeapRb}; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 01e5edb..6106ef5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,7 +1,7 @@ #![feature(exclusive_range_pattern, let_chains, bigint_helper_methods)] use crate::{processor::memory::Memory, util::pause}; -use connect::{AudioOutput, EmulatorMessage, Renderer, RomFile}; +use connect::{AudioOutput, EmulatorMessage, Renderer, RomFile, SerialTarget}; use once_cell::sync::OnceCell; use processor::{ memory::{mmio::gpu::Colour, Rom}, @@ -28,7 +28,7 @@ pub struct Options { pub save_path: Option, pub no_save: bool, pub bootrom: Option, - pub connect_serial: bool, + pub serial_target: SerialTarget, pub verbose: bool, } @@ -39,7 +39,7 @@ impl Options { save_path: None, no_save: false, bootrom: None, - connect_serial: false, + serial_target: SerialTarget::None, verbose: false, } } @@ -59,8 +59,13 @@ impl Options { self } - pub fn with_serial(mut self) -> Self { - self.connect_serial = true; + pub fn with_stdout(mut self) -> Self { + self.serial_target = SerialTarget::Stdout; + self + } + + pub fn with_serial_target(mut self, target: SerialTarget) -> Self { + self.serial_target = target; self } @@ -141,7 +146,7 @@ impl + Clone> EmulatorCore { rom, window, output, - options.connect_serial, + options.serial_target, tile_window, ), bootrom_enabled, @@ -212,6 +217,7 @@ impl + Clone> EmulatorCore { receiver: Receiver, window: Box>, output: AudioOutput, + serial_target: SerialTarget, ) -> Self { let data = match rom { RomFile::Path(path) => match fs::read(path) { @@ -225,7 +231,7 @@ impl + Clone> EmulatorCore { }; Self { receiver, - cpu: Cpu::from_save_state(state, data, window, output), + cpu: Cpu::from_save_state(state, data, window, output, serial_target), } } } diff --git a/lib/src/processor/memory.rs b/lib/src/processor/memory.rs index 4726633..2090ca1 100644 --- a/lib/src/processor/memory.rs +++ b/lib/src/processor/memory.rs @@ -3,12 +3,13 @@ use self::{ mmio::{ apu::ApuSaveState, gpu::{Colour, GpuSaveState}, + serial::SerialSaveState, Apu, Gpu, Joypad, Serial, Timer, }, rom::RomSaveState, }; use crate::{ - connect::{AudioOutput, JoypadState, Renderer}, + connect::{AudioOutput, JoypadState, Renderer, SerialTarget}, processor::SplitRegister, verbose_println, Cpu, }; @@ -52,7 +53,7 @@ pub struct MemorySaveState + Clone> { joypad: Joypad, gpu: GpuSaveState, apu: ApuSaveState, - serial: Serial, + serial: SerialSaveState, timers: Timer, } @@ -69,7 +70,7 @@ impl + Clone> MemorySaveState { joypad: memory.joypad, gpu: GpuSaveState::create(&memory.gpu), apu: ApuSaveState::create(&memory.apu), - serial: memory.serial, + serial: SerialSaveState::create(&memory.serial), timers: memory.timers, } } @@ -81,14 +82,9 @@ impl + Clone> Memory { rom: Rom, window: Box>, output: AudioOutput, - connect_serial: bool, + serial_target: SerialTarget, tile_window: Option>>, ) -> Self { - let serial = if connect_serial { - Serial::default().connected() - } else { - Serial::default() - }; Self { bootrom, rom, @@ -101,7 +97,7 @@ impl + Clone> Memory { joypad: Joypad::default(), gpu: Gpu::new(window, tile_window), apu: Apu::new(output), - serial, + serial: Serial::new(serial_target), timers: Timer::init(), } } @@ -282,6 +278,7 @@ impl + Clone> Memory { data: Vec, window: Box>, output: AudioOutput, + serial_target: SerialTarget, ) -> Self { Self { bootrom: None, @@ -295,7 +292,7 @@ impl + Clone> Memory { joypad: state.joypad, gpu: Gpu::from_save_state(state.gpu, window, None), apu: Apu::from_save_state(state.apu, output), - serial: state.serial, + serial: Serial::from_save_state(state.serial, serial_target), timers: state.timers, } } diff --git a/lib/src/processor/memory/mmio/mod.rs b/lib/src/processor/memory/mmio/mod.rs index 20e7425..8b64cce 100644 --- a/lib/src/processor/memory/mmio/mod.rs +++ b/lib/src/processor/memory/mmio/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod apu; pub(crate) mod gpu; pub(crate) mod joypad; -mod serial; +pub(crate) mod serial; mod timer; pub use apu::Apu; pub use gpu::Gpu; diff --git a/lib/src/processor/memory/mmio/serial.rs b/lib/src/processor/memory/mmio/serial.rs index 6e7925c..1e73062 100644 --- a/lib/src/processor/memory/mmio/serial.rs +++ b/lib/src/processor/memory/mmio/serial.rs @@ -1,24 +1,27 @@ -use std::io::{stdout, Write}; +use std::{ + io::{stdout, Write}, + sync::mpsc::{Receiver, Sender}, +}; use serde::{Deserialize, Serialize}; use crate::util::get_bit; -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)] enum ClockSource { Internal, External, } #[allow(dead_code)] -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, Debug)] enum ClockSpeed { Normal, Fast, } #[allow(dead_code)] -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, Debug)] struct SerialControl { transfer_in_progress: bool, clock_speed: ClockSpeed, @@ -35,36 +38,157 @@ impl Default for SerialControl { } } -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] +pub enum SerialTarget { + Stdout, + Custom { + #[serde(skip)] + rx: Option>, + #[serde(skip)] + tx: Option>, + }, + None, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Default)] +struct InputByte { + byte: Option, + progress: u8, + input_delay: usize, +} + +const BYTE_DELAY: usize = 20000; + +impl InputByte { + fn advance(&mut self, rx: &Option>) -> u8 { + if let Some(byte) = self.byte { + let val = (byte >> (7 - self.progress)) & 0b1; + self.progress += 1; + if self.progress >= 8 { + self.byte = None; + } + val + } else if let Some(byte) = self.try_fill(rx) { + self.progress += 1; + (byte >> 7) & 0b1 + } else { + 0 + } + } + + fn try_fill(&mut self, rx: &Option>) -> Option { + self.byte = None; + self.progress = 0; + if let Some(rx) = rx { + if let Ok(new) = rx.try_recv() { + self.byte = Some(new); + self.input_delay = BYTE_DELAY; + return self.byte; + } + } + None + } + + fn is_ready(&mut self, rx: &Option>) -> bool { + if self.byte.is_none() { + self.try_fill(rx); + } + self.byte.is_some() && self.input_delay == 0 + } +} + pub struct Serial { byte: u8, output_byte: u8, + input_byte: InputByte, bits_remaining: u8, control: SerialControl, - is_connected: bool, + target: SerialTarget, +} + +#[derive(Serialize, Deserialize)] +pub struct SerialSaveState { + byte: u8, + output_byte: u8, + input_byte: InputByte, + bits_remaining: u8, + control: SerialControl, +} + +impl SerialSaveState { + pub fn create(serial: &Serial) -> Self { + Self { + byte: serial.byte, + output_byte: serial.output_byte, + input_byte: serial.input_byte, + bits_remaining: serial.bits_remaining, + control: serial.control, + } + } } impl Serial { - pub fn connected(mut self) -> Self { - self.is_connected = true; - self + pub fn new(target: SerialTarget) -> Self { + Self { + byte: 0, + output_byte: 0, + input_byte: InputByte::default(), + bits_remaining: 7, + control: SerialControl::default(), + target, + } + } + + pub fn from_save_state(state: SerialSaveState, target: SerialTarget) -> Self { + Self { + byte: state.byte, + output_byte: state.output_byte, + input_byte: state.input_byte, + bits_remaining: state.bits_remaining, + control: state.control, + target, + } + } + + fn is_connected(&self) -> bool { + !matches!(&self.target, SerialTarget::None) } pub fn tick(&mut self, steps: usize) -> bool { let mut will_interrupt = false; - if !self.is_connected { + if !self.is_connected() { return false; } + let rx = if let SerialTarget::Custom { rx, tx: _ } = &self.target { + rx + } else { + &None + }; for _ in 0..steps { - if self.control.transfer_in_progress { + self.input_byte.input_delay = self.input_byte.input_delay.saturating_sub(1); + if (self.control.transfer_in_progress + && self.control.clock_source == ClockSource::Internal) + || (self.control.clock_source == ClockSource::External + && self.input_byte.is_ready(rx)) + { self.output_byte = self.output_byte << 1 | self.byte >> 7; - self.byte = (self.byte << 1) | 0b1; + self.byte = (self.byte << 1) | self.input_byte.advance(rx); let (remainder, finished) = self.bits_remaining.overflowing_sub(1); self.bits_remaining = if finished { self.control.transfer_in_progress = false; will_interrupt = true; - print!("{}", self.output_byte as char); - stdout().flush().unwrap(); + match &self.target { + SerialTarget::Stdout => { + print!("{}", self.output_byte as char); + stdout().flush().unwrap(); + } + SerialTarget::Custom { rx: _, tx } => { + if let Some(tx) = tx { + tx.send(self.output_byte).unwrap(); + } + } + SerialTarget::None => {} + } 7 } else { remainder @@ -84,6 +208,7 @@ impl Serial { pub fn update_control(&mut self, data: u8) { self.control.transfer_in_progress = get_bit(data, 7); + // CGB - add clock speed self.control.clock_source = if get_bit(data, 0) { ClockSource::Internal @@ -105,15 +230,3 @@ impl Serial { } } } - -impl Default for Serial { - fn default() -> Self { - Self { - byte: 0, - output_byte: 0, - bits_remaining: 7, - control: SerialControl::default(), - is_connected: false, - } - } -} diff --git a/lib/src/processor/mod.rs b/lib/src/processor/mod.rs index 583071a..af8aca0 100644 --- a/lib/src/processor/mod.rs +++ b/lib/src/processor/mod.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use self::memory::{mmio::gpu::Colour, Interrupt, Memory, MemorySaveState}; use crate::{ - connect::{AudioOutput, Renderer}, + connect::{AudioOutput, Renderer, SerialTarget}, verbose_println, }; @@ -159,9 +159,10 @@ impl + Clone> Cpu { data: Vec, window: Box>, output: AudioOutput, + serial_target: SerialTarget, ) -> Self { Self { - memory: Memory::from_save_state(state.memory, data, window, output), + memory: Memory::from_save_state(state.memory, data, window, output, serial_target), reg: state.reg, last_instruction: state.last_instruction, last_instruction_addr: state.last_instruction_addr,