Skip to content
Snippets Groups Projects

Support for Libretro

Merged João Magalhães requested to merge joamag/libretro into master
Files
5
+ 425
0
#![allow(clippy::uninlined_format_args)]
pub mod consts;
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
os::raw::{c_char, c_float, c_uint, c_void},
slice::from_raw_parts,
};
use boytacean::{
debugln,
gb::{AudioProvider, GameBoy},
pad::PadKey,
ppu::{DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_RGB155_SIZE, RGB1555_SIZE},
rom::Cartridge,
};
use consts::{
RETRO_DEVICE_ID_JOYPAD_A, RETRO_DEVICE_ID_JOYPAD_B, RETRO_DEVICE_ID_JOYPAD_DOWN,
RETRO_DEVICE_ID_JOYPAD_L, RETRO_DEVICE_ID_JOYPAD_L2, RETRO_DEVICE_ID_JOYPAD_L3,
RETRO_DEVICE_ID_JOYPAD_LEFT, RETRO_DEVICE_ID_JOYPAD_R, RETRO_DEVICE_ID_JOYPAD_R2,
RETRO_DEVICE_ID_JOYPAD_R3, RETRO_DEVICE_ID_JOYPAD_RIGHT, RETRO_DEVICE_ID_JOYPAD_SELECT,
RETRO_DEVICE_ID_JOYPAD_START, RETRO_DEVICE_ID_JOYPAD_UP, RETRO_DEVICE_ID_JOYPAD_X,
RETRO_DEVICE_ID_JOYPAD_Y, RETRO_DEVICE_JOYPAD,
};
use crate::consts::{REGION_NTSC, RETRO_API_VERSION};
static mut EMULATOR: Option<GameBoy> = None;
static mut KEY_STATES: Option<HashMap<RetroJoypad, bool>> = None;
static mut FRAME_BUFFER: [u8; FRAME_BUFFER_RGB155_SIZE] = [0x00; FRAME_BUFFER_RGB155_SIZE];
static mut PENDING_CYCLES: u32 = 0_u32;
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 KEYS: [RetroJoypad; 8] = [
RetroJoypad::RetroDeviceIdJoypadUp,
RetroJoypad::RetroDeviceIdJoypadDown,
RetroJoypad::RetroDeviceIdJoypadLeft,
RetroJoypad::RetroDeviceIdJoypadRight,
RetroJoypad::RetroDeviceIdJoypadStart,
RetroJoypad::RetroDeviceIdJoypadSelect,
RetroJoypad::RetroDeviceIdJoypadA,
RetroJoypad::RetroDeviceIdJoypadB,
];
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum RetroJoypad {
RetroDeviceIdJoypadB = RETRO_DEVICE_ID_JOYPAD_B,
RetroDeviceIdJoypadY = 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())
}
}
#[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,
}
#[no_mangle]
pub extern "C" fn retro_api_version() -> c_uint {
debugln!("retro_api_version()");
RETRO_API_VERSION
}
#[no_mangle]
pub extern "C" fn retro_init() {
debugln!("retro_init()");
unsafe {
EMULATOR = Some(GameBoy::new(None));
KEY_STATES = Some(HashMap::new());
}
}
#[no_mangle]
pub extern "C" fn retro_deinit() {
debugln!("retro_deinit()");
}
#[no_mangle]
pub extern "C" fn retro_reset() {
debugln!("retro_reset()");
let emulator = unsafe { EMULATOR.as_mut().unwrap() };
emulator.reload();
}
/// # 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) {
debugln!("retro_get_system_info()");
(*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) {
debugln!("retro_get_system_av_info()");
(*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>,
) {
debugln!("retro_set_environment()");
unsafe {
ENVIRONMENT_CALLBACK = callback;
}
}
#[no_mangle]
pub extern "C" fn retro_set_controller_port_device() {
debugln!("retro_set_controller_port_device()");
}
#[no_mangle]
pub extern "C" fn retro_run() {
let emulator = unsafe { EMULATOR.as_mut().unwrap() };
let video_refresh_cb = unsafe { VIDEO_REFRESH_CALLBACK.as_ref().unwrap() };
let sample_batch_cb = unsafe { AUDIO_SAMPLE_BATCH_CALLBACK.as_ref().unwrap() };
let input_poll_cb = unsafe { INPUT_POLL_CALLBACK.as_ref().unwrap() };
let input_state_cb = unsafe { INPUT_STATE_CALLBACK.as_ref().unwrap() };
let key_states = unsafe { KEY_STATES.as_mut().unwrap() };
let channels = emulator.audio_channels();
let mut last_frame = emulator.ppu_frame();
let mut counter_cycles = unsafe { PENDING_CYCLES };
let cycle_limit = (GameBoy::CPU_FREQ as f32 * emulator.multiplier() as f32
/ GameBoy::VISUAL_FREQ)
.round() as u32;
loop {
// limits the number of ticks to the typical number
// of cycles expected for the current logic cycle
if counter_cycles >= cycle_limit {
unsafe { 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;
// in case a new frame is available in the emulator
// then the frame must be pushed into display
if emulator.ppu_frame() != last_frame {
let frame_buffer = emulator.frame_buffer_rgb1555();
unsafe {
FRAME_BUFFER.copy_from_slice(&frame_buffer);
video_refresh_cb(
FRAME_BUFFER.as_ptr(),
DISPLAY_WIDTH as u32,
DISPLAY_HEIGHT as u32,
DISPLAY_WIDTH * RGB1555_SIZE,
);
}
// obtains the index of the current PPU frame, this value
// is going to be used to detect for new frame presence
last_frame = emulator.ppu_frame();
}
// obtains the audio buffer reference and queues it
// in a batch manner using the audio callback at the
// the end of the operation clears the buffer
let audio_buffer = emulator
.audio_buffer()
.iter()
.map(|v| *v as i16 * 256)
.collect::<Vec<i16>>();
sample_batch_cb(
audio_buffer.as_ptr(),
audio_buffer.len() / channels as usize,
);
emulator.clear_audio_buffer();
}
input_poll_cb();
for key in KEYS {
let key_pad = retro_key_to_pad(key).unwrap();
let current = input_state_cb(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);
}
}
#[no_mangle]
pub extern "C" fn retro_get_region() -> u32 {
debugln!("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 {
debugln!("retro_load_game()");
let instance = EMULATOR.as_mut().unwrap();
let data_buffer = from_raw_parts((*game).data as *const u8, (*game).size);
let rom = Cartridge::from_data(data_buffer);
let mode = rom.gb_mode();
instance.set_mode(mode);
instance.reset();
instance.load(true);
instance.load_cartridge(rom);
true
}
#[no_mangle]
pub extern "C" fn retro_load_game_special(
_system: u32,
_info: *const RetroGameInfo,
_num_info: usize,
) -> bool {
debugln!("retro_load_game_special()");
false
}
#[no_mangle]
pub extern "C" fn retro_unload_game() {
debugln!("retro_unload_game()");
}
#[no_mangle]
pub extern "C" fn retro_get_memory_data(_memory_id: u32) -> *mut c_void {
debugln!("retro_get_memory_data()");
std::ptr::null_mut()
}
#[no_mangle]
pub extern "C" fn retro_get_memory_size(_memory_id: u32) -> usize {
debugln!("retro_get_memory_size()");
0
}
#[no_mangle]
pub extern "C" fn retro_serialize_size() {
debugln!("retro_serialize_size()");
}
#[no_mangle]
pub extern "C" fn retro_serialize() {
debugln!("retro_serialize()");
}
#[no_mangle]
pub extern "C" fn retro_unserialize() {
debugln!("retro_unserialize()");
}
#[no_mangle]
pub extern "C" fn retro_cheat_reset() {
debugln!("retro_cheat_reset()");
}
#[no_mangle]
pub extern "C" fn retro_cheat_set() {
debugln!("retro_cheat_set()");
}
#[no_mangle]
pub extern "C" fn retro_set_video_refresh(
callback: Option<extern "C" fn(*const u8, c_uint, c_uint, usize)>,
) {
debugln!("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)>) {
debugln!("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)>) {
debugln!("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()>) {
debugln!("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>,
) {
debugln!("retro_set_input_state()");
unsafe {
INPUT_STATE_CALLBACK = callback;
}
}
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,
}
}
Loading