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