From e6614f3e1557a1c6bc7351b9ccb4a8a37d50bfbd Mon Sep 17 00:00:00 2001 From: transistor Date: Tue, 14 Mar 2023 20:05:29 -0700 Subject: [PATCH] Refactored to allow dummy audio for console frontend --- .gitignore | 2 + Cargo.lock | 1 + docs/discussion.txt | 74 +++++++++++++++++++ emulator/frontends/common/src/audio.rs | 67 ++--------------- emulator/frontends/common/src/cpal.rs | 63 ++++++++++++++++ emulator/frontends/common/src/lib.rs | 7 +- emulator/frontends/console/Cargo.toml | 1 + .../console/src/bin/moa-console-genesis.rs | 23 ++++++ emulator/frontends/console/src/lib.rs | 15 +++- emulator/frontends/minifb/src/lib.rs | 3 +- .../frontends/pixels/src/bin/moa-genesis.rs | 2 +- emulator/frontends/pixels/src/frontend.rs | 2 +- 12 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 docs/discussion.txt create mode 100644 emulator/frontends/common/src/cpal.rs create mode 100644 emulator/frontends/console/src/bin/moa-console-genesis.rs diff --git a/.gitignore b/.gitignore index 0f03ea3..085c4ed 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ perf.data.old binaries/*/*.asm binaries/*/*.bin binaries/*/*.smd + +emulator/frontends/pixels/dist/ diff --git a/Cargo.lock b/Cargo.lock index af57cc9..bcc946a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,6 +600,7 @@ dependencies = [ name = "moa_console" version = "0.1.0" dependencies = [ + "clap 3.2.22", "log", "moa_common", "moa_computie", diff --git a/docs/discussion.txt b/docs/discussion.txt new file mode 100644 index 0000000..a93ee3f --- /dev/null +++ b/docs/discussion.txt @@ -0,0 +1,74 @@ + +2021/10/21: + +Frontend/Backend Interface +-------------------------- + +- need a way for the frontend window to be updated with graphics data from a backend device +- also need access to input key and joystick presses; other interfaces can be used for audio, etc +- it would be nice if it was possible to have multiple video output devices in a system + outputting to different windows (ie. one window per video device) + + +- either the frontend calls a function in the backend to update the window, or the backend + calls an indirect function on the frontend to update the window + +- Frontend calling backend: + - Opt 1 - supply a callback and object separately, pass object to callback (no closure) + - older way but should work + - object needs to be Arc in order to share, but doesn't need to be wrapped in a tuple struct + - con: if object is device itself, would still need to use tuple struct wrapper for Addressable + - Opt 2 - supply a trait object with an update method + - frontend would store a Box + - device would have to make a wrapper: `struct WindowWrapper(Arc)` and + impl Window on WindowWrapper, and `struct DeviceWrapper(Arc)` and impl + Addressable/Steppable on DeviceWrapper + - pro: in-sync on-demand rendering + - con: lots of complications and indirection + - Opt 3 - supply a common object that devices can update, and that then updates the window + - backend would define a Frame type object which would contain a rendered frame + - system thread would render to the Frame, ui thread would then copy the Frame to window + - con: lots of copying of pixel data + - con: out of sync rendering + - pro: ui can handle any scaling + +- Backend calling frontend: + - Opt 4 - host can produce a generic window object that satisfies Window trait + - the device struct which needs the window would have a type parameter for the window + object, and the system thread (step) would call an update function on the generic + window to copy the rendered frame to a ui buffer, which then copies again on ui thread + - window object can only have a buffer, but it can use a native format rather than internal format + - con: out of sync rendering + - Opt 5 - host can produce a Window trait object + - the device would just store the trait object so no need for a type param + - the frontend can't put the native window in the window object so it would need to be a buffer + - con: would need a wrapper if the frontend needs internal access to the common device + - this is + +- Opt 3, 4, and 5 would all involve an intermediate buffer, but with Opt 4 or 5, that buffer can be native-compatible +- Opt 3, 4, and 5 are out of sync updating + +- Opt 1 and 2 can be in-sync updating if the device object is supplied, but not if it uses an intermediate buffer + +- it seems like Opt 4 isn't working because you can't make an existential generic (can't return MiniWindow as W: Window) +- it seems Opt 5 works, but is trying to update the screen waaay too much, and causing the sim to almost never move. + Even with a simple count limit, it seems to pause when it tries updating the screen, probably due to lock contention +- Opt 1 is misleading because you still need a shared object, and it can't be a generic, so it's either a backend-specific + struct or a dyn trait object, so really it's the same as Opt 2, or Opt 3 +- it might be possible to have a common data struct that contains 2 frames, one that can only be updated by the sim, and one + that can only be read by in-sync update function, and they are swapped on update (if the writable one isn't locked), so + that the updating is still in sync in the ui thread, but the rendering is happening in another thread + + +2021/12/07: + +Signals, Etc. +------------- + +* think more about what kinds of signals are used: + - one setter with one or more passive listeners (bank_register, updated by writing to and used whenever a value is read) + - one or more setters and one listeners (reset, bus_request for the CPUs) + - one one-shot setter (no reset) with one active listener that resets the signal (certain interrupts, including a vsync interrupt) + - what about interrupt controller? + + diff --git a/emulator/frontends/common/src/audio.rs b/emulator/frontends/common/src/audio.rs index f2e3143..04172d8 100644 --- a/emulator/frontends/common/src/audio.rs +++ b/emulator/frontends/common/src/audio.rs @@ -1,17 +1,16 @@ use std::sync::{Arc, Mutex}; use std::collections::VecDeque; -use cpal::{Stream, SampleRate, SampleFormat, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}}; -use moa_core::{Clock, warn, error}; +use moa_core::Clock; use moa_core::host::{Audio, ClockedQueue}; -const SAMPLE_RATE: usize = 48000; +pub const SAMPLE_RATE: usize = 48000; #[derive(Clone, Default)] pub struct AudioFrame { - data: Vec<(f32, f32)>, + pub data: Vec<(f32, f32)>, } pub struct AudioSource { @@ -233,6 +232,10 @@ impl AudioOutput { })) } + pub fn set_frame_size(&mut self, frame_size: usize) { + self.frame_size = frame_size + } + pub fn add_frame(&mut self, frame: AudioFrame) { self.output.push_back(frame); self.sequence_num = self.sequence_num.wrapping_add(1); @@ -253,59 +256,3 @@ impl AudioOutput { } } - -#[allow(dead_code)] -pub struct CpalAudioOutput { - stream: Stream, -} - -impl CpalAudioOutput { - pub fn create_audio_output(output: Arc>) -> CpalAudioOutput { - let device = cpal::default_host() - .default_output_device() - .expect("No sound output device available"); - - let config: StreamConfig = device - .supported_output_configs() - .expect("error while querying configs") - .find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2) - .expect("no supported config?!") - .with_sample_rate(SampleRate(SAMPLE_RATE as u32)) - .into(); - - let data_callback = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - let result = if let Ok(mut output) = output.lock() { - output.frame_size = data.len() / 2; - output.pop_next() - } else { - return; - }; - - if let Some(frame) = result { - let (start, middle, end) = unsafe { frame.data.align_to::() }; - if !start.is_empty() || !end.is_empty() { - warn!("audio: frame wasn't aligned"); - } - let length = middle.len().min(data.len()); - data[..length].copy_from_slice(&middle[..length]); - } else { - warn!("missed an audio frame"); - } - }; - - let stream = device.build_output_stream( - &config, - data_callback, - move |err| { - error!("ERROR: {:?}", err); - }, - ).unwrap(); - - stream.play().unwrap(); - - CpalAudioOutput { - stream, - } - } -} - diff --git a/emulator/frontends/common/src/cpal.rs b/emulator/frontends/common/src/cpal.rs new file mode 100644 index 0000000..cc2e11a --- /dev/null +++ b/emulator/frontends/common/src/cpal.rs @@ -0,0 +1,63 @@ + +use std::sync::{Arc, Mutex}; +use cpal::{Stream, SampleRate, SampleFormat, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}}; + +use moa_core::{warn, error}; + +use crate::audio::{AudioOutput, SAMPLE_RATE}; + +#[allow(dead_code)] +pub struct CpalAudioOutput { + stream: Stream, +} + +impl CpalAudioOutput { + pub fn create_audio_output(output: Arc>) -> CpalAudioOutput { + let device = cpal::default_host() + .default_output_device() + .expect("No sound output device available"); + + let config: StreamConfig = device + .supported_output_configs() + .expect("error while querying configs") + .find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2) + .expect("no supported config?!") + .with_sample_rate(SampleRate(SAMPLE_RATE as u32)) + .into(); + + let data_callback = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let result = if let Ok(mut output) = output.lock() { + output.set_frame_size(data.len() / 2); + output.pop_next() + } else { + return; + }; + + if let Some(frame) = result { + let (start, middle, end) = unsafe { frame.data.align_to::() }; + if !start.is_empty() || !end.is_empty() { + warn!("audio: frame wasn't aligned"); + } + let length = middle.len().min(data.len()); + data[..length].copy_from_slice(&middle[..length]); + } else { + warn!("missed an audio frame"); + } + }; + + let stream = device.build_output_stream( + &config, + data_callback, + move |err| { + error!("ERROR: {:?}", err); + }, + ).unwrap(); + + stream.play().unwrap(); + + CpalAudioOutput { + stream, + } + } +} + diff --git a/emulator/frontends/common/src/lib.rs b/emulator/frontends/common/src/lib.rs index d7491ff..46aac76 100644 --- a/emulator/frontends/common/src/lib.rs +++ b/emulator/frontends/common/src/lib.rs @@ -2,6 +2,11 @@ #[cfg(feature = "tty")] pub mod tty; -#[cfg(feature = "audio")] pub mod audio; +pub use crate::audio::{AudioMixer, AudioSource}; + +#[cfg(feature = "audio")] +pub mod cpal; +#[cfg(feature = "audio")] +pub use crate::cpal::CpalAudioOutput; diff --git a/emulator/frontends/console/Cargo.toml b/emulator/frontends/console/Cargo.toml index f4ae77a..370ac8d 100644 --- a/emulator/frontends/console/Cargo.toml +++ b/emulator/frontends/console/Cargo.toml @@ -6,6 +6,7 @@ default-run = "moa-computie" [dependencies] log = "0.4" +clap = "3.2.20" simple_logger = "2.3.0" moa_core = { path = "../../core" } diff --git a/emulator/frontends/console/src/bin/moa-console-genesis.rs b/emulator/frontends/console/src/bin/moa-console-genesis.rs new file mode 100644 index 0000000..394ef2b --- /dev/null +++ b/emulator/frontends/console/src/bin/moa-console-genesis.rs @@ -0,0 +1,23 @@ + +use clap::{App, Arg}; + +use moa_console::ConsoleFrontend; +use moa_genesis::{build_genesis, SegaGenesisOptions}; + +fn main() { + let matches = App::new("Sega Genesis/Mega Drive Emulator") + .arg(Arg::new("ROM") + .help("ROM file to load (must be flat binary)")) + .get_matches(); + + let mut frontend = ConsoleFrontend; + + let mut options = SegaGenesisOptions::default(); + if let Some(filename) = matches.value_of("ROM") { + options.rom = filename.to_string(); + } + + let mut system = build_genesis(&mut frontend, options).unwrap(); + system.run_loop(); +} + diff --git a/emulator/frontends/console/src/lib.rs b/emulator/frontends/console/src/lib.rs index f9f95fc..a47c539 100644 --- a/emulator/frontends/console/src/lib.rs +++ b/emulator/frontends/console/src/lib.rs @@ -1,6 +1,8 @@ use moa_core::Error; -use moa_core::host::{Host, Tty, WindowUpdater}; +use moa_core::host::{Host, Tty, WindowUpdater, ControllerDevice, ControllerUpdater, Audio}; + +use moa_common::audio::{AudioMixer, AudioSource}; pub struct ConsoleFrontend; @@ -14,5 +16,16 @@ impl Host for ConsoleFrontend { println!("console: add_window() is not supported from the console; ignoring request..."); Ok(()) } + + fn register_controller(&mut self, _device: ControllerDevice, _input: Box) -> Result<(), Error> { + println!("console: register_controller() is not supported from the console; ignoring request..."); + Ok(()) + } + + fn create_audio_source(&mut self) -> Result, Error> { + println!("console: create_audio_source() is not supported from the console; returning dummy device..."); + let source = AudioSource::new(AudioMixer::with_default_rate()); + Ok(Box::new(source)) + } } diff --git a/emulator/frontends/minifb/src/lib.rs b/emulator/frontends/minifb/src/lib.rs index 7d3db28..7709ce9 100644 --- a/emulator/frontends/minifb/src/lib.rs +++ b/emulator/frontends/minifb/src/lib.rs @@ -11,7 +11,8 @@ use moa_core::{System, Error}; use moa_core::host::{Host, ControllerUpdater, KeyboardUpdater, KeyEvent, MouseUpdater, MouseState, WindowUpdater, Audio, ControllerDevice}; use moa_core::host::gfx::Frame; -use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput}; +use moa_common::{AudioMixer, AudioSource}; +use moa_common::CpalAudioOutput; mod keys; mod controllers; diff --git a/emulator/frontends/pixels/src/bin/moa-genesis.rs b/emulator/frontends/pixels/src/bin/moa-genesis.rs index c9b0763..b5caebe 100644 --- a/emulator/frontends/pixels/src/bin/moa-genesis.rs +++ b/emulator/frontends/pixels/src/bin/moa-genesis.rs @@ -5,7 +5,7 @@ use moa_core::{System, Error}; use moa_genesis::{SegaGenesisOptions, build_genesis}; fn load_system(host: &mut PixelsFrontend, rom_data: Vec) -> Result { - let mut options = SegaGenesisOptions::new(); + let mut options = SegaGenesisOptions::default(); options.rom_data = Some(rom_data); build_genesis(host, options) } diff --git a/emulator/frontends/pixels/src/frontend.rs b/emulator/frontends/pixels/src/frontend.rs index 7c5c72e..6da3e92 100644 --- a/emulator/frontends/pixels/src/frontend.rs +++ b/emulator/frontends/pixels/src/frontend.rs @@ -7,7 +7,7 @@ use winit::event_loop::{ControlFlow, EventLoop}; use moa_core::{System, Error}; use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio}; use moa_core::host::gfx::Frame; -use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput}; +use moa_common::{AudioMixer, AudioSource, CpalAudioOutput}; use crate::settings; use crate::create_window;