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        }
261        Emulator { core: emu }
262    }
263    pub fn get_library(&mut self) -> &Library {
264        &self.core.core_lib
265    }
266    #[must_use]
267    pub fn get_symbol<'a, T>(&'a self, symbol: &[u8]) -> Option<Symbol<'a, T>> {
268        let dll = &self.core.core_lib;
269        let sym: Result<Symbol<T>, _> = unsafe { dll.get(symbol) };
270        sym.ok()
271    }
272    #[allow(clippy::missing_panics_doc)]
273    pub fn run(&mut self, inputs: [Buttons; 2]) {
274        CTX.with_borrow_mut(|ctx| {
275            let ctx = ctx.as_mut().unwrap();
276            //clear audio buffers and whatever else
277            ctx.audio_sample.clear();
278            //set inputs on CB
279            ctx.buttons = inputs;
280            ctx.button_callback = None;
281        });
282        unsafe {
283            //run one step
284            (self.core.core.retro_run)();
285        }
286    }
287    #[allow(clippy::missing_panics_doc)]
288    pub fn run_with_button_callback(&mut self, input: Box<dyn Fn(u32, u32, u32, u32) -> i16>) {
289        CTX.with_borrow_mut(|ctx| {
290            let ctx = ctx.as_mut().unwrap();
291            //clear audio buffers and whatever else
292            ctx.audio_sample.clear();
293            //set inputs on CB
294            ctx.button_callback = Some(Box::new(input));
295        });
296        unsafe {
297            //run one step
298            (self.core.core.retro_run)();
299        }
300    }
301    #[allow(clippy::missing_panics_doc)]
302    pub fn reset(&mut self) {
303        CTX.with_borrow_mut(|ctx| {
304            let ctx = ctx.as_mut().unwrap();
305            // clear audio buffers and whatever else
306            ctx.audio_sample.clear();
307            // set inputs on CB
308            ctx.buttons = [Buttons::new(), Buttons::new()];
309            ctx.button_callback = None;
310            // clear fb
311            ctx.frame_ptr = ptr::null();
312        });
313        unsafe { (self.core.core.retro_reset)() }
314    }
315    #[must_use]
316    fn get_ram_size(&self, rtype: libc::c_uint) -> usize {
317        unsafe { (self.core.core.retro_get_memory_size)(rtype) }
318    }
319    #[must_use]
320    pub fn get_video_ram_size(&self) -> usize {
321        self.get_ram_size(RETRO_MEMORY_VIDEO_RAM)
322    }
323    #[must_use]
324    pub fn get_system_ram_size(&self) -> usize {
325        self.get_ram_size(RETRO_MEMORY_SYSTEM_RAM)
326    }
327    #[must_use]
328    pub fn get_save_ram_size(&self) -> usize {
329        self.get_ram_size(RETRO_MEMORY_SAVE_RAM)
330    }
331    #[must_use]
332    pub fn video_ram_ref(&self) -> &[u8] {
333        self.get_ram(RETRO_MEMORY_VIDEO_RAM)
334    }
335    #[must_use]
336    pub fn system_ram_ref(&self) -> &[u8] {
337        self.get_ram(RETRO_MEMORY_SYSTEM_RAM)
338    }
339    #[must_use]
340    pub fn system_ram_mut(&mut self) -> &mut [u8] {
341        self.get_ram_mut(RETRO_MEMORY_SYSTEM_RAM)
342    }
343    #[must_use]
344    pub fn save_ram(&self) -> &[u8] {
345        self.get_ram(RETRO_MEMORY_SAVE_RAM)
346    }
347
348    #[must_use]
349    fn get_ram(&self, ramtype: libc::c_uint) -> &[u8] {
350        let len = self.get_ram_size(ramtype);
351        unsafe {
352            let ptr: *const u8 = (self.core.core.retro_get_memory_data)(ramtype).cast();
353            std::slice::from_raw_parts(ptr, len)
354        }
355    }
356    #[must_use]
357    fn get_ram_mut(&mut self, ramtype: libc::c_uint) -> &mut [u8] {
358        let len = self.get_ram_size(ramtype);
359        unsafe {
360            let ptr: *mut u8 = (self.core.core.retro_get_memory_data)(ramtype).cast();
361            std::slice::from_raw_parts_mut(ptr, len)
362        }
363    }
364    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
365    #[must_use]
366    pub fn memory_regions(&self) -> Vec<MemoryRegion> {
367        CTX.with_borrow(|ctx| {
368            let map = &ctx.as_ref().unwrap().memory_map;
369            map.iter()
370                .enumerate()
371                .map(|(i, mdesc)| MemoryRegion {
372                    which: i,
373                    flags: mdesc.flags,
374                    len: mdesc.len,
375                    start: mdesc.start,
376                    offset: mdesc.offset,
377                    select: mdesc.select,
378                    disconnect: mdesc.disconnect,
379                    name: if mdesc.addrspace.is_null() {
380                        String::new()
381                    } else {
382                        unsafe { CStr::from_ptr(mdesc.addrspace) }
383                            .to_string_lossy()
384                            .into_owned()
385                    },
386                })
387                .collect()
388        })
389    }
390    /// # Errors
391    /// [`RetroRsError::RAMCopyNotMappedIntoMemoryRegionError`]: Returns an error if the desired address is not mapped into memory regions
392    pub fn memory_ref(&self, start: usize) -> Result<&[u8], RetroRsError> {
393        for mr in self.memory_regions() {
394            if mr.select != 0 && (start & mr.select) == 0 {
395                continue;
396            }
397            if start >= mr.start && start < mr.start + mr.len {
398                return CTX.with_borrow(|ctx| {
399                    let maps = &ctx.as_ref().unwrap().memory_map;
400                    if mr.which >= maps.len() {
401                        // TODO more aggressive checking of mr vs map
402                        return Err(RetroRsError::RAMMapOutOfRangeError);
403                    }
404                    let start = (start - mr.start) & !mr.disconnect;
405                    let map = &maps[mr.which];
406                    //0-based at this point, modulo offset
407                    let ptr: *mut u8 = map.ptr.cast();
408                    let slice = unsafe {
409                        let ptr = ptr.add(start).add(map.offset);
410                        std::slice::from_raw_parts(ptr, map.len - start)
411                    };
412                    Ok(slice)
413                });
414            } else if start < mr.start {
415                return Err(RetroRsError::RAMCopySrcOutOfBoundsError);
416            }
417        }
418        Err(RetroRsError::RAMCopyNotMappedIntoMemoryRegionError)
419    }
420    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
421    /// # Errors
422    /// [`RetroRsError::RAMMapOutOfRangeError`]: The desired address is out of mapped range
423    /// [`RetroRsError::RAMCopySrcOutOfBoundsError`]: The desired range is not in the requested region
424    pub fn memory_ref_mut(
425        &mut self,
426        mr: &MemoryRegion,
427        start: usize,
428    ) -> Result<&mut [u8], RetroRsError> {
429        CTX.with_borrow_mut(|ctx| {
430            let maps = &mut ctx.as_mut().unwrap().memory_map;
431            if mr.which >= maps.len() {
432                // TODO more aggressive checking of mr vs map
433                return Err(RetroRsError::RAMMapOutOfRangeError);
434            }
435            if start < mr.start {
436                return Err(RetroRsError::RAMCopySrcOutOfBoundsError);
437            }
438            let start = (start - mr.start) & !mr.disconnect;
439            let map = &maps[mr.which];
440            //0-based at this point, modulo offset
441            let ptr: *mut u8 = map.ptr.cast();
442            let slice = unsafe {
443                let ptr = ptr.add(start).add(map.offset);
444                std::slice::from_raw_parts_mut(ptr, map.len - start)
445            };
446            Ok(slice)
447        })
448    }
449    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
450    #[must_use]
451    pub fn pixel_format(&self) -> retro_pixel_format {
452        CTX.with_borrow(|ctx| ctx.as_ref().unwrap().pixfmt)
453    }
454    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
455    #[must_use]
456    pub fn framebuffer_size(&self) -> (usize, usize) {
457        CTX.with_borrow(|ctx| {
458            let ctx = ctx.as_ref().unwrap();
459            (ctx.frame_width as usize, ctx.frame_height as usize)
460        })
461    }
462    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
463    #[must_use]
464    pub fn framebuffer_pitch(&self) -> usize {
465        CTX.with_borrow(|ctx| ctx.as_ref().unwrap().frame_pitch)
466    }
467    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
468    /// # Errors
469    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
470    fn peek_framebuffer<FBPeek, FBPeekRet>(&self, f: FBPeek) -> Result<FBPeekRet, RetroRsError>
471    where
472        FBPeek: FnOnce(&[u8]) -> FBPeekRet,
473    {
474        CTX.with_borrow(|ctx| {
475            let ctx = ctx.as_ref().unwrap();
476            if ctx.frame_ptr.is_null() {
477                Err(RetroRsError::NoFramebufferError)
478            } else {
479                unsafe {
480                    #[allow(clippy::cast_possible_truncation)]
481                    let frame_slice = std::slice::from_raw_parts(
482                        ctx.frame_ptr.cast(),
483                        (ctx.frame_height * (ctx.frame_pitch as u32)) as usize,
484                    );
485                    Ok(f(frame_slice))
486                }
487            }
488        })
489    }
490    #[allow(clippy::missing_panics_doc, clippy::unused_self)]
491    #[must_use]
492    pub fn peek_audio_sample<AudioPeek, AudioPeekRet>(&self, f: AudioPeek) -> AudioPeekRet
493    where
494        AudioPeek: FnOnce(&[i16]) -> AudioPeekRet,
495    {
496        CTX.with_borrow(|ctx| f(&ctx.as_ref().unwrap().audio_sample))
497    }
498    #[must_use]
499    pub fn get_audio_sample_rate(&self) -> f64 {
500        CTX.with_borrow_mut(|ctx| ctx.as_ref().unwrap().av_info.timing.sample_rate)
501    }
502    #[must_use]
503    pub fn get_video_fps(&self) -> f64 {
504        CTX.with_borrow_mut(|ctx| ctx.as_ref().unwrap().av_info.timing.fps)
505    }
506
507    #[must_use]
508    pub fn save(&self, bytes: &mut [u8]) -> bool {
509        let size = self.save_size();
510        if bytes.len() < size {
511            return false;
512        }
513        unsafe { (self.core.core.retro_serialize)(bytes.as_mut_ptr().cast(), size) }
514    }
515    #[must_use]
516    pub fn load(&mut self, bytes: &[u8]) -> bool {
517        let size = self.save_size();
518        if bytes.len() < size {
519            return false;
520        }
521        unsafe { (self.core.core.retro_unserialize)(bytes.as_ptr().cast(), size) }
522    }
523    #[must_use]
524    pub fn save_size(&self) -> usize {
525        unsafe { (self.core.core.retro_serialize_size)() }
526    }
527    pub fn clear_cheats(&mut self) {
528        unsafe { (self.core.core.retro_cheat_reset)() }
529    }
530    /// # Panics
531    /// May panic if code can't be converted to a [`CString`]
532    pub fn set_cheat(&mut self, index: usize, enabled: bool, code: &str) {
533        unsafe {
534            // 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.
535            #[allow(clippy::cast_possible_truncation)]
536            (self.core.core.retro_cheat_set)(
537                index as u32,
538                enabled,
539                CString::new(code).unwrap().into_raw(),
540            );
541        }
542    }
543    /// # Panics
544    /// Panics if the pixel format used by the core is not supported for reads.
545    /// # Errors
546    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
547    pub fn get_pixel(&self, x: usize, y: usize) -> Result<(u8, u8, u8), RetroRsError> {
548        let (w, _h) = self.framebuffer_size();
549        self.peek_framebuffer(move |fb| match self.pixel_format() {
550            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
551                let start = y * w + x;
552                let gb = fb[start * 2];
553                let arg = fb[start * 2 + 1];
554                let (red, green, blue) = argb555to888(gb, arg);
555                (red, green, blue)
556            }
557            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
558                let off = (y * w + x) * 4;
559                (fb[off + 1], fb[off + 2], fb[off + 3])
560            }
561            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
562                let start = y * w + x;
563                let gb = fb[start * 2];
564                let rg = fb[start * 2 + 1];
565                let (red, green, blue) = rgb565to888(gb, rg);
566                (red, green, blue)
567            }
568            _ => panic!("Unsupported pixel format"),
569        })
570    }
571    /// # Panics
572    /// Panics if the pixel format used by the core is not supported for reads.
573    /// # Errors
574    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
575    #[allow(clippy::many_single_char_names)]
576    pub fn for_each_pixel(
577        &self,
578        mut f: impl FnMut(usize, usize, u8, u8, u8),
579    ) -> Result<(), RetroRsError> {
580        let (w, h) = self.framebuffer_size();
581        let fmt = self.pixel_format();
582        self.peek_framebuffer(move |fb| {
583            let mut x = 0;
584            let mut y = 0;
585            match fmt {
586                retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
587                    for components in fb.chunks_exact(2) {
588                        let gb = components[0];
589                        let arg = components[1];
590                        let (red, green, blue) = argb555to888(gb, arg);
591                        f(x, y, red, green, blue);
592                        x += 1;
593                        if x >= w {
594                            y += 1;
595                            x = 0;
596                        }
597                    }
598                }
599                retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
600                    for components in fb.chunks_exact(4) {
601                        let red = components[1];
602                        let green = components[2];
603                        let blue = components[3];
604                        f(x, y, red, green, blue);
605                        x += 1;
606                        if x >= w {
607                            y += 1;
608                            x = 0;
609                        }
610                    }
611                }
612                retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
613                    for components in fb.chunks_exact(2) {
614                        let gb = components[0];
615                        let rg = components[1];
616                        let (red, green, blue) = rgb565to888(gb, rg);
617                        f(x, y, red, green, blue);
618                        x += 1;
619                        if x >= w {
620                            y += 1;
621                            x = 0;
622                        }
623                    }
624                }
625                _ => panic!("Unsupported pixel format"),
626            }
627            assert_eq!(y, h);
628            assert_eq!(x, 0);
629        })
630    }
631    /// # Panics
632    /// Panics if the pixel format used by the core is not supported for reads.
633    /// # Errors
634    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
635    pub fn copy_framebuffer_rgb888(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
636        let fmt = self.pixel_format();
637        self.peek_framebuffer(move |fb| match fmt {
638            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
639                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(3)) {
640                    let gb = components[0];
641                    let arg = components[1];
642                    let (red, green, blue) = argb555to888(gb, arg);
643                    dst[0] = red;
644                    dst[1] = green;
645                    dst[2] = blue;
646                }
647            }
648            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
649                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(3)) {
650                    let r = components[1];
651                    let g = components[2];
652                    let b = components[3];
653                    dst[0] = r;
654                    dst[1] = g;
655                    dst[2] = b;
656                }
657            }
658            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
659                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(3)) {
660                    let gb = components[0];
661                    let rg = components[1];
662                    let (red, green, blue) = rgb565to888(gb, rg);
663                    dst[0] = red;
664                    dst[1] = green;
665                    dst[2] = blue;
666                }
667            }
668            _ => panic!("Unsupported pixel format"),
669        })
670    }
671    /// # Panics
672    /// Panics if the pixel format used by the core is not supported for reads.
673    /// # Errors
674    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
675    pub fn copy_framebuffer_rgba8888(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
676        let fmt = self.pixel_format();
677        self.peek_framebuffer(move |fb| match fmt {
678            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
679                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
680                    let gb = components[0];
681                    let arg = components[1];
682                    let (red, green, blue) = argb555to888(gb, arg);
683                    dst[0] = red;
684                    dst[1] = green;
685                    dst[2] = blue;
686                    dst[3] = (arg >> 7) * 0xFF;
687                }
688            }
689            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
690                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(4)) {
691                    let a = components[0];
692                    let r = components[1];
693                    let g = components[2];
694                    let b = components[3];
695                    dst[0] = r;
696                    dst[1] = g;
697                    dst[2] = b;
698                    dst[3] = a;
699                }
700            }
701            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
702                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
703                    let gb = components[0];
704                    let rg = components[1];
705                    let (red, green, blue) = rgb565to888(gb, rg);
706                    dst[0] = red;
707                    dst[1] = green;
708                    dst[2] = blue;
709                    dst[3] = 0xFF;
710                }
711            }
712            _ => panic!("Unsupported pixel format"),
713        })
714    }
715    /// # Panics
716    /// Panics if the pixel format used by the core is not supported for reads.
717    /// # Errors
718    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
719    pub fn copy_framebuffer_rgb332(&self, slice: &mut [u8]) -> Result<(), RetroRsError> {
720        let fmt = self.pixel_format();
721        self.peek_framebuffer(move |fb| match fmt {
722            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
723                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
724                    let gb = components[0];
725                    let arg = components[1];
726                    let (red, green, blue) = argb555to888(gb, arg);
727                    *dst = rgb888_to_rgb332(red, green, blue);
728                }
729            }
730            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
731                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
732                    let r = components[1];
733                    let g = components[2];
734                    let b = components[3];
735                    *dst = rgb888_to_rgb332(r, g, b);
736                }
737            }
738            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
739                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
740                    let gb = components[0];
741                    let rg = components[1];
742                    let (red, green, blue) = rgb565to888(gb, rg);
743                    *dst = rgb888_to_rgb332(red, green, blue);
744                }
745            }
746            _ => panic!("Unsupported pixel format"),
747        })
748    }
749    /// # Panics
750    /// Panics if the pixel format used by the core is not supported for reads.
751    /// # Errors
752    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
753    pub fn copy_framebuffer_argb32(&self, slice: &mut [u32]) -> Result<(), RetroRsError> {
754        let fmt = self.pixel_format();
755        self.peek_framebuffer(move |fb| match fmt {
756            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
757                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
758                    let gb = components[0];
759                    let arg = components[1];
760                    let (red, green, blue) = argb555to888(gb, arg);
761                    *dst = (0xFF00_0000 * (u32::from(arg) >> 7))
762                        | (u32::from(red) << 16)
763                        | (u32::from(green) << 8)
764                        | u32::from(blue);
765                }
766            }
767            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
768                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
769                    *dst = (u32::from(components[0]) << 24)
770                        | (u32::from(components[1]) << 16)
771                        | (u32::from(components[2]) << 8)
772                        | u32::from(components[3]);
773                }
774            }
775            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
776                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
777                    let gb = components[0];
778                    let rg = components[1];
779                    let (red, green, blue) = rgb565to888(gb, rg);
780                    *dst = 0xFF00_0000
781                        | (u32::from(red) << 16)
782                        | (u32::from(green) << 8)
783                        | u32::from(blue);
784                }
785            }
786            _ => panic!("Unsupported pixel format"),
787        })
788    }
789    /// # Panics
790    /// Panics if the pixel format used by the core is not supported for reads.
791    /// # Errors
792    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
793    pub fn copy_framebuffer_rgba32(&self, slice: &mut [u32]) -> Result<(), RetroRsError> {
794        let fmt = self.pixel_format();
795        self.peek_framebuffer(move |fb| match fmt {
796            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
797                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
798                    let gb = components[0];
799                    let arg = components[1];
800                    let (red, green, blue) = argb555to888(gb, arg);
801                    *dst = (u32::from(red) << 24)
802                        | (u32::from(green) << 16)
803                        | (u32::from(blue) << 8)
804                        | (u32::from(arg >> 7) * 0x0000_00FF);
805                }
806            }
807            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
808                for (components, dst) in fb.chunks_exact(4).zip(slice.iter_mut()) {
809                    *dst = (u32::from(components[1]) << 24)
810                        | (u32::from(components[2]) << 16)
811                        | (u32::from(components[3]) << 8)
812                        | u32::from(components[0]);
813                }
814            }
815            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
816                for (components, dst) in fb.chunks_exact(2).zip(slice.iter_mut()) {
817                    let gb = components[0];
818                    let rg = components[1];
819                    let (red, green, blue) = rgb565to888(gb, rg);
820                    *dst = (u32::from(red) << 24)
821                        | (u32::from(green) << 16)
822                        | (u32::from(blue) << 8)
823                        | 0x0000_00FF;
824                }
825            }
826            _ => panic!("Unsupported pixel format"),
827        })
828    }
829    /// # Panics
830    /// Panics if the pixel format used by the core is not supported for reads.
831    /// # Errors
832    /// [`RetroRsError::NoFramebufferError`]: Emulator has not created a framebuffer.
833    pub fn copy_framebuffer_rgba_f32x4(&self, slice: &mut [f32]) -> Result<(), RetroRsError> {
834        let fmt = self.pixel_format();
835        self.peek_framebuffer(move |fb| match fmt {
836            retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => {
837                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
838                    let gb = components[0];
839                    let arg = components[1];
840                    let (red, green, blue) = argb555to888(gb, arg);
841                    let alpha = f32::from(arg >> 7);
842                    dst[0] = f32::from(red) / 255.;
843                    dst[1] = f32::from(green) / 255.;
844                    dst[2] = f32::from(blue) / 255.;
845                    dst[3] = alpha;
846                }
847            }
848            retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => {
849                for (components, dst) in fb.chunks_exact(4).zip(slice.chunks_exact_mut(4)) {
850                    dst[0] = f32::from(components[0]) / 255.;
851                    dst[1] = f32::from(components[1]) / 255.;
852                    dst[2] = f32::from(components[2]) / 255.;
853                    dst[3] = f32::from(components[3]) / 255.;
854                }
855            }
856            retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => {
857                for (components, dst) in fb.chunks_exact(2).zip(slice.chunks_exact_mut(4)) {
858                    let gb = components[0];
859                    let rg = components[1];
860                    let (red, green, blue) = rgb565to888(gb, rg);
861                    let alpha = 1.;
862                    dst[0] = f32::from(red) / 255.;
863                    dst[1] = f32::from(green) / 255.;
864                    dst[2] = f32::from(blue) / 255.;
865                    dst[3] = alpha;
866                }
867            }
868            _ => panic!("Unsupported pixel format"),
869        })
870    }
871}
872
873unsafe extern "C" fn callback_environment(cmd: u32, data: *mut c_void) -> bool {
874    let result = panic::catch_unwind(|| {
875        CTX.with_borrow_mut(|ctx| {
876            let ctx = ctx.as_mut().unwrap();
877            match cmd {
878                RETRO_ENVIRONMENT_SET_CONTROLLER_INFO => true,
879                RETRO_ENVIRONMENT_SET_PIXEL_FORMAT => {
880                    let pixfmt = unsafe { *(data as *const retro_pixel_format) };
881                    ctx.image_depth = match pixfmt {
882                        retro_pixel_format::RETRO_PIXEL_FORMAT_0RGB1555 => 15,
883                        retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888 => 32,
884                        retro_pixel_format::RETRO_PIXEL_FORMAT_RGB565 => 16,
885                        _ => panic!("Unsupported pixel format"),
886                    };
887                    ctx.pixfmt = pixfmt;
888                    true
889                }
890                RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY => unsafe {
891                    *(data.cast()) = ctx.core_path.as_ptr();
892                    true
893                },
894                RETRO_ENVIRONMENT_GET_CAN_DUPE => unsafe {
895                    *(data.cast()) = true;
896                    true
897                },
898                RETRO_ENVIRONMENT_SET_MEMORY_MAPS => unsafe {
899                    let map: *const retro_memory_map = data.cast();
900                    let desc_slice = std::slice::from_raw_parts(
901                        (*map).descriptors,
902                        (*map).num_descriptors as usize,
903                    );
904                    // Don't know who owns map or how long it will last
905                    ctx.memory_map = Vec::new();
906                    // So we had better copy it
907                    ctx.memory_map.extend_from_slice(desc_slice);
908                    // (Implicitly we also want to drop the old one, which we did by reassigning)
909                    true
910                },
911                RETRO_ENVIRONMENT_GET_INPUT_BITMASKS => true,
912                _ => false,
913            }
914        })
915    });
916    result.unwrap_or(false)
917}
918
919extern "C" fn callback_video_refresh(data: *const c_void, width: u32, height: u32, pitch: usize) {
920    // Can't panic
921    // context's framebuffer just points to the given data.  Seems to work OK for gym-retro.
922    if !data.is_null() {
923        CTX.with_borrow_mut(|ctx| {
924            let ctx = ctx.as_mut().unwrap();
925            ctx.frame_ptr = data;
926            ctx.frame_pitch = pitch;
927            ctx.frame_width = width;
928            ctx.frame_height = height;
929        });
930    }
931}
932extern "C" fn callback_audio_sample(left: i16, right: i16) {
933    // Can't panic
934    CTX.with_borrow_mut(|ctx| {
935        let ctx = ctx.as_mut().unwrap();
936        let sample_buf = &mut ctx.audio_sample;
937        sample_buf.push(left);
938        sample_buf.push(right);
939    });
940}
941extern "C" fn callback_audio_sample_batch(data: *const i16, frames: usize) -> usize {
942    // Can't panic
943    CTX.with_borrow_mut(|ctx| {
944        let ctx = ctx.as_mut().unwrap();
945        let sample_buf = &mut ctx.audio_sample;
946        let slice = unsafe { std::slice::from_raw_parts(data, frames * 2) };
947        sample_buf.extend_from_slice(slice);
948        frames
949    })
950}
951
952extern "C" fn callback_input_poll() {}
953
954extern "C" fn callback_input_state(port: u32, device: u32, index: u32, id: u32) -> i16 {
955    // Can't panic
956    if port > 1 || device != 1 || index != 0 {
957        // Unsupported port/device/index
958        println!("Unsupported port/device/index {port}/{device}/{index}");
959        return 0;
960    }
961    let bitmask_enabled = (device == RETRO_DEVICE_JOYPAD) && (id == RETRO_DEVICE_ID_JOYPAD_MASK);
962    CTX.with_borrow(|ctx| {
963        let ctx = ctx.as_ref().unwrap();
964        if let Some(cb) = &ctx.button_callback {
965            cb(port, device, index, id)
966        } else if bitmask_enabled {
967            let port = port as usize;
968            i16::from(ctx.buttons[port])
969        } else {
970            let port = port as usize;
971            i16::from(ctx.buttons[port].get(id))
972        }
973    })
974}
975
976impl Drop for Emulator {
977    fn drop(&mut self) {
978        unsafe {
979            (self.core.core.retro_unload_game)();
980            (self.core.core.retro_deinit)();
981        }
982        CTX.with_borrow_mut(Option::take);
983    }
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989    use std::path::Path;
990    #[cfg(feature = "use_image")]
991    extern crate image;
992    #[cfg(feature = "use_image")]
993    use crate::fb_to_image::*;
994
995    fn mario_is_dead(emu: &Emulator) -> bool {
996        emu.system_ram_ref()[0x0770] == 0x03
997    }
998
999    // const PPU_BIT: usize = 1 << 31;
1000
1001    // fn get_byte(emu: &Emulator, addr: usize) -> u8 {
1002    // emu.memory_ref(addr).expect("Couldn't read RAM!")[0]
1003    // }
1004
1005    #[test]
1006    fn create_drop_create() {
1007        // TODO change to a public domain rom or maybe 2048 core or something
1008        let mut emu = Emulator::create(
1009            Path::new("../../.config/retroarch/cores/fceumm_libretro0"),
1010            Path::new("roms/mario.nes"),
1011        );
1012        drop(emu);
1013        emu = Emulator::create(
1014            Path::new("../../.config/retroarch/cores/fceumm_libretro1"),
1015            Path::new("roms/mario.nes"),
1016        );
1017        drop(emu);
1018    }
1019    #[cfg(feature = "use_image")]
1020    #[test]
1021    fn it_works() {
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_libretro2"),
1025            Path::new("roms/mario.nes"),
1026        );
1027        emu.run([Buttons::new(), Buttons::new()]);
1028        emu.reset();
1029        for i in 0..150 {
1030            emu.run([
1031                Buttons::new()
1032                    .start(i > 80 && i < 100)
1033                    .right(i >= 100)
1034                    .a(i >= 100),
1035                Buttons::new(),
1036            ]);
1037        }
1038        let fb = emu.create_imagebuffer();
1039        fb.unwrap().save("out.png").unwrap();
1040        let mut died = false;
1041        for _ in 0..10000 {
1042            emu.run([Buttons::new().right(true), Buttons::new()]);
1043            if mario_is_dead(&emu) {
1044                died = true;
1045                let fb = emu.create_imagebuffer();
1046                fb.unwrap().save("out2.png").unwrap();
1047                break;
1048            }
1049        }
1050        assert!(died);
1051        emu.reset();
1052        for i in 0..250 {
1053            emu.run([
1054                Buttons::new()
1055                    .start(i > 80 && i < 100)
1056                    .right(i >= 100)
1057                    .a((100..=150).contains(&i) || (i >= 180)),
1058                Buttons::new(),
1059            ]);
1060        }
1061
1062        //emu will drop naturally
1063    }
1064    #[test]
1065    fn it_works_with_callback() {
1066        // TODO change to a public domain rom or maybe 2048 core or something
1067        let mut emu = Emulator::create(
1068            Path::new("../../.config/retroarch/cores/fceumm_libretro3"),
1069            Path::new("roms/mario.nes"),
1070        );
1071        emu.run([Buttons::new(), Buttons::new()]);
1072        emu.reset();
1073        for i in 0..150 {
1074            emu.run_with_button_callback(Box::new(move |port, _dev, _idx, id| {
1075                if port == 0 {
1076                    let buttons = Buttons::new()
1077                        .start(i > 80 && i < 100)
1078                        .right(i >= 100)
1079                        .a((100..=150).contains(&i) || (i >= 180));
1080                    if id == RETRO_DEVICE_ID_JOYPAD_MASK {
1081                        i16::from(buttons)
1082                    } else {
1083                        i16::from(buttons.get(id))
1084                    }
1085                } else {
1086                    0
1087                }
1088            }));
1089        }
1090        let mut died = false;
1091        for _ in 0..10000 {
1092            emu.run_with_button_callback(Box::new(|_port, _dev, _idx, id| {
1093                let buttons = Buttons::new().right(true);
1094                if id == RETRO_DEVICE_ID_JOYPAD_MASK {
1095                    i16::from(buttons)
1096                } else {
1097                    i16::from(buttons.get(id))
1098                }
1099            }));
1100            if mario_is_dead(&emu) {
1101                died = true;
1102                break;
1103            }
1104        }
1105        assert!(died);
1106        //emu will drop naturally
1107    }
1108}