use std::error::Error; use std::ffi::CStr; use std::path::Path; use std::time::{Duration, Instant}; use crate::prelude::*; use crate::components::ControlFlow; use sdl2::audio::{AudioCallback, AudioFormat, AudioSpec, AudioSpecDesired, AudioDevice}; use sdl2::controller::{GameController, Button, Axis}; use sdl2::event::Event; use sdl2::keyboard::Keycode; use sdl2::rect::Rect; use sdl2::render::WindowCanvas; // TODO: split up between video/audio/input! pub struct Sdl2Component { preferred_pad: Option, av_info: SystemAvInfo, _sdl_context: sdl2::Sdl, // video bits canvas: WindowCanvas, pixel_format: sdl2::pixels::PixelFormatEnum, // audio bits audio_buffer: Vec, audio_spec: AudioSpec, audio_device: AudioDevice, audio_sender: crossbeam_channel::Sender>, // input bits gamepad_subsys: sdl2::GameControllerSubsystem, gamepads: Vec, // timing and events frame_begin: Instant, event_pump: sdl2::EventPump, } impl RetroComponent for Sdl2Component { fn pre_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { self.frame_begin = Instant::now(); for event in self.event_pump.poll_iter() { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => return ControlFlow::Break, _ => {} } } ControlFlow::Continue } fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { // The rest of the game loop goes here... self.canvas.present(); // similar hack to the sample rate, make sure we don't divide by zero. let mut spf = 1.0 / self.av_info.timing.fps; if spf.is_nan() || spf.is_infinite() { spf = 1.0 / 60.0; } Duration::from_secs_f64(spf) .checked_sub(self.frame_begin.elapsed()) .map(std::thread::sleep); ControlFlow::Continue } fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box> { if let Some(device) = self.preferred_pad { for port in 0..self.gamepads.len() as u32 { retro.set_controller_port_device(port, device); } } self.audio_device.resume(); Ok(()) } } impl Sdl2Component { pub fn new(retro: &LibretroWrapper) -> Self { let sys_info = retro.get_system_info(); let title = format!( "{} - ferretro", unsafe { CStr::from_ptr(sys_info.library_name) }.to_string_lossy() ); let mut av_info = retro.get_system_av_info(); let pixel_format = sdl2::pixels::PixelFormatEnum::ARGB1555; // HACK: some cores don't report this 'til we get an environ call to set_system_av_info... // which is too late for this constructor to pass along to SDL. if av_info.timing.sample_rate == 0.0 { av_info.timing.sample_rate = 32040.0; } let sdl_context = sdl2::init().unwrap(); let window = sdl_context .video() .unwrap() .window(title.as_str(), av_info.geometry.base_width, av_info.geometry.base_height) .opengl() .build() .unwrap(); let canvas = window.into_canvas().build().unwrap(); let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2); let audio = sdl_context.audio().unwrap(); let desired_spec = AudioSpecDesired { freq: Some(av_info.timing.sample_rate.round() as i32), channels: Some(2), samples: None, }; let mut audio_spec = None; let audio_device = audio .open_playback(None, &desired_spec, |spec| { if spec.format != AudioFormat::S16LSB { eprintln!("unsupported audio format {:?}", spec.format); } audio_spec = Some(spec.clone()); MySdlAudio { audio_spec: spec, audio_receiver, } }) .unwrap(); let gamepad_subsys = sdl_context.game_controller().unwrap(); let mut gamepads = Vec::new(); for i in 0..gamepad_subsys.num_joysticks().unwrap() { gamepads.extend(gamepad_subsys.open(i).into_iter()); } let event_pump = sdl_context.event_pump().unwrap(); Sdl2Component { preferred_pad: None, av_info, _sdl_context: sdl_context, canvas, pixel_format, audio_buffer: Default::default(), audio_spec: audio_spec.unwrap(), audio_device, audio_sender, gamepad_subsys, gamepads, frame_begin: Instant::now(), event_pump, } } fn send_audio_samples(&mut self) { let stereo_samples = self.audio_spec.samples as usize * 2; while self.audio_buffer.len() >= stereo_samples { let remainder = self.audio_buffer.split_off(stereo_samples); let msg = std::mem::replace(&mut self.audio_buffer, remainder); let _ = self.audio_sender.try_send(msg); } } } impl RetroCallbacks for Sdl2Component { fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { let rect = Rect::new(0, 0, width, height); if let Ok(mut tex) = self.canvas .texture_creator() .create_texture_static(self.pixel_format, width, height) { if tex.update(rect, data, pitch as usize).is_ok() { self.canvas.clear(); self.canvas.copy(&tex, None, None).unwrap(); } } } fn audio_sample(&mut self, left: i16, right: i16) { self.audio_buffer.push(left); self.audio_buffer.push(right); self.send_audio_samples() } fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { self.audio_buffer.extend(stereo_pcm); self.send_audio_samples(); stereo_pcm.len() } fn input_poll(&mut self) { self.gamepad_subsys.update(); } fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { match self.gamepads.get(port as usize) { Some(gamepad) => { match device { InputDeviceId::Joypad(button) => { match button_map(&button) { Some(x) => gamepad.button(x) as i16, None => match button { JoypadButton::L2 => gamepad.axis(Axis::TriggerLeft), JoypadButton::R2 => gamepad.axis(Axis::TriggerRight), _ => 0, } } } InputDeviceId::Analog(axis) => gamepad.axis(axis_map(index, axis)), _ => 0, } } None => 0, } } fn set_pixel_format(&mut self, pix_fmt: PixelFormat) -> Option { self.pixel_format = match pix_fmt { PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, PixelFormat::ARGB8888 => sdl2::pixels::PixelFormatEnum::ARGB8888, PixelFormat::RGB565 => sdl2::pixels::PixelFormatEnum::RGB565, }; Some(true) } fn get_variable(&mut self, key: &str) -> Option { match key { "beetle_saturn_analog_stick_deadzone" => Some("15%".to_string()), "parallel-n64-gfxplugin" => Some("angrylion".to_string()), "parallel-n64-astick-deadzone" => Some("15%".to_string()), _ => None, } } fn get_input_device_capabilities(&mut self) -> Option { let bits = (1 << (DeviceType::Joypad as u32)) | (1 << (DeviceType::Analog as u32)); Some(bits as u64) } fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { self.set_geometry(&av_info.geometry); self.av_info = av_info.clone(); Some(true) } fn set_controller_info(&mut self, controller_info: &Vec) -> Option { for ci in controller_info { // so we can have analog support in beetle/mednafen saturn if ci.name.as_str() == "3D Control Pad" { self.preferred_pad = Some(ci.device_id()); break; } } Some(true) } fn set_geometry(&mut self, geom: &GameGeometry) -> Option { let _ = self.canvas.window_mut().set_size(geom.base_width, geom.base_height); let _ = self.canvas.set_logical_size(geom.base_width, geom.base_height); self.av_info.geometry = geom.clone(); Some(true) } } struct MySdlAudio { audio_spec: AudioSpec, audio_receiver: crossbeam_channel::Receiver>, } impl AudioCallback for MySdlAudio { type Channel = i16; fn callback(&mut self, out: &mut [Self::Channel]) { if self.audio_spec.format == AudioFormat::S16LSB { if let Ok(samples) = self.audio_receiver.recv_timeout(Duration::from_millis(500)) { out.copy_from_slice(&samples[..out.len()]); } } } } fn button_map(retro_button: &JoypadButton) -> Option