#![allow(clippy::uninlined_format_args)] use std::{ collections::HashMap, ffi::CStr, fmt::{self, Display, Formatter}, os::raw::{c_char, c_float, c_uint, c_void}, slice::from_raw_parts, }; use boytacean::{ gb::GameBoy, pad::PadKey, ppu::{DISPLAY_HEIGHT, DISPLAY_WIDTH, RGB1555_SIZE}, rom::Cartridge, }; const RETRO_API_VERSION: u32 = 1; const REGION_NTSC: u32 = 0; //const RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: u32 = 10; //const RETRO_PIXEL_FORMAT_0RGB1555: usize = 0; //const RETRO_PIXEL_FORMAT_XRGB8888: usize = 1; //const RETRO_PIXEL_FORMAT_RGB565: usize = 2; //const RETRO_MEMORY_SAVE_RAM: u32 = 0; //const RETRO_MEMORY_SYSTEM_RAM: u32 = 0; const RETRO_DEVICE_JOYPAD: usize = 1; static mut EMULATOR: Option<GameBoy> = None; static mut KEY_STATES: Option<HashMap<RetroJoypad, bool>> = None; static mut ENVIRONMENT_CALLBACK: Option<extern "C" fn(u32, *const c_void) -> bool> = None; static mut VIDEO_REFRESH_CALLBACK: Option<extern "C" fn(*const u8, c_uint, c_uint, usize)> = None; static mut AUDIO_SAMPLE_CALLBACK: Option<extern "C" fn(i16, i16)> = None; static mut AUDIO_SAMPLE_BATCH_CALLBACK: Option<extern "C" fn(*const i16, usize)> = None; static mut INPUT_POLL_CALLBACK: Option<extern "C" fn()> = None; static mut INPUT_STATE_CALLBACK: Option< extern "C" fn(port: u32, device: u32, index: u32, id: u32) -> i16, > = None; const RETRO_DEVICE_ID_JOYPAD_B: isize = 0; const RETRO_DEVICE_ID_JOYPAD_Y: isize = 1; const RETRO_DEVICE_ID_JOYPAD_SELECT: isize = 2; const RETRO_DEVICE_ID_JOYPAD_START: isize = 3; const RETRO_DEVICE_ID_JOYPAD_UP: isize = 4; const RETRO_DEVICE_ID_JOYPAD_DOWN: isize = 5; const RETRO_DEVICE_ID_JOYPAD_LEFT: isize = 6; const RETRO_DEVICE_ID_JOYPAD_RIGHT: isize = 7; const RETRO_DEVICE_ID_JOYPAD_A: isize = 8; const RETRO_DEVICE_ID_JOYPAD_X: isize = 9; const RETRO_DEVICE_ID_JOYPAD_L: isize = 10; const RETRO_DEVICE_ID_JOYPAD_R: isize = 11; const RETRO_DEVICE_ID_JOYPAD_L2: isize = 12; const RETRO_DEVICE_ID_JOYPAD_R2: isize = 13; const RETRO_DEVICE_ID_JOYPAD_L3: isize = 14; const RETRO_DEVICE_ID_JOYPAD_R3: isize = 15; #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum RetroJoypad { RetroDeviceIdJoypadY = RETRO_DEVICE_ID_JOYPAD_B, RetroDeviceIdJoypadB = RETRO_DEVICE_ID_JOYPAD_Y, RetroDeviceIdJoypadSelect = RETRO_DEVICE_ID_JOYPAD_SELECT, RetroDeviceIdJoypadStart = RETRO_DEVICE_ID_JOYPAD_START, RetroDeviceIdJoypadUp = RETRO_DEVICE_ID_JOYPAD_UP, RetroDeviceIdJoypadDown = RETRO_DEVICE_ID_JOYPAD_DOWN, RetroDeviceIdJoypadLeft = RETRO_DEVICE_ID_JOYPAD_LEFT, RetroDeviceIdJoypadRight = RETRO_DEVICE_ID_JOYPAD_RIGHT, RetroDeviceIdJoypadA = RETRO_DEVICE_ID_JOYPAD_A, RetroDeviceIdJoypadX = RETRO_DEVICE_ID_JOYPAD_X, RetroDeviceIdJoypadL = RETRO_DEVICE_ID_JOYPAD_L, RetroDeviceIdJoypadR = RETRO_DEVICE_ID_JOYPAD_R, RetroDeviceIdJoypadL2 = RETRO_DEVICE_ID_JOYPAD_L2, RetroDeviceIdJoypadR2 = RETRO_DEVICE_ID_JOYPAD_R2, RetroDeviceIdJoypadL3 = RETRO_DEVICE_ID_JOYPAD_L3, RetroDeviceIdJoypadR3 = RETRO_DEVICE_ID_JOYPAD_R3, } impl RetroJoypad { pub fn description(&self) -> &'static str { match self { RetroJoypad::RetroDeviceIdJoypadY => "Y", RetroJoypad::RetroDeviceIdJoypadB => "B", RetroJoypad::RetroDeviceIdJoypadSelect => "Select", RetroJoypad::RetroDeviceIdJoypadStart => "Start", RetroJoypad::RetroDeviceIdJoypadUp => "Up", RetroJoypad::RetroDeviceIdJoypadDown => "Down", RetroJoypad::RetroDeviceIdJoypadLeft => "Left", RetroJoypad::RetroDeviceIdJoypadRight => "Right", RetroJoypad::RetroDeviceIdJoypadA => "A", RetroJoypad::RetroDeviceIdJoypadX => "X", RetroJoypad::RetroDeviceIdJoypadL => "L", RetroJoypad::RetroDeviceIdJoypadR => "R", RetroJoypad::RetroDeviceIdJoypadL2 => "L2", RetroJoypad::RetroDeviceIdJoypadR2 => "R2", RetroJoypad::RetroDeviceIdJoypadL3 => "L3", RetroJoypad::RetroDeviceIdJoypadR3 => "R3", } } } impl Display for RetroJoypad { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description()) } } const KEYS: [RetroJoypad; 8] = [ RetroJoypad::RetroDeviceIdJoypadUp, RetroJoypad::RetroDeviceIdJoypadDown, RetroJoypad::RetroDeviceIdJoypadLeft, RetroJoypad::RetroDeviceIdJoypadRight, RetroJoypad::RetroDeviceIdJoypadStart, RetroJoypad::RetroDeviceIdJoypadSelect, RetroJoypad::RetroDeviceIdJoypadA, RetroJoypad::RetroDeviceIdJoypadB, ]; #[repr(C)] pub struct RetroGameInfo { pub path: *const c_char, pub data: *const c_void, pub size: usize, pub meta: *const c_char, } #[repr(C)] pub struct RetroSystemInfo { pub library_name: *const c_char, pub library_version: *const c_char, pub valid_extensions: *const c_char, pub need_fullpath: bool, pub block_extract: bool, } #[repr(C)] pub struct RetroGameGeometry { pub base_width: c_uint, pub base_height: c_uint, pub max_width: c_uint, pub max_height: c_uint, pub aspect_ratio: c_float, } #[repr(C)] pub struct RetroSystemAvInfo { geometry: RetroGameGeometry, timing: RetroSystemTiming, } #[repr(C)] pub struct RetroSystemTiming { fps: f64, sample_rate: f64, } /// # Safety /// /// This function should not be called only within Lib Retro context. #[no_mangle] pub unsafe extern "C" fn retro_init() { println!("retro_init()"); unsafe { EMULATOR = Some(GameBoy::new(None)); KEY_STATES = Some(HashMap::new()); } } #[no_mangle] pub extern "C" fn retro_deinit() { println!("retro_deinit()"); } #[no_mangle] pub extern "C" fn retro_reset() { println!("retro_reset()"); } #[no_mangle] pub extern "C" fn retro_api_version() -> c_uint { println!("retro_api_version()"); RETRO_API_VERSION } /// # Safety /// /// This function should not be called only within Lib Retro context. #[no_mangle] pub unsafe extern "C" fn retro_get_system_info(info: *mut RetroSystemInfo) { println!("retro_get_system_info()"); unsafe { (*info).library_name = "Boytacean\0".as_ptr() as *const c_char; (*info).library_version = "v0.9.6\0".as_ptr() as *const c_char; (*info).valid_extensions = "gb|gbc\0".as_ptr() as *const c_char; (*info).need_fullpath = false; (*info).block_extract = false; } } /// # Safety /// /// This function should not be called only within Lib Retro context. #[no_mangle] pub unsafe extern "C" fn retro_get_system_av_info(info: *mut RetroSystemAvInfo) { println!("retro_get_system_av_info()"); unsafe { (*info).geometry.base_width = DISPLAY_WIDTH as u32; (*info).geometry.base_height = DISPLAY_HEIGHT as u32; (*info).geometry.max_width = DISPLAY_WIDTH as u32 * 64; (*info).geometry.max_height = DISPLAY_HEIGHT as u32 * 64; (*info).geometry.aspect_ratio = DISPLAY_WIDTH as f32 / DISPLAY_HEIGHT as f32; (*info).timing.fps = GameBoy::VISUAL_FREQ as f64; (*info).timing.sample_rate = EMULATOR.as_ref().unwrap().audio_sampling_rate() as f64; } } #[no_mangle] pub extern "C" fn retro_set_environment( callback: Option<extern "C" fn(u32, *const c_void) -> bool>, ) { println!("retro_set_environment()"); unsafe { ENVIRONMENT_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_set_video_refresh( callback: Option<extern "C" fn(*const u8, c_uint, c_uint, usize)>, ) { println!("retro_set_video_refresh()"); unsafe { VIDEO_REFRESH_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_set_audio_sample(callback: Option<extern "C" fn(i16, i16)>) { println!("retro_set_audio_sample()"); unsafe { AUDIO_SAMPLE_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_set_audio_sample_batch(callback: Option<extern "C" fn(*const i16, usize)>) { println!("retro_set_audio_sample_batch()"); unsafe { AUDIO_SAMPLE_BATCH_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_set_input_poll(callback: Option<extern "C" fn()>) { println!("retro_set_input_poll()"); unsafe { INPUT_POLL_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_set_input_state( callback: Option<extern "C" fn(port: u32, device: u32, index: u32, id: u32) -> i16>, ) { println!("retro_set_input_state()"); unsafe { INPUT_STATE_CALLBACK = callback; } } #[no_mangle] pub extern "C" fn retro_load_game_special( _system: u32, _info: *const RetroGameInfo, _num_info: usize, ) -> bool { println!("retro_load_game_special()"); false } #[no_mangle] pub extern "C" fn retro_set_controller_port_device() { println!("retro_set_controller_port_device()"); } #[no_mangle] pub extern "C" fn retro_run() { let emulator = unsafe { EMULATOR.as_mut().unwrap() }; let mut counter_cycles = 0_u32; let cycle_limit = 4194304 / 60; //@TODO this is super tricky loop { // limits the number of ticks to the typical number // of cycles expected for the current logic cycle if counter_cycles >= cycle_limit { //pending_cycles = counter_cycles - cycle_limit; break; } // runs the Game Boy clock, this operation should // include the advance of both the CPU, PPU, APU // and any other frequency based component of the system counter_cycles += emulator.clock() as u32; } unsafe { INPUT_POLL_CALLBACK.as_ref().unwrap()(); let key_states = KEY_STATES.as_mut().unwrap(); for key in KEYS { let key_pad = retro_key_to_pad(key).unwrap(); let current = INPUT_STATE_CALLBACK.as_ref().unwrap()( 0, RETRO_DEVICE_JOYPAD as u32, 0, key as u32, ) > 0; let previous = key_states.get(&key).unwrap_or(&false); if current != *previous { if current { emulator.key_press(key_pad); } else { emulator.key_lift(key_pad); } } key_states.insert(key, current); } } let frame_buffer = emulator.frame_buffer_rgb1555(); unsafe { VIDEO_REFRESH_CALLBACK.unwrap()( frame_buffer.as_ptr(), DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32, DISPLAY_WIDTH * RGB1555_SIZE, ); } } #[no_mangle] pub extern "C" fn retro_get_region() -> u32 { println!("retro_get_region()"); REGION_NTSC } /// # Safety /// /// This function should not be called only within Lib Retro context. #[no_mangle] pub unsafe extern "C" fn retro_load_game(game: *const RetroGameInfo) -> bool { println!("retro_load_game()"); unsafe { let instance = EMULATOR.as_mut().unwrap(); let data_buffer = from_raw_parts((*game).data as *const u8, (*game).size); let file_path_c = CStr::from_ptr((*game).path); let file_path = file_path_c.to_str().unwrap(); let mode = Cartridge::from_file(file_path).gb_mode(); instance.set_mode(mode); instance.reset(); instance.load(true); instance.load_rom(data_buffer, None); } true } #[no_mangle] pub extern "C" fn retro_unload_game() { println!("retro_unload_game()"); } #[no_mangle] pub extern "C" fn retro_get_memory_data(_memory_id: u32) -> *mut c_void { println!("retro_get_memory_data()"); std::ptr::null_mut() } #[no_mangle] pub extern "C" fn retro_get_memory_size(_memory_id: u32) -> usize { println!("retro_get_memory_size()"); 0 } #[no_mangle] pub extern "C" fn retro_serialize_size() { println!("retro_serialize_size()"); } #[no_mangle] pub extern "C" fn retro_serialize() { println!("retro_serialize()"); } #[no_mangle] pub extern "C" fn retro_unserialize() { println!("retro_unserialize()"); } #[no_mangle] pub extern "C" fn retro_cheat_reset() { println!("retro_cheat_reset()"); } #[no_mangle] pub extern "C" fn retro_cheat_set() { println!("retro_cheat_set()"); } fn retro_key_to_pad(retro_key: RetroJoypad) -> Option<PadKey> { match retro_key { RetroJoypad::RetroDeviceIdJoypadUp => Some(PadKey::Up), RetroJoypad::RetroDeviceIdJoypadDown => Some(PadKey::Down), RetroJoypad::RetroDeviceIdJoypadLeft => Some(PadKey::Left), RetroJoypad::RetroDeviceIdJoypadRight => Some(PadKey::Right), RetroJoypad::RetroDeviceIdJoypadStart => Some(PadKey::Start), RetroJoypad::RetroDeviceIdJoypadSelect => Some(PadKey::Select), RetroJoypad::RetroDeviceIdJoypadA => Some(PadKey::A), RetroJoypad::RetroDeviceIdJoypadB => Some(PadKey::B), _ => None, } }