retro_rs/
emulator.rs

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