retro_rs/
emulator.rs

1use crate::buttons::Buttons;
2use crate::error::RetroRsError;
3use crate::pixels::{argb555to888, rgb565to888, rgb888_to_rgb332};
4use libloading::Library;
5use libloading::Symbol;
6#[allow(clippy::wildcard_imports)]
7use rust_libretro_sys::*;
8use std::ffi::{CStr, CString, c_char, c_uint, c_void};
9use std::fs::File;
10use std::io::Read;
11use std::marker::PhantomData;
12use std::panic;
13use std::path::{Path, PathBuf};
14use std::ptr;
15
16thread_local! {
17    static CTX:std::cell::RefCell<Option<EmulatorContext>> = const{std::cell::RefCell::new(None)};
18}
19
20type NotSendSync = *const [u8; 0];
21struct EmulatorCore {
22    core_lib: Library,
23    rom_path: CString,
24    core: CoreFns,
25    _marker: PhantomData<NotSendSync>,
26}
27
28#[allow(dead_code, clippy::struct_field_names)]
29struct CoreFns {
30    retro_api_version: unsafe extern "C" fn() -> c_uint,
31    retro_cheat_reset: unsafe extern "C" fn(),
32    retro_cheat_set: unsafe extern "C" fn(c_uint, bool, *const c_char),
33    retro_deinit: unsafe extern "C" fn(),
34    retro_get_memory_data: unsafe extern "C" fn(c_uint) -> *mut c_void,
35    retro_get_memory_size: unsafe extern "C" fn(c_uint) -> usize,
36    retro_get_region: unsafe extern "C" fn() -> c_uint,
37    retro_get_system_av_info: unsafe extern "C" fn(*mut retro_system_av_info),
38    retro_get_system_info: unsafe extern "C" fn(*mut retro_system_info),
39    retro_init: unsafe extern "C" fn(),
40    retro_load_game: unsafe extern "C" fn(*const retro_game_info) -> bool,
41    retro_load_game_special: unsafe extern "C" fn(c_uint, *const retro_game_info, usize) -> bool,
42    retro_reset: unsafe extern "C" fn(),
43    retro_run: unsafe extern "C" fn(),
44    retro_serialize: unsafe extern "C" fn(*mut c_void, usize) -> bool,
45    retro_serialize_size: unsafe extern "C" fn() -> usize,
46    retro_set_audio_sample: unsafe extern "C" fn(retro_audio_sample_t),
47    retro_set_audio_sample_batch: unsafe extern "C" fn(retro_audio_sample_batch_t),
48    retro_set_controller_port_device: unsafe extern "C" fn(c_uint, c_uint),
49    retro_set_environment: unsafe extern "C" fn(retro_environment_t),
50    retro_set_input_poll: unsafe extern "C" fn(retro_input_poll_t),
51    retro_set_input_state: unsafe extern "C" fn(retro_input_state_t),
52    retro_set_video_refresh: unsafe extern "C" fn(retro_video_refresh_t),
53    retro_unload_game: unsafe extern "C" fn(),
54    retro_unserialize: unsafe extern "C" fn(*const c_void, usize) -> bool,
55}
56
57pub type ButtonCallback = Box<dyn Fn(u32, u32, u32, u32) -> i16>;
58
59#[allow(dead_code)]
60struct EmulatorContext {
61    audio_sample: Vec<i16>,
62    buttons: [Buttons; 2],
63    button_callback: Option<ButtonCallback>,
64    core_path: CString,
65    frame_ptr: *const c_void,
66    frame_pitch: usize,
67    frame_width: u32,
68    frame_height: u32,
69    pixfmt: retro_pixel_format,
70    image_depth: usize,
71    memory_map: Vec<retro_memory_descriptor>,
72    av_info: retro_system_av_info,
73    sys_info: retro_system_info,
74    _marker: PhantomData<NotSendSync>,
75}
76
77// A more pleasant wrapper over MemoryDescriptor
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct MemoryRegion {
80    which: usize,
81    pub flags: u64,
82    pub len: usize,
83    pub start: usize,
84    pub offset: usize,
85    pub name: String,
86    pub select: usize,
87    pub disconnect: usize,
88}
89
90pub struct Emulator {
91    core: EmulatorCore,
92}
93
94impl Emulator {
95    /// # Panics
96    /// If the platform is not Windows, Mac, or Linux; if the dylib fails to load successfully; if any Emulator has been created on this thread but not yet dropped.
97    #[must_use]
98    #[allow(clippy::too_many_lines)]
99    pub fn create(core_path: &Path, rom_path: &Path) -> Emulator {
100        let emu = CTX.with_borrow_mut(move |ctx_opt| {
101            assert!(
102                ctx_opt.is_none(),
103                "Can't use multiple emulators in one thread currently"
104            );
105            let suffix = if cfg!(target_os = "windows") {
106                "dll"
107            } else if cfg!(target_os = "macos") {
108                "dylib"
109            } else if cfg!(target_os = "linux") {
110                "so"
111            } else {
112                panic!("Unsupported platform")
113            };
114            let path: PathBuf = core_path.with_extension(suffix);
115            #[cfg(target_os = "linux")]
116            let dll: Library = unsafe {
117                use libc::RTLD_NODELETE;
118                use libloading::os::unix::{self, RTLD_LOCAL, RTLD_NOW};
119                // Load library with `RTLD_NOW | RTLD_LOCAL | RTLD_NODELETE` to fix a SIGSEGV
120                unix::Library::open(Some(path), RTLD_NOW | RTLD_LOCAL | RTLD_NODELETE)
121                    .unwrap()
122                    .into()
123            };
124            #[cfg(not(target_os = "linux"))]
125            let dll = unsafe { Library::new(path).unwrap() };
126            unsafe {
127                let retro_set_environment = *(dll.get(b"retro_set_environment").unwrap());
128                let retro_set_video_refresh = *(dll.get(b"retro_set_video_refresh").unwrap());
129                let retro_set_audio_sample = *(dll.get(b"retro_set_audio_sample").unwrap());
130                let retro_set_audio_sample_batch =
131                    *(dll.get(b"retro_set_audio_sample_batch").unwrap());
132                let retro_set_input_poll = *(dll.get(b"retro_set_input_poll").unwrap());
133                let retro_set_input_state = *(dll.get(b"retro_set_input_state").unwrap());
134                let retro_init = *(dll.get(b"retro_init").unwrap());
135                let retro_deinit = *(dll.get(b"retro_deinit").unwrap());
136                let retro_api_version = *(dll.get(b"retro_api_version").unwrap());
137                let retro_get_system_info = *(dll.get(b"retro_get_system_info").unwrap());
138                let retro_get_system_av_info = *(dll.get(b"retro_get_system_av_info").unwrap());
139                let retro_set_controller_port_device =
140                    *(dll.get(b"retro_set_controller_port_device").unwrap());
141                let retro_reset = *(dll.get(b"retro_reset").unwrap());
142                let retro_run = *(dll.get(b"retro_run").unwrap());
143                let retro_serialize_size = *(dll.get(b"retro_serialize_size").unwrap());
144                let retro_serialize = *(dll.get(b"retro_serialize").unwrap());
145                let retro_unserialize = *(dll.get(b"retro_unserialize").unwrap());
146                let retro_cheat_reset = *(dll.get(b"retro_cheat_reset").unwrap());
147                let retro_cheat_set = *(dll.get(b"retro_cheat_set").unwrap());
148                let retro_load_game = *(dll.get(b"retro_load_game").unwrap());
149                let retro_load_game_special = *(dll.get(b"retro_load_game_special").unwrap());
150                let retro_unload_game = *(dll.get(b"retro_unload_game").unwrap());
151                let retro_get_region = *(dll.get(b"retro_get_region").unwrap());
152                let retro_get_memory_data = *(dll.get(b"retro_get_memory_data").unwrap());
153                let retro_get_memory_size = *(dll.get(b"retro_get_memory_size").unwrap());
154                let emu = EmulatorCore {
155                    core_lib: dll,
156                    rom_path: CString::new(rom_path.to_str().unwrap()).unwrap(),
157                    core: CoreFns {
158                        retro_api_version,
159                        retro_cheat_reset,
160                        retro_cheat_set,
161                        retro_deinit,
162                        retro_get_memory_data,
163                        retro_get_memory_size,
164
165                        retro_get_region,
166                        retro_get_system_av_info,
167
168                        retro_get_system_info,
169
170                        retro_init,
171                        retro_load_game,
172                        retro_load_game_special,
173
174                        retro_reset,
175                        retro_run,
176
177                        retro_serialize,
178                        retro_serialize_size,
179                        retro_set_audio_sample,
180
181                        retro_set_audio_sample_batch,
182                        retro_set_controller_port_device,
183
184                        retro_set_environment,
185                        retro_set_input_poll,
186                        retro_set_input_state,
187
188                        retro_set_video_refresh,
189                        retro_unload_game,
190                        retro_unserialize,
191                    },
192                    _marker: PhantomData,
193                };
194                let sys_info = retro_system_info {
195                    library_name: ptr::null(),
196                    library_version: ptr::null(),
197                    valid_extensions: ptr::null(),
198                    need_fullpath: false,
199                    block_extract: false,
200                };
201                let av_info = retro_system_av_info {
202                    geometry: retro_game_geometry {
203                        base_width: 0,
204                        base_height: 0,
205                        max_width: 0,
206                        max_height: 0,
207                        aspect_ratio: 0.0,
208                    },
209                    timing: retro_system_timing {
210                        fps: 0.0,
211                        sample_rate: 0.0,
212                    },
213                };
214
215                let mut ctx = EmulatorContext {
216                    av_info,
217                    sys_info,
218                    core_path: CString::new(core_path.to_str().unwrap()).unwrap(),
219                    audio_sample: Vec::new(),
220                    buttons: [Buttons::new(), Buttons::new()],
221                    button_callback: None,
222                    frame_ptr: ptr::null(),
223                    frame_pitch: 0,
224                    frame_width: 0,
225                    frame_height: 0,
226                    pixfmt: retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555,
227                    image_depth: 0,
228                    memory_map: Vec::new(),
229                    _marker: PhantomData,
230                };
231                (emu.core.retro_get_system_info)(&raw mut ctx.sys_info);
232                (emu.core.retro_get_system_av_info)(&raw mut ctx.av_info);
233
234                *ctx_opt = Some(ctx);
235                emu
236            }
237        });
238        unsafe {
239            // Set up callbacks
240            (emu.core.retro_set_environment)(Some(callback_environment));
241            (emu.core.retro_set_video_refresh)(Some(callback_video_refresh));
242            (emu.core.retro_set_audio_sample)(Some(callback_audio_sample));
243            (emu.core.retro_set_audio_sample_batch)(Some(callback_audio_sample_batch));
244            (emu.core.retro_set_input_poll)(Some(callback_input_poll));
245            (emu.core.retro_set_input_state)(Some(callback_input_state));
246            // Load the game
247            (emu.core.retro_init)();
248            let rom_cstr = emu.rom_path.clone();
249            let mut rom_file = File::open(rom_path).unwrap();
250            let mut buffer = Vec::new();
251            rom_file.read_to_end(&mut buffer).unwrap();
252            buffer.shrink_to_fit();
253            let game_info = retro_game_info {
254                path: rom_cstr.as_ptr(),
255                data: buffer.as_ptr().cast(),
256                size: buffer.len(),
257                meta: ptr::null(),
258            };
259            (emu.core.retro_load_game)(&raw const game_info);
260            CTX.with_borrow_mut(|ctx| {
261                let ctx = ctx.as_mut().unwrap();
262                (emu.core.retro_get_system_info)(&raw mut ctx.sys_info);
263                (emu.core.retro_get_system_av_info)(&raw mut ctx.av_info);
264            });
265        }
266        Emulator { core: emu }
267    }
268    pub fn get_library(&mut self) -> &Library {
269        &self.core.core_lib
270    }
271    #[must_use]
272    pub fn get_symbol<'a, T>(&'a self, symbol: &[u8]) -> Option<Symbol<'a, T>> {
273        let dll = &self.core.core_lib;
274        let sym: Result<Symbol<T>, _> = unsafe { dll.get(symbol) };
275        sym.ok()
276    }
277    #[allow(clippy::missing_panics_doc)]
278    pub fn run(&mut self, inputs: [Buttons; 2]) {
279        CTX.with_borrow_mut(|ctx| {
280            let ctx = ctx.as_mut().unwrap();
281            //clear audio buffers and whatever else
282            ctx.audio_sample.clear();
283            //set inputs on CB
284            ctx.buttons = inputs;
285            ctx.button_callback = None;
286        });
287        unsafe {
288            //run one step
289            (self.core.core.retro_run)();
290        }
291    }
292    #[allow(clippy::missing_panics_doc)]
293    pub fn run_with_button_callback(&mut self, input: Box<dyn Fn(u32, u32, u32, u32) -> i16>) {
294        CTX.with_borrow_mut(|ctx| {
295            let ctx = ctx.as_mut().unwrap();
296            //clear audio buffers and whatever else
297            ctx.audio_sample.clear();
298            //set inputs on CB
299            ctx.button_callback = Some(Box::new(input));
300        });
301        unsafe {
302            //run one step
303            (self.core.core.retro_run)();
304        }
305    }
306    #[allow(clippy::missing_panics_doc)]
307    pub fn reset(&mut self) {
308        CTX.with_borrow_mut(|ctx| {
309            let ctx = ctx.as_mut().unwrap();
310            // clear audio buffers and whatever else
311            ctx.audio_sample.clear();
312            // set inputs on CB
313            ctx.buttons = [Buttons::new(), Buttons::new()];
314            ctx.button_callback = None;
315            // clear fb
316            ctx.frame_ptr = ptr::null();
317        });
318        unsafe { (self.core.core.retro_reset)() }
319    }
320    #[must_use]
321    fn get_ram_size(&self, rtype: libc::c_uint) -> usize {
322        unsafe { (self.core.core.retro_get_memory_size)(rtype) }
323    }
324    #[must_use]
325    pub fn get_video_ram_size(&self) -> usize {
326        self.get_ram_size(RETRO_MEMORY_VIDEO_RAM)
327    }
328    #[must_use]
329    pub fn get_system_ram_size(&self) -> usize {
330        self.get_ram_size(RETRO_MEMORY_SYSTEM_RAM)
331    }
332    #[must_use]
333    pub fn get_save_ram_size(&self) -> usize {
334        self.get_ram_size(RETRO_MEMORY_SAVE_RAM)
335    }
336    #[must_use]
337    pub fn video_ram_ref(&self) -> &[u8] {
338        self.get_ram(RETRO_MEMORY_VIDEO_RAM)
339    }
340    #[must_use]
341    pub fn system_ram_ref(&self) -> &[u8] {
342        self.get_ram(RETRO_MEMORY_SYSTEM_RAM)
343    }
344    #[must_use]
345    pub fn system_ram_mut(&mut self) -> &mut [u8] {
346        self.get_ram_mut(RETRO_MEMORY_SYSTEM_RAM)
347    }
348    #[must_use]
349    pub fn save_ram(&self) -> &[u8] {
350        self.get_ram(RETRO_MEMORY_SAVE_RAM)
351    }
352
353    #[must_use]
354    fn get_ram(&self, ramtype: libc::c_uint) -> &[u8] {
355        let len = self.get_ram_size(ramtype);
356        unsafe {
357            let ptr: *const u8 = (self.core.core.retro_get_memory_data)(ramtype).cast();
358            std::slice::from_raw_parts(ptr, len)
359        }
360    }
361    #[must_use]
362    fn get_ram_mut(&mut self, ramtype: libc::c_uint) -> &mut [u8] {
363        let len = self.get_ram_size(ramtype);
364        unsafe {
365            let ptr: *mut u8 = (self.core.core.retro_get_memory_data)(ramtype).cast();
366            std::slice::from_raw_parts_mut(ptr, len)
367        }
368    }
369    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
370    #[must_use]
371    pub fn memory_regions(&self) -> Vec<MemoryRegion> {
372        CTX.with_borrow(|ctx| {
373            let map = &ctx.as_ref().unwrap().memory_map;
374            map.iter()
375                .enumerate()
376                .map(|(i, mdesc)| MemoryRegion {
377                    which: i,
378                    flags: mdesc.flags,
379                    len: mdesc.len,
380                    start: mdesc.start,
381                    offset: mdesc.offset,
382                    select: mdesc.select,
383                    disconnect: mdesc.disconnect,
384                    name: if mdesc.addrspace.is_null() {
385                        String::new()
386                    } else {
387                        unsafe { CStr::from_ptr(mdesc.addrspace) }
388                            .to_string_lossy()
389                            .into_owned()
390                    },
391                })
392                .collect()
393        })
394    }
395    /// # Errors
396    /// [`RetroRsError::RAMCopyNotMappedIntoMemoryRegionError`]: Returns an error if the desired address is not mapped into memory regions
397    pub fn memory_ref(&self, start: usize) -> Result<&[u8], RetroRsError> {
398        for mr in self.memory_regions() {
399            if mr.select != 0 && (start & mr.select) == 0 {
400                continue;
401            }
402            if start >= mr.start && start < mr.start + mr.len {
403                return CTX.with_borrow(|ctx| {
404                    let maps = &ctx.as_ref().unwrap().memory_map;
405                    if mr.which >= maps.len() {
406                        // TODO more aggressive checking of mr vs map
407                        return Err(RetroRsError::RAMMapOutOfRangeError);
408                    }
409                    let start = (start - mr.start) & !mr.disconnect;
410                    let map = &maps[mr.which];
411                    //0-based at this point, modulo offset
412                    let ptr: *mut u8 = map.ptr.cast();
413                    let slice = unsafe {
414                        let ptr = ptr.add(start).add(map.offset);
415                        std::slice::from_raw_parts(ptr, map.len - start)
416                    };
417                    Ok(slice)
418                });
419            } else if start < mr.start {
420                return Err(RetroRsError::RAMCopySrcOutOfBoundsError);
421            }
422        }
423        Err(RetroRsError::RAMCopyNotMappedIntoMemoryRegionError)
424    }
425    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
426    /// # Errors
427    /// [`RetroRsError::RAMMapOutOfRangeError`]: The desired address is out of mapped range
428    /// [`RetroRsError::RAMCopySrcOutOfBoundsError`]: The desired range is not in the requested region
429    pub fn memory_ref_mut(
430        &mut self,
431        mr: &MemoryRegion,
432        start: usize,
433    ) -> Result<&mut [u8], RetroRsError> {
434        CTX.with_borrow_mut(|ctx| {
435            let maps = &mut ctx.as_mut().unwrap().memory_map;
436            if mr.which >= maps.len() {
437                // TODO more aggressive checking of mr vs map
438                return Err(RetroRsError::RAMMapOutOfRangeError);
439            }
440            if start < mr.start {
441                return Err(RetroRsError::RAMCopySrcOutOfBoundsError);
442            }
443            let start = (start - mr.start) & !mr.disconnect;
444            let map = &maps[mr.which];
445            //0-based at this point, modulo offset
446            let ptr: *mut u8 = map.ptr.cast();
447            let slice = unsafe {
448                let ptr = ptr.add(start).add(map.offset);
449                std::slice::from_raw_parts_mut(ptr, map.len - start)
450            };
451            Ok(slice)
452        })
453    }
454    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
455    #[must_use]
456    pub fn pixel_format(&self) -> retro_pixel_format {
457        CTX.with_borrow(|ctx| ctx.as_ref().unwrap().pixfmt)
458    }
459    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
460    #[must_use]
461    pub fn framebuffer_size(&self) -> (usize, usize) {
462        CTX.with_borrow(|ctx| {
463            let ctx = ctx.as_ref().unwrap();
464            (ctx.frame_width as usize, ctx.frame_height as usize)
465        })
466    }
467    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
468    #[must_use]
469    pub fn framebuffer_pitch(&self) -> usize {
470        CTX.with_borrow(|ctx| ctx.as_ref().unwrap().frame_pitch)
471    }
472    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
473    /// # Errors
474    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
475    pub fn peek_framebuffer<FBPeek, FBPeekRet>(&self, f: FBPeek) -> Result<FBPeekRet, RetroRsError>
476    where
477        FBPeek: FnOnce(&[u8]) -> FBPeekRet,
478    {
479        CTX.with_borrow(|ctx| {
480            let ctx = ctx.as_ref().unwrap();
481            if ctx.frame_ptr.is_null() {
482                Err(RetroRsError::NoFramebufferError)
483            } else {
484                unsafe {
485                    #[allow(clippy::cast_possible_truncation)]
486                    let frame_slice = std::slice::from_raw_parts(
487                        ctx.frame_ptr.cast(),
488                        (ctx.frame_height * (ctx.frame_pitch as u32)) as usize,
489                    );
490                    Ok(f(frame_slice))
491                }
492            }
493        })
494    }
495    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
496    pub fn peek_audio_sample<AudioPeek, AudioPeekRet>(&self, f: AudioPeek) -> AudioPeekRet
497    where
498        AudioPeek: FnOnce(&[i16]) -> AudioPeekRet,
499    {
500        CTX.with_borrow(|ctx| f(&ctx.as_ref().unwrap().audio_sample))
501    }
502    #[must_use]
503    pub fn get_audio_sample_rate(&self) -> f64 {
504        CTX.with_borrow_mut(|ctx| ctx.as_ref().unwrap().av_info.timing.sample_rate)
505    }
506    #[must_use]
507    pub fn get_video_fps(&self) -> f64 {
508        CTX.with_borrow_mut(|ctx| ctx.as_ref().unwrap().av_info.timing.fps)
509    }
510    #[must_use]
511    pub fn get_aspect_ratio(&self) -> f32 {
512        CTX.with_borrow_mut(|ctx| ctx.as_ref().unwrap().av_info.geometry.aspect_ratio)
513    }
514
515    #[must_use]
516    pub fn save(&self, bytes: &mut [u8]) -> bool {
517        let size = self.save_size();
518        if bytes.len() < size {
519            return false;
520        }
521        unsafe { (self.core.core.retro_serialize)(bytes.as_mut_ptr().cast(), size) }
522    }
523    #[must_use]
524    pub fn load(&mut self, bytes: &[u8]) -> bool {
525        let size = self.save_size();
526        if bytes.len() < size {
527            return false;
528        }
529        unsafe { (self.core.core.retro_unserialize)(bytes.as_ptr().cast(), size) }
530    }
531    #[must_use]
532    pub fn save_size(&self) -> usize {
533        unsafe { (self.core.core.retro_serialize_size)() }
534    }
535    pub fn clear_cheats(&mut self) {
536        unsafe { (self.core.core.retro_cheat_reset)() }
537    }
538    /// # Panics
539    /// May panic if code can't be converted to a [`CString`]
540    pub fn set_cheat(&mut self, index: usize, enabled: bool, code: &str) {
541        unsafe {
542            // FIXME: Creates a memory leak since the libretro api won't let me from_raw() it back and drop it.  I don't know if libretro guarantees anything about ownership of this str to cores.
543            #[allow(clippy::cast_possible_truncation)]
544            (self.core.core.retro_cheat_set)(
545                index as u32,
546                enabled,
547                CString::new(code).unwrap().into_raw(),
548            );
549        }
550    }
551    /// # Panics
552    /// Panics if the pixel format used by the core is not supported for reads.
553    /// # Errors
554    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
555    pub fn get_pixel(&self, x: usize, y: usize) -> Result<(u8, u8, u8), RetroRsError> {
556        let (w, _h) = self.framebuffer_size();
557        self.peek_framebuffer(move |fb| match self.pixel_format() {
558            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
559                let start = y * w + x;
560                let gb = fb[start * 2];
561                let arg = fb[start * 2 + 1];
562                let (red, green, blue) = argb555to888(gb, arg);
563                (red, green, blue)
564            }
565            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
566                let off = (y * w + x) * 4;
567                (fb[off + 1], fb[off + 2], fb[off + 3])
568            }
569            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
570                let start = y * w + x;
571                let gb = fb[start * 2];
572                let rg = fb[start * 2 + 1];
573                let (red, green, blue) = rgb565to888(gb, rg);
574                (red, green, blue)
575            }
576            _ => panic!("Unsupported pixel format"),
577        })
578    }
579    /// # Panics
580    /// Panics if the pixel format used by the core is not supported for reads.
581    /// # Errors
582    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
583    #[allow(clippy::many_single_char_names)]
584    pub fn for_each_pixel(
585        &self,
586        mut f: impl FnMut(usize, usize, u8, u8, u8),
587    ) -> Result<(), RetroRsError> {
588        let (w, h) = self.framebuffer_size();
589        let fmt = self.pixel_format();
590        self.peek_framebuffer(move |fb| {
591            let mut x = 0;
592            let mut y = 0;
593            match fmt {
594                retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
595                    for components in fb.chunks_exact(2) {
596                        let gb = components[0];
597                        let arg = components[1];
598                        let (red, green, blue) = argb555to888(gb, arg);
599                        f(x, y, red, green, blue);
600                        x += 1;
601                        if x >= w {
602                            y += 1;
603                            x = 0;
604                        }
605                    }
606                }
607                retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
608                    for components in fb.chunks_exact(4) {
609                        let red = components[1];
610                        let green = components[2];
611                        let blue = components[3];
612                        f(x, y, red, green, blue);
613                        x += 1;
614                        if x >= w {
615                            y += 1;
616                            x = 0;
617                        }
618                    }
619                }
620                retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
621                    for components in fb.chunks_exact(2) {
622                        let gb = components[0];
623                        let rg = components[1];
624                        let (red, green, blue) = rgb565to888(gb, rg);
625                        f(x, y, red, green, blue);
626                        x += 1;
627                        if x >= w {
628                            y += 1;
629                            x = 0;
630                        }
631                    }
632                }
633                _ => panic!("Unsupported pixel format"),
634            }
635            assert_eq!(y, h);
636            assert_eq!(x, 0);
637        })
638    }
639    /// # Panics
640    /// Panics if the pixel format used by the core is not supported for reads.
641    /// # Errors
642    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
643    pub fn copy_framebuffer_rgb888(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
644        let fmt = self.pixel_format();
645        self.peek_framebuffer(move |fb| match fmt {
646            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
647                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(3)) {
648                    let gb = components[0];
649                    let arg = components[1];
650                    let (red, green, blue) = argb555to888(gb, arg);
651                    dst[0] = red;
652                    dst[1] = green;
653                    dst[2] = blue;
654                }
655            }
656            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
657                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(3)) {
658                    let r = components[1];
659                    let g = components[2];
660                    let b = components[3];
661                    dst[0] = r;
662                    dst[1] = g;
663                    dst[2] = b;
664                }
665            }
666            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
667                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(3)) {
668                    let gb = components[0];
669                    let rg = components[1];
670                    let (red, green, blue) = rgb565to888(gb, rg);
671                    dst[0] = red;
672                    dst[1] = green;
673                    dst[2] = blue;
674                }
675            }
676            _ => panic!("Unsupported pixel format"),
677        })
678    }
679    /// # Panics
680    /// Panics if the pixel format used by the core is not supported for reads.
681    /// # Errors
682    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
683    pub fn copy_framebuffer_rgba8888(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
684        let fmt = self.pixel_format();
685        self.peek_framebuffer(move |fb| match fmt {
686            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
687                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
688                    let gb = components[0];
689                    let arg = components[1];
690                    let (red, green, blue) = argb555to888(gb, arg);
691                    dst[0] = red;
692                    dst[1] = green;
693                    dst[2] = blue;
694                    dst[3] = (arg >> 7) * 0xFF;
695                }
696            }
697            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
698                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(4)) {
699                    let a = components[0];
700                    let r = components[1];
701                    let g = components[2];
702                    let b = components[3];
703                    dst[0] = r;
704                    dst[1] = g;
705                    dst[2] = b;
706                    dst[3] = a;
707                }
708            }
709            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
710                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
711                    let gb = components[0];
712                    let rg = components[1];
713                    let (red, green, blue) = rgb565to888(gb, rg);
714                    dst[0] = red;
715                    dst[1] = green;
716                    dst[2] = blue;
717                    dst[3] = 0xFF;
718                }
719            }
720            _ => panic!("Unsupported pixel format"),
721        })
722    }
723    /// # Panics
724    /// Panics if the pixel format used by the core is not supported for reads.
725    /// # Errors
726    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
727    pub fn copy_framebuffer_rgb332(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
728        let fmt = self.pixel_format();
729        self.peek_framebuffer(move |fb| match fmt {
730            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
731                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
732                    let gb = components[0];
733                    let arg = components[1];
734                    let (red, green, blue) = argb555to888(gb, arg);
735                    *dst = rgb888_to_rgb332(red, green, blue);
736                }
737            }
738            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
739                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
740                    let r = components[1];
741                    let g = components[2];
742                    let b = components[3];
743                    *dst = rgb888_to_rgb332(r, g, b);
744                }
745            }
746            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
747                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
748                    let gb = components[0];
749                    let rg = components[1];
750                    let (red, green, blue) = rgb565to888(gb, rg);
751                    *dst = rgb888_to_rgb332(red, green, blue);
752                }
753            }
754            _ => panic!("Unsupported pixel format"),
755        })
756    }
757    /// # Panics
758    /// Panics if the pixel format used by the core is not supported for reads.
759    /// # Errors
760    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
761    pub fn copy_framebuffer_argb32(&self, slice: &mut [u32]) -> Result<(), RetroRsError> {
762        let fmt = self.pixel_format();
763        self.peek_framebuffer(move |fb| match fmt {
764            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
765                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
766                    let gb = components[0];
767                    let arg = components[1];
768                    let (red, green, blue) = argb555to888(gb, arg);
769                    *dst = (0xFF00_0000 * (u32::from(arg) >> 7))
770                        | (u32::from(red) << 16)
771                        | (u32::from(green) << 8)
772                        | u32::from(blue);
773                }
774            }
775            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
776                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
777                    *dst = (u32::from(components[0]) << 24)
778                        | (u32::from(components[1]) << 16)
779                        | (u32::from(components[2]) << 8)
780                        | u32::from(components[3]);
781                }
782            }
783            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
784                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
785                    let gb = components[0];
786                    let rg = components[1];
787                    let (red, green, blue) = rgb565to888(gb, rg);
788                    *dst = 0xFF00_0000
789                        | (u32::from(red) << 16)
790                        | (u32::from(green) << 8)
791                        | u32::from(blue);
792                }
793            }
794            _ => panic!("Unsupported pixel format"),
795        })
796    }
797    /// # Panics
798    /// Panics if the pixel format used by the core is not supported for reads.
799    /// # Errors
800    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
801    pub fn copy_framebuffer_rgba32(&self, slice: &mut [u32]) -> Result<(), RetroRsError> {
802        let fmt = self.pixel_format();
803        self.peek_framebuffer(move |fb| match fmt {
804            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
805                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
806                    let gb = components[0];
807                    let arg = components[1];
808                    let (red, green, blue) = argb555to888(gb, arg);
809                    *dst = (u32::from(red) << 24)
810                        | (u32::from(green) << 16)
811                        | (u32::from(blue) << 8)
812                        | (u32::from(arg >> 7) * 0x0000_00FF);
813                }
814            }
815            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
816                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
817                    *dst = (u32::from(components[1]) << 24)
818                        | (u32::from(components[2]) << 16)
819                        | (u32::from(components[3]) << 8)
820                        | u32::from(components[0]);
821                }
822            }
823            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
824                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
825                    let gb = components[0];
826                    let rg = components[1];
827                    let (red, green, blue) = rgb565to888(gb, rg);
828                    *dst = (u32::from(red) << 24)
829                        | (u32::from(green) << 16)
830                        | (u32::from(blue) << 8)
831                        | 0x0000_00FF;
832                }
833            }
834            _ => panic!("Unsupported pixel format"),
835        })
836    }
837    /// # Panics
838    /// Panics if the pixel format used by the core is not supported for reads.
839    /// # Errors
840    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
841    pub fn copy_framebuffer_rgba_f32x4(&self, slice: &mut [f32]) -> Result<(), RetroRsError> {
842        let fmt = self.pixel_format();
843        self.peek_framebuffer(move |fb| match fmt {
844            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
845                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
846                    let gb = components[0];
847                    let arg = components[1];
848                    let (red, green, blue) = argb555to888(gb, arg);
849                    let alpha = f32::from(arg >> 7);
850                    dst[0] = f32::from(red) / 255.;
851                    dst[1] = f32::from(green) / 255.;
852                    dst[2] = f32::from(blue) / 255.;
853                    dst[3] = alpha;
854                }
855            }
856            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
857                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(4)) {
858                    dst[0] = f32::from(components[0]) / 255.;
859                    dst[1] = f32::from(components[1]) / 255.;
860                    dst[2] = f32::from(components[2]) / 255.;
861                    dst[3] = f32::from(components[3]) / 255.;
862                }
863            }
864            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
865                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
866                    let gb = components[0];
867                    let rg = components[1];
868                    let (red, green, blue) = rgb565to888(gb, rg);
869                    let alpha = 1.;
870                    dst[0] = f32::from(red) / 255.;
871                    dst[1] = f32::from(green) / 255.;
872                    dst[2] = f32::from(blue) / 255.;
873                    dst[3] = alpha;
874                }
875            }
876            _ => panic!("Unsupported pixel format"),
877        })
878    }
879}
880
881unsafe extern "C" fn callback_environment(cmd: u32, data: *mut c_void) -> bool {
882    let result = panic::catch_unwind(|| {
883        CTX.with_borrow_mut(|ctx| {
884            let ctx = ctx.as_mut().unwrap();
885            match cmd {
886                RETRO_ENVIRONMENT_SET_CONTROLLER_INFO => true,
887                RETRO_ENVIRONMENT_SET_PIXEL_FORMAT => {
888                    let pixfmt = unsafe { *(data as *const retro_pixel_format) };
889                    ctx.image_depth = match pixfmt {
890                        retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => 15,
891                        retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => 32,
892                        retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => 16,
893                        _ => panic!("Unsupported pixel format"),
894                    };
895                    ctx.pixfmt = pixfmt;
896                    true
897                }
898                RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY => unsafe {
899                    *(data.cast()) = ctx.core_path.as_ptr();
900                    true
901                },
902                RETRO_ENVIRONMENT_GET_CAN_DUPE => unsafe {
903                    *(data.cast()) = true;
904                    true
905                },
906                RETRO_ENVIRONMENT_SET_MEMORY_MAPS => unsafe {
907                    let map: *const retro_memory_map = data.cast();
908                    let desc_slice = std::slice::from_raw_parts(
909                        (*map).descriptors,
910                        (*map).num_descriptors as usize,
911                    );
912                    // Don't know who owns map or how long it will last
913                    ctx.memory_map = Vec::new();
914                    // So we had better copy it
915                    ctx.memory_map.extend_from_slice(desc_slice);
916                    // (Implicitly we also want to drop the old one, which we did by reassigning)
917                    true
918                },
919                RETRO_ENVIRONMENT_GET_INPUT_BITMASKS => true,
920                _ => false,
921            }
922        })
923    });
924    result.unwrap_or(false)
925}
926
927extern "C" fn callback_video_refresh(data: *const c_void, width: u32, height: u32, pitch: usize) {
928    // Can't panic
929    // context's framebuffer just points to the given data.  Seems to work OK for gym-retro.
930    if !data.is_null() {
931        CTX.with_borrow_mut(|ctx| {
932            let ctx = ctx.as_mut().unwrap();
933            ctx.frame_ptr = data;
934            ctx.frame_pitch = pitch;
935            ctx.frame_width = width;
936            ctx.frame_height = height;
937        });
938    }
939}
940extern "C" fn callback_audio_sample(left: i16, right: i16) {
941    // Can't panic
942    CTX.with_borrow_mut(|ctx| {
943        let ctx = ctx.as_mut().unwrap();
944        let sample_buf = &mut ctx.audio_sample;
945        sample_buf.push(left);
946        sample_buf.push(right);
947    });
948}
949extern "C" fn callback_audio_sample_batch(data: *const i16, frames: usize) -> usize {
950    // Can't panic
951    CTX.with_borrow_mut(|ctx| {
952        let ctx = ctx.as_mut().unwrap();
953        let sample_buf = &mut ctx.audio_sample;
954        let slice = unsafe { std::slice::from_raw_parts(data, frames * 2) };
955        sample_buf.extend_from_slice(slice);
956        frames
957    })
958}
959
960extern "C" fn callback_input_poll() {}
961
962extern "C" fn callback_input_state(port: u32, device: u32, index: u32, id: u32) -> i16 {
963    // Can't panic
964    if port > 1 || device != 1 || index != 0 {
965        // Unsupported port/device/index
966        println!("Unsupported port/device/index {port}/{device}/{index}");
967        return 0;
968    }
969    let bitmask_enabled = (device == RETRO_DEVICE_JOYPAD) && (id == RETRO_DEVICE_ID_JOYPAD_MASK);
970    CTX.with_borrow(|ctx| {
971        let ctx = ctx.as_ref().unwrap();
972        if let Some(cb) = &ctx.button_callback {
973            cb(port, device, index, id)
974        } else if bitmask_enabled {
975            let port = port as usize;
976            i16::from(ctx.buttons[port])
977        } else {
978            let port = port as usize;
979            i16::from(ctx.buttons[port].get(id))
980        }
981    })
982}
983
984impl Drop for Emulator {
985    fn drop(&mut self) {
986        unsafe {
987            (self.core.core.retro_unload_game)();
988            (self.core.core.retro_deinit)();
989        }
990        CTX.with_borrow_mut(Option::take);
991    }
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997    use std::path::Path;
998    #[cfg(feature = "use_image")]
999    extern crate image;
1000    #[cfg(feature = "use_image")]
1001    use crate::fb_to_image::*;
1002
1003    fn mario_is_dead(emu: &Emulator) -> bool {
1004        emu.system_ram_ref()[0x0770] == 0x03
1005    }
1006
1007    // const PPU_BIT: usize = 1 << 31;
1008
1009    // fn get_byte(emu: &Emulator, addr: usize) -> u8 {
1010    // emu.memory_ref(addr).expect("Couldn't read RAM!")[0]
1011    // }
1012
1013    #[test]
1014    fn create_drop_create() {
1015        // TODO change to a public domain rom or maybe 2048 core or something
1016        let mut emu = Emulator::create(
1017            Path::new("../../.config/retroarch/cores/fceumm_libretro0"),
1018            Path::new("roms/mario.nes"),
1019        );
1020        drop(emu);
1021        emu = Emulator::create(
1022            Path::new("../../.config/retroarch/cores/fceumm_libretro1"),
1023            Path::new("roms/mario.nes"),
1024        );
1025        drop(emu);
1026    }
1027    #[cfg(feature = "use_image")]
1028    #[test]
1029    fn it_works() {
1030        // TODO change to a public domain rom or maybe 2048 core or something
1031        let mut emu = Emulator::create(
1032            Path::new("../../.config/retroarch/cores/fceumm_libretro2"),
1033            Path::new("roms/mario.nes"),
1034        );
1035        emu.run([Buttons::new(), Buttons::new()]);
1036        emu.reset();
1037        for i in 0..150 {
1038            emu.run([
1039                Buttons::new()
1040                    .start(i > 80 && i < 100)
1041                    .right(i >= 100)
1042                    .a(i >= 100),
1043                Buttons::new(),
1044            ]);
1045        }
1046        let fb = emu.create_imagebuffer();
1047        fb.unwrap().save("out.png").unwrap();
1048        let mut died = false;
1049        for _ in 0..10000 {
1050            emu.run([Buttons::new().right(true), Buttons::new()]);
1051            if mario_is_dead(&emu) {
1052                died = true;
1053                let fb = emu.create_imagebuffer();
1054                fb.unwrap().save("out2.png").unwrap();
1055                break;
1056            }
1057        }
1058        assert!(died);
1059        emu.reset();
1060        for i in 0..250 {
1061            emu.run([
1062                Buttons::new()
1063                    .start(i > 80 && i < 100)
1064                    .right(i >= 100)
1065                    .a((100..=150).contains(&i) || (i >= 180)),
1066                Buttons::new(),
1067            ]);
1068        }
1069
1070        //emu will drop naturally
1071    }
1072    #[test]
1073    fn it_works_with_callback() {
1074        // TODO change to a public domain rom or maybe 2048 core or something
1075        let mut emu = Emulator::create(
1076            Path::new("../../.config/retroarch/cores/fceumm_libretro3"),
1077            Path::new("roms/mario.nes"),
1078        );
1079        emu.run([Buttons::new(), Buttons::new()]);
1080        emu.reset();
1081        for i in 0..150 {
1082            emu.run_with_button_callback(Box::new(move |port, _dev, _idx, id| {
1083                if port == 0 {
1084                    let buttons = Buttons::new()
1085                        .start(i > 80 && i < 100)
1086                        .right(i >= 100)
1087                        .a((100..=150).contains(&i) || (i >= 180));
1088                    if id == RETRO_DEVICE_ID_JOYPAD_MASK {
1089                        i16::from(buttons)
1090                    } else {
1091                        i16::from(buttons.get(id))
1092                    }
1093                } else {
1094                    0
1095                }
1096            }));
1097        }
1098        let mut died = false;
1099        for _ in 0..10000 {
1100            emu.run_with_button_callback(Box::new(|_port, _dev, _idx, id| {
1101                let buttons = Buttons::new().right(true);
1102                if id == RETRO_DEVICE_ID_JOYPAD_MASK {
1103                    i16::from(buttons)
1104                } else {
1105                    i16::from(buttons.get(id))
1106                }
1107            }));
1108            if mario_is_dead(&emu) {
1109                died = true;
1110                break;
1111            }
1112        }
1113        assert!(died);
1114        //emu will drop naturally
1115    }
1116}