#![allow(clippy::missing_safety_doc)]
use libloading::Library;
use std::collections::HashSet;
use std::ffi::{CStr, CString, c_char, c_void};
use std::fs;
use std::path::Path;
use std::ptr;
use std::sync::{Mutex, MutexGuard};
const RETRO_API_VERSION: u32 = 1;
const RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY: u32 = 9;
const RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: u32 = 10;
const RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK: u32 = 21;
const RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY: u32 = 30;
const RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: u32 = 31;
const RETRO_ENVIRONMENT_SET_GEOMETRY: u32 = 37;
const RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK: u32 = 12;
const RETRO_PIXEL_FORMAT_0RGB1555: u32 = 0;
const RETRO_PIXEL_FORMAT_XRGB8888: u32 = 1;
const RETRO_PIXEL_FORMAT_RGB565: u32 = 2;
const RETRO_DEVICE_JOYPAD: u32 = 1;
const RETRO_DEVICE_MOUSE: u32 = 2;
const RETRO_DEVICE_KEYBOARD: u32 = 3;
const RETRO_DEVICE_MASK: u32 = 0xff;
const MAX_DIMENSION: usize = 2048;
const BUTTON_COUNT: usize = 12;
#[repr(C)]
struct RetroGameInfo {
path: *const c_char,
data: *const c_void,
size: usize,
meta: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct RetroGameGeometry {
base_width: u32,
base_height: u32,
max_width: u32,
max_height: u32,
aspect_ratio: f32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct RetroSystemTiming {
fps: f64,
sample_rate: f64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct RetroSystemAvInfo {
geometry: RetroGameGeometry,
timing: RetroSystemTiming,
}
type EnvironmentCallback = unsafe extern "C" fn(u32, *mut c_void) -> bool;
type VideoCallback = unsafe extern "C" fn(*const c_void, u32, u32, usize);
type AudioCallback = unsafe extern "C" fn(i16, i16);
type AudioBatchCallback = unsafe extern "C" fn(*const i16, usize) -> usize;
type InputPollCallback = unsafe extern "C" fn();
type InputStateCallback = unsafe extern "C" fn(u32, u32, u32, u32) -> i16;
type KeyboardEventCallback = unsafe extern "C" fn(bool, u32, u32, u16);
type FrameTimeCallback = unsafe extern "C" fn(i64);
#[repr(C)]
#[derive(Clone, Copy)]
struct RetroFrameTimeCallback {
callback: Option<FrameTimeCallback>,
reference: i64,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct RetroKeyboardCallback {
callback: Option<KeyboardEventCallback>,
}
type RetroApiVersion = unsafe extern "C" fn() -> u32;
type RetroSetEnvironment = unsafe extern "C" fn(EnvironmentCallback);
type RetroSetVideo = unsafe extern "C" fn(VideoCallback);
type RetroSetAudio = unsafe extern "C" fn(AudioCallback);
type RetroSetAudioBatch = unsafe extern "C" fn(AudioBatchCallback);
type RetroSetInputPoll = unsafe extern "C" fn(InputPollCallback);
type RetroSetInputState = unsafe extern "C" fn(InputStateCallback);
type RetroInit = unsafe extern "C" fn();
type RetroDeinit = unsafe extern "C" fn();
type RetroGetSystemAvInfo = unsafe extern "C" fn(*mut RetroSystemAvInfo);
type RetroSetControllerPortDevice = unsafe extern "C" fn(u32, u32);
type RetroLoadGame = unsafe extern "C" fn(*const RetroGameInfo) -> bool;
type RetroUnloadGame = unsafe extern "C" fn();
type RetroRun = unsafe extern "C" fn();
type RetroSerializeSize = unsafe extern "C" fn() -> usize;
type RetroSerialize = unsafe extern "C" fn(*mut c_void, usize) -> bool;
type RetroUnserialize = unsafe extern "C" fn(*const c_void, usize) -> bool;
type RetroGetMemoryData = unsafe extern "C" fn(u32) -> *mut c_void;
type RetroGetMemorySize = unsafe extern "C" fn(u32) -> usize;
struct CoreApi {
api_version: RetroApiVersion,
set_environment: RetroSetEnvironment,
set_video: RetroSetVideo,
set_audio: RetroSetAudio,
set_audio_batch: RetroSetAudioBatch,
set_input_poll: RetroSetInputPoll,
set_input_state: RetroSetInputState,
init: RetroInit,
deinit: RetroDeinit,
av_info: RetroGetSystemAvInfo,
set_port_device: RetroSetControllerPortDevice,
load_game: RetroLoadGame,
unload_game: RetroUnloadGame,
run: RetroRun,
serialize_size: RetroSerializeSize,
serialize: RetroSerialize,
unserialize: RetroUnserialize,
memory_data: RetroGetMemoryData,
memory_size: RetroGetMemorySize,
}
struct Capture {
pixels: Vec<u32>,
audio: Vec<i16>,
width: u32,
height: u32,
pixel_format: u32,
av: RetroSystemAvInfo,
pad: [u8; BUTTON_COUNT],
keys: HashSet<u32>,
key_events: Vec<(bool, u32)>,
keyboard_callback: Option<KeyboardEventCallback>,
frame_time_callback: Option<FrameTimeCallback>,
frame_time_reference: i64,
mouse: [i16; 11],
system_directory: CString,
content_directory: CString,
save_directory: CString,
}
impl Default for Capture {
fn default() -> Self {
Self {
pixels: Vec::new(),
audio: Vec::new(),
width: 0,
height: 0,
pixel_format: RETRO_PIXEL_FORMAT_XRGB8888,
av: RetroSystemAvInfo::default(),
pad: [0; BUTTON_COUNT],
keys: HashSet::new(),
key_events: Vec::new(),
keyboard_callback: None,
frame_time_callback: None,
frame_time_reference: 0,
mouse: [0; 11],
system_directory: CString::new(".").unwrap(),
content_directory: CString::new(".").unwrap(),
save_directory: CString::new(".").unwrap(),
}
}
}
static mut ACTIVE_CAPTURE: *mut Capture = ptr::null_mut();
static SESSION_LOCK: Mutex<()> = Mutex::new(());
unsafe extern "C" fn environment_callback(command: u32, data: *mut c_void) -> bool {
let Some(capture) = (unsafe { ACTIVE_CAPTURE.as_mut() }) else {
return false;
};
match command {
RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY => {
if data.is_null() {
return false;
}
unsafe { *(data as *mut *const c_char) = capture.system_directory.as_ptr() };
true
}
RETRO_ENVIRONMENT_SET_PIXEL_FORMAT => {
if data.is_null() {
return false;
}
let pixel_format = unsafe { *(data as *const u32) };
if matches!(
pixel_format,
RETRO_PIXEL_FORMAT_0RGB1555
| RETRO_PIXEL_FORMAT_XRGB8888
| RETRO_PIXEL_FORMAT_RGB565
) {
capture.pixel_format = pixel_format;
true
} else {
false
}
}
RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK => {
if data.is_null() {
return false;
}
let callback = unsafe { *(data as *const RetroFrameTimeCallback) };
capture.frame_time_callback = callback.callback;
capture.frame_time_reference = callback.reference;
true
}
RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY => {
if data.is_null() {
return false;
}
unsafe { *(data as *mut *const c_char) = capture.content_directory.as_ptr() };
true
}
RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY => {
if data.is_null() {
return false;
}
unsafe { *(data as *mut *const c_char) = capture.save_directory.as_ptr() };
true
}
RETRO_ENVIRONMENT_SET_GEOMETRY => {
if data.is_null() {
return false;
}
capture.av.geometry = unsafe { *(data as *const RetroGameGeometry) };
true
}
RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK => {
if data.is_null() {
return false;
}
capture.keyboard_callback = unsafe { *(data as *const RetroKeyboardCallback) }.callback;
true
}
_ => false,
}
}
fn xrgb_from_pixel(pixel_format: u32, pixel: u32) -> Option<u32> {
let expand_5 = |value: u32| (value << 3) | (value >> 2);
let expand_6 = |value: u32| (value << 2) | (value >> 4);
match pixel_format {
RETRO_PIXEL_FORMAT_XRGB8888 => Some(pixel & 0x00ff_ffff),
RETRO_PIXEL_FORMAT_RGB565 => Some(
(expand_5((pixel >> 11) & 0x1f) << 16)
| (expand_6((pixel >> 5) & 0x3f) << 8)
| expand_5(pixel & 0x1f),
),
RETRO_PIXEL_FORMAT_0RGB1555 => Some(
(expand_5((pixel >> 10) & 0x1f) << 16)
| (expand_5((pixel >> 5) & 0x1f) << 8)
| expand_5(pixel & 0x1f),
),
_ => None,
}
}
unsafe extern "C" fn video_callback(data: *const c_void, width: u32, height: u32, pitch: usize) {
let Some(capture) = (unsafe { ACTIVE_CAPTURE.as_mut() }) else {
return;
};
let width = width as usize;
let height = height as usize;
let bytes_per_pixel = match capture.pixel_format {
RETRO_PIXEL_FORMAT_XRGB8888 => 4,
RETRO_PIXEL_FORMAT_0RGB1555 | RETRO_PIXEL_FORMAT_RGB565 => 2,
_ => return,
};
if width == 0
|| height == 0
|| width > MAX_DIMENSION
|| height > MAX_DIMENSION
|| pitch < width * bytes_per_pixel
{
return;
}
capture.width = width as u32;
capture.height = height as u32;
if data.is_null() {
return;
}
capture.pixels.resize(width * height, 0);
let bytes = data as *const u8;
for y in 0..height {
let row =
unsafe { std::slice::from_raw_parts(bytes.add(y * pitch), width * bytes_per_pixel) };
for x in 0..width {
let offset = x * bytes_per_pixel;
let pixel = match bytes_per_pixel {
2 => u32::from(u16::from_ne_bytes([row[offset], row[offset + 1]])),
4 => u32::from_ne_bytes([
row[offset],
row[offset + 1],
row[offset + 2],
row[offset + 3],
]),
_ => unreachable!(),
};
capture.pixels[y * width + x] = xrgb_from_pixel(capture.pixel_format, pixel).unwrap();
}
}
}
unsafe extern "C" fn audio_callback(left: i16, right: i16) {
if let Some(capture) = unsafe { ACTIVE_CAPTURE.as_mut() } {
capture.audio.extend_from_slice(&[left, right]);
}
}
unsafe extern "C" fn audio_batch_callback(data: *const i16, frames: usize) -> usize {
if !data.is_null()
&& let Some(capture) = unsafe { ACTIVE_CAPTURE.as_mut() }
{
let samples = unsafe { std::slice::from_raw_parts(data, frames.saturating_mul(2)) };
capture.audio.extend_from_slice(samples);
}
frames
}
unsafe extern "C" fn input_poll_callback() {
let Some(capture) = (unsafe { ACTIVE_CAPTURE.as_mut() }) else {
return;
};
if let Some(callback) = capture.keyboard_callback {
for (pressed, keycode) in capture.key_events.drain(..) {
let character = if (32..=126).contains(&keycode) {
keycode
} else {
0
};
unsafe { callback(pressed, keycode, character, 0) };
}
} else {
capture.key_events.clear();
}
}
unsafe extern "C" fn input_state_callback(_: u32, device: u32, _: u32, id: u32) -> i16 {
let Some(capture) = (unsafe { ACTIVE_CAPTURE.as_ref() }) else {
return 0;
};
match device & RETRO_DEVICE_MASK {
RETRO_DEVICE_JOYPAD => {
const MAP: [u32; BUTTON_COUNT] = [4, 5, 6, 7, 8, 0, 9, 1, 10, 11, 3, 2];
MAP.iter()
.position(|&button| button == id)
.map(|index| i16::from(capture.pad[index] != 0))
.unwrap_or(0)
}
RETRO_DEVICE_KEYBOARD => i16::from(capture.keys.contains(&id)),
RETRO_DEVICE_MOUSE => capture.mouse.get(id as usize).copied().unwrap_or(0),
_ => 0,
}
}
unsafe fn symbol<T: Copy>(library: &Library, name: &[u8]) -> Result<T, Error> {
unsafe { library.get::<T>(name) }
.map(|symbol| *symbol)
.map_err(|error| {
Error::new(format!(
"missing libretro symbol {}: {error}",
String::from_utf8_lossy(&name[..name.len() - 1])
))
})
}
unsafe fn load_api(library: &Library) -> Result<CoreApi, Error> {
Ok(CoreApi {
api_version: unsafe { symbol(library, b"retro_api_version\0")? },
set_environment: unsafe { symbol(library, b"retro_set_environment\0")? },
set_video: unsafe { symbol(library, b"retro_set_video_refresh\0")? },
set_audio: unsafe { symbol(library, b"retro_set_audio_sample\0")? },
set_audio_batch: unsafe { symbol(library, b"retro_set_audio_sample_batch\0")? },
set_input_poll: unsafe { symbol(library, b"retro_set_input_poll\0")? },
set_input_state: unsafe { symbol(library, b"retro_set_input_state\0")? },
init: unsafe { symbol(library, b"retro_init\0")? },
deinit: unsafe { symbol(library, b"retro_deinit\0")? },
av_info: unsafe { symbol(library, b"retro_get_system_av_info\0")? },
set_port_device: unsafe { symbol(library, b"retro_set_controller_port_device\0")? },
load_game: unsafe { symbol(library, b"retro_load_game\0")? },
unload_game: unsafe { symbol(library, b"retro_unload_game\0")? },
run: unsafe { symbol(library, b"retro_run\0")? },
serialize_size: unsafe { symbol(library, b"retro_serialize_size\0")? },
serialize: unsafe { symbol(library, b"retro_serialize\0")? },
unserialize: unsafe { symbol(library, b"retro_unserialize\0")? },
memory_data: unsafe { symbol(library, b"retro_get_memory_data\0")? },
memory_size: unsafe { symbol(library, b"retro_get_memory_size\0")? },
})
}
#[derive(Debug, Clone)]
pub struct Error(String);
impl Error {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(formatter)
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Color {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
impl Color {
pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
Self {
red,
green,
blue,
alpha: 255,
}
}
pub const fn xrgb(self) -> u32 {
((self.red as u32) << 16) | ((self.green as u32) << 8) | self.blue as u32
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(usize)]
pub enum Button {
Up = 0,
Down = 1,
Left = 2,
Right = 3,
A = 4,
B = 5,
X = 6,
Y = 7,
L1 = 8,
R1 = 9,
Start = 10,
Select = 11,
}
pub mod key {
pub const BACKSPACE: u32 = 8;
pub const TAB: u32 = 9;
pub const RETURN: u32 = 13;
pub const ESCAPE: u32 = 27;
pub const SPACE: u32 = 32;
pub const NUM_0: u32 = b'0' as u32;
pub const NUM_1: u32 = b'1' as u32;
pub const NUM_2: u32 = b'2' as u32;
pub const NUM_3: u32 = b'3' as u32;
pub const NUM_4: u32 = b'4' as u32;
pub const NUM_5: u32 = b'5' as u32;
pub const NUM_6: u32 = b'6' as u32;
pub const NUM_7: u32 = b'7' as u32;
pub const NUM_8: u32 = b'8' as u32;
pub const NUM_9: u32 = b'9' as u32;
pub const A: u32 = b'a' as u32;
pub const B: u32 = b'b' as u32;
pub const C: u32 = b'c' as u32;
pub const D: u32 = b'd' as u32;
pub const E: u32 = b'e' as u32;
pub const F: u32 = b'f' as u32;
pub const G: u32 = b'g' as u32;
pub const H: u32 = b'h' as u32;
pub const I: u32 = b'i' as u32;
pub const J: u32 = b'j' as u32;
pub const K: u32 = b'k' as u32;
pub const L: u32 = b'l' as u32;
pub const M: u32 = b'm' as u32;
pub const N: u32 = b'n' as u32;
pub const O: u32 = b'o' as u32;
pub const P: u32 = b'p' as u32;
pub const Q: u32 = b'q' as u32;
pub const R: u32 = b'r' as u32;
pub const S: u32 = b's' as u32;
pub const T: u32 = b't' as u32;
pub const U: u32 = b'u' as u32;
pub const V: u32 = b'v' as u32;
pub const W: u32 = b'w' as u32;
pub const X: u32 = b'x' as u32;
pub const Y: u32 = b'y' as u32;
pub const Z: u32 = b'z' as u32;
pub const DELETE: u32 = 127;
pub const UP: u32 = 273;
pub const DOWN: u32 = 274;
pub const RIGHT: u32 = 275;
pub const LEFT: u32 = 276;
pub const INSERT: u32 = 277;
pub const HOME: u32 = 278;
pub const END: u32 = 279;
pub const PAGE_UP: u32 = 280;
pub const PAGE_DOWN: u32 = 281;
pub const F1: u32 = 282;
pub const F2: u32 = 283;
pub const F3: u32 = 284;
pub const F4: u32 = 285;
pub const F5: u32 = 286;
pub const F6: u32 = 287;
pub const F7: u32 = 288;
pub const F8: u32 = 289;
pub const F9: u32 = 290;
pub const F10: u32 = 291;
pub const F11: u32 = 292;
pub const F12: u32 = 293;
pub const RIGHT_SHIFT: u32 = 303;
pub const LEFT_SHIFT: u32 = 304;
pub const RIGHT_CTRL: u32 = 305;
pub const LEFT_CTRL: u32 = 306;
pub const RIGHT_ALT: u32 = 307;
pub const LEFT_ALT: u32 = 308;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(usize)]
pub enum MouseButton {
Left = 2,
Right = 3,
Middle = 6,
Button4 = 9,
Button5 = 10,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[repr(u32)]
pub enum InputDevice {
None = 0,
#[default]
Joypad = RETRO_DEVICE_JOYPAD,
Mouse = RETRO_DEVICE_MOUSE,
Keyboard = RETRO_DEVICE_KEYBOARD,
}
pub struct Session {
_session_guard: MutexGuard<'static, ()>,
library: Library,
api: CoreApi,
capture: Box<Capture>,
cartridge: Vec<u8>,
cartridge_path: CString,
loaded: bool,
}
impl Session {
pub fn new(
core_path: impl AsRef<Path>,
cartridge_path: impl AsRef<Path>,
) -> Result<Self, Error> {
let session_guard = SESSION_LOCK.try_lock().map_err(|_| {
Error::new(
"another play96 session is active; libretro cores can only run one at a time",
)
})?;
let library = unsafe { Library::new(core_path.as_ref()) }
.map_err(|error| Error::new(format!("failed to load core: {error}")))?;
let api = unsafe { load_api(&library)? };
if unsafe { (api.api_version)() } != RETRO_API_VERSION {
return Err(Error::new("unsupported libretro API version"));
}
let cartridge = fs::read(cartridge_path.as_ref())
.map_err(|error| Error::new(format!("failed to read cartridge: {error}")))?;
let cartridge_c_path = CString::new(cartridge_path.as_ref().to_string_lossy().as_bytes())
.map_err(|_| Error::new("cartridge path contains a NUL byte"))?;
let directory_string = |path: &Path, name: &str| {
let directory = path
.parent()
.filter(|directory| !directory.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
CString::new(directory.to_string_lossy().as_bytes())
.map_err(|_| Error::new(format!("{name} directory contains a NUL byte")))
};
let system_directory = directory_string(core_path.as_ref(), "system")?;
let content_directory = directory_string(cartridge_path.as_ref(), "content")?;
let mut capture = Box::<Capture>::default();
capture.system_directory = system_directory;
capture.content_directory = content_directory.clone();
capture.save_directory = content_directory;
let mut session = Self {
_session_guard: session_guard,
library,
api,
capture,
cartridge,
cartridge_path: cartridge_c_path,
loaded: false,
};
session.activate();
unsafe {
(session.api.set_environment)(environment_callback);
(session.api.set_video)(video_callback);
(session.api.set_audio)(audio_callback);
(session.api.set_audio_batch)(audio_batch_callback);
(session.api.set_input_poll)(input_poll_callback);
(session.api.set_input_state)(input_state_callback);
(session.api.init)();
(session.api.set_port_device)(0, RETRO_DEVICE_JOYPAD);
}
let game = RetroGameInfo {
path: session.cartridge_path.as_ptr(),
data: session.cartridge.as_ptr().cast(),
size: session.cartridge.len(),
meta: ptr::null(),
};
if !unsafe { (session.api.load_game)(&game) } {
return Err(Error::new("failed to load cartridge"));
}
session.loaded = true;
unsafe { (session.api.av_info)(&mut session.capture.av) };
Ok(session)
}
fn activate(&mut self) {
unsafe { ACTIVE_CAPTURE = self.capture.as_mut() };
}
pub fn run_frame(&mut self) -> Result<(), Error> {
self.activate();
self.capture.audio.clear();
invoke_frame_time_callback(&self.capture);
unsafe { (self.api.run)() };
for id in [0, 1, 4, 5, 7, 8] {
self.capture.mouse[id] = 0;
}
Ok(())
}
pub fn run_frames(&mut self, frames: usize) -> Result<(), Error> {
for _ in 0..frames {
self.run_frame()?;
}
Ok(())
}
pub fn set_button(&mut self, button: Button, pressed: bool) {
self.capture.pad[button as usize] = u8::from(pressed);
}
pub fn set_controller_device(&mut self, port: u32, device: InputDevice) {
unsafe { (self.api.set_port_device)(port, device as u32) };
}
pub fn clear_buttons(&mut self) {
self.capture.pad = [0; BUTTON_COUNT];
}
pub fn set_key(&mut self, keycode: u32, pressed: bool) {
let changed = if pressed {
self.capture.keys.insert(keycode)
} else {
self.capture.keys.remove(&keycode)
};
if changed {
self.capture.key_events.push((pressed, keycode));
}
}
pub fn key_pressed(&self, keycode: u32) -> bool {
self.capture.keys.contains(&keycode)
}
pub fn clear_keys(&mut self) {
let mut keys = self.capture.keys.drain().collect::<Vec<_>>();
keys.sort_unstable();
self.capture
.key_events
.extend(keys.into_iter().map(|keycode| (false, keycode)));
}
pub fn set_mouse_delta(&mut self, x: i16, y: i16) {
self.capture.mouse[0] = x;
self.capture.mouse[1] = y;
}
pub fn set_mouse_button(&mut self, button: MouseButton, pressed: bool) {
self.capture.mouse[button as usize] = i16::from(pressed);
}
pub fn clear_mouse_buttons(&mut self) {
for button in [
MouseButton::Left,
MouseButton::Right,
MouseButton::Middle,
MouseButton::Button4,
MouseButton::Button5,
] {
self.capture.mouse[button as usize] = 0;
}
}
pub fn set_mouse_wheel(&mut self, vertical: i16, horizontal: i16) {
(self.capture.mouse[4], self.capture.mouse[5]) = if vertical >= 0 {
(vertical, 0)
} else {
(0, vertical.saturating_abs())
};
(self.capture.mouse[7], self.capture.mouse[8]) = if horizontal >= 0 {
(horizontal, 0)
} else {
(0, horizontal.saturating_abs())
};
}
pub fn framebuffer_width(&self) -> u32 {
self.capture.width
}
pub fn framebuffer_height(&self) -> u32 {
self.capture.height
}
pub fn framebuffer(&self) -> &[u32] {
&self.capture.pixels
}
pub fn frame_hash(&self) -> u64 {
fnv1a_u32(&self.capture.pixels)
}
pub fn audio_samples(&self) -> &[i16] {
&self.capture.audio
}
pub fn audio_frame_count(&self) -> usize {
self.capture.audio.len() / 2
}
pub fn audio_sample_rate(&self) -> f64 {
self.capture.av.timing.sample_rate
}
pub fn audio_frame(&self, frame: usize) -> Result<[i16; 2], Error> {
let offset = frame.saturating_mul(2);
if offset + 1 >= self.capture.audio.len() {
return Err(Error::new(format!(
"audio frame {frame} is outside {} captured frames",
self.audio_frame_count()
)));
}
Ok([self.capture.audio[offset], self.capture.audio[offset + 1]])
}
pub fn audio_hash(&self) -> u64 {
fnv1a_i16(&self.capture.audio)
}
pub fn assert_audio_sample(
&self,
frame: usize,
channel: usize,
expected: i16,
tolerance: u16,
) -> Result<(), Error> {
if channel >= 2 {
return Err(Error::new(format!(
"audio channel {channel} is outside stereo channels 0 and 1"
)));
}
let actual = self.audio_frame(frame)?[channel];
if (i32::from(actual) - i32::from(expected)).unsigned_abs() <= u32::from(tolerance) {
Ok(())
} else {
Err(Error::new(format!(
"audio frame {frame} channel {channel} expected {expected} +/- {tolerance}, got {actual}"
)))
}
}
pub fn assert_audio_hash(&self, expected: u64) -> Result<(), Error> {
let actual = self.audio_hash();
if actual == expected {
Ok(())
} else {
Err(Error::new(format!(
"audio hash expected {expected:016x}, got {actual:016x}"
)))
}
}
pub fn save_ram_hash(&mut self) -> u64 {
self.activate();
const RETRO_MEMORY_SAVE_RAM: u32 = 0;
let size = unsafe { (self.api.memory_size)(RETRO_MEMORY_SAVE_RAM) };
let data = unsafe { (self.api.memory_data)(RETRO_MEMORY_SAVE_RAM) };
if size == 0 || data.is_null() {
return 0;
}
let bytes = unsafe { std::slice::from_raw_parts(data.cast::<u8>(), size) };
fnv1a(bytes)
}
pub fn pixel_xrgb(&self, x: u32, y: u32) -> Result<u32, Error> {
if x >= self.capture.width || y >= self.capture.height {
return Err(Error::new(format!(
"pixel ({x}, {y}) is outside {}x{} framebuffer",
self.capture.width, self.capture.height
)));
}
Ok(self.capture.pixels[y as usize * self.capture.width as usize + x as usize])
}
pub fn pixel(&self, x: u32, y: u32) -> Result<Color, Error> {
let value = self.pixel_xrgb(x, y)?;
Ok(Color::rgb(
(value >> 16) as u8,
(value >> 8) as u8,
value as u8,
))
}
pub fn assert_pixel(&self, x: u32, y: u32, expected: Color) -> Result<(), Error> {
let actual = self.pixel(x, y)?;
if actual == expected {
Ok(())
} else {
Err(Error::new(format!(
"pixel ({x}, {y}) expected #{:02x}{:02x}{:02x}{:02x}, got #{:02x}{:02x}{:02x}{:02x}",
expected.red,
expected.green,
expected.blue,
expected.alpha,
actual.red,
actual.green,
actual.blue,
actual.alpha
)))
}
}
pub fn assert_pixel_xrgb(&self, x: u32, y: u32, expected: u32) -> Result<(), Error> {
let actual = self.pixel_xrgb(x, y)?;
if actual == expected {
Ok(())
} else {
Err(Error::new(format!(
"pixel ({x}, {y}) expected #{expected:06x}, got #{actual:06x}"
)))
}
}
pub fn save_state(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
self.activate();
let size = unsafe { (self.api.serialize_size)() };
if size == 0 {
return Err(Error::new("core reported an empty save state"));
}
let mut state = vec![0; size];
if !unsafe { (self.api.serialize)(state.as_mut_ptr().cast(), state.len()) } {
return Err(Error::new("failed to save state"));
}
fs::write(path, state)
.map_err(|error| Error::new(format!("failed to write state: {error}")))
}
pub fn load_state(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
self.activate();
let state =
fs::read(path).map_err(|error| Error::new(format!("failed to read state: {error}")))?;
if unsafe { (self.api.unserialize)(state.as_ptr().cast(), state.len()) } {
Ok(())
} else {
Err(Error::new("failed to load state"))
}
}
pub fn write_png(&self, path: impl AsRef<Path>) -> Result<(), Error> {
if self.capture.pixels.is_empty() {
return Err(Error::new("no framebuffer has been received"));
}
let file = fs::File::create(path)
.map_err(|error| Error::new(format!("failed to create PNG: {error}")))?;
let mut encoder = png::Encoder::new(file, self.capture.width, self.capture.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|error| Error::new(format!("failed to write PNG header: {error}")))?;
let mut rgba = Vec::with_capacity(self.capture.pixels.len() * 4);
for pixel in &self.capture.pixels {
rgba.extend_from_slice(&[(pixel >> 16) as u8, (pixel >> 8) as u8, *pixel as u8, 255]);
}
writer
.write_image_data(&rgba)
.map_err(|error| Error::new(format!("failed to write PNG: {error}")))
}
}
fn invoke_frame_time_callback(capture: &Capture) {
if let Some(callback) = capture.frame_time_callback {
unsafe { callback(capture.frame_time_reference) };
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
bytes.iter().fold(1_469_598_103_934_665_603, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(1_099_511_628_211)
})
}
fn fnv1a_u32(values: &[u32]) -> u64 {
let mut hash = 1_469_598_103_934_665_603_u64;
for value in values {
for byte in value.to_ne_bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(1_099_511_628_211);
}
}
hash
}
fn fnv1a_i16(values: &[i16]) -> u64 {
let mut hash = 1_469_598_103_934_665_603_u64;
for value in values {
for byte in value.to_le_bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(1_099_511_628_211);
}
}
hash
}
impl Drop for Session {
fn drop(&mut self) {
self.activate();
unsafe {
if self.loaded {
(self.api.unload_game)();
}
(self.api.deinit)();
ACTIVE_CAPTURE = ptr::null_mut();
}
let _ = &self.library;
}
}
#[repr(C)]
pub struct Play96Color {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
#[repr(C)]
pub struct play96_session {
session: Session,
error: CString,
}
fn c_error(error: impl std::fmt::Display) -> *const c_char {
thread_local! { static ERROR: std::cell::RefCell<CString> = std::cell::RefCell::new(CString::new("unknown error").unwrap()); }
ERROR.with(|slot| {
*slot.borrow_mut() = CString::new(error.to_string())
.unwrap_or_else(|_| CString::new("error contained a NUL byte").unwrap());
slot.borrow().as_ptr()
})
}
unsafe fn required_string<'a>(value: *const c_char, name: &str) -> Result<&'a str, *const c_char> {
if value.is_null() {
return Err(c_error(format!("{name} must not be null")));
}
unsafe { CStr::from_ptr(value) }
.to_str()
.map_err(|_| c_error(format!("{name} is not valid UTF-8")))
}
fn session_result(session: *mut play96_session, result: Result<(), Error>) -> *const c_char {
match result {
Ok(()) => ptr::null(),
Err(error) => unsafe {
if session.is_null() {
c_error(error)
} else {
(*session).error = CString::new(error.to_string()).unwrap();
(*session).error.as_ptr()
}
},
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_create(
core: *const c_char,
cartridge: *const c_char,
out: *mut *mut play96_session,
) -> *const c_char {
if out.is_null() {
return c_error("out_session must not be null");
}
let Ok(core) = (unsafe { required_string(core, "core_path") }) else {
return c_error("invalid core_path");
};
let Ok(cartridge) = (unsafe { required_string(cartridge, "cartridge_path") }) else {
return c_error("invalid cartridge_path");
};
match Session::new(core, cartridge) {
Ok(session) => {
unsafe {
*out = Box::into_raw(Box::new(play96_session {
session,
error: CString::new("").unwrap(),
}));
}
ptr::null()
}
Err(error) => c_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_destroy(session: *mut play96_session) {
if !session.is_null() {
unsafe {
drop(Box::from_raw(session));
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_run_frame(session: *mut play96_session) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { session_result(session, (*session).session.run_frame()) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_run_frames(
session: *mut play96_session,
frames: usize,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { session_result(session, (*session).session.run_frames(frames)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_button(
session: *mut play96_session,
port: u32,
button: u32,
pressed: i32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
if port != 0 || button as usize >= BUTTON_COUNT {
return c_error("only port 0 and buttons 0 through 11 are supported");
}
unsafe {
(*session).session.capture.pad[button as usize] = u8::from(pressed != 0);
}
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_clear_buttons(
session: *mut play96_session,
port: u32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
if port != 0 {
return c_error("only port 0 is supported");
}
unsafe {
(*session).session.clear_buttons();
}
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_controller_device(
session: *mut play96_session,
port: u32,
device: u32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let device = match device {
0 => InputDevice::None,
1 => InputDevice::Joypad,
2 => InputDevice::Mouse,
3 => InputDevice::Keyboard,
_ => return c_error("input device must be 0, 1, 2, or 3"),
};
unsafe { (*session).session.set_controller_device(port, device) };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_key(
session: *mut play96_session,
keycode: u32,
pressed: i32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).session.set_key(keycode, pressed != 0) };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_clear_keys(session: *mut play96_session) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).session.clear_keys() };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_mouse_delta(
session: *mut play96_session,
x: i16,
y: i16,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).session.set_mouse_delta(x, y) };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_mouse_button(
session: *mut play96_session,
button: usize,
pressed: i32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let button = match button {
2 => MouseButton::Left,
3 => MouseButton::Right,
6 => MouseButton::Middle,
9 => MouseButton::Button4,
10 => MouseButton::Button5,
_ => return c_error("mouse button must be 2, 3, 6, 9, or 10"),
};
unsafe { (*session).session.set_mouse_button(button, pressed != 0) };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_clear_mouse_buttons(session: *mut play96_session) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).session.clear_mouse_buttons() };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_set_mouse_wheel(
session: *mut play96_session,
vertical: i16,
horizontal: i16,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).session.set_mouse_wheel(vertical, horizontal) };
ptr::null()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_framebuffer_width(session: *const play96_session) -> u32 {
if session.is_null() {
0
} else {
unsafe { (*session).session.framebuffer_width() }
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_framebuffer_height(session: *const play96_session) -> u32 {
if session.is_null() {
0
} else {
unsafe { (*session).session.framebuffer_height() }
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_pixel_xrgb(session: *const play96_session, x: u32, y: u32) -> u32 {
if session.is_null() {
return 0;
}
unsafe { (*session).session.pixel_xrgb(x, y).unwrap_or(0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_assert_pixel(
session: *const play96_session,
x: u32,
y: u32,
expected: Play96Color,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let expected = Color {
red: expected.red,
green: expected.green,
blue: expected.blue,
alpha: expected.alpha,
};
unsafe {
(*session)
.session
.assert_pixel(x, y, expected)
.map(|_| ptr::null())
.unwrap_or_else(c_error)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_assert_pixel_xrgb(
session: *const play96_session,
x: u32,
y: u32,
expected: u32,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe {
(*session)
.session
.assert_pixel_xrgb(x, y, expected)
.map(|_| ptr::null())
.unwrap_or_else(c_error)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_audio_samples(session: *const play96_session) -> *const i16 {
if session.is_null() {
return ptr::null();
}
let samples = unsafe { (*session).session.audio_samples() };
if samples.is_empty() {
ptr::null()
} else {
samples.as_ptr()
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_audio_sample_count(session: *const play96_session) -> usize {
if session.is_null() {
return 0;
}
unsafe { (*session).session.audio_samples().len() }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_audio_frame_count(session: *const play96_session) -> usize {
if session.is_null() {
return 0;
}
unsafe { (*session).session.audio_frame_count() }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_audio_sample_rate(session: *const play96_session) -> f64 {
if session.is_null() {
return 0.0;
}
unsafe { (*session).session.audio_sample_rate() }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_audio_hash(session: *const play96_session) -> u64 {
if session.is_null() {
return 0;
}
unsafe { (*session).session.audio_hash() }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_assert_audio_sample(
session: *const play96_session,
frame: usize,
channel: usize,
expected: i16,
tolerance: u16,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe {
(*session)
.session
.assert_audio_sample(frame, channel, expected, tolerance)
.map(|_| ptr::null())
.unwrap_or_else(c_error)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_assert_audio_hash(
session: *const play96_session,
expected: u64,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe {
(*session)
.session
.assert_audio_hash(expected)
.map(|_| ptr::null())
.unwrap_or_else(c_error)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_save_state(
session: *mut play96_session,
path: *const c_char,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let Ok(path) = (unsafe { required_string(path, "path") }) else {
return c_error("invalid path");
};
unsafe { session_result(session, (*session).session.save_state(path)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_load_state(
session: *mut play96_session,
path: *const c_char,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let Ok(path) = (unsafe { required_string(path, "path") }) else {
return c_error("invalid path");
};
unsafe { session_result(session, (*session).session.load_state(path)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_write_png(
session: *const play96_session,
path: *const c_char,
) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
let Ok(path) = (unsafe { required_string(path, "path") }) else {
return c_error("invalid path");
};
unsafe {
(*session)
.session
.write_png(path)
.map(|_| ptr::null())
.unwrap_or_else(c_error)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn play96_last_error(session: *const play96_session) -> *const c_char {
if session.is_null() {
return c_error("session must not be null");
}
unsafe { (*session).error.as_ptr() }
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
static KEY_EVENT_PRESSED: AtomicBool = AtomicBool::new(false);
static KEY_EVENT_CODE: AtomicU32 = AtomicU32::new(0);
static FRAME_TIME: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
unsafe extern "C" fn record_frame_time(reference: i64) {
FRAME_TIME.store(reference, Ordering::Relaxed);
}
unsafe extern "C" fn record_key_event(pressed: bool, keycode: u32, _: u32, _: u16) {
KEY_EVENT_PRESSED.store(pressed, Ordering::Relaxed);
KEY_EVENT_CODE.store(keycode, Ordering::Relaxed);
}
#[test]
fn color_conversion_is_xrgb() {
assert_eq!(Color::rgb(0x12, 0x34, 0x56).xrgb(), 0x123456);
}
#[test]
fn pixel_formats_expand_to_xrgb() {
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_XRGB8888, 0xaa123456),
Some(0x123456)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_RGB565, 0xf800),
Some(0xff0000)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_RGB565, 0x07e0),
Some(0x00ff00)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_RGB565, 0x001f),
Some(0x0000ff)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_0RGB1555, 0xfc00),
Some(0xff0000)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_0RGB1555, 0x83e0),
Some(0x00ff00)
);
assert_eq!(
xrgb_from_pixel(RETRO_PIXEL_FORMAT_0RGB1555, 0x801f),
Some(0x0000ff)
);
assert_eq!(xrgb_from_pixel(99, 0), None);
}
#[test]
fn video_callback_accepts_padded_16_bit_rows() {
let _guard = SESSION_LOCK.lock().unwrap();
let mut capture = Capture {
pixel_format: RETRO_PIXEL_FORMAT_RGB565,
..Default::default()
};
let pixels = [0u8, 0xf8, 0, 0, 0xe0, 0x07, 0, 0];
unsafe { ACTIVE_CAPTURE = &mut capture };
unsafe { video_callback(pixels.as_ptr().cast(), 1, 2, 4) };
unsafe { ACTIVE_CAPTURE = ptr::null_mut() };
assert_eq!(capture.width, 1);
assert_eq!(capture.height, 2);
assert_eq!(capture.pixels, [0xff0000, 0x00ff00]);
}
#[test]
fn hashes_are_stable() {
assert_eq!(fnv1a(&[1, 2, 3]), 0xa094014f35a4929d);
assert_eq!(fnv1a_u32(&[0x030201]), fnv1a(&0x030201_u32.to_ne_bytes()));
assert_eq!(fnv1a_i16(&[0x0201, 0x0403]), fnv1a(&[1, 2, 3, 4]));
}
#[test]
fn pixel_assertion_reports_coordinates_and_colors() {
let capture = Capture {
pixels: vec![0x112233],
width: 1,
height: 1,
..Default::default()
};
let actual = Color::rgb(
(capture.pixels[0] >> 16) as u8,
(capture.pixels[0] >> 8) as u8,
capture.pixels[0] as u8,
);
let expected = Color::rgb(0, 0, 0);
let error = Error::new(format!(
"pixel (0, 0) expected #{:02x}{:02x}{:02x}{:02x}, got #{:02x}{:02x}{:02x}{:02x}",
expected.red,
expected.green,
expected.blue,
expected.alpha,
actual.red,
actual.green,
actual.blue,
actual.alpha
));
assert!(error.to_string().contains("pixel (0, 0)"));
assert!(error.to_string().contains("#000000ff"));
}
#[test]
fn frame_time_callback_is_registered_and_invoked_with_its_reference() {
let _guard = SESSION_LOCK.lock().unwrap();
let mut capture = Capture::default();
let callback = RetroFrameTimeCallback {
callback: Some(record_frame_time),
reference: 16_667,
};
unsafe { ACTIVE_CAPTURE = &mut capture };
assert!(unsafe {
environment_callback(
RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK,
(&raw const callback).cast_mut().cast(),
)
});
invoke_frame_time_callback(&capture);
assert_eq!(FRAME_TIME.load(Ordering::Relaxed), 16_667);
unsafe { ACTIVE_CAPTURE = ptr::null_mut() };
}
#[test]
fn directory_callbacks_return_session_owned_paths() {
let _guard = SESSION_LOCK.lock().unwrap();
let mut capture = Capture {
system_directory: CString::new("system").unwrap(),
content_directory: CString::new("content").unwrap(),
save_directory: CString::new("save").unwrap(),
..Default::default()
};
unsafe { ACTIVE_CAPTURE = &mut capture };
for (command, expected) in [
(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, "system"),
(RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY, "content"),
(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, "save"),
] {
let mut directory: *const c_char = ptr::null();
assert!(unsafe { environment_callback(command, (&raw mut directory).cast()) });
assert_eq!(
unsafe { CStr::from_ptr(directory) }.to_str().unwrap(),
expected
);
}
unsafe { ACTIVE_CAPTURE = ptr::null_mut() };
}
#[test]
fn pixel_format_negotiation_accepts_libretro_formats() {
let _guard = SESSION_LOCK.lock().unwrap();
let mut capture = Capture::default();
unsafe { ACTIVE_CAPTURE = &mut capture };
for pixel_format in [
RETRO_PIXEL_FORMAT_0RGB1555,
RETRO_PIXEL_FORMAT_XRGB8888,
RETRO_PIXEL_FORMAT_RGB565,
] {
assert!(unsafe {
environment_callback(
RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,
(&raw const pixel_format).cast_mut().cast(),
)
});
assert_eq!(capture.pixel_format, pixel_format);
}
let invalid = 99_u32;
assert!(!unsafe {
environment_callback(
RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,
(&raw const invalid).cast_mut().cast(),
)
});
assert_eq!(capture.pixel_format, RETRO_PIXEL_FORMAT_RGB565);
unsafe { ACTIVE_CAPTURE = ptr::null_mut() };
}
#[test]
fn keyboard_and_mouse_callbacks_report_input() {
let _guard = SESSION_LOCK.lock().unwrap();
let mut capture = Capture::default();
capture.keys.insert(key::SPACE);
capture.key_events.push((true, key::SPACE));
capture.keyboard_callback = Some(record_key_event);
capture.mouse[0] = 7;
capture.mouse[1] = -3;
capture.mouse[MouseButton::Left as usize] = 1;
unsafe { ACTIVE_CAPTURE = &mut capture };
assert_eq!(
unsafe { input_state_callback(0, RETRO_DEVICE_KEYBOARD, 0, key::SPACE) },
1
);
assert_eq!(
unsafe { input_state_callback(0, RETRO_DEVICE_MOUSE, 0, 0) },
7
);
assert_eq!(
unsafe { input_state_callback(0, RETRO_DEVICE_MOUSE, 0, 1) },
-3
);
assert_eq!(
unsafe { input_state_callback(0, RETRO_DEVICE_MOUSE, 0, MouseButton::Left as u32) },
1
);
unsafe { input_poll_callback() };
assert!(KEY_EVENT_PRESSED.load(Ordering::Relaxed));
assert_eq!(KEY_EVENT_CODE.load(Ordering::Relaxed), key::SPACE);
unsafe { ACTIVE_CAPTURE = ptr::null_mut() };
}
}