Skip to main content

libretro_core/
lib.rs

1//! Minimal libretro wrapper for implementing Rust cores.
2//!
3//! The crate exposes typed Rust wrappers for `libretro.h` plus a trait/macro
4//! pair for exporting the required `retro_*` symbols from a Rust core.
5//!
6//! Methodology:
7//! - Keep public APIs Rust-first even when the underlying libretro ABI is C-first.
8//! - Prefer strings, slices, enums, and return values over raw pointers or
9//!   mutable out-params whenever the wrapper can do that conversion centrally.
10//! - Keep raw ABI details private unless exposing them is necessary for a real
11//!   core-development use case.
12//! - Match libretro/OpenGL naming where it helps recognition, but not at the
13//!   cost of forcing callers back into manual FFI plumbing.
14//!
15//! API map:
16//! - `Core`, `Runtime`, `Environment`, and `export_core!` are the primary core
17//!   authoring surface.
18//! - `ContentContract`, `SystemInfo`, `GameInfo`, and `SystemAvInfo` describe
19//!   startup metadata, content loading, geometry, and timing.
20//! - Input polling uses `Runtime` helpers with typed devices such as
21//!   `JoypadButton`, `AnalogStick`, `MouseButton`, and `PointerAxis`.
22//! - Event-shaped frontend callbacks are registered through `CoreEventConfig`
23//!   with DOM-style listener methods such as
24//!   `CoreEventConfig::add_keyboard_event_listener`; callback-shaped frontend
25//!   hooks with one active registration, such as frame timing, use explicit
26//!   `set_*_callback` methods.
27//! - Optional frontend services are exposed as typed interfaces such as
28//!   `VfsInterface`, `MidiInterface`, `MicrophoneInterface`, `PerfInterface`,
29//!   `SensorInterface`, `LocationInterface`, and `CameraInterface`.
30//! - Hardware rendering uses `HwRenderConfig` for context negotiation and
31//!   `Gl` for typed OpenGL access.
32//!
33//! ```ignore
34//! use libretro::{
35//!     ContentContract, Core, CoreEventConfig, Environment, GameGeometry, HwRenderConfig,
36//!     JoypadButton, KeyboardEvent, PixelFormat, Runtime, SystemAvInfo, SystemInfo, SystemTiming,
37//!     VariableDefinition,
38//! };
39//!
40//! #[derive(Default)]
41//! struct GlCore {
42//!     width: u32,
43//!     height: u32,
44//! }
45//!
46//! impl Core for GlCore {
47//!     fn system_info(&self) -> SystemInfo {
48//!         let mut info = SystemInfo::new("TestCore GL", "v1");
49//!         info.need_fullpath = false;
50//!         info
51//!     }
52//!
53//!     fn av_info(&self) -> SystemAvInfo {
54//!         SystemAvInfo {
55//!             geometry: GameGeometry {
56//!                 base_width: 320,
57//!                 base_height: 240,
58//!                 max_width: 1024,
59//!                 max_height: 1024,
60//!                 aspect_ratio: 4.0 / 3.0,
61//!             },
62//!             timing: SystemTiming {
63//!                 fps: 60.0,
64//!                 sample_rate: 0.0,
65//!             },
66//!         }
67//!     }
68//!
69//!     fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
70//!         events.add_keyboard_event_listener(Self::keyboard_event);
71//!     }
72//!
73//!     fn on_set_environment(&mut self, env: &mut Environment<'_>) {
74//!         ContentContract::new("bin")
75//!             .with_support_no_game(true)
76//!             .with_persistent_data(true)
77//!             .register_environment(env);
78//!         env.set_variables(&[VariableDefinition::new(
79//!             "testgl_resolution",
80//!             "Internal resolution; 320x240|640x480|1024x768",
81//!         )]);
82//!     }
83//!
84//!     fn load_game(
85//!         &mut self,
86//!         _game: Option<libretro::GameInfo<'_>>,
87//!         runtime: &mut Runtime<'_>,
88//!     ) -> bool {
89//!         let mut env = runtime.environment();
90//!         env.set_pixel_format(PixelFormat::Xrgb8888)
91//!             && env
92//!                 .set_hw_render_from_candidates(&[
93//!                     HwRenderConfig::opengl()
94//!                         .with_depth(true)
95//!                         .with_stencil(true)
96//!                         .with_bottom_left_origin(true),
97//!                     HwRenderConfig::opengles3()
98//!                         .with_depth(true)
99//!                         .with_stencil(true)
100//!                         .with_bottom_left_origin(true),
101//!                 ])
102//!                 .is_some()
103//!     }
104//!
105//!     fn run(&mut self, runtime: &mut Runtime<'_>) {
106//!         runtime.poll_input();
107//!         if runtime.joypad_pressed(0, JoypadButton::Up) {
108//!             // update state
109//!         }
110//!         runtime.video_refresh_hw(self.width, self.height, 0);
111//!     }
112//!
113//!     fn keyboard_event(&mut self, event: KeyboardEvent) {
114//!         if event.down {
115//!             // handle typed keyboard event
116//!         }
117//!     }
118//! }
119//!
120//! libretro::export_core!(GlCore::default());
121//! ```
122
123mod av;
124mod callbacks;
125mod camera;
126mod content;
127mod disk;
128mod environment;
129#[path = "glsym.rs"]
130mod glsym_impl;
131mod glsym_raw;
132mod hw_render;
133mod input;
134mod memory;
135mod microphone;
136mod midi;
137mod netplay;
138mod options;
139mod perf;
140mod raw;
141mod sensors;
142mod subsystem;
143mod vfs;
144
145use std::any::Any;
146use std::borrow::Cow;
147use std::ffi::{CStr, CString, c_char, c_void};
148use std::mem;
149use std::panic::{AssertUnwindSafe, catch_unwind};
150use std::ptr;
151use std::sync::{Mutex, OnceLock};
152
153pub use av::{
154    GameGeometry, SystemAvInfo, SystemTiming, bounded_game_geometry,
155    exact_audio_frames_per_video_frame, fixed_system_av_info, game_geometry, silent_stereo_frames,
156    silent_stereo_frames_for_video_frame, system_av_info,
157};
158pub use callbacks::{
159    AudioBufferOccupancy, AudioBufferStatus, AudioCallbackState, CoreProcAddress, FrameTime,
160};
161pub use camera::{
162    CameraCapabilities, CameraCapability, CameraFrameSize, CameraInterface, CameraRawFrame,
163    CameraRequest, CameraTextureFrame, CameraTextureId, CameraTextureTarget,
164};
165pub use content::ContentContract;
166pub use disk::{DiskControlInterfaceVersion, DiskIndex, DiskTrayState};
167pub use environment::{
168    AudioLatencyMillis, AudioSampleRateHz, AvEnable, AvEnableFlags, DevicePower, ExtendedMessage,
169    FastForwardRatio, FastForwardingOverride, Language, MessageKind, MessageProgress,
170    MessageTarget, PerformanceLevel, PowerState, RefreshRateHz, RunLoopRateHz, ThrottleMode,
171    ThrottleState, VideoRotation,
172};
173pub use glsym_impl::{
174    CompatGl, CompatGlClear, CompatTextureGl, FakeAttachShaderCall, FakeBindAttribLocationCall,
175    FakeBindBufferBaseCall, FakeBindBufferRangeCall, FakeBlendEquationSeparateCall,
176    FakeBlendFuncSeparateCall, FakeCopyBufferSubDataCall, FakeCreateShaderCall, FakeDrawArraysCall,
177    FakeDrawElementsCall, FakeGlConfig, FakeGlSnapshot, FakeVertexAttribPointerCall, Gl,
178    GlBlendEquation, GlBlendFactor, GlBuffer, GlBufferBindingIndex, GlBufferByteOffset,
179    GlBufferByteSize, GlBufferRange, GlBufferTarget, GlBufferUsage, GlCapability, GlColorWriteMask,
180    GlCullFaceMode, GlDepthFunction, GlDrawMode, GlDrawRange, GlElementByteOffset, GlElementRange,
181    GlElementVertexRange, GlFramebuffer, GlFramebufferAttachment, GlFramebufferBuffer,
182    GlFramebufferTarget, GlFramebufferTexture2DTarget, GlFrontFaceWinding, GlIndexType,
183    GlIndexedBufferTarget, GlInstanceCount, GlPixelStoreAlignment, GlPolygonOffset, GlProgram,
184    GlQuery, GlQueryTarget, GlRect, GlRenderbuffer, GlRenderbufferInternalFormat,
185    GlRenderbufferSize, GlRenderbufferTarget, GlShader, GlShaderStage, GlStencilFace,
186    GlStencilFunction, GlStencilMask, GlStencilOperation, GlStencilReference, GlSync,
187    GlSyncTimeout, GlSyncWaitResult, GlTexture, GlTextureDataType, GlTextureFilter,
188    GlTextureFormat, GlTextureInternalFormat, GlTextureLevel, GlTextureMagFilter,
189    GlTextureMinFilter, GlTextureOffset2D, GlTextureOffset3D, GlTextureSize2D, GlTextureSize3D,
190    GlTextureTarget, GlTextureUnit, GlTextureWrap, GlUniformLocation, GlVersionInfo, GlVertexArray,
191    GlVertexAttribByteOffset, GlVertexAttribDivisor, GlVertexAttribF32Components,
192    GlVertexAttribF32Layout, GlVertexAttribLocation, GlVertexAttribStride,
193    configure_fake_gl_for_testing, fake_get_proc_address_for_testing, glsym,
194    reset_fake_gl_for_testing, snapshot_fake_gl_for_testing,
195};
196pub use hw_render::{
197    HwRenderContextNegotiationInterface, HwRenderContextNegotiationInterfaceType,
198    HwRenderInterface, HwRenderInterfaceType, OPENGL_COMPATIBILITY_HW_RENDER_LABEL,
199    OPENGL_MODERN_PREFERRED_HW_RENDER_LABEL, opengl_compatibility_hw_render_candidates,
200    opengl_modern_preferred_hw_render_candidates,
201};
202pub use input::{
203    AnalogAxis, AnalogStick, ControllerDescription, ControllerDevice, ControllerDeviceSubclass,
204    ControllerInfo, InputDescriptor, InputDescriptorId, InputDescriptorIndex,
205    InputDeviceCapabilities, InputDeviceCapability, InputPort, JoypadButton, JoypadButtonSet,
206    KeyboardCharacter, KeyboardEvent, KeyboardKey, KeyboardModifier, KeyboardModifiers, LedIndex,
207    LedInterface, LedState, LightgunAxis, LightgunButton, MouseAxis, MouseButton, MouseWheel,
208    PointerAxis, PointerIndex, RumbleEffect, RumbleInterface, RumbleStrength,
209};
210pub use memory::{
211    CoreMemory, EmulatedAddress, ExtendedGameInfo, FramebufferMemoryAccess,
212    FramebufferMemoryAccessFlags, FramebufferMemoryType, FramebufferMemoryTypes,
213    MemoryDescriptorAlignment, MemoryDescriptorFlag, MemoryDescriptorFlags,
214    MemoryDescriptorMinAccessSize, MemoryMapDescriptor, MemoryMapLen, MemoryMapMask,
215    MemoryMapOffset, MemoryRegion, SavestateContext, SerializationQuirk, SerializationQuirks,
216    SoftwareFramebuffer, SoftwareFramebufferRequest,
217};
218pub use microphone::{
219    Microphone, MicrophoneInterface, MicrophoneParams, MicrophoneRateHz, MicrophoneReadError,
220};
221pub use midi::{MidiDeltaMicros, MidiInterface};
222pub use netplay::{
223    Netpacket, NetpacketDelivery, NetpacketFlags, NetpacketSession, NetpacketTarget,
224    NetplayClientId,
225};
226pub use options::{
227    CoreOptionCategory, CoreOptionDefinition, CoreOptionDisplay, CoreOptionValue, CoreOptions,
228    CoreOptionsBuildError, CoreOptionsVersion, VariableDefinition,
229};
230pub use perf::{CpuFeature, CpuFeatures, PerfCounter, PerfInterface, PerfTick, PerfTimeMicros};
231pub use raw::{
232    RETRO_API_VERSION, RETRO_DEVICE_ANALOG, RETRO_DEVICE_ID_ANALOG_X, RETRO_DEVICE_ID_ANALOG_Y,
233    RETRO_DEVICE_ID_JOYPAD_A, RETRO_DEVICE_ID_JOYPAD_B, RETRO_DEVICE_ID_JOYPAD_DOWN,
234    RETRO_DEVICE_ID_JOYPAD_L, RETRO_DEVICE_ID_JOYPAD_L2, RETRO_DEVICE_ID_JOYPAD_L3,
235    RETRO_DEVICE_ID_JOYPAD_LEFT, RETRO_DEVICE_ID_JOYPAD_MASK, RETRO_DEVICE_ID_JOYPAD_R,
236    RETRO_DEVICE_ID_JOYPAD_R2, RETRO_DEVICE_ID_JOYPAD_R3, RETRO_DEVICE_ID_JOYPAD_RIGHT,
237    RETRO_DEVICE_ID_JOYPAD_SELECT, RETRO_DEVICE_ID_JOYPAD_START, RETRO_DEVICE_ID_JOYPAD_UP,
238    RETRO_DEVICE_ID_JOYPAD_X, RETRO_DEVICE_ID_JOYPAD_Y, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN,
239    RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y,
240    RETRO_DEVICE_ID_LIGHTGUN_TRIGGER, RETRO_DEVICE_ID_MOUSE_LEFT, RETRO_DEVICE_ID_MOUSE_WHEELUP,
241    RETRO_DEVICE_ID_MOUSE_X, RETRO_DEVICE_ID_POINTER_PRESSED, RETRO_DEVICE_ID_POINTER_Y,
242    RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_JOYPAD,
243    RETRO_DEVICE_KEYBOARD, RETRO_DEVICE_LIGHTGUN, RETRO_DEVICE_MOUSE, RETRO_DEVICE_NONE,
244    RETRO_DEVICE_POINTER, RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER,
245    RETRO_ENVIRONMENT_GET_GAME_INFO_EXT, RETRO_ENVIRONMENT_GET_LOG_INTERFACE,
246    RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, RETRO_ENVIRONMENT_GET_VARIABLE,
247    RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE,
248    RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE,
249    RETRO_ENVIRONMENT_SET_GEOMETRY, RETRO_ENVIRONMENT_SET_HW_RENDER,
250    RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK,
251    RETRO_ENVIRONMENT_SET_MESSAGE, RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,
252    RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK, RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME,
253    RETRO_ENVIRONMENT_SET_VARIABLES, RETRO_HW_FRAME_BUFFER_VALID, RETRO_MEMORY_ROM,
254    RETRO_MEMORY_RTC, RETRO_MEMORY_SAVE_RAM, RETRO_MEMORY_SYSTEM_RAM, RETRO_MEMORY_VIDEO_RAM,
255    RETRO_REGION_NTSC, RETRO_REGION_PAL,
256    retro_audio_buffer_status_callback as RawAudioBufferStatusCallback,
257    retro_audio_callback as RawAudioCallback, retro_audio_sample_batch_t, retro_audio_sample_t,
258    retro_environment_t, retro_frame_time_callback as RawFrameTimeCallback,
259    retro_game_info as RawGameInfo, retro_hw_context_type as HwContextType,
260    retro_hw_render_callback as RawHwRenderCallback, retro_input_descriptor as RawInputDescriptor,
261    retro_input_poll_t, retro_input_state_t, retro_keyboard_callback as RawKeyboardCallback,
262    retro_log_callback as RawLogCallback, retro_log_level as LogLevel, retro_message as RawMessage,
263    retro_pixel_format as PixelFormat,
264    retro_system_content_info_override as RawContentInfoOverride,
265    retro_system_info as RawSystemInfo, retro_variable as RawVariable, retro_video_refresh_t,
266};
267pub use sensors::{
268    LocationInterface, LocationIntervalMeters, LocationIntervalMillis, LocationPosition, Sensor,
269    SensorAction, SensorInput, SensorInterface, SensorRateHz,
270};
271pub use subsystem::{
272    SubsystemId, SubsystemInfo, SubsystemMemoryInfo, SubsystemMemoryType, SubsystemRomInfo,
273};
274pub use vfs::{
275    VfsDirectory, VfsFile, VfsFileAccess, VfsFileAccessFlags, VfsFileAccessHint,
276    VfsFileAccessHints, VfsInterface, VfsInterfaceVersion, VfsMetadata, VfsSeekPosition,
277    VfsStatFlag, VfsStatFlags,
278};
279
280type CoreFactory = fn() -> CoreBundle;
281
282static FACTORY: OnceLock<CoreFactory> = OnceLock::new();
283static STATE: OnceLock<Mutex<CoreState>> = OnceLock::new();
284
285/// Static metadata returned to the frontend by `Core::system_info`.
286///
287/// Prefer applying a `ContentContract` to this value so `valid_extensions`,
288/// `need_fullpath`, and `block_extract` stay consistent with the environment
289/// registration done in `Core::on_set_environment`.
290#[derive(Clone, Debug)]
291pub struct SystemInfo {
292    pub library_name: String,
293    pub library_version: String,
294    pub valid_extensions: Option<String>,
295    pub need_fullpath: bool,
296    pub block_extract: bool,
297}
298
299impl SystemInfo {
300    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
301        Self {
302            library_name: name.into(),
303            library_version: version.into(),
304            valid_extensions: None,
305            need_fullpath: false,
306            block_extract: false,
307        }
308    }
309}
310
311/// Per-extension content override registered with the frontend.
312///
313/// Most cores should create these through `ContentContract` instead of building
314/// overrides directly.
315#[derive(Clone, Debug)]
316pub struct ContentInfoOverride {
317    pub extensions: String,
318    pub need_fullpath: bool,
319    pub persistent_data: bool,
320}
321
322impl ContentInfoOverride {
323    pub fn new(extensions: impl Into<String>) -> Self {
324        Self {
325            extensions: extensions.into(),
326            need_fullpath: false,
327            persistent_data: false,
328        }
329    }
330}
331
332/// Frontend logger with stderr fallback.
333///
334/// `Environment::logger` and `Runtime::logger` return this wrapper so core code
335/// can log without touching the raw `retro_log_callback` table.
336#[derive(Clone, Copy, Debug, Default)]
337pub struct Logger {
338    callback: Option<raw::retro_log_printf_t>,
339}
340
341impl Logger {
342    pub fn debug(&self, message: impl AsRef<str>) {
343        self.log(LogLevel::Debug, message);
344    }
345
346    pub fn info(&self, message: impl AsRef<str>) {
347        self.log(LogLevel::Info, message);
348    }
349
350    pub fn warn(&self, message: impl AsRef<str>) {
351        self.log(LogLevel::Warn, message);
352    }
353
354    pub fn error(&self, message: impl AsRef<str>) {
355        self.log(LogLevel::Error, message);
356    }
357
358    fn log(&self, level: LogLevel, message: impl AsRef<str>) {
359        let message = message.as_ref();
360        if let Some(callback) = self.callback.flatten() {
361            let message = sanitize_cstring(message);
362            static FORMAT: &[u8] = b"%s\n\0";
363            // SAFETY: The callback comes from the frontend and the format string is static.
364            unsafe { callback(level, FORMAT.as_ptr().cast::<c_char>(), message.as_ptr()) };
365        } else {
366            eprintln!("{message}");
367        }
368    }
369}
370
371/// Hardware-rendering context request sent to the frontend.
372///
373/// Use the constructors such as `HwRenderConfig::opengl_core` or the candidate
374/// helpers in `hw_render` instead of filling raw context IDs manually.
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
376pub struct HwRenderConfig {
377    pub context_type: HwContextType,
378    pub depth: bool,
379    pub stencil: bool,
380    pub bottom_left_origin: bool,
381    pub version_major: u32,
382    pub version_minor: u32,
383    pub cache_context: bool,
384    pub debug_context: bool,
385}
386
387impl HwRenderConfig {
388    pub fn new(context_type: HwContextType) -> Self {
389        Self {
390            context_type,
391            ..Self::default()
392        }
393    }
394
395    pub fn opengl() -> Self {
396        Self::new(HwContextType::OpenGl)
397    }
398
399    pub fn opengl_core(version_major: u32, version_minor: u32) -> Self {
400        Self::new(HwContextType::OpenGlCore).with_version(version_major, version_minor)
401    }
402
403    pub fn opengles2() -> Self {
404        Self::new(HwContextType::OpenGlEs2)
405    }
406
407    pub fn opengles3() -> Self {
408        Self::new(HwContextType::OpenGlEs3)
409    }
410
411    pub fn opengles_version(version_major: u32, version_minor: u32) -> Self {
412        Self::new(HwContextType::OpenGlEsVersion).with_version(version_major, version_minor)
413    }
414
415    pub fn with_depth(mut self, depth: bool) -> Self {
416        self.depth = depth;
417        self
418    }
419
420    pub fn with_stencil(mut self, stencil: bool) -> Self {
421        self.stencil = stencil;
422        self
423    }
424
425    pub fn with_bottom_left_origin(mut self, bottom_left_origin: bool) -> Self {
426        self.bottom_left_origin = bottom_left_origin;
427        self
428    }
429
430    pub fn with_version(mut self, version_major: u32, version_minor: u32) -> Self {
431        self.version_major = version_major;
432        self.version_minor = version_minor;
433        self
434    }
435
436    pub fn with_cache_context(mut self, cache_context: bool) -> Self {
437        self.cache_context = cache_context;
438        self
439    }
440
441    pub fn with_debug_context(mut self, debug_context: bool) -> Self {
442        self.debug_context = debug_context;
443        self
444    }
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq)]
448pub struct PreferredHwRender {
449    pub context_type: HwContextType,
450    pub supports_non_preferred_context: bool,
451}
452
453impl HwContextType {
454    pub fn is_opengl_family(self) -> bool {
455        matches!(
456            self,
457            Self::OpenGl
458                | Self::OpenGlCore
459                | Self::OpenGlEs2
460                | Self::OpenGlEs3
461                | Self::OpenGlEsVersion
462        )
463    }
464}
465
466fn describe_hw_render_config(config: HwRenderConfig) -> String {
467    if config.version_major == 0 && config.version_minor == 0 {
468        format!("{:?}", config.context_type)
469    } else {
470        format!(
471            "{:?} {}.{}",
472            config.context_type, config.version_major, config.version_minor
473        )
474    }
475}
476
477fn is_opengl_es_family(context_type: HwContextType) -> bool {
478    matches!(
479        context_type,
480        HwContextType::OpenGlEs2 | HwContextType::OpenGlEs3 | HwContextType::OpenGlEsVersion
481    )
482}
483
484#[derive(Clone, Copy, Debug, PartialEq, Eq)]
485pub enum Region {
486    Ntsc,
487    Pal,
488}
489
490impl Region {
491    fn as_raw(self) -> u32 {
492        match self {
493            Self::Ntsc => RETRO_REGION_NTSC,
494            Self::Pal => RETRO_REGION_PAL,
495        }
496    }
497}
498
499#[derive(Clone, Copy, Debug)]
500pub struct GameInfo<'a> {
501    pub path: Option<&'a CStr>,
502    pub data: Option<&'a [u8]>,
503    pub meta: Option<&'a CStr>,
504}
505
506impl<'a> GameInfo<'a> {
507    pub fn path_lossy(&self) -> Option<Cow<'a, str>> {
508        self.path.map(CStr::to_string_lossy)
509    }
510
511    pub fn meta_lossy(&self) -> Option<Cow<'a, str>> {
512        self.meta.map(CStr::to_string_lossy)
513    }
514
515    unsafe fn from_raw(raw: *const RawGameInfo) -> Option<Self> {
516        if raw.is_null() {
517            return None;
518        }
519
520        // SAFETY: The caller guarantees `raw` is valid for the duration of the call.
521        let raw = unsafe { &*raw };
522        let path = if raw.path.is_null() {
523            None
524        } else {
525            // SAFETY: `path` follows the libretro ABI contract.
526            Some(unsafe { CStr::from_ptr(raw.path) })
527        };
528        let data = if raw.data.is_null() {
529            None
530        } else {
531            // SAFETY: `data` and `size` follow the libretro ABI contract.
532            Some(unsafe { std::slice::from_raw_parts(raw.data.cast::<u8>(), raw.size) })
533        };
534        let meta = if raw.meta.is_null() {
535            None
536        } else {
537            // SAFETY: `meta` follows the libretro ABI contract.
538            Some(unsafe { CStr::from_ptr(raw.meta) })
539        };
540
541        Some(Self { path, data, meta })
542    }
543}
544
545#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
546pub struct CheatIndex(u32);
547
548impl CheatIndex {
549    pub const fn new(index: u32) -> Self {
550        Self(index)
551    }
552
553    pub const fn get(self) -> u32 {
554        self.0
555    }
556}
557
558impl From<u32> for CheatIndex {
559    fn from(index: u32) -> Self {
560        Self::new(index)
561    }
562}
563
564#[derive(Clone, Copy, Debug)]
565pub struct CheatCode<'a> {
566    raw: &'a CStr,
567}
568
569impl<'a> CheatCode<'a> {
570    fn from_c_str(raw: &'a CStr) -> Self {
571        Self { raw }
572    }
573
574    pub fn as_c_str(self) -> &'a CStr {
575        self.raw
576    }
577
578    pub fn to_str(self) -> Result<&'a str, std::str::Utf8Error> {
579        self.raw.to_str()
580    }
581
582    pub fn to_string_lossy(self) -> Cow<'a, str> {
583        self.raw.to_string_lossy()
584    }
585}
586
587type KeyboardEventHandler = Box<dyn Fn(&mut dyn Core, KeyboardEvent) + Send + Sync>;
588type AudioCallbackHandler = Box<dyn Fn(&mut dyn Core) + Send + Sync>;
589type AudioStateHandler = Box<dyn Fn(&mut dyn Core, AudioCallbackState) + Send + Sync>;
590type AudioBufferStatusHandler = Box<dyn Fn(&mut dyn Core, AudioBufferStatus) + Send + Sync>;
591type FrameTimeHandler = Box<dyn Fn(&mut dyn Core, FrameTime) + Send + Sync>;
592type LocationLifecycleHandler = Box<dyn Fn(&mut dyn Core) + Send + Sync>;
593type CameraRawFrameHandler = Box<dyn Fn(&mut dyn Core, CameraRawFrame<'_>) + Send + Sync>;
594type CameraTextureFrameHandler = Box<dyn Fn(&mut dyn Core, CameraTextureFrame) + Send + Sync>;
595
596type ListenerId = usize;
597
598struct EventListener<T> {
599    id: ListenerId,
600    callback: T,
601}
602
603impl<T> EventListener<T> {
604    fn new(id: ListenerId, callback: T) -> Self {
605        Self { id, callback }
606    }
607}
608
609fn add_listener<T>(listeners: &mut Vec<EventListener<T>>, id: ListenerId, callback: T) {
610    if listeners.iter().any(|listener| listener.id == id) {
611        return;
612    }
613    listeners.push(EventListener::new(id, callback));
614}
615
616fn remove_listener<T>(listeners: &mut Vec<EventListener<T>>, id: ListenerId) {
617    listeners.retain(|listener| listener.id != id);
618}
619
620#[derive(Default)]
621struct CoreEventHandlers {
622    keyboard_event: Vec<EventListener<KeyboardEventHandler>>,
623    audio_callback: Vec<EventListener<AudioCallbackHandler>>,
624    audio_callback_state_changed: Vec<EventListener<AudioStateHandler>>,
625    audio_buffer_status: Vec<EventListener<AudioBufferStatusHandler>>,
626    frame_time: Option<(FrameTime, FrameTimeHandler)>,
627    location_initialized: Vec<EventListener<LocationLifecycleHandler>>,
628    location_deinitialized: Vec<EventListener<LocationLifecycleHandler>>,
629    camera_initialized: Vec<EventListener<LocationLifecycleHandler>>,
630    camera_deinitialized: Vec<EventListener<LocationLifecycleHandler>>,
631    camera_raw_frame: Vec<EventListener<CameraRawFrameHandler>>,
632    camera_texture_frame: Vec<EventListener<CameraTextureFrameHandler>>,
633}
634
635impl CoreEventHandlers {
636    fn has_keyboard_event(&self) -> bool {
637        !self.keyboard_event.is_empty()
638    }
639
640    fn has_audio_callback(&self) -> bool {
641        !self.audio_callback.is_empty() || !self.audio_callback_state_changed.is_empty()
642    }
643
644    fn has_audio_buffer_status(&self) -> bool {
645        !self.audio_buffer_status.is_empty()
646    }
647
648    fn frame_time_reference(&self) -> Option<FrameTime> {
649        self.frame_time.as_ref().map(|(reference, _)| *reference)
650    }
651
652    fn dispatch_keyboard_event(&self, core: &mut dyn Core, event: KeyboardEvent) {
653        for listener in &self.keyboard_event {
654            (listener.callback)(core, event);
655        }
656    }
657
658    fn dispatch_audio_callback(&self, core: &mut dyn Core) {
659        for listener in &self.audio_callback {
660            (listener.callback)(core);
661        }
662    }
663
664    fn dispatch_audio_callback_state_changed(
665        &self,
666        core: &mut dyn Core,
667        state: AudioCallbackState,
668    ) {
669        for listener in &self.audio_callback_state_changed {
670            (listener.callback)(core, state);
671        }
672    }
673
674    fn dispatch_audio_buffer_status(&self, core: &mut dyn Core, status: AudioBufferStatus) {
675        for listener in &self.audio_buffer_status {
676            (listener.callback)(core, status);
677        }
678    }
679
680    fn dispatch_frame_time(&self, core: &mut dyn Core, time: FrameTime) {
681        if let Some((_, callback)) = &self.frame_time {
682            callback(core, time);
683        }
684    }
685
686    fn dispatch_location_initialized(&self, core: &mut dyn Core) {
687        for listener in &self.location_initialized {
688            (listener.callback)(core);
689        }
690    }
691
692    fn dispatch_location_deinitialized(&self, core: &mut dyn Core) {
693        for listener in &self.location_deinitialized {
694            (listener.callback)(core);
695        }
696    }
697
698    fn dispatch_camera_initialized(&self, core: &mut dyn Core) {
699        for listener in &self.camera_initialized {
700            (listener.callback)(core);
701        }
702    }
703
704    fn dispatch_camera_deinitialized(&self, core: &mut dyn Core) {
705        for listener in &self.camera_deinitialized {
706            (listener.callback)(core);
707        }
708    }
709
710    fn dispatch_camera_raw_frame(&self, core: &mut dyn Core, frame: CameraRawFrame<'_>) {
711        for listener in &self.camera_raw_frame {
712            (listener.callback)(core, frame);
713        }
714    }
715
716    fn dispatch_camera_texture_frame(&self, core: &mut dyn Core, frame: CameraTextureFrame) {
717        for listener in &self.camera_texture_frame {
718            (listener.callback)(core, frame);
719        }
720    }
721}
722
723/// Event-listener registration for frontend-to-core notifications.
724///
725/// Register listeners from `Core::configure_events`. The wrapper installs the
726/// matching low-level libretro callbacks during environment setup, avoiding
727/// call-order bugs where a core defines a callback but forgets to enable the raw
728/// callback separately. Listeners are dispatched in registration order.
729/// Registering the same callback function more than once for the same event is
730/// a no-op, matching DOM `addEventListener` behavior. Use the matching
731/// `remove_*_listener` method with the same callback function to remove a
732/// listener during configuration. Callback-shaped frontend hooks with one
733/// active registration, such as frame timing, use explicit `set_*_callback` and
734/// `clear_*_callback` methods instead.
735pub struct CoreEventConfig<C: Core> {
736    handlers: CoreEventHandlers,
737    _core: std::marker::PhantomData<fn() -> C>,
738}
739
740impl<C: Core> Default for CoreEventConfig<C> {
741    fn default() -> Self {
742        Self {
743            handlers: CoreEventHandlers::default(),
744            _core: std::marker::PhantomData,
745        }
746    }
747}
748
749impl<C: Core> CoreEventConfig<C> {
750    pub fn add_keyboard_event_listener(
751        &mut self,
752        listener: fn(&mut C, KeyboardEvent),
753    ) -> &mut Self {
754        add_listener(
755            &mut self.handlers.keyboard_event,
756            listener as ListenerId,
757            Box::new(move |core, event| {
758                let core = (core as &mut dyn Any)
759                    .downcast_mut::<C>()
760                    .expect("registered keyboard event listener received the wrong core type");
761                listener(core, event);
762            }),
763        );
764        self
765    }
766
767    pub fn remove_keyboard_event_listener(
768        &mut self,
769        listener: fn(&mut C, KeyboardEvent),
770    ) -> &mut Self {
771        remove_listener(&mut self.handlers.keyboard_event, listener as ListenerId);
772        self
773    }
774
775    pub fn add_audio_callback_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
776        add_listener(
777            &mut self.handlers.audio_callback,
778            listener as ListenerId,
779            Box::new(move |core| {
780                let core = (core as &mut dyn Any)
781                    .downcast_mut::<C>()
782                    .expect("registered audio callback listener received the wrong core type");
783                listener(core);
784            }),
785        );
786        self
787    }
788
789    pub fn remove_audio_callback_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
790        remove_listener(&mut self.handlers.audio_callback, listener as ListenerId);
791        self
792    }
793
794    pub fn add_audio_callback_state_changed_listener(
795        &mut self,
796        listener: fn(&mut C, AudioCallbackState),
797    ) -> &mut Self {
798        add_listener(
799            &mut self.handlers.audio_callback_state_changed,
800            listener as ListenerId,
801            Box::new(move |core, state| {
802                let core = (core as &mut dyn Any)
803                    .downcast_mut::<C>()
804                    .expect("registered audio state listener received the wrong core type");
805                listener(core, state);
806            }),
807        );
808        self
809    }
810
811    pub fn remove_audio_callback_state_changed_listener(
812        &mut self,
813        listener: fn(&mut C, AudioCallbackState),
814    ) -> &mut Self {
815        remove_listener(
816            &mut self.handlers.audio_callback_state_changed,
817            listener as ListenerId,
818        );
819        self
820    }
821
822    pub fn add_audio_buffer_status_listener(
823        &mut self,
824        listener: fn(&mut C, AudioBufferStatus),
825    ) -> &mut Self {
826        add_listener(
827            &mut self.handlers.audio_buffer_status,
828            listener as ListenerId,
829            Box::new(move |core, status| {
830                let core = (core as &mut dyn Any)
831                    .downcast_mut::<C>()
832                    .expect("registered audio buffer status listener received the wrong core type");
833                listener(core, status);
834            }),
835        );
836        self
837    }
838
839    pub fn remove_audio_buffer_status_listener(
840        &mut self,
841        listener: fn(&mut C, AudioBufferStatus),
842    ) -> &mut Self {
843        remove_listener(
844            &mut self.handlers.audio_buffer_status,
845            listener as ListenerId,
846        );
847        self
848    }
849
850    pub fn set_frame_time_callback(
851        &mut self,
852        reference: FrameTime,
853        callback: fn(&mut C, FrameTime),
854    ) -> &mut Self {
855        self.handlers.frame_time = Some((
856            reference,
857            Box::new(move |core, time| {
858                let core = (core as &mut dyn Any)
859                    .downcast_mut::<C>()
860                    .expect("registered frame-time callback received the wrong core type");
861                callback(core, time);
862            }),
863        ));
864        self
865    }
866
867    pub fn clear_frame_time_callback(&mut self) -> &mut Self {
868        self.handlers.frame_time = None;
869        self
870    }
871
872    pub fn add_location_initialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
873        add_listener(
874            &mut self.handlers.location_initialized,
875            listener as ListenerId,
876            Box::new(move |core| {
877                let core = (core as &mut dyn Any).downcast_mut::<C>().expect(
878                    "registered location initialized listener received the wrong core type",
879                );
880                listener(core);
881            }),
882        );
883        self
884    }
885
886    pub fn remove_location_initialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
887        remove_listener(
888            &mut self.handlers.location_initialized,
889            listener as ListenerId,
890        );
891        self
892    }
893
894    pub fn add_location_deinitialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
895        add_listener(
896            &mut self.handlers.location_deinitialized,
897            listener as ListenerId,
898            Box::new(move |core| {
899                let core = (core as &mut dyn Any).downcast_mut::<C>().expect(
900                    "registered location deinitialized listener received the wrong core type",
901                );
902                listener(core);
903            }),
904        );
905        self
906    }
907
908    pub fn remove_location_deinitialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
909        remove_listener(
910            &mut self.handlers.location_deinitialized,
911            listener as ListenerId,
912        );
913        self
914    }
915
916    pub fn add_camera_initialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
917        add_listener(
918            &mut self.handlers.camera_initialized,
919            listener as ListenerId,
920            Box::new(move |core| {
921                let core = (core as &mut dyn Any)
922                    .downcast_mut::<C>()
923                    .expect("registered camera initialized listener received the wrong core type");
924                listener(core);
925            }),
926        );
927        self
928    }
929
930    pub fn remove_camera_initialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
931        remove_listener(
932            &mut self.handlers.camera_initialized,
933            listener as ListenerId,
934        );
935        self
936    }
937
938    pub fn add_camera_deinitialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
939        add_listener(
940            &mut self.handlers.camera_deinitialized,
941            listener as ListenerId,
942            Box::new(move |core| {
943                let core = (core as &mut dyn Any).downcast_mut::<C>().expect(
944                    "registered camera deinitialized listener received the wrong core type",
945                );
946                listener(core);
947            }),
948        );
949        self
950    }
951
952    pub fn remove_camera_deinitialized_listener(&mut self, listener: fn(&mut C)) -> &mut Self {
953        remove_listener(
954            &mut self.handlers.camera_deinitialized,
955            listener as ListenerId,
956        );
957        self
958    }
959
960    pub fn add_camera_raw_frame_listener(
961        &mut self,
962        listener: fn(&mut C, CameraRawFrame<'_>),
963    ) -> &mut Self {
964        add_listener(
965            &mut self.handlers.camera_raw_frame,
966            listener as ListenerId,
967            Box::new(move |core, frame| {
968                let core = (core as &mut dyn Any)
969                    .downcast_mut::<C>()
970                    .expect("registered camera raw-frame listener received the wrong core type");
971                listener(core, frame);
972            }),
973        );
974        self
975    }
976
977    pub fn remove_camera_raw_frame_listener(
978        &mut self,
979        listener: fn(&mut C, CameraRawFrame<'_>),
980    ) -> &mut Self {
981        remove_listener(&mut self.handlers.camera_raw_frame, listener as ListenerId);
982        self
983    }
984
985    pub fn add_camera_texture_frame_listener(
986        &mut self,
987        listener: fn(&mut C, CameraTextureFrame),
988    ) -> &mut Self {
989        add_listener(
990            &mut self.handlers.camera_texture_frame,
991            listener as ListenerId,
992            Box::new(move |core, frame| {
993                let core = (core as &mut dyn Any).downcast_mut::<C>().expect(
994                    "registered camera texture-frame listener received the wrong core type",
995                );
996                listener(core, frame);
997            }),
998        );
999        self
1000    }
1001
1002    pub fn remove_camera_texture_frame_listener(
1003        &mut self,
1004        listener: fn(&mut C, CameraTextureFrame),
1005    ) -> &mut Self {
1006        remove_listener(
1007            &mut self.handlers.camera_texture_frame,
1008            listener as ListenerId,
1009        );
1010        self
1011    }
1012
1013    fn into_handlers(self) -> CoreEventHandlers {
1014        self.handlers
1015    }
1016}
1017
1018#[doc(hidden)]
1019pub struct CoreBundle {
1020    core: Box<dyn Core>,
1021    event_handlers: CoreEventHandlers,
1022}
1023
1024#[doc(hidden)]
1025pub fn create_core<C: Core>(mut core: C) -> CoreBundle {
1026    let mut events = CoreEventConfig::<C>::default();
1027    core.configure_events(&mut events);
1028    CoreBundle {
1029        core: Box::new(core),
1030        event_handlers: events.into_handlers(),
1031    }
1032}
1033
1034/// Trait implemented by a Rust libretro core.
1035///
1036/// Required methods describe metadata, AV timing, and per-frame execution.
1037/// Optional methods cover setup, content loading, savestates, disk control,
1038/// hardware-render lifecycle, netpacket callbacks, and other libretro surfaces.
1039/// Export an implementation with `export_core!`.
1040pub trait Core: Any + Send + 'static {
1041    fn system_info(&self) -> SystemInfo;
1042    fn av_info(&self) -> SystemAvInfo;
1043    fn run(&mut self, runtime: &mut Runtime<'_>);
1044
1045    fn configure_events(&mut self, _events: &mut CoreEventConfig<Self>)
1046    where
1047        Self: Sized,
1048    {
1049    }
1050    fn on_set_environment(&mut self, _env: &mut Environment<'_>) {}
1051    fn init(&mut self, _env: &mut Environment<'_>) {}
1052    fn deinit(&mut self) {}
1053    fn set_controller_port_device(&mut self, _port: InputPort, _device: ControllerDevice) {}
1054    fn reset(&mut self) {}
1055    fn load_game(&mut self, _game: Option<GameInfo<'_>>, _runtime: &mut Runtime<'_>) -> bool {
1056        true
1057    }
1058    fn load_game_special(
1059        &mut self,
1060        _subsystem: SubsystemId,
1061        _games: &[GameInfo<'_>],
1062        _runtime: &mut Runtime<'_>,
1063    ) -> bool {
1064        false
1065    }
1066    fn unload_game(&mut self) {}
1067    fn serialize_size(&self) -> usize {
1068        0
1069    }
1070    fn serialize(&self, _data: &mut [u8]) -> bool {
1071        false
1072    }
1073    fn unserialize(&mut self, _data: &[u8]) -> bool {
1074        false
1075    }
1076    fn cheat_reset(&mut self) {}
1077    fn cheat_set(&mut self, _index: CheatIndex, _enabled: bool, _code: Option<CheatCode<'_>>) {}
1078    fn region(&self) -> Region {
1079        Region::Ntsc
1080    }
1081    fn memory_region(&mut self, _region: MemoryRegion) -> Option<CoreMemory<'_>> {
1082        None
1083    }
1084    fn proc_address(&mut self, _symbol: &CStr) -> Option<CoreProcAddress> {
1085        None
1086    }
1087    fn disk_set_tray_state(&mut self, _state: DiskTrayState) -> bool {
1088        false
1089    }
1090    fn disk_tray_state(&mut self) -> DiskTrayState {
1091        DiskTrayState::Closed
1092    }
1093    fn disk_image_index(&mut self) -> DiskIndex {
1094        DiskIndex::new(0)
1095    }
1096    fn disk_set_image_index(&mut self, _index: DiskIndex) -> bool {
1097        false
1098    }
1099    fn disk_image_count(&mut self) -> u32 {
1100        0
1101    }
1102    fn disk_replace_image_index(&mut self, _index: DiskIndex, _game: Option<GameInfo<'_>>) -> bool {
1103        false
1104    }
1105    fn disk_add_image_index(&mut self) -> bool {
1106        false
1107    }
1108    fn disk_set_initial_image(&mut self, _index: DiskIndex, _path: &CStr) -> bool {
1109        false
1110    }
1111    fn disk_image_path(&mut self, _index: DiskIndex) -> Option<String> {
1112        None
1113    }
1114    fn disk_image_label(&mut self, _index: DiskIndex) -> Option<String> {
1115        None
1116    }
1117    fn netpacket_start(&mut self, _session: NetpacketSession) {}
1118    fn netpacket_receive(&mut self, _packet: Netpacket<'_>) {}
1119    fn netpacket_stop(&mut self) {}
1120    fn netpacket_poll(&mut self) {}
1121    fn netpacket_connected(&mut self, _client_id: NetplayClientId) -> bool {
1122        true
1123    }
1124    fn netpacket_disconnected(&mut self, _client_id: NetplayClientId) {}
1125    fn core_options_update_display(&mut self, _env: &mut Environment<'_>) -> bool {
1126        false
1127    }
1128    fn hw_context_reset(&mut self, _runtime: &mut Runtime<'_>) {}
1129    fn hw_context_destroy(&mut self, _runtime: &mut Runtime<'_>) {}
1130}
1131
1132/// Typed wrapper around libretro environment commands.
1133///
1134/// Use this during `Core::on_set_environment`, `Core::init`, and through
1135/// `Runtime::environment` for runtime-safe commands. Methods retain backing
1136/// storage when libretro allows the frontend to keep pointers after a call.
1137pub struct Environment<'a> {
1138    state: &'a mut CoreState,
1139}
1140
1141impl<'a> Environment<'a> {
1142    pub fn logger(&mut self) -> Logger {
1143        if self.state.log_callback.is_none() {
1144            let mut callback = RawLogCallback::default();
1145            let ok = self.call_env(
1146                RETRO_ENVIRONMENT_GET_LOG_INTERFACE,
1147                (&mut callback as *mut RawLogCallback).cast::<c_void>(),
1148            );
1149            if ok {
1150                self.state.log_callback = Some(callback);
1151            }
1152        }
1153
1154        Logger {
1155            callback: self.state.log_callback.map(|callback| callback.log),
1156        }
1157    }
1158
1159    pub fn set_support_no_game(&mut self, enabled: bool) -> bool {
1160        self.call_env(
1161            RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME,
1162            &enabled as *const bool as *mut c_void,
1163        )
1164    }
1165
1166    pub fn set_message(&mut self, message: impl AsRef<str>, frames: u32) -> bool {
1167        let message = sanitize_cstring(message.as_ref());
1168        let mut raw = raw::retro_message {
1169            msg: message.as_ptr(),
1170            frames,
1171        };
1172        self.call_env(
1173            RETRO_ENVIRONMENT_SET_MESSAGE,
1174            (&mut raw as *mut raw::retro_message).cast::<c_void>(),
1175        )
1176    }
1177
1178    pub fn set_variables(&mut self, variables: &[VariableDefinition]) -> bool {
1179        let mut storage = options::CoreOptionsStorage::variables(variables);
1180
1181        let ok = self.call_env(
1182            RETRO_ENVIRONMENT_SET_VARIABLES,
1183            storage.variables_ptr().cast::<c_void>(),
1184        );
1185        if ok {
1186            self.state.variables = Some(storage);
1187        }
1188        ok
1189    }
1190
1191    pub fn core_options_version(&mut self) -> CoreOptionsVersion {
1192        let mut version = 0u32;
1193        if self.call_env(
1194            raw::RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION,
1195            (&mut version as *mut u32).cast::<c_void>(),
1196        ) {
1197            CoreOptionsVersion::new(version)
1198        } else {
1199            CoreOptionsVersion::LEGACY_VARIABLES
1200        }
1201    }
1202
1203    pub fn set_core_options(
1204        &mut self,
1205        options: &CoreOptions,
1206    ) -> Result<bool, CoreOptionsBuildError> {
1207        let version = self.core_options_version();
1208        if version.supports_v2() {
1209            self.set_core_options_v2(options)
1210        } else if version.supports_v1() {
1211            self.set_core_options_v1(&options.definitions)
1212        } else {
1213            self.set_core_options_legacy(options)
1214        }
1215    }
1216
1217    pub fn set_core_options_legacy(
1218        &mut self,
1219        options: &CoreOptions,
1220    ) -> Result<bool, CoreOptionsBuildError> {
1221        let mut storage = options::CoreOptionsStorage::legacy_from_options(options)?;
1222        let ok = self.call_env(
1223            RETRO_ENVIRONMENT_SET_VARIABLES,
1224            storage.variables_ptr().cast::<c_void>(),
1225        );
1226        if ok {
1227            self.state.variables = Some(storage);
1228        }
1229        Ok(ok)
1230    }
1231
1232    pub fn set_core_options_v1(
1233        &mut self,
1234        definitions: &[CoreOptionDefinition],
1235    ) -> Result<bool, CoreOptionsBuildError> {
1236        let mut storage = options::CoreOptionsStorage::v1(definitions)?;
1237        let ok = self.call_env(
1238            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS,
1239            storage.v1_definitions_ptr().cast::<c_void>(),
1240        );
1241        if ok {
1242            self.state.variables = Some(storage);
1243        }
1244        Ok(ok)
1245    }
1246
1247    pub fn set_core_options_v1_intl(
1248        &mut self,
1249        us: &[CoreOptionDefinition],
1250        local: Option<&[CoreOptionDefinition]>,
1251    ) -> Result<bool, CoreOptionsBuildError> {
1252        let mut storage = options::CoreOptionsStorage::v1_intl(us, local)?;
1253        let mut raw = storage.v1_intl_raw();
1254        let ok = self.call_env(
1255            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL,
1256            (&mut raw as *mut raw::retro_core_options_intl).cast::<c_void>(),
1257        );
1258        if ok {
1259            self.state.variables = Some(storage);
1260        }
1261        Ok(ok)
1262    }
1263
1264    pub fn set_core_options_v2(
1265        &mut self,
1266        options: &CoreOptions,
1267    ) -> Result<bool, CoreOptionsBuildError> {
1268        let mut storage = options::CoreOptionsStorage::v2(options)?;
1269        let mut raw = storage.v2_raw();
1270        let ok = self.call_env(
1271            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
1272            (&mut raw as *mut raw::retro_core_options_v2).cast::<c_void>(),
1273        );
1274        if ok {
1275            self.state.variables = Some(storage);
1276        }
1277        Ok(ok)
1278    }
1279
1280    pub fn set_core_options_v2_intl(
1281        &mut self,
1282        us: &CoreOptions,
1283        local: Option<&CoreOptions>,
1284    ) -> Result<bool, CoreOptionsBuildError> {
1285        let mut storage = options::CoreOptionsStorage::v2_intl(us, local)?;
1286        let mut us = storage.v2_raw();
1287        let mut local = storage.local_v2_raw();
1288        let mut raw = raw::retro_core_options_v2_intl {
1289            us: &mut us,
1290            local: local.as_mut().map_or(ptr::null_mut(), |local| {
1291                local as *mut raw::retro_core_options_v2
1292            }),
1293        };
1294        let ok = self.call_env(
1295            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL,
1296            (&mut raw as *mut raw::retro_core_options_v2_intl).cast::<c_void>(),
1297        );
1298        if ok {
1299            self.state.variables = Some(storage);
1300        }
1301        Ok(ok)
1302    }
1303
1304    pub fn set_core_option_display(&mut self, display: CoreOptionDisplay) -> bool {
1305        let key = sanitize_cstring(display.key);
1306        let mut raw = raw::retro_core_option_display {
1307            key: key.as_ptr(),
1308            visible: display.visible,
1309        };
1310        self.call_env(
1311            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY,
1312            (&mut raw as *mut raw::retro_core_option_display).cast::<c_void>(),
1313        )
1314    }
1315
1316    pub fn set_core_options_update_display_callback(&mut self) -> bool {
1317        let mut raw = raw::retro_core_options_update_display_callback {
1318            callback: Some(core_options_update_display_trampoline),
1319        };
1320        self.call_env(
1321            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK,
1322            (&mut raw as *mut raw::retro_core_options_update_display_callback).cast::<c_void>(),
1323        )
1324    }
1325
1326    pub fn set_variable(&mut self, key: &str, value: Option<&str>) -> bool {
1327        let key = sanitize_cstring(key);
1328        let value = value.map(sanitize_cstring);
1329        let mut raw = RawVariable {
1330            key: key.as_ptr(),
1331            value: value.as_ref().map_or(ptr::null(), |value| value.as_ptr()),
1332        };
1333        self.call_env(
1334            raw::RETRO_ENVIRONMENT_SET_VARIABLE,
1335            (&mut raw as *mut RawVariable).cast::<c_void>(),
1336        )
1337    }
1338
1339    pub fn vfs_interface(&mut self, version: VfsInterfaceVersion) -> Option<VfsInterface> {
1340        let mut info = raw::retro_vfs_interface_info {
1341            required_interface_version: version.get(),
1342            iface: ptr::null_mut(),
1343        };
1344        let ok = self.call_env(
1345            raw::RETRO_ENVIRONMENT_GET_VFS_INTERFACE,
1346            (&mut info as *mut raw::retro_vfs_interface_info).cast::<c_void>(),
1347        );
1348        if ok && !info.iface.is_null() {
1349            // SAFETY: On success, the frontend populated `iface` with a valid
1350            // interface table. Copying the function pointers avoids borrowing
1351            // frontend-owned storage through the public wrapper.
1352            Some(VfsInterface::new(
1353                VfsInterfaceVersion::new(info.required_interface_version),
1354                unsafe { *info.iface },
1355            ))
1356        } else {
1357            None
1358        }
1359    }
1360
1361    pub fn set_content_info_overrides(&mut self, overrides: &[ContentInfoOverride]) -> bool {
1362        let mut extensions = Vec::with_capacity(overrides.len());
1363        let mut raw = Vec::with_capacity(overrides.len() + 1);
1364
1365        for override_info in overrides {
1366            let extensions_cstring = sanitize_cstring(&override_info.extensions);
1367            raw.push(RawContentInfoOverride {
1368                extensions: extensions_cstring.as_ptr(),
1369                need_fullpath: override_info.need_fullpath,
1370                persistent_data: override_info.persistent_data,
1371            });
1372            extensions.push(extensions_cstring);
1373        }
1374        raw.push(RawContentInfoOverride::default());
1375
1376        let ok = self.call_env(
1377            RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE,
1378            raw.as_mut_ptr().cast::<c_void>(),
1379        );
1380        if ok {
1381            self.state.content_info_overrides = Some(ContentInfoOverrideStorage {
1382                _extensions: extensions,
1383                _raw: raw,
1384            });
1385        }
1386        ok
1387    }
1388
1389    pub fn set_pixel_format(&mut self, format: PixelFormat) -> bool {
1390        let mut format = format;
1391        self.call_env(
1392            RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,
1393            (&mut format as *mut PixelFormat).cast::<c_void>(),
1394        )
1395    }
1396
1397    pub fn set_geometry(&mut self, geometry: GameGeometry) -> bool {
1398        let mut geometry = geometry.as_raw();
1399        self.call_env(
1400            RETRO_ENVIRONMENT_SET_GEOMETRY,
1401            (&mut geometry as *mut raw::retro_game_geometry).cast::<c_void>(),
1402        )
1403    }
1404
1405    pub fn set_hw_render(&mut self, config: HwRenderConfig) -> bool {
1406        let mut callback = RawHwRenderCallback {
1407            context_type: config.context_type,
1408            context_reset: Some(hw_context_reset_trampoline),
1409            get_current_framebuffer: None,
1410            get_proc_address: None,
1411            depth: config.depth,
1412            stencil: config.stencil,
1413            bottom_left_origin: config.bottom_left_origin,
1414            version_major: config.version_major,
1415            version_minor: config.version_minor,
1416            cache_context: config.cache_context,
1417            context_destroy: Some(hw_context_destroy_trampoline),
1418            debug_context: config.debug_context,
1419        };
1420
1421        let ok = self.call_env(
1422            RETRO_ENVIRONMENT_SET_HW_RENDER,
1423            (&mut callback as *mut RawHwRenderCallback).cast::<c_void>(),
1424        );
1425        if ok {
1426            self.state.hw_render = Some(callback);
1427        }
1428        ok
1429    }
1430
1431    pub fn set_hw_shared_context(&mut self) -> bool {
1432        self.call_env(
1433            raw::RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT,
1434            ptr::null_mut(),
1435        )
1436    }
1437
1438    pub fn hw_render_interface(&mut self) -> Option<HwRenderInterface<'_>> {
1439        let mut interface = ptr::null::<raw::retro_hw_render_interface>();
1440        let ok = self.call_env(
1441            raw::RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE,
1442            (&mut interface as *mut *const raw::retro_hw_render_interface).cast::<c_void>(),
1443        );
1444        if ok && !interface.is_null() {
1445            // SAFETY: On success, the frontend stored a valid frontend-owned
1446            // base interface pointer for the active hardware API.
1447            Some(HwRenderInterface::from_raw(unsafe { &*interface }))
1448        } else {
1449            None
1450        }
1451    }
1452
1453    pub fn hw_render_context_negotiation_interface_support(
1454        &mut self,
1455        interface_type: HwRenderContextNegotiationInterfaceType,
1456    ) -> Option<u32> {
1457        let mut interface = raw::retro_hw_render_context_negotiation_interface {
1458            interface_type: interface_type.as_raw(),
1459            interface_version: 0,
1460        };
1461        self.call_env(
1462            raw::RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT,
1463            (&mut interface as *mut raw::retro_hw_render_context_negotiation_interface)
1464                .cast::<c_void>(),
1465        )
1466        .then_some(interface.interface_version)
1467    }
1468
1469    pub fn set_hw_render_context_negotiation_interface(
1470        &mut self,
1471        interface: HwRenderContextNegotiationInterface,
1472    ) -> bool {
1473        self.state.hw_render_context_negotiation = Some(interface.as_raw());
1474        let stored = {
1475            let stored = self
1476                .state
1477                .hw_render_context_negotiation
1478                .as_mut()
1479                .expect("just stored HW render context negotiation interface");
1480            stored as *mut raw::retro_hw_render_context_negotiation_interface
1481        };
1482        let ok = self.call_env(
1483            raw::RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE,
1484            stored.cast::<c_void>(),
1485        );
1486        if !ok {
1487            self.state.hw_render_context_negotiation = None;
1488        }
1489        ok
1490    }
1491
1492    pub fn preferred_hw_render(&mut self) -> Option<PreferredHwRender> {
1493        let mut context_type = HwContextType::None;
1494        let supports_non_preferred_context = self.call_env(
1495            RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER,
1496            (&mut context_type as *mut HwContextType).cast::<c_void>(),
1497        );
1498        if context_type == HwContextType::None {
1499            return None;
1500        }
1501
1502        Some(PreferredHwRender {
1503            context_type,
1504            supports_non_preferred_context,
1505        })
1506    }
1507
1508    pub fn set_hw_render_from_candidates(
1509        &mut self,
1510        candidates: &[HwRenderConfig],
1511    ) -> Option<HwRenderConfig> {
1512        let logger = self.logger();
1513        let preferred = self.preferred_hw_render();
1514        let preferred_context_type = preferred.map(|render| render.context_type);
1515        let mut preferred_candidate_rejected = None;
1516
1517        match preferred {
1518            Some(PreferredHwRender {
1519                context_type,
1520                supports_non_preferred_context,
1521            }) => logger.info(format!(
1522                "libretro wrapper: frontend preferred hw render {:?} (non-preferred allowed = {})",
1523                context_type, supports_non_preferred_context
1524            )),
1525            None => logger.info(
1526                "libretro wrapper: frontend did not report a preferred hw render; probing configured candidates",
1527            ),
1528        }
1529
1530        if let Some(preferred) = preferred_context_type
1531            && let Some(config) = candidates
1532                .iter()
1533                .copied()
1534                .find(|config| config.context_type == preferred)
1535        {
1536            logger.info(format!(
1537                "libretro wrapper: attempting preferred hw render candidate {}",
1538                describe_hw_render_config(config)
1539            ));
1540            if self.set_hw_render(config) {
1541                logger.info(format!(
1542                    "libretro wrapper: frontend accepted preferred hw render candidate {}",
1543                    describe_hw_render_config(config)
1544                ));
1545                return Some(config);
1546            }
1547            preferred_candidate_rejected = Some(config);
1548            logger.warn(format!(
1549                "libretro wrapper: frontend rejected preferred hw render candidate {}",
1550                describe_hw_render_config(config)
1551            ));
1552        }
1553
1554        let allow_gles_family_recovery = matches!(
1555            (
1556                preferred,
1557                preferred_candidate_rejected.map(|config| config.context_type),
1558            ),
1559            (
1560                Some(PreferredHwRender {
1561                    context_type: HwContextType::OpenGl,
1562                    supports_non_preferred_context: false,
1563                }),
1564                Some(HwContextType::OpenGl),
1565            )
1566        );
1567
1568        if matches!(
1569            preferred,
1570            Some(PreferredHwRender {
1571                supports_non_preferred_context: false,
1572                ..
1573            })
1574        ) && !allow_gles_family_recovery
1575        {
1576            logger.warn(
1577                "libretro wrapper: frontend rejected the preferred hw render candidate and disallowed non-preferred fallbacks",
1578            );
1579            return None;
1580        }
1581
1582        if allow_gles_family_recovery {
1583            logger.warn(
1584                "libretro wrapper: frontend rejected preferred generic OpenGl; probing OpenGL ES family fallbacks for compatibility with GLES-only frontends",
1585            );
1586
1587            for config in candidates.iter().copied().filter(|config| {
1588                is_opengl_es_family(config.context_type)
1589                    && Some(config.context_type) != preferred_context_type
1590            }) {
1591                logger.info(format!(
1592                    "libretro wrapper: attempting OpenGL ES family recovery candidate {}",
1593                    describe_hw_render_config(config)
1594                ));
1595                if self.set_hw_render(config) {
1596                    logger.info(format!(
1597                        "libretro wrapper: frontend accepted OpenGL ES family recovery candidate {}",
1598                        describe_hw_render_config(config)
1599                    ));
1600                    return Some(config);
1601                }
1602                logger.warn(format!(
1603                    "libretro wrapper: frontend rejected OpenGL ES family recovery candidate {}",
1604                    describe_hw_render_config(config)
1605                ));
1606            }
1607
1608            logger.warn(
1609                "libretro wrapper: frontend rejected preferred generic OpenGl and every OpenGL ES family recovery candidate",
1610            );
1611            return None;
1612        }
1613
1614        for config in candidates.iter().copied() {
1615            if Some(config.context_type) == preferred_context_type {
1616                continue;
1617            }
1618
1619            logger.info(format!(
1620                "libretro wrapper: attempting fallback hw render candidate {}",
1621                describe_hw_render_config(config)
1622            ));
1623            if self.set_hw_render(config) {
1624                logger.info(format!(
1625                    "libretro wrapper: frontend accepted fallback hw render candidate {}",
1626                    describe_hw_render_config(config)
1627                ));
1628                return Some(config);
1629            }
1630            logger.warn(format!(
1631                "libretro wrapper: frontend rejected fallback hw render candidate {}",
1632                describe_hw_render_config(config)
1633            ));
1634        }
1635
1636        logger.warn("libretro wrapper: frontend rejected every configured hw render candidate");
1637        None
1638    }
1639
1640    pub fn get_variable(&mut self, key: &str) -> Option<String> {
1641        let key = sanitize_cstring(key);
1642        let mut variable = RawVariable {
1643            key: key.as_ptr(),
1644            value: ptr::null(),
1645        };
1646
1647        let ok = self.call_env(
1648            RETRO_ENVIRONMENT_GET_VARIABLE,
1649            (&mut variable as *mut RawVariable).cast::<c_void>(),
1650        );
1651        if !ok || variable.value.is_null() {
1652            return None;
1653        }
1654
1655        // SAFETY: Frontend returns a valid NUL-terminated value on success.
1656        let value = unsafe { CStr::from_ptr(variable.value) };
1657        Some(value.to_string_lossy().into_owned())
1658    }
1659
1660    pub fn variables_updated(&mut self) -> bool {
1661        let mut updated = false;
1662        self.call_env(
1663            RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE,
1664            (&mut updated as *mut bool).cast::<c_void>(),
1665        ) && updated
1666    }
1667
1668    pub(crate) fn call_env(&mut self, command: u32, data: *mut c_void) -> bool {
1669        let Some(callback) = self.state.callbacks.environment else {
1670            return false;
1671        };
1672        // SAFETY: `callback` comes from the frontend via the libretro ABI.
1673        unsafe { callback(command, data) }
1674    }
1675}
1676
1677/// Per-frame access to frontend callbacks and services.
1678///
1679/// `Runtime` is passed to `Core::run`, `Core::load_game`, and hardware context
1680/// hooks. It owns typed helpers for input polling, video/audio submission,
1681/// frontend messages, hardware framebuffers, memory maps, and service queries.
1682pub struct Runtime<'a> {
1683    state: &'a mut CoreState,
1684}
1685
1686impl<'a> Runtime<'a> {
1687    pub fn environment(&mut self) -> Environment<'_> {
1688        Environment { state: self.state }
1689    }
1690
1691    pub fn logger(&mut self) -> Logger {
1692        self.environment().logger()
1693    }
1694
1695    pub fn poll_input(&self) {
1696        if let Some(callback) = self.state.callbacks.input_poll {
1697            // SAFETY: `callback` comes from the frontend via the libretro ABI.
1698            unsafe { callback() };
1699        }
1700    }
1701
1702    fn input_state_raw(
1703        &self,
1704        port: InputPort,
1705        device: ControllerDevice,
1706        index: u32,
1707        id: u32,
1708    ) -> i16 {
1709        let Some(callback) = self.state.callbacks.input_state else {
1710            return 0;
1711        };
1712        // SAFETY: `callback` comes from the frontend via the libretro ABI.
1713        unsafe { callback(port.as_raw(), device.as_raw(), index, id) }
1714    }
1715
1716    pub fn joypad_pressed(&self, port: impl Into<InputPort>, button: JoypadButton) -> bool {
1717        self.input_state_raw(port.into(), ControllerDevice::Joypad, 0, button.as_raw()) != 0
1718    }
1719
1720    pub fn joypad_buttons(&self, port: impl Into<InputPort>) -> JoypadButtonSet {
1721        JoypadButtonSet::from_raw_bits(self.input_state_raw(
1722            port.into(),
1723            ControllerDevice::Joypad,
1724            0,
1725            input::joypad_mask_query_id(),
1726        ) as u16)
1727    }
1728
1729    pub fn analog_axis(
1730        &self,
1731        port: impl Into<InputPort>,
1732        stick: AnalogStick,
1733        axis: AnalogAxis,
1734    ) -> i16 {
1735        self.input_state_raw(
1736            port.into(),
1737            ControllerDevice::Analog,
1738            stick.as_raw(),
1739            axis.as_raw(),
1740        )
1741    }
1742
1743    pub fn analog_button(&self, port: impl Into<InputPort>, button: JoypadButton) -> i16 {
1744        self.input_state_raw(
1745            port.into(),
1746            ControllerDevice::Analog,
1747            input::analog_button_index(),
1748            button.as_raw(),
1749        )
1750    }
1751
1752    pub fn mouse_axis(&self, port: impl Into<InputPort>, axis: MouseAxis) -> i16 {
1753        self.input_state_raw(port.into(), ControllerDevice::Mouse, 0, axis.as_raw())
1754    }
1755
1756    pub fn mouse_button_pressed(&self, port: impl Into<InputPort>, button: MouseButton) -> bool {
1757        self.input_state_raw(port.into(), ControllerDevice::Mouse, 0, button.as_raw()) != 0
1758    }
1759
1760    pub fn mouse_wheel_moved(&self, port: impl Into<InputPort>, direction: MouseWheel) -> bool {
1761        self.input_state_raw(port.into(), ControllerDevice::Mouse, 0, direction.as_raw()) != 0
1762    }
1763
1764    pub fn pointer_axis(
1765        &self,
1766        port: impl Into<InputPort>,
1767        index: impl Into<PointerIndex>,
1768        axis: PointerAxis,
1769    ) -> i16 {
1770        self.input_state_raw(
1771            port.into(),
1772            ControllerDevice::Pointer,
1773            index.into().as_raw(),
1774            axis.as_raw(),
1775        )
1776    }
1777
1778    pub fn pointer_pressed(
1779        &self,
1780        port: impl Into<InputPort>,
1781        index: impl Into<PointerIndex>,
1782    ) -> bool {
1783        self.input_state_raw(
1784            port.into(),
1785            ControllerDevice::Pointer,
1786            index.into().as_raw(),
1787            input::pointer_pressed_id(),
1788        ) != 0
1789    }
1790
1791    pub fn pointer_count(&self, port: impl Into<InputPort>) -> i16 {
1792        self.input_state_raw(
1793            port.into(),
1794            ControllerDevice::Pointer,
1795            0,
1796            input::pointer_count_id(),
1797        )
1798    }
1799
1800    pub fn pointer_is_offscreen(
1801        &self,
1802        port: impl Into<InputPort>,
1803        index: impl Into<PointerIndex>,
1804    ) -> bool {
1805        self.input_state_raw(
1806            port.into(),
1807            ControllerDevice::Pointer,
1808            index.into().as_raw(),
1809            input::pointer_is_offscreen_id(),
1810        ) != 0
1811    }
1812
1813    pub fn lightgun_axis(&self, port: impl Into<InputPort>, axis: LightgunAxis) -> i16 {
1814        self.input_state_raw(port.into(), ControllerDevice::Lightgun, 0, axis.as_raw())
1815    }
1816
1817    pub fn lightgun_button_pressed(
1818        &self,
1819        port: impl Into<InputPort>,
1820        button: LightgunButton,
1821    ) -> bool {
1822        self.input_state_raw(port.into(), ControllerDevice::Lightgun, 0, button.as_raw()) != 0
1823    }
1824
1825    pub fn lightgun_is_offscreen(&self, port: impl Into<InputPort>) -> bool {
1826        self.input_state_raw(
1827            port.into(),
1828            ControllerDevice::Lightgun,
1829            0,
1830            input::lightgun_is_offscreen_id(),
1831        ) != 0
1832    }
1833
1834    fn video_refresh_raw(&self, data: *const c_void, width: u32, height: u32, pitch: usize) {
1835        if let Some(callback) = self.state.callbacks.video_refresh {
1836            // SAFETY: `callback` comes from the frontend via the libretro ABI.
1837            unsafe { callback(data, width, height, pitch) };
1838        }
1839    }
1840
1841    pub fn video_refresh_frame<T>(
1842        &self,
1843        pixels: &[T],
1844        width: u32,
1845        height: u32,
1846        pitch: usize,
1847    ) -> bool {
1848        if let Err(error) = validate_software_frame_buffer::<T>(pixels, width, height, pitch) {
1849            self.cached_logger().error(format!(
1850                "libretro wrapper: refusing invalid software video frame: {error}"
1851            ));
1852            return false;
1853        }
1854        self.video_refresh_raw(pixels.as_ptr().cast::<c_void>(), width, height, pitch);
1855        true
1856    }
1857
1858    pub fn video_refresh_frame_with_audio<T>(
1859        &self,
1860        pixels: &[T],
1861        width: u32,
1862        height: u32,
1863        pitch: usize,
1864        audio_frames: &[[i16; 2]],
1865    ) -> usize {
1866        let _ = self.video_refresh_frame(pixels, width, height, pitch);
1867        self.audio_sample_batch(audio_frames)
1868    }
1869
1870    pub fn video_refresh_hw(&self, width: u32, height: u32, pitch: usize) {
1871        self.video_refresh_raw(RETRO_HW_FRAME_BUFFER_VALID, width, height, pitch);
1872    }
1873
1874    pub fn video_refresh_hw_with_audio(
1875        &self,
1876        width: u32,
1877        height: u32,
1878        pitch: usize,
1879        audio_frames: &[[i16; 2]],
1880    ) -> usize {
1881        self.video_refresh_hw(width, height, pitch);
1882        self.audio_sample_batch(audio_frames)
1883    }
1884
1885    pub fn video_refresh_software_framebuffer(&self, framebuffer: SoftwareFramebuffer) {
1886        let (data, width, height, pitch) = framebuffer.video_refresh_args();
1887        self.video_refresh_raw(data, width, height, pitch);
1888    }
1889
1890    pub fn video_refresh_software_framebuffer_with_audio(
1891        &self,
1892        framebuffer: SoftwareFramebuffer,
1893        audio_frames: &[[i16; 2]],
1894    ) -> usize {
1895        self.video_refresh_software_framebuffer(framebuffer);
1896        self.audio_sample_batch(audio_frames)
1897    }
1898
1899    pub fn video_refresh_dupe(&self, width: u32, height: u32) {
1900        self.video_refresh_raw(std::ptr::null(), width, height, 0);
1901    }
1902
1903    pub fn video_refresh_dupe_with_audio(
1904        &self,
1905        width: u32,
1906        height: u32,
1907        audio_frames: &[[i16; 2]],
1908    ) -> usize {
1909        self.video_refresh_dupe(width, height);
1910        self.audio_sample_batch(audio_frames)
1911    }
1912
1913    pub fn set_geometry(&mut self, geometry: GameGeometry) -> bool {
1914        self.environment().set_geometry(geometry)
1915    }
1916
1917    pub fn set_message(&mut self, message: impl AsRef<str>, frames: u32) -> bool {
1918        self.environment().set_message(message, frames)
1919    }
1920
1921    pub fn set_message_ext(&mut self, message: ExtendedMessage) -> bool {
1922        self.environment().set_message_ext(message)
1923    }
1924
1925    pub fn audio_sample(&self, left: i16, right: i16) {
1926        if let Some(callback) = self.state.callbacks.audio_sample {
1927            // SAFETY: `callback` comes from the frontend via the libretro ABI.
1928            unsafe { callback(left, right) };
1929        }
1930    }
1931
1932    pub fn audio_sample_batch(&self, frames: &[[i16; 2]]) -> usize {
1933        let Some(callback) = self.state.callbacks.audio_sample_batch else {
1934            return 0;
1935        };
1936        // SAFETY: `callback` comes from the frontend via the libretro ABI.
1937        unsafe { callback(frames.as_ptr().cast::<i16>(), frames.len()) }
1938    }
1939
1940    pub fn current_framebuffer(&self) -> Option<u32> {
1941        let callback = self.state.hw_render?.get_current_framebuffer?;
1942        // SAFETY: Frontend provided the callback through `SET_HW_RENDER`.
1943        let framebuffer = u32::try_from(unsafe { callback() }).ok()?;
1944        if framebuffer == 0 {
1945            None
1946        } else {
1947            Some(framebuffer)
1948        }
1949    }
1950
1951    pub fn hw_context_type(&self) -> Option<HwContextType> {
1952        Some(self.state.hw_render?.context_type)
1953    }
1954
1955    fn get_proc_address(&self, symbol: &str) -> Result<raw::retro_proc_address_t, String> {
1956        let symbol = sanitize_cstring(symbol);
1957        let hw_render = self
1958            .state
1959            .hw_render
1960            .ok_or_else(|| "hardware render callbacks are not available".to_string())?;
1961        let callback = hw_render
1962            .get_proc_address
1963            .ok_or_else(|| "get_proc_address callback is not available".to_string())?;
1964        // SAFETY: Frontend provided the callback through `SET_HW_RENDER`.
1965        Ok(unsafe { callback(symbol.as_ptr()) })
1966    }
1967
1968    pub fn hw_proc_address(&self, symbol: &str) -> Result<*const c_void, String> {
1969        if let Some(symbol_address) = self.get_proc_address(symbol)? {
1970            return Ok(symbol_address as *const () as *const c_void);
1971        }
1972
1973        if let Some(symbol_address) = fallback_global_proc_address(symbol) {
1974            return Ok(symbol_address);
1975        }
1976
1977        Err(format!(
1978            "missing GL symbol {symbol:?} from frontend proc lookup and process global symbols"
1979        ))
1980    }
1981
1982    fn cached_logger(&self) -> Logger {
1983        Logger {
1984            callback: self.state.log_callback.map(|callback| callback.log),
1985        }
1986    }
1987}
1988
1989fn validate_software_frame_buffer<T>(
1990    pixels: &[T],
1991    width: u32,
1992    height: u32,
1993    pitch: usize,
1994) -> Result<(), String> {
1995    if width == 0 || height == 0 {
1996        return Err(format!(
1997            "software frame dimensions must be non-zero, got {width}x{height}"
1998        ));
1999    }
2000
2001    let pixel_size = mem::size_of::<T>();
2002    if pixel_size == 0 {
2003        return Err("software frame pixel type must not be zero-sized".to_string());
2004    }
2005
2006    let row_bytes = (width as usize)
2007        .checked_mul(pixel_size)
2008        .ok_or_else(|| format!("software frame row byte size overflowed for width {width}"))?;
2009    if pitch < row_bytes {
2010        return Err(format!(
2011            "software frame pitch {pitch} is smaller than row byte size {row_bytes}"
2012        ));
2013    }
2014
2015    let required_bytes = pitch
2016        .checked_mul(height as usize)
2017        .ok_or_else(|| format!("software frame byte size overflowed for height {height}"))?;
2018    let available_bytes = mem::size_of_val(pixels);
2019    if available_bytes < required_bytes {
2020        return Err(format!(
2021            "software frame buffer has {available_bytes} bytes but {required_bytes} bytes are required"
2022        ));
2023    }
2024
2025    Ok(())
2026}
2027
2028#[cfg(unix)]
2029fn fallback_global_proc_address(symbol: &str) -> Option<*const c_void> {
2030    let symbol = sanitize_cstring(symbol);
2031    // Some old EGL stacks only expose core GLES2 entry points through the
2032    // process symbol table even when RetroArch's proc callback delegates to
2033    // eglGetProcAddress. This keeps the frontend-owned context contract while
2034    // avoiding a direct dependency on a platform GL library.
2035    let pointer = unsafe { dlsym(std::ptr::null_mut(), symbol.as_ptr()) };
2036    if pointer.is_null() {
2037        None
2038    } else {
2039        Some(pointer.cast_const())
2040    }
2041}
2042
2043#[cfg(not(unix))]
2044fn fallback_global_proc_address(_symbol: &str) -> Option<*const c_void> {
2045    None
2046}
2047
2048#[cfg(unix)]
2049#[cfg_attr(target_os = "linux", link(name = "dl"))]
2050unsafe extern "C" {
2051    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
2052}
2053
2054#[doc(hidden)]
2055// These are safe Rust wrappers around libretro's C ABI entrypoints. The exported
2056// `extern "C"` functions generated by `export_core!` keep the ABI safe to call
2057// from the frontend while the raw pointer validation stays centralized here.
2058#[allow(clippy::not_unsafe_ptr_arg_deref)]
2059pub mod __private {
2060    use super::*;
2061
2062    pub type RawSystemAvInfo = raw::retro_system_av_info;
2063
2064    pub fn set_factory(factory: CoreFactory) {
2065        if FACTORY.get().is_none() {
2066            let _ = FACTORY.set(factory);
2067        }
2068    }
2069
2070    pub fn retro_set_environment(cb: raw::retro_environment_t) {
2071        with_state(|state| {
2072            state.callbacks.environment = cb;
2073            catch_state_callback(state, "retro_set_environment", (), |state| {
2074                state.with_core(|core, state| {
2075                    {
2076                        let mut env = Environment { state };
2077                        core.on_set_environment(&mut env);
2078                    }
2079                    let has_keyboard_event = state.event_handlers.has_keyboard_event();
2080                    let has_audio_callback = state.event_handlers.has_audio_callback();
2081                    let has_audio_buffer_status = state.event_handlers.has_audio_buffer_status();
2082                    let frame_time_reference = state.event_handlers.frame_time_reference();
2083                    let mut env = Environment { state };
2084                    if has_keyboard_event {
2085                        let _ = env.set_keyboard_callback();
2086                    }
2087                    if has_audio_callback {
2088                        let _ = env.set_audio_callback();
2089                    }
2090                    if has_audio_buffer_status {
2091                        let _ = env.set_audio_buffer_status_callback(true);
2092                    }
2093                    if let Some(reference) = frame_time_reference {
2094                        let _ = env.set_frame_time_callback(reference);
2095                    }
2096                });
2097            });
2098        });
2099    }
2100
2101    pub fn retro_set_video_refresh(cb: raw::retro_video_refresh_t) {
2102        with_state(|state| state.callbacks.video_refresh = cb);
2103    }
2104
2105    pub fn retro_set_audio_sample(cb: raw::retro_audio_sample_t) {
2106        with_state(|state| state.callbacks.audio_sample = cb);
2107    }
2108
2109    pub fn retro_set_audio_sample_batch(cb: raw::retro_audio_sample_batch_t) {
2110        with_state(|state| state.callbacks.audio_sample_batch = cb);
2111    }
2112
2113    pub fn retro_set_input_poll(cb: raw::retro_input_poll_t) {
2114        with_state(|state| state.callbacks.input_poll = cb);
2115    }
2116
2117    pub fn retro_set_input_state(cb: raw::retro_input_state_t) {
2118        with_state(|state| state.callbacks.input_state = cb);
2119    }
2120
2121    pub fn retro_init() {
2122        with_state(|state| {
2123            catch_state_callback(state, "retro_init", (), |state| {
2124                state.with_core(|core, state| {
2125                    let mut env = Environment { state };
2126                    core.init(&mut env);
2127                });
2128            });
2129        });
2130    }
2131
2132    pub fn retro_deinit() {
2133        with_state(|state| {
2134            catch_state_callback(state, "retro_deinit", (), |state| {
2135                if let Some(core) = state.core.as_mut() {
2136                    core.deinit();
2137                }
2138            });
2139            state.reset_frontend_state();
2140            state.core = None;
2141        });
2142    }
2143
2144    pub fn retro_api_version() -> u32 {
2145        RETRO_API_VERSION
2146    }
2147
2148    pub fn retro_get_system_info(info: *mut RawSystemInfo) {
2149        if info.is_null() {
2150            return;
2151        }
2152
2153        // Keep the frontend-facing out-param initialized even if the core callback fails.
2154        unsafe { *info = RawSystemInfo::default() };
2155        with_state(|state| {
2156            catch_state_callback(state, "retro_get_system_info", (), |state| {
2157                if state.system_info.is_none() {
2158                    let system_info = state.with_core(|core, _| core.system_info());
2159                    // libretro requires these pointers to remain valid until
2160                    // retro_deinit(); RetroArch may keep fields from an earlier
2161                    // call while making later retro_get_system_info() calls.
2162                    state.system_info = Some(OwnedSystemInfo::new(system_info));
2163                }
2164                if let Some(storage) = &state.system_info {
2165                    // SAFETY: `info` is provided by the frontend.
2166                    unsafe {
2167                        *info = RawSystemInfo {
2168                            library_name: storage.library_name.as_ptr(),
2169                            library_version: storage.library_version.as_ptr(),
2170                            valid_extensions: storage
2171                                .valid_extensions
2172                                .as_ref()
2173                                .map_or(ptr::null(), |value| value.as_ptr()),
2174                            need_fullpath: storage.need_fullpath,
2175                            block_extract: storage.block_extract,
2176                        };
2177                    }
2178                }
2179            });
2180        });
2181    }
2182
2183    pub fn retro_get_system_av_info(info: *mut raw::retro_system_av_info) {
2184        if info.is_null() {
2185            return;
2186        }
2187
2188        // Keep the frontend-facing out-param initialized even if the core callback fails.
2189        unsafe { *info = raw::retro_system_av_info::default() };
2190        with_state(|state| {
2191            catch_state_callback(state, "retro_get_system_av_info", (), |state| {
2192                let av = state.with_core(|core, _| core.av_info());
2193                // SAFETY: `info` is provided by the frontend.
2194                unsafe { *info = av.as_raw() };
2195            });
2196        });
2197    }
2198
2199    pub fn retro_set_controller_port_device(port: u32, device: u32) {
2200        with_state(|state| {
2201            catch_state_callback(state, "retro_set_controller_port_device", (), |state| {
2202                state.with_core(|core, _| {
2203                    core.set_controller_port_device(
2204                        InputPort::from(port),
2205                        ControllerDevice::from_raw(device),
2206                    );
2207                });
2208            });
2209        });
2210    }
2211
2212    pub fn retro_reset() {
2213        with_state(|state| {
2214            catch_state_callback(state, "retro_reset", (), |state| {
2215                state.with_core(|core, _| core.reset());
2216            });
2217        });
2218    }
2219
2220    pub fn retro_run() {
2221        with_state(|state| {
2222            catch_state_callback(state, "retro_run", (), |state| {
2223                state.with_core(|core, state| {
2224                    let mut runtime = Runtime { state };
2225                    core.run(&mut runtime);
2226                });
2227            });
2228        });
2229    }
2230
2231    pub fn retro_serialize_size() -> usize {
2232        with_state(|state| {
2233            catch_state_callback(state, "retro_serialize_size", 0, |state| {
2234                state.with_core(|core, _| core.serialize_size())
2235            })
2236        })
2237    }
2238
2239    pub fn retro_serialize(data: *mut c_void, len: usize) -> bool {
2240        if data.is_null() {
2241            return false;
2242        }
2243
2244        with_state(|state| {
2245            // SAFETY: Caller provided a writable buffer of `len` bytes.
2246            let buffer = unsafe { std::slice::from_raw_parts_mut(data.cast::<u8>(), len) };
2247            catch_state_callback(state, "retro_serialize", false, |state| {
2248                state.with_core(|core, _| core.serialize(buffer))
2249            })
2250        })
2251    }
2252
2253    pub fn retro_unserialize(data: *const c_void, len: usize) -> bool {
2254        if data.is_null() {
2255            return false;
2256        }
2257
2258        with_state(|state| {
2259            // SAFETY: Caller provided a readable buffer of `len` bytes.
2260            let buffer = unsafe { std::slice::from_raw_parts(data.cast::<u8>(), len) };
2261            catch_state_callback(state, "retro_unserialize", false, |state| {
2262                state.with_core(|core, _| core.unserialize(buffer))
2263            })
2264        })
2265    }
2266
2267    pub fn retro_cheat_reset() {
2268        with_state(|state| {
2269            catch_state_callback(state, "retro_cheat_reset", (), |state| {
2270                state.with_core(|core, _| core.cheat_reset());
2271            });
2272        });
2273    }
2274
2275    pub fn retro_cheat_set(index: u32, enabled: bool, code: *const c_char) {
2276        with_state(|state| {
2277            let code = if code.is_null() {
2278                None
2279            } else {
2280                // SAFETY: The frontend provides a valid C string.
2281                Some(CheatCode::from_c_str(unsafe { CStr::from_ptr(code) }))
2282            };
2283            catch_state_callback(state, "retro_cheat_set", (), |state| {
2284                state.with_core(|core, _| core.cheat_set(CheatIndex::from(index), enabled, code));
2285            });
2286        });
2287    }
2288
2289    pub fn retro_load_game(game: *const RawGameInfo) -> bool {
2290        with_state(|state| {
2291            // SAFETY: `game` follows the libretro ABI for the duration of the call.
2292            let game = unsafe { GameInfo::from_raw(game) };
2293            catch_state_callback(state, "retro_load_game", false, |state| {
2294                state.with_core(|core, state| {
2295                    let mut runtime = Runtime { state };
2296                    core.load_game(game, &mut runtime)
2297                })
2298            })
2299        })
2300    }
2301
2302    pub fn retro_load_game_special(
2303        game_type: u32,
2304        info: *const RawGameInfo,
2305        num_info: usize,
2306    ) -> bool {
2307        with_state(|state| {
2308            let games = if info.is_null() || num_info == 0 {
2309                Vec::new()
2310            } else {
2311                // SAFETY: The frontend provides a valid array for the duration of the call.
2312                let raw = unsafe { std::slice::from_raw_parts(info, num_info) };
2313                raw.iter()
2314                    .filter_map(|entry| unsafe { GameInfo::from_raw(entry) })
2315                    .collect::<Vec<_>>()
2316            };
2317            catch_state_callback(state, "retro_load_game_special", false, |state| {
2318                state.with_core(|core, state| {
2319                    let mut runtime = Runtime { state };
2320                    core.load_game_special(SubsystemId::new(game_type), &games, &mut runtime)
2321                })
2322            })
2323        })
2324    }
2325
2326    pub fn retro_unload_game() {
2327        with_state(|state| {
2328            catch_state_callback(state, "retro_unload_game", (), |state| {
2329                state.with_core(|core, _| core.unload_game());
2330            });
2331        });
2332    }
2333
2334    pub fn retro_get_region() -> u32 {
2335        with_state(|state| {
2336            catch_state_callback(state, "retro_get_region", Region::Ntsc.as_raw(), |state| {
2337                state.with_core(|core, _| core.region().as_raw())
2338            })
2339        })
2340    }
2341
2342    pub fn retro_get_memory_data(id: u32) -> *mut c_void {
2343        with_state(|state| {
2344            catch_state_callback(state, "retro_get_memory_data", ptr::null_mut(), |state| {
2345                state.with_core(|core, _| {
2346                    core.memory_region(MemoryRegion::from_raw(id))
2347                        .map_or(ptr::null_mut(), |mut memory| memory.as_mut_ptr())
2348                })
2349            })
2350        })
2351    }
2352
2353    pub fn retro_get_memory_size(id: u32) -> usize {
2354        with_state(|state| {
2355            catch_state_callback(state, "retro_get_memory_size", 0, |state| {
2356                state.with_core(|core, _| {
2357                    core.memory_region(MemoryRegion::from_raw(id))
2358                        .map_or(0, |memory| memory.len())
2359                })
2360            })
2361        })
2362    }
2363}
2364
2365/// Export a `Core` implementation as the required libretro `retro_*` symbols.
2366///
2367/// The macro keeps ABI exports uniform and routes callbacks through the crate's
2368/// typed panic-catching boundaries.
2369#[macro_export]
2370macro_rules! export_core {
2371    ($factory:expr) => {
2372        #[doc(hidden)]
2373        fn __libretro_create_core() -> $crate::CoreBundle {
2374            $crate::create_core($factory)
2375        }
2376
2377        #[unsafe(no_mangle)]
2378        pub extern "C" fn retro_set_environment(cb: $crate::retro_environment_t) {
2379            $crate::__private::set_factory(__libretro_create_core);
2380            $crate::__private::retro_set_environment(cb);
2381        }
2382
2383        #[unsafe(no_mangle)]
2384        pub extern "C" fn retro_set_video_refresh(cb: $crate::retro_video_refresh_t) {
2385            $crate::__private::set_factory(__libretro_create_core);
2386            $crate::__private::retro_set_video_refresh(cb);
2387        }
2388
2389        #[unsafe(no_mangle)]
2390        pub extern "C" fn retro_set_audio_sample(cb: $crate::retro_audio_sample_t) {
2391            $crate::__private::set_factory(__libretro_create_core);
2392            $crate::__private::retro_set_audio_sample(cb);
2393        }
2394
2395        #[unsafe(no_mangle)]
2396        pub extern "C" fn retro_set_audio_sample_batch(cb: $crate::retro_audio_sample_batch_t) {
2397            $crate::__private::set_factory(__libretro_create_core);
2398            $crate::__private::retro_set_audio_sample_batch(cb);
2399        }
2400
2401        #[unsafe(no_mangle)]
2402        pub extern "C" fn retro_set_input_poll(cb: $crate::retro_input_poll_t) {
2403            $crate::__private::set_factory(__libretro_create_core);
2404            $crate::__private::retro_set_input_poll(cb);
2405        }
2406
2407        #[unsafe(no_mangle)]
2408        pub extern "C" fn retro_set_input_state(cb: $crate::retro_input_state_t) {
2409            $crate::__private::set_factory(__libretro_create_core);
2410            $crate::__private::retro_set_input_state(cb);
2411        }
2412
2413        #[unsafe(no_mangle)]
2414        pub extern "C" fn retro_init() {
2415            $crate::__private::set_factory(__libretro_create_core);
2416            $crate::__private::retro_init();
2417        }
2418
2419        #[unsafe(no_mangle)]
2420        pub extern "C" fn retro_deinit() {
2421            $crate::__private::set_factory(__libretro_create_core);
2422            $crate::__private::retro_deinit();
2423        }
2424
2425        #[unsafe(no_mangle)]
2426        pub extern "C" fn retro_api_version() -> u32 {
2427            $crate::__private::set_factory(__libretro_create_core);
2428            $crate::__private::retro_api_version()
2429        }
2430
2431        #[unsafe(no_mangle)]
2432        pub extern "C" fn retro_get_system_info(info: *mut $crate::RawSystemInfo) {
2433            $crate::__private::set_factory(__libretro_create_core);
2434            $crate::__private::retro_get_system_info(info);
2435        }
2436
2437        #[unsafe(no_mangle)]
2438        pub extern "C" fn retro_get_system_av_info(info: *mut $crate::__private::RawSystemAvInfo) {
2439            $crate::__private::set_factory(__libretro_create_core);
2440            $crate::__private::retro_get_system_av_info(info);
2441        }
2442
2443        #[unsafe(no_mangle)]
2444        pub extern "C" fn retro_set_controller_port_device(port: u32, device: u32) {
2445            $crate::__private::set_factory(__libretro_create_core);
2446            $crate::__private::retro_set_controller_port_device(port, device);
2447        }
2448
2449        #[unsafe(no_mangle)]
2450        pub extern "C" fn retro_reset() {
2451            $crate::__private::set_factory(__libretro_create_core);
2452            $crate::__private::retro_reset();
2453        }
2454
2455        #[unsafe(no_mangle)]
2456        pub extern "C" fn retro_run() {
2457            $crate::__private::set_factory(__libretro_create_core);
2458            $crate::__private::retro_run();
2459        }
2460
2461        #[unsafe(no_mangle)]
2462        pub extern "C" fn retro_serialize_size() -> usize {
2463            $crate::__private::set_factory(__libretro_create_core);
2464            $crate::__private::retro_serialize_size()
2465        }
2466
2467        #[unsafe(no_mangle)]
2468        pub extern "C" fn retro_serialize(data: *mut std::ffi::c_void, len: usize) -> bool {
2469            $crate::__private::set_factory(__libretro_create_core);
2470            $crate::__private::retro_serialize(data, len)
2471        }
2472
2473        #[unsafe(no_mangle)]
2474        pub extern "C" fn retro_unserialize(data: *const std::ffi::c_void, len: usize) -> bool {
2475            $crate::__private::set_factory(__libretro_create_core);
2476            $crate::__private::retro_unserialize(data, len)
2477        }
2478
2479        #[unsafe(no_mangle)]
2480        pub extern "C" fn retro_cheat_reset() {
2481            $crate::__private::set_factory(__libretro_create_core);
2482            $crate::__private::retro_cheat_reset();
2483        }
2484
2485        #[unsafe(no_mangle)]
2486        pub extern "C" fn retro_cheat_set(
2487            index: u32,
2488            enabled: bool,
2489            code: *const std::ffi::c_char,
2490        ) {
2491            $crate::__private::set_factory(__libretro_create_core);
2492            $crate::__private::retro_cheat_set(index, enabled, code);
2493        }
2494
2495        #[unsafe(no_mangle)]
2496        pub extern "C" fn retro_load_game(game: *const $crate::RawGameInfo) -> bool {
2497            $crate::__private::set_factory(__libretro_create_core);
2498            $crate::__private::retro_load_game(game)
2499        }
2500
2501        #[unsafe(no_mangle)]
2502        pub extern "C" fn retro_load_game_special(
2503            game_type: u32,
2504            info: *const $crate::RawGameInfo,
2505            num_info: usize,
2506        ) -> bool {
2507            $crate::__private::set_factory(__libretro_create_core);
2508            $crate::__private::retro_load_game_special(game_type, info, num_info)
2509        }
2510
2511        #[unsafe(no_mangle)]
2512        pub extern "C" fn retro_unload_game() {
2513            $crate::__private::set_factory(__libretro_create_core);
2514            $crate::__private::retro_unload_game();
2515        }
2516
2517        #[unsafe(no_mangle)]
2518        pub extern "C" fn retro_get_region() -> u32 {
2519            $crate::__private::set_factory(__libretro_create_core);
2520            $crate::__private::retro_get_region()
2521        }
2522
2523        #[unsafe(no_mangle)]
2524        pub extern "C" fn retro_get_memory_data(id: u32) -> *mut std::ffi::c_void {
2525            $crate::__private::set_factory(__libretro_create_core);
2526            $crate::__private::retro_get_memory_data(id)
2527        }
2528
2529        #[unsafe(no_mangle)]
2530        pub extern "C" fn retro_get_memory_size(id: u32) -> usize {
2531            $crate::__private::set_factory(__libretro_create_core);
2532            $crate::__private::retro_get_memory_size(id)
2533        }
2534    };
2535}
2536
2537#[derive(Default)]
2538struct CoreCallbacks {
2539    environment: raw::retro_environment_t,
2540    video_refresh: raw::retro_video_refresh_t,
2541    audio_sample: raw::retro_audio_sample_t,
2542    audio_sample_batch: raw::retro_audio_sample_batch_t,
2543    input_poll: raw::retro_input_poll_t,
2544    input_state: raw::retro_input_state_t,
2545}
2546
2547struct ContentInfoOverrideStorage {
2548    _extensions: Vec<CString>,
2549    _raw: Vec<RawContentInfoOverride>,
2550}
2551
2552pub(crate) struct InputDescriptorStorage {
2553    pub(crate) _descriptions: Vec<CString>,
2554    pub(crate) _raw: Vec<RawInputDescriptor>,
2555}
2556
2557struct OwnedSystemInfo {
2558    library_name: CString,
2559    library_version: CString,
2560    valid_extensions: Option<CString>,
2561    need_fullpath: bool,
2562    block_extract: bool,
2563}
2564
2565impl OwnedSystemInfo {
2566    fn new(info: SystemInfo) -> Self {
2567        let library_name = sanitize_cstring(info.library_name);
2568        let library_version = sanitize_cstring(info.library_version);
2569        let valid_extensions = info.valid_extensions.map(sanitize_cstring);
2570
2571        Self {
2572            library_name,
2573            library_version,
2574            valid_extensions,
2575            need_fullpath: info.need_fullpath,
2576            block_extract: info.block_extract,
2577        }
2578    }
2579}
2580
2581#[derive(Default)]
2582struct CoreState {
2583    core: Option<Box<dyn Core>>,
2584    event_handlers: CoreEventHandlers,
2585    callbacks: CoreCallbacks,
2586    system_info: Option<OwnedSystemInfo>,
2587    variables: Option<options::CoreOptionsStorage>,
2588    content_info_overrides: Option<ContentInfoOverrideStorage>,
2589    input_descriptors: Option<InputDescriptorStorage>,
2590    subsystem_info: Option<subsystem::SubsystemInfoStorage>,
2591    netpacket_interface: Option<netplay::NetpacketInterfaceStorage>,
2592    log_callback: Option<RawLogCallback>,
2593    hw_render: Option<RawHwRenderCallback>,
2594    hw_render_context_negotiation: Option<raw::retro_hw_render_context_negotiation_interface>,
2595}
2596
2597impl CoreState {
2598    fn with_core<T>(&mut self, f: impl FnOnce(&mut dyn Core, &mut CoreState) -> T) -> T {
2599        let core = self.core.take().unwrap_or_else(|| {
2600            let factory = *FACTORY
2601                .get()
2602                .expect("libretro core factory was not registered");
2603            let bundle = factory();
2604            self.event_handlers = bundle.event_handlers;
2605            bundle.core
2606        });
2607        let mut restore_guard = CoreRestoreGuard::new(self, core);
2608        let result = f(restore_guard.core_mut(), self);
2609        self.core = Some(restore_guard.into_core());
2610        result
2611    }
2612
2613    fn reset_frontend_state(&mut self) {
2614        self.callbacks = CoreCallbacks::default();
2615        self.system_info = None;
2616        self.variables = None;
2617        self.content_info_overrides = None;
2618        self.input_descriptors = None;
2619        self.subsystem_info = None;
2620        self.netpacket_interface = None;
2621        self.log_callback = None;
2622        self.hw_render = None;
2623        self.hw_render_context_negotiation = None;
2624    }
2625}
2626
2627unsafe impl Send for CoreState {}
2628
2629struct CoreRestoreGuard {
2630    state: *mut CoreState,
2631    core: Option<Box<dyn Core>>,
2632    armed: bool,
2633}
2634
2635impl CoreRestoreGuard {
2636    fn new(state: &mut CoreState, core: Box<dyn Core>) -> Self {
2637        Self {
2638            state,
2639            core: Some(core),
2640            armed: true,
2641        }
2642    }
2643
2644    fn core_mut(&mut self) -> &mut dyn Core {
2645        self.core
2646            .as_deref_mut()
2647            .expect("libretro core restore guard always owns a core")
2648    }
2649
2650    fn into_core(mut self) -> Box<dyn Core> {
2651        self.armed = false;
2652        self.core
2653            .take()
2654            .expect("libretro core restore guard always owns a core")
2655    }
2656}
2657
2658impl Drop for CoreRestoreGuard {
2659    fn drop(&mut self) {
2660        if !self.armed {
2661            return;
2662        }
2663
2664        if let Some(core) = self.core.take() {
2665            // SAFETY: The guard is created from the active `CoreState` stack
2666            // borrow and is dropped before that state can go out of scope.
2667            let state = unsafe { &mut *self.state };
2668            if state.core.is_none() {
2669                state.core = Some(core);
2670            }
2671        }
2672    }
2673}
2674
2675pub(crate) fn sanitize_cstring(value: impl AsRef<str>) -> CString {
2676    let bytes = value.as_ref().as_bytes();
2677    if bytes.contains(&0) {
2678        let bytes = bytes
2679            .iter()
2680            .copied()
2681            .filter(|byte| *byte != 0)
2682            .collect::<Vec<_>>();
2683        // SAFETY: The filter above removes every interior NUL byte.
2684        unsafe { CString::from_vec_unchecked(bytes) }
2685    } else {
2686        // SAFETY: The contains check proves this byte vector has no interior NUL.
2687        unsafe { CString::from_vec_unchecked(bytes.to_vec()) }
2688    }
2689}
2690
2691fn with_state<T>(f: impl FnOnce(&mut CoreState) -> T) -> T {
2692    let state = STATE.get_or_init(|| Mutex::new(CoreState::default()));
2693    let mut guard = state.lock().unwrap_or_else(|poisoned| {
2694        eprintln!("libretro wrapper: recovering from poisoned state mutex after callback panic");
2695        poisoned.into_inner()
2696    });
2697    f(&mut guard)
2698}
2699
2700fn panic_payload_message(payload: &(dyn Any + Send)) -> String {
2701    if let Some(message) = payload.downcast_ref::<&'static str>() {
2702        (*message).to_string()
2703    } else if let Some(message) = payload.downcast_ref::<String>() {
2704        message.clone()
2705    } else {
2706        "non-string panic payload".to_string()
2707    }
2708}
2709
2710fn log_callback_panic(state: &CoreState, callback_name: &str, payload: Box<dyn Any + Send>) {
2711    Logger {
2712        callback: state.log_callback.map(|callback| callback.log),
2713    }
2714    .error(format!(
2715        "libretro wrapper: panic escaped core {callback_name} callback: {}",
2716        panic_payload_message(&*payload)
2717    ));
2718}
2719
2720fn catch_state_callback<T>(
2721    state: &mut CoreState,
2722    callback_name: &'static str,
2723    fallback: T,
2724    f: impl FnOnce(&mut CoreState) -> T,
2725) -> T {
2726    match catch_unwind(AssertUnwindSafe(|| f(state))) {
2727        Ok(value) => value,
2728        Err(payload) => {
2729            log_callback_panic(state, callback_name, payload);
2730            fallback
2731        }
2732    }
2733}
2734
2735unsafe extern "C" fn hw_context_reset_trampoline() {
2736    with_state(|state| {
2737        catch_state_callback(state, "hw_context_reset", (), |state| {
2738            state.with_core(|core, state| {
2739                let mut runtime = Runtime { state };
2740                core.hw_context_reset(&mut runtime);
2741            });
2742        });
2743    });
2744}
2745
2746unsafe extern "C" fn hw_context_destroy_trampoline() {
2747    with_state(|state| {
2748        catch_state_callback(state, "hw_context_destroy", (), |state| {
2749            state.with_core(|core, state| {
2750                let mut runtime = Runtime { state };
2751                core.hw_context_destroy(&mut runtime);
2752            });
2753        });
2754    });
2755}
2756
2757unsafe extern "C" fn core_options_update_display_trampoline() -> bool {
2758    with_state(|state| {
2759        catch_state_callback(state, "core_options_update_display", false, |state| {
2760            state.with_core(|core, state| {
2761                let mut env = Environment { state };
2762                core.core_options_update_display(&mut env)
2763            })
2764        })
2765    })
2766}
2767
2768pub(crate) unsafe extern "C" fn audio_buffer_status_trampoline(
2769    active: bool,
2770    occupancy: u32,
2771    underrun_likely: bool,
2772) {
2773    with_state(|state| {
2774        catch_state_callback(state, "audio_buffer_status", (), |state| {
2775            let status = AudioBufferStatus::from_raw(active, occupancy, underrun_likely);
2776            state.with_core(|core, state| {
2777                state
2778                    .event_handlers
2779                    .dispatch_audio_buffer_status(core, status);
2780            });
2781        });
2782    });
2783}
2784
2785pub(crate) unsafe extern "C" fn audio_callback_trampoline() {
2786    with_state(|state| {
2787        catch_state_callback(state, "audio_callback", (), |state| {
2788            state.with_core(|core, state| {
2789                state.event_handlers.dispatch_audio_callback(core);
2790            });
2791        });
2792    });
2793}
2794
2795pub(crate) unsafe extern "C" fn audio_set_state_trampoline(enabled: bool) {
2796    with_state(|state| {
2797        catch_state_callback(state, "audio_callback_state_changed", (), |state| {
2798            let enabled = AudioCallbackState::from_active(enabled);
2799            state.with_core(|core, state| {
2800                state
2801                    .event_handlers
2802                    .dispatch_audio_callback_state_changed(core, enabled);
2803            });
2804        });
2805    });
2806}
2807
2808pub(crate) unsafe extern "C" fn frame_time_trampoline(usec: raw::retro_usec_t) {
2809    with_state(|state| {
2810        catch_state_callback(state, "frame_time", (), |state| {
2811            let time = FrameTime::from_micros(usec);
2812            state.with_core(|core, state| {
2813                state.event_handlers.dispatch_frame_time(core, time);
2814            });
2815        });
2816    });
2817}
2818
2819pub(crate) unsafe extern "C" fn keyboard_event_trampoline(
2820    down: bool,
2821    keycode: u32,
2822    character: u32,
2823    key_modifiers: u16,
2824) {
2825    with_state(|state| {
2826        catch_state_callback(state, "keyboard_event", (), |state| {
2827            let event = KeyboardEvent::from_raw(down, keycode, character, key_modifiers);
2828            state.with_core(|core, state| {
2829                state.event_handlers.dispatch_keyboard_event(core, event);
2830            });
2831        });
2832    });
2833}
2834
2835pub(crate) unsafe extern "C" fn proc_address_trampoline(
2836    symbol: *const c_char,
2837) -> raw::retro_proc_address_t {
2838    if symbol.is_null() {
2839        return None;
2840    }
2841
2842    with_state(|state| {
2843        catch_state_callback(state, "proc_address", None, |state| {
2844            // SAFETY: The frontend provides a non-null NUL-terminated symbol
2845            // name for the immediate duration of this callback.
2846            let symbol = unsafe { CStr::from_ptr(symbol) };
2847            state.with_core(|core, _| core.proc_address(symbol).and_then(CoreProcAddress::as_raw))
2848        })
2849    })
2850}
2851
2852pub(crate) unsafe extern "C" fn location_initialized_trampoline() {
2853    with_state(|state| {
2854        catch_state_callback(state, "location_initialized", (), |state| {
2855            state.with_core(|core, state| {
2856                state.event_handlers.dispatch_location_initialized(core);
2857            });
2858        });
2859    });
2860}
2861
2862pub(crate) unsafe extern "C" fn location_deinitialized_trampoline() {
2863    with_state(|state| {
2864        catch_state_callback(state, "location_deinitialized", (), |state| {
2865            state.with_core(|core, state| {
2866                state.event_handlers.dispatch_location_deinitialized(core);
2867            });
2868        });
2869    });
2870}
2871
2872pub(crate) unsafe extern "C" fn camera_initialized_trampoline() {
2873    with_state(|state| {
2874        catch_state_callback(state, "camera_initialized", (), |state| {
2875            state.with_core(|core, state| {
2876                state.event_handlers.dispatch_camera_initialized(core);
2877            });
2878        });
2879    });
2880}
2881
2882pub(crate) unsafe extern "C" fn camera_deinitialized_trampoline() {
2883    with_state(|state| {
2884        catch_state_callback(state, "camera_deinitialized", (), |state| {
2885            state.with_core(|core, state| {
2886                state.event_handlers.dispatch_camera_deinitialized(core);
2887            });
2888        });
2889    });
2890}
2891
2892pub(crate) unsafe extern "C" fn camera_frame_raw_trampoline(
2893    buffer: *const u32,
2894    width: u32,
2895    height: u32,
2896    pitch: usize,
2897) {
2898    let Some(frame) = (unsafe { CameraRawFrame::from_raw(buffer, width, height, pitch) }) else {
2899        return;
2900    };
2901    with_state(|state| {
2902        catch_state_callback(state, "camera_frame_raw", (), |state| {
2903            state.with_core(|core, state| {
2904                state.event_handlers.dispatch_camera_raw_frame(core, frame);
2905            });
2906        });
2907    });
2908}
2909
2910pub(crate) unsafe extern "C" fn camera_frame_opengl_texture_trampoline(
2911    texture_id: u32,
2912    texture_target: u32,
2913    affine: *const f32,
2914) {
2915    if affine.is_null() {
2916        return;
2917    }
2918    let mut transform = [0.0f32; 9];
2919    transform.copy_from_slice(unsafe { std::slice::from_raw_parts(affine, 9) });
2920    let frame = CameraTextureFrame {
2921        texture_id: CameraTextureId::new(texture_id),
2922        texture_target: CameraTextureTarget::new(texture_target),
2923        affine: transform,
2924    };
2925    with_state(|state| {
2926        catch_state_callback(state, "camera_frame_texture", (), |state| {
2927            state.with_core(|core, state| {
2928                state
2929                    .event_handlers
2930                    .dispatch_camera_texture_frame(core, frame);
2931            });
2932        });
2933    });
2934}
2935
2936pub(crate) unsafe extern "C" fn disk_set_eject_state_trampoline(ejected: bool) -> bool {
2937    with_state(|state| {
2938        catch_state_callback(state, "disk_set_tray_state", false, |state| {
2939            state
2940                .with_core(|core, _| core.disk_set_tray_state(DiskTrayState::from_ejected(ejected)))
2941        })
2942    })
2943}
2944
2945pub(crate) unsafe extern "C" fn disk_get_eject_state_trampoline() -> bool {
2946    with_state(|state| {
2947        catch_state_callback(state, "disk_tray_state", false, |state| {
2948            state.with_core(|core, _| core.disk_tray_state().is_ejected())
2949        })
2950    })
2951}
2952
2953pub(crate) unsafe extern "C" fn disk_get_image_index_trampoline() -> u32 {
2954    with_state(|state| {
2955        catch_state_callback(state, "disk_image_index", 0, |state| {
2956            state.with_core(|core, _| core.disk_image_index().as_raw())
2957        })
2958    })
2959}
2960
2961pub(crate) unsafe extern "C" fn disk_set_image_index_trampoline(index: u32) -> bool {
2962    with_state(|state| {
2963        catch_state_callback(state, "disk_set_image_index", false, |state| {
2964            state.with_core(|core, _| core.disk_set_image_index(DiskIndex::new(index)))
2965        })
2966    })
2967}
2968
2969pub(crate) unsafe extern "C" fn disk_get_num_images_trampoline() -> u32 {
2970    with_state(|state| {
2971        catch_state_callback(state, "disk_image_count", 0, |state| {
2972            state.with_core(|core, _| core.disk_image_count())
2973        })
2974    })
2975}
2976
2977pub(crate) unsafe extern "C" fn disk_replace_image_index_trampoline(
2978    index: u32,
2979    info: *const RawGameInfo,
2980) -> bool {
2981    with_state(|state| {
2982        catch_state_callback(state, "disk_replace_image_index", false, |state| {
2983            let game = unsafe { GameInfo::from_raw(info) };
2984            state.with_core(|core, _| core.disk_replace_image_index(DiskIndex::new(index), game))
2985        })
2986    })
2987}
2988
2989pub(crate) unsafe extern "C" fn disk_add_image_index_trampoline() -> bool {
2990    with_state(|state| {
2991        catch_state_callback(state, "disk_add_image_index", false, |state| {
2992            state.with_core(|core, _| core.disk_add_image_index())
2993        })
2994    })
2995}
2996
2997pub(crate) unsafe extern "C" fn disk_set_initial_image_trampoline(
2998    index: u32,
2999    path: *const c_char,
3000) -> bool {
3001    if path.is_null() {
3002        return false;
3003    }
3004
3005    with_state(|state| {
3006        catch_state_callback(state, "disk_set_initial_image", false, |state| {
3007            let path = unsafe { CStr::from_ptr(path) };
3008            state.with_core(|core, _| core.disk_set_initial_image(DiskIndex::new(index), path))
3009        })
3010    })
3011}
3012
3013pub(crate) unsafe extern "C" fn disk_get_image_path_trampoline(
3014    index: u32,
3015    out: *mut c_char,
3016    len: usize,
3017) -> bool {
3018    with_state(|state| {
3019        catch_state_callback(state, "disk_image_path", false, |state| {
3020            let value = state.with_core(|core, _| core.disk_image_path(DiskIndex::new(index)));
3021            disk::write_frontend_string(value, out, len)
3022        })
3023    })
3024}
3025
3026pub(crate) unsafe extern "C" fn disk_get_image_label_trampoline(
3027    index: u32,
3028    out: *mut c_char,
3029    len: usize,
3030) -> bool {
3031    with_state(|state| {
3032        catch_state_callback(state, "disk_image_label", false, |state| {
3033            let value = state.with_core(|core, _| core.disk_image_label(DiskIndex::new(index)));
3034            disk::write_frontend_string(value, out, len)
3035        })
3036    })
3037}
3038
3039pub(crate) unsafe extern "C" fn netpacket_start_trampoline(
3040    client_id: u16,
3041    send_fn: raw::retro_netpacket_send_t,
3042    poll_receive_fn: raw::retro_netpacket_poll_receive_t,
3043) {
3044    let Some(session) =
3045        NetpacketSession::new(NetplayClientId::new(client_id), send_fn, poll_receive_fn)
3046    else {
3047        return;
3048    };
3049
3050    with_state(|state| {
3051        catch_state_callback(state, "netpacket_start", (), |state| {
3052            state.with_core(|core, _| core.netpacket_start(session));
3053        });
3054    });
3055}
3056
3057pub(crate) unsafe extern "C" fn netpacket_receive_trampoline(
3058    buf: *const c_void,
3059    len: usize,
3060    client_id: u16,
3061) {
3062    if buf.is_null() && len != 0 {
3063        return;
3064    }
3065
3066    with_state(|state| {
3067        catch_state_callback(state, "netpacket_receive", (), |state| {
3068            let data = if len == 0 {
3069                &[]
3070            } else {
3071                unsafe { std::slice::from_raw_parts(buf.cast::<u8>(), len) }
3072            };
3073            state.with_core(|core, _| {
3074                core.netpacket_receive(Netpacket {
3075                    client_id: NetplayClientId::new(client_id),
3076                    data,
3077                });
3078            });
3079        });
3080    });
3081}
3082
3083pub(crate) unsafe extern "C" fn netpacket_stop_trampoline() {
3084    with_state(|state| {
3085        catch_state_callback(state, "netpacket_stop", (), |state| {
3086            state.with_core(|core, _| core.netpacket_stop());
3087        });
3088    });
3089}
3090
3091pub(crate) unsafe extern "C" fn netpacket_poll_trampoline() {
3092    with_state(|state| {
3093        catch_state_callback(state, "netpacket_poll", (), |state| {
3094            state.with_core(|core, _| core.netpacket_poll());
3095        });
3096    });
3097}
3098
3099pub(crate) unsafe extern "C" fn netpacket_connected_trampoline(client_id: u16) -> bool {
3100    with_state(|state| {
3101        catch_state_callback(state, "netpacket_connected", false, |state| {
3102            state.with_core(|core, _| core.netpacket_connected(NetplayClientId::new(client_id)))
3103        })
3104    })
3105}
3106
3107pub(crate) unsafe extern "C" fn netpacket_disconnected_trampoline(client_id: u16) {
3108    with_state(|state| {
3109        catch_state_callback(state, "netpacket_disconnected", (), |state| {
3110            state.with_core(|core, _| {
3111                core.netpacket_disconnected(NetplayClientId::new(client_id));
3112            });
3113        });
3114    });
3115}
3116
3117#[cfg(test)]
3118mod tests {
3119    use super::*;
3120    use std::sync::Arc;
3121    use std::sync::MutexGuard;
3122    use std::sync::atomic::{AtomicUsize, Ordering};
3123
3124    #[derive(Clone, Debug, PartialEq, Eq)]
3125    struct CapturedContentOverride {
3126        extensions: String,
3127        need_fullpath: bool,
3128        persistent_data: bool,
3129    }
3130
3131    #[derive(Clone, Debug, PartialEq, Eq)]
3132    struct CapturedMessage {
3133        message: String,
3134        frames: u32,
3135    }
3136
3137    #[derive(Clone, Debug, PartialEq, Eq)]
3138    struct CapturedExtendedMessage {
3139        message: String,
3140        duration: u32,
3141        priority: u32,
3142        level: LogLevel,
3143        target: raw::retro_message_target,
3144        kind: raw::retro_message_type,
3145        progress: i8,
3146    }
3147
3148    #[derive(Clone, Debug, PartialEq, Eq)]
3149    struct CapturedVideoRefresh {
3150        data_kind: CapturedVideoDataKind,
3151        data_addr: usize,
3152        width: u32,
3153        height: u32,
3154        pitch: usize,
3155    }
3156
3157    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3158    struct CapturedInputQuery {
3159        port: u32,
3160        device: u32,
3161        index: u32,
3162        id: u32,
3163    }
3164
3165    #[derive(Clone, Debug, PartialEq, Eq)]
3166    struct CapturedInputDescriptor {
3167        port: u32,
3168        device: u32,
3169        index: u32,
3170        id: u32,
3171        description: String,
3172    }
3173
3174    #[derive(Clone, Debug, PartialEq, Eq)]
3175    struct CapturedControllerDescription {
3176        description: String,
3177        id: u32,
3178    }
3179
3180    #[derive(Clone, Debug, PartialEq, Eq)]
3181    struct CapturedCoreOptionValue {
3182        value: String,
3183        label: Option<String>,
3184    }
3185
3186    #[derive(Clone, Debug, PartialEq, Eq)]
3187    struct CapturedCoreOptionDefinition {
3188        key: String,
3189        description: String,
3190        description_categorized: Option<String>,
3191        info: Option<String>,
3192        info_categorized: Option<String>,
3193        category_key: Option<String>,
3194        values: Vec<CapturedCoreOptionValue>,
3195        default_value: String,
3196    }
3197
3198    #[derive(Clone, Debug, PartialEq, Eq)]
3199    struct CapturedCoreOptionCategory {
3200        key: String,
3201        description: String,
3202        info: Option<String>,
3203    }
3204
3205    #[derive(Clone, Debug, Default, PartialEq, Eq)]
3206    struct CapturedCoreOptionsV2 {
3207        categories: Vec<CapturedCoreOptionCategory>,
3208        definitions: Vec<CapturedCoreOptionDefinition>,
3209    }
3210
3211    #[derive(Clone, Debug, PartialEq, Eq)]
3212    struct CapturedCoreOptionDisplay {
3213        key: String,
3214        visible: bool,
3215    }
3216
3217    #[derive(Clone, Debug, PartialEq, Eq)]
3218    struct CapturedVariable {
3219        key: String,
3220        value: Option<String>,
3221    }
3222
3223    #[derive(Clone, Debug, PartialEq, Eq)]
3224    struct CapturedVfsOpen {
3225        path: String,
3226        mode: u32,
3227        hints: u32,
3228    }
3229
3230    #[derive(Clone, Debug, PartialEq, Eq)]
3231    struct CapturedVfsRename {
3232        old_path: String,
3233        new_path: String,
3234    }
3235
3236    #[derive(Clone, Debug, PartialEq, Eq)]
3237    struct CapturedMemoryDescriptor {
3238        flags: u64,
3239        ptr_is_null: bool,
3240        offset: usize,
3241        start: usize,
3242        select: usize,
3243        disconnect: usize,
3244        len: usize,
3245        addrspace: Option<String>,
3246    }
3247
3248    #[derive(Clone, Debug, PartialEq, Eq)]
3249    struct CapturedSubsystemMemory {
3250        extension: String,
3251        memory_type: u32,
3252    }
3253
3254    #[derive(Clone, Debug, PartialEq, Eq)]
3255    struct CapturedSubsystemRom {
3256        description: String,
3257        valid_extensions: String,
3258        need_fullpath: bool,
3259        block_extract: bool,
3260        required: bool,
3261        memory: Vec<CapturedSubsystemMemory>,
3262    }
3263
3264    #[derive(Clone, Debug, PartialEq, Eq)]
3265    struct CapturedSubsystem {
3266        description: String,
3267        identifier: String,
3268        id: u32,
3269        roms: Vec<CapturedSubsystemRom>,
3270    }
3271
3272    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3273    enum CapturedVideoDataKind {
3274        Software,
3275        Hardware,
3276        Dupe,
3277    }
3278
3279    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3280    enum LocationLifecycleEvent {
3281        Initialized,
3282        Deinitialized,
3283    }
3284
3285    #[derive(Clone, Debug, PartialEq)]
3286    enum CameraEvent {
3287        Initialized,
3288        Deinitialized,
3289        Raw {
3290            width: u32,
3291            height: u32,
3292            pitch: usize,
3293            pixels: Vec<u32>,
3294        },
3295        Texture {
3296            texture_id: u32,
3297            texture_target: u32,
3298            affine: [f32; 9],
3299        },
3300    }
3301
3302    #[derive(Clone, Debug, PartialEq, Eq)]
3303    enum DiskControlEvent {
3304        SetTray(DiskTrayState),
3305        SetImage(DiskIndex),
3306        ReplaceImage(DiskIndex, bool),
3307        AddImage,
3308        SetInitialImage(DiskIndex, String),
3309    }
3310
3311    #[derive(Clone, Debug, PartialEq, Eq)]
3312    enum NetpacketEvent {
3313        Start(NetplayClientId, bool),
3314        Receive(NetplayClientId, Vec<u8>),
3315        Stop,
3316        Poll,
3317        Connected(NetplayClientId),
3318        Disconnected(NetplayClientId),
3319    }
3320
3321    #[derive(Clone, Debug, PartialEq, Eq)]
3322    struct CapturedNetpacketSend {
3323        flags: i32,
3324        data: Vec<u8>,
3325        client_id: u16,
3326    }
3327
3328    #[derive(Clone, Copy, Debug)]
3329    struct CapturedNetpacketCallback {
3330        start: raw::retro_netpacket_start_t,
3331        receive: raw::retro_netpacket_receive_t,
3332        stop: raw::retro_netpacket_stop_t,
3333        poll: raw::retro_netpacket_poll_t,
3334        connected: raw::retro_netpacket_connected_t,
3335        disconnected: raw::retro_netpacket_disconnected_t,
3336        protocol_version: usize,
3337    }
3338
3339    impl CapturedNetpacketCallback {
3340        fn from_raw(callback: raw::retro_netpacket_callback) -> Self {
3341            Self {
3342                start: callback.start,
3343                receive: callback.receive,
3344                stop: callback.stop,
3345                poll: callback.poll,
3346                connected: callback.connected,
3347                disconnected: callback.disconnected,
3348                protocol_version: callback.protocol_version as usize,
3349            }
3350        }
3351    }
3352
3353    static CAPTURED_CONTENT_OVERRIDES: OnceLock<Mutex<Vec<CapturedContentOverride>>> =
3354        OnceLock::new();
3355    static CAPTURED_SUPPORT_NO_GAME: OnceLock<Mutex<Vec<bool>>> = OnceLock::new();
3356    static CAPTURED_MESSAGES: OnceLock<Mutex<Vec<CapturedMessage>>> = OnceLock::new();
3357    static CAPTURED_EXTENDED_MESSAGES: OnceLock<Mutex<Vec<CapturedExtendedMessage>>> =
3358        OnceLock::new();
3359    static CAPTURED_VIDEO_REFRESHES: OnceLock<Mutex<Vec<CapturedVideoRefresh>>> = OnceLock::new();
3360    static CAPTURED_INPUT_QUERIES: OnceLock<Mutex<Vec<CapturedInputQuery>>> = OnceLock::new();
3361    static CAPTURED_INPUT_DESCRIPTORS: OnceLock<Mutex<Vec<CapturedInputDescriptor>>> =
3362        OnceLock::new();
3363    static CAPTURED_CONTROLLER_INFO: OnceLock<Mutex<Vec<Vec<CapturedControllerDescription>>>> =
3364        OnceLock::new();
3365    static CAPTURED_CORE_OPTIONS_VERSION: OnceLock<Mutex<Option<u32>>> = OnceLock::new();
3366    static CAPTURED_CORE_OPTIONS_V2: OnceLock<Mutex<Option<CapturedCoreOptionsV2>>> =
3367        OnceLock::new();
3368    static CAPTURED_CORE_OPTIONS_V1: OnceLock<Mutex<Vec<CapturedCoreOptionDefinition>>> =
3369        OnceLock::new();
3370    static CAPTURED_CORE_OPTION_DISPLAYS: OnceLock<Mutex<Vec<CapturedCoreOptionDisplay>>> =
3371        OnceLock::new();
3372    static CAPTURED_VARIABLES: OnceLock<Mutex<Vec<CapturedVariable>>> = OnceLock::new();
3373    static CAPTURED_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK: OnceLock<
3374        Mutex<Option<raw::retro_core_options_update_display_callback>>,
3375    > = OnceLock::new();
3376    static CAPTURED_VFS_INTERFACE_REQUESTS: OnceLock<Mutex<Vec<u32>>> = OnceLock::new();
3377    static CAPTURED_VFS_OPENS: OnceLock<Mutex<Vec<CapturedVfsOpen>>> = OnceLock::new();
3378    static CAPTURED_VFS_CLOSES: OnceLock<Mutex<u32>> = OnceLock::new();
3379    static CAPTURED_VFS_DIR_CLOSES: OnceLock<Mutex<u32>> = OnceLock::new();
3380    static CAPTURED_VFS_WRITES: OnceLock<Mutex<Vec<Vec<u8>>>> = OnceLock::new();
3381    static CAPTURED_VFS_REMOVES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
3382    static CAPTURED_VFS_RENAMES: OnceLock<Mutex<Vec<CapturedVfsRename>>> = OnceLock::new();
3383    static CAPTURED_VFS_MKDIRS: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
3384    static CAPTURED_VFS_READDIRS: OnceLock<Mutex<u32>> = OnceLock::new();
3385    static CAPTURED_MEMORY_DESCRIPTORS: OnceLock<Mutex<Vec<CapturedMemoryDescriptor>>> =
3386        OnceLock::new();
3387    static CAPTURED_SUBSYSTEM_INFO: OnceLock<Mutex<Vec<CapturedSubsystem>>> = OnceLock::new();
3388    static SOFTWARE_FRAMEBUFFER_PIXELS: OnceLock<Mutex<Vec<u32>>> = OnceLock::new();
3389    static CAPTURED_LED_STATES: OnceLock<Mutex<Vec<(i32, i32)>>> = OnceLock::new();
3390    static CAPTURED_RUMBLE_STATES: OnceLock<Mutex<Vec<(u32, raw::retro_rumble_effect, u16)>>> =
3391        OnceLock::new();
3392    static CAPTURED_SENSOR_STATES: OnceLock<Mutex<Vec<(u32, raw::retro_sensor_action, u32)>>> =
3393        OnceLock::new();
3394    static CAPTURED_LOCATION_INTERVALS: OnceLock<Mutex<Vec<(u32, u32)>>> = OnceLock::new();
3395    static CAPTURED_LOCATION_STARTS: OnceLock<Mutex<u32>> = OnceLock::new();
3396    static CAPTURED_LOCATION_STOPS: OnceLock<Mutex<u32>> = OnceLock::new();
3397    static CAPTURED_LOCATION_CALLBACK: OnceLock<Mutex<Option<raw::retro_location_callback>>> =
3398        OnceLock::new();
3399    static CAPTURED_CAMERA_CALLBACK: OnceLock<Mutex<Option<raw::retro_camera_callback>>> =
3400        OnceLock::new();
3401    static CAPTURED_CAMERA_STARTS: OnceLock<Mutex<u32>> = OnceLock::new();
3402    static CAPTURED_CAMERA_STOPS: OnceLock<Mutex<u32>> = OnceLock::new();
3403    static CAPTURED_DISK_CONTROL_CALLBACK: OnceLock<
3404        Mutex<Option<raw::retro_disk_control_callback>>,
3405    > = OnceLock::new();
3406    static CAPTURED_DISK_CONTROL_EXT_CALLBACK: OnceLock<
3407        Mutex<Option<raw::retro_disk_control_ext_callback>>,
3408    > = OnceLock::new();
3409    static CAPTURED_NETPACKET_CALLBACK: OnceLock<Mutex<Option<CapturedNetpacketCallback>>> =
3410        OnceLock::new();
3411    static CAPTURED_NETPACKET_SENDS: OnceLock<Mutex<Vec<CapturedNetpacketSend>>> = OnceLock::new();
3412    static CAPTURED_NETPACKET_POLLS: OnceLock<Mutex<u32>> = OnceLock::new();
3413    static CAPTURED_MIC_OPEN_PARAMS: OnceLock<Mutex<Vec<Option<u32>>>> = OnceLock::new();
3414    static CAPTURED_MIC_STATES: OnceLock<Mutex<Vec<bool>>> = OnceLock::new();
3415    static CAPTURED_MIC_CLOSES: OnceLock<Mutex<u32>> = OnceLock::new();
3416    static CAPTURED_MIDI_WRITES: OnceLock<Mutex<Vec<(u8, u32)>>> = OnceLock::new();
3417    static CAPTURED_MIDI_FLUSHES: OnceLock<Mutex<u32>> = OnceLock::new();
3418    static CAPTURED_MIDI_PROBES: OnceLock<Mutex<u32>> = OnceLock::new();
3419    static CAPTURED_KEYBOARD_CALLBACK: OnceLock<Mutex<Option<RawKeyboardCallback>>> =
3420        OnceLock::new();
3421    static CAPTURED_AUDIO_LATENCIES: OnceLock<Mutex<Vec<Option<u32>>>> = OnceLock::new();
3422    static CAPTURED_AUDIO_BUFFER_STATUS_CALLBACK: OnceLock<
3423        Mutex<Option<RawAudioBufferStatusCallback>>,
3424    > = OnceLock::new();
3425    static CAPTURED_AUDIO_CALLBACK: OnceLock<Mutex<Option<RawAudioCallback>>> = OnceLock::new();
3426    static CAPTURED_AUDIO_CALLBACK_PROBES: OnceLock<Mutex<u32>> = OnceLock::new();
3427    static CAPTURED_FRAME_TIME_CALLBACK: OnceLock<Mutex<Option<RawFrameTimeCallback>>> =
3428        OnceLock::new();
3429    static CAPTURED_PROC_ADDRESS_INTERFACE: OnceLock<
3430        Mutex<Option<raw::retro_get_proc_address_interface>>,
3431    > = OnceLock::new();
3432    static CAPTURED_FASTFORWARDING_OVERRIDES: OnceLock<
3433        Mutex<Vec<Option<raw::retro_fastforwarding_override>>>,
3434    > = OnceLock::new();
3435    static CAPTURED_ACHIEVEMENT_SUPPORT: OnceLock<Mutex<Vec<bool>>> = OnceLock::new();
3436    static CAPTURED_PERFORMANCE_LEVELS: OnceLock<Mutex<Vec<u32>>> = OnceLock::new();
3437    static CAPTURED_PERF_LOGS: OnceLock<Mutex<u32>> = OnceLock::new();
3438    static CAPTURED_PERF_REGISTERED_IDENTS: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
3439    static CAPTURED_ROTATIONS: OnceLock<Mutex<Vec<u32>>> = OnceLock::new();
3440    static CAPTURED_SYSTEM_AV_INFOS: OnceLock<Mutex<Vec<SystemAvInfo>>> = OnceLock::new();
3441    static CAPTURED_SHUTDOWNS: OnceLock<Mutex<u32>> = OnceLock::new();
3442    static CAPTURED_HW_SHARED_CONTEXTS: OnceLock<Mutex<u32>> = OnceLock::new();
3443    static CAPTURED_SERIALIZATION_QUIRKS: OnceLock<Mutex<Vec<u64>>> = OnceLock::new();
3444    static CAPTURED_HW_RENDER_STATE: OnceLock<Mutex<CapturedHwRenderState>> = OnceLock::new();
3445    static CAPTURED_GEOMETRIES: OnceLock<Mutex<Vec<GameGeometry>>> = OnceLock::new();
3446    static CAPTURED_LIFECYCLE_COUNTS: OnceLock<Mutex<LifecycleCallCounts>> = OnceLock::new();
3447    static EXTENDED_GAME_INFO_PTR: OnceLock<usize> = OnceLock::new();
3448    static TEST_SERIAL_GUARD: OnceLock<Mutex<()>> = OnceLock::new();
3449    static EXTENDED_GAME_CONTENT: &[u8] = b"ROM";
3450    static FRONTEND_HW_RENDER_INTERFACE: raw::retro_hw_render_interface =
3451        raw::retro_hw_render_interface {
3452            interface_type: raw::retro_hw_render_interface_type::Vulkan as i32,
3453            interface_version: 1,
3454        };
3455    static FRONTEND_VFS_INTERFACE: raw::retro_vfs_interface = raw::retro_vfs_interface {
3456        get_path: Some(capture_vfs_get_path),
3457        open: Some(capture_vfs_open),
3458        close: Some(capture_vfs_close),
3459        size: Some(capture_vfs_size),
3460        tell: Some(capture_vfs_tell),
3461        seek: Some(capture_vfs_seek),
3462        read: Some(capture_vfs_read),
3463        write: Some(capture_vfs_write),
3464        flush: Some(capture_vfs_flush),
3465        remove: Some(capture_vfs_remove),
3466        rename: Some(capture_vfs_rename),
3467        truncate: Some(capture_vfs_truncate),
3468        stat: Some(capture_vfs_stat),
3469        mkdir: Some(capture_vfs_mkdir),
3470        opendir: Some(capture_vfs_opendir),
3471        readdir: Some(capture_vfs_readdir),
3472        dirent_get_name: Some(capture_vfs_dirent_get_name),
3473        dirent_is_dir: Some(capture_vfs_dirent_is_dir),
3474        closedir: Some(capture_vfs_closedir),
3475    };
3476
3477    #[derive(Clone, Copy, Debug, Default)]
3478    struct CapturedHwRenderState {
3479        preferred_context_type: HwContextType,
3480        supports_non_preferred_context: bool,
3481        context_negotiation_support_version: Option<u32>,
3482        accept_contexts: [Option<HwContextType>; 4],
3483        accept_any_context: bool,
3484        attempts: [Option<HwContextType>; 4],
3485        attempt_count: usize,
3486        last_callback: Option<RawHwRenderCallback>,
3487        last_context_negotiation: Option<HwRenderContextNegotiationInterface>,
3488        inject_runtime_callbacks: bool,
3489    }
3490
3491    impl CapturedHwRenderState {
3492        fn reset(&mut self) {
3493            *self = Self::default();
3494        }
3495
3496        fn set_accept_contexts(&mut self, contexts: &[HwContextType]) {
3497            self.accept_contexts = [None; 4];
3498            for (slot, context) in self
3499                .accept_contexts
3500                .iter_mut()
3501                .zip(contexts.iter().copied())
3502            {
3503                *slot = Some(context);
3504            }
3505        }
3506
3507        fn accepts(&self, context_type: HwContextType) -> bool {
3508            self.accept_any_context || self.accept_contexts.contains(&Some(context_type))
3509        }
3510
3511        fn record_attempt(&mut self, context_type: HwContextType) {
3512            if let Some(slot) = self.attempts.get_mut(self.attempt_count) {
3513                *slot = Some(context_type);
3514            }
3515            self.attempt_count = self.attempt_count.saturating_add(1);
3516        }
3517
3518        fn attempted_contexts(&self) -> Vec<HwContextType> {
3519            self.attempts.iter().flatten().copied().collect()
3520        }
3521    }
3522
3523    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3524    struct LifecycleCallCounts {
3525        resets: usize,
3526        destroys: usize,
3527    }
3528
3529    #[derive(Default)]
3530    struct LifecycleRecordingCore;
3531
3532    impl Core for LifecycleRecordingCore {
3533        fn system_info(&self) -> SystemInfo {
3534            SystemInfo::new("test-core", "0.0.0")
3535        }
3536
3537        fn av_info(&self) -> SystemAvInfo {
3538            SystemAvInfo::default()
3539        }
3540
3541        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3542
3543        fn hw_context_reset(&mut self, _runtime: &mut Runtime<'_>) {
3544            lifecycle_call_counts()
3545                .lock()
3546                .expect("lifecycle count mutex poisoned")
3547                .resets += 1;
3548        }
3549
3550        fn hw_context_destroy(&mut self, _runtime: &mut Runtime<'_>) {
3551            lifecycle_call_counts()
3552                .lock()
3553                .expect("lifecycle count mutex poisoned")
3554                .destroys += 1;
3555        }
3556    }
3557
3558    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3559    enum PanicAt {
3560        SystemInfo,
3561        Init,
3562        LoadGame,
3563    }
3564
3565    struct PanickingCore {
3566        panic_at: PanicAt,
3567    }
3568
3569    impl PanickingCore {
3570        fn new(panic_at: PanicAt) -> Self {
3571            Self { panic_at }
3572        }
3573    }
3574
3575    impl Core for PanickingCore {
3576        fn system_info(&self) -> SystemInfo {
3577            if self.panic_at == PanicAt::SystemInfo {
3578                panic!("intentional system info panic");
3579            }
3580            SystemInfo::new("panic-test-core", "0.0.0")
3581        }
3582
3583        fn av_info(&self) -> SystemAvInfo {
3584            SystemAvInfo::default()
3585        }
3586
3587        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3588
3589        fn init(&mut self, _env: &mut Environment<'_>) {
3590            if self.panic_at == PanicAt::Init {
3591                panic!("intentional init panic");
3592            }
3593        }
3594
3595        fn load_game(&mut self, _game: Option<GameInfo<'_>>, _runtime: &mut Runtime<'_>) -> bool {
3596            if self.panic_at == PanicAt::LoadGame {
3597                panic!("intentional load game panic");
3598            }
3599            true
3600        }
3601    }
3602
3603    struct ChangingSystemInfoCore {
3604        calls: Arc<AtomicUsize>,
3605    }
3606
3607    impl Core for ChangingSystemInfoCore {
3608        fn system_info(&self) -> SystemInfo {
3609            let call = self.calls.fetch_add(1, Ordering::SeqCst);
3610            let mut info = if call == 0 {
3611                SystemInfo::new("cached-test-core", "first")
3612            } else {
3613                SystemInfo::new("cached-test-core-mutated", "second")
3614            };
3615            info.valid_extensions = Some(if call == 0 {
3616                "first".to_string()
3617            } else {
3618                "second".to_string()
3619            });
3620            info
3621        }
3622
3623        fn av_info(&self) -> SystemAvInfo {
3624            SystemAvInfo::default()
3625        }
3626
3627        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3628    }
3629
3630    struct RunPanicThenResetCore {
3631        reset_calls: Arc<AtomicUsize>,
3632    }
3633
3634    impl Core for RunPanicThenResetCore {
3635        fn system_info(&self) -> SystemInfo {
3636            SystemInfo::new("panic-preserve-core", "0.0.0")
3637        }
3638
3639        fn av_info(&self) -> SystemAvInfo {
3640            SystemAvInfo::default()
3641        }
3642
3643        fn run(&mut self, _runtime: &mut Runtime<'_>) {
3644            panic!("intentional run panic");
3645        }
3646
3647        fn reset(&mut self) {
3648            self.reset_calls.fetch_add(1, Ordering::SeqCst);
3649        }
3650    }
3651
3652    struct MemoryRecordingCore {
3653        calls: Arc<Mutex<Vec<MemoryRegion>>>,
3654        save_ram: [u8; 4],
3655    }
3656
3657    impl MemoryRecordingCore {
3658        fn new(calls: Arc<Mutex<Vec<MemoryRegion>>>) -> Self {
3659            Self {
3660                calls,
3661                save_ram: [1, 2, 3, 4],
3662            }
3663        }
3664    }
3665
3666    struct ControllerDeviceRecordingCore {
3667        calls: Arc<Mutex<Vec<(InputPort, ControllerDevice)>>>,
3668    }
3669
3670    struct CheatRecordingCore {
3671        calls: Arc<Mutex<Vec<(CheatIndex, bool, Option<String>)>>>,
3672    }
3673
3674    struct KeyboardRecordingCore {
3675        calls: Arc<Mutex<Vec<KeyboardEvent>>>,
3676    }
3677
3678    struct ConfiguredEventCore {
3679        keyboard_calls: Arc<Mutex<Vec<KeyboardEvent>>>,
3680    }
3681
3682    struct MultiKeyboardListenerCore {
3683        calls: Arc<Mutex<Vec<&'static str>>>,
3684    }
3685
3686    struct AudioBufferStatusRecordingCore {
3687        calls: Arc<Mutex<Vec<AudioBufferStatus>>>,
3688    }
3689
3690    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3691    enum AudioCallbackEvent {
3692        Request,
3693        State(AudioCallbackState),
3694    }
3695
3696    struct AudioCallbackRecordingCore {
3697        calls: Arc<Mutex<Vec<AudioCallbackEvent>>>,
3698    }
3699
3700    struct FrameTimeRecordingCore {
3701        calls: Arc<Mutex<Vec<FrameTime>>>,
3702    }
3703
3704    struct FrameTimeReplacementCore {
3705        calls: Arc<Mutex<Vec<&'static str>>>,
3706    }
3707
3708    struct FrameTimeClearedCore;
3709
3710    struct ProcAddressRecordingCore {
3711        calls: Arc<Mutex<Vec<String>>>,
3712    }
3713
3714    struct LocationRecordingCore {
3715        calls: Arc<Mutex<Vec<LocationLifecycleEvent>>>,
3716    }
3717
3718    struct CameraRecordingCore {
3719        calls: Arc<Mutex<Vec<CameraEvent>>>,
3720    }
3721
3722    struct DiskControlRecordingCore {
3723        calls: Arc<Mutex<Vec<DiskControlEvent>>>,
3724    }
3725
3726    struct NetpacketRecordingCore {
3727        calls: Arc<Mutex<Vec<NetpacketEvent>>>,
3728    }
3729
3730    struct CoreOptionsDisplayRecordingCore {
3731        calls: Arc<Mutex<Vec<&'static str>>>,
3732    }
3733
3734    unsafe extern "C" fn test_extension_proc() {}
3735
3736    impl Core for AudioCallbackRecordingCore {
3737        fn system_info(&self) -> SystemInfo {
3738            SystemInfo::new("audio-callback-test-core", "0.0.0")
3739        }
3740
3741        fn av_info(&self) -> SystemAvInfo {
3742            SystemAvInfo::default()
3743        }
3744
3745        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3746
3747        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3748            events
3749                .add_audio_callback_listener(Self::audio_callback)
3750                .add_audio_callback_state_changed_listener(Self::audio_callback_state_changed);
3751        }
3752    }
3753
3754    impl AudioCallbackRecordingCore {
3755        fn audio_callback(&mut self) {
3756            self.calls
3757                .lock()
3758                .expect("audio callback calls mutex poisoned")
3759                .push(AudioCallbackEvent::Request);
3760        }
3761
3762        fn audio_callback_state_changed(&mut self, state: AudioCallbackState) {
3763            self.calls
3764                .lock()
3765                .expect("audio callback calls mutex poisoned")
3766                .push(AudioCallbackEvent::State(state));
3767        }
3768    }
3769
3770    impl Core for KeyboardRecordingCore {
3771        fn system_info(&self) -> SystemInfo {
3772            SystemInfo::new("keyboard-test-core", "0.0.0")
3773        }
3774
3775        fn av_info(&self) -> SystemAvInfo {
3776            SystemAvInfo::default()
3777        }
3778
3779        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3780
3781        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3782            events.add_keyboard_event_listener(Self::keyboard_event);
3783        }
3784    }
3785
3786    impl KeyboardRecordingCore {
3787        fn keyboard_event(&mut self, event: KeyboardEvent) {
3788            self.calls
3789                .lock()
3790                .expect("keyboard event calls mutex poisoned")
3791                .push(event);
3792        }
3793    }
3794
3795    impl Core for ConfiguredEventCore {
3796        fn system_info(&self) -> SystemInfo {
3797            SystemInfo::new("auto-event-test-core", "0.0.0")
3798        }
3799
3800        fn av_info(&self) -> SystemAvInfo {
3801            SystemAvInfo::default()
3802        }
3803
3804        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3805
3806        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3807            events
3808                .add_keyboard_event_listener(Self::keyboard_event)
3809                .add_audio_callback_listener(Self::audio_callback)
3810                .add_audio_callback_state_changed_listener(Self::audio_callback_state_changed)
3811                .add_audio_buffer_status_listener(Self::audio_buffer_status)
3812                .set_frame_time_callback(FrameTime::from_micros(16_667), Self::frame_time);
3813        }
3814    }
3815
3816    impl ConfiguredEventCore {
3817        fn keyboard_event(&mut self, event: KeyboardEvent) {
3818            self.keyboard_calls
3819                .lock()
3820                .expect("keyboard event calls mutex poisoned")
3821                .push(event);
3822        }
3823
3824        fn audio_callback(&mut self) {}
3825
3826        fn audio_callback_state_changed(&mut self, _state: AudioCallbackState) {}
3827
3828        fn audio_buffer_status(&mut self, _status: AudioBufferStatus) {}
3829
3830        fn frame_time(&mut self, _time: FrameTime) {}
3831    }
3832
3833    impl Core for MultiKeyboardListenerCore {
3834        fn system_info(&self) -> SystemInfo {
3835            SystemInfo::new("multi-keyboard-listener-test-core", "0.0.0")
3836        }
3837
3838        fn av_info(&self) -> SystemAvInfo {
3839            SystemAvInfo::default()
3840        }
3841
3842        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3843
3844        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3845            events
3846                .add_keyboard_event_listener(Self::first_keyboard_event)
3847                .add_keyboard_event_listener(Self::second_keyboard_event)
3848                .add_keyboard_event_listener(Self::first_keyboard_event)
3849                .remove_keyboard_event_listener(Self::second_keyboard_event)
3850                .add_keyboard_event_listener(Self::third_keyboard_event)
3851                .remove_keyboard_event_listener(Self::second_keyboard_event);
3852        }
3853    }
3854
3855    impl MultiKeyboardListenerCore {
3856        fn first_keyboard_event(&mut self, _event: KeyboardEvent) {
3857            self.calls
3858                .lock()
3859                .expect("multi keyboard listener calls mutex poisoned")
3860                .push("first");
3861        }
3862
3863        fn second_keyboard_event(&mut self, _event: KeyboardEvent) {
3864            self.calls
3865                .lock()
3866                .expect("multi keyboard listener calls mutex poisoned")
3867                .push("second");
3868        }
3869
3870        fn third_keyboard_event(&mut self, _event: KeyboardEvent) {
3871            self.calls
3872                .lock()
3873                .expect("multi keyboard listener calls mutex poisoned")
3874                .push("third");
3875        }
3876    }
3877
3878    impl Core for CoreOptionsDisplayRecordingCore {
3879        fn system_info(&self) -> SystemInfo {
3880            SystemInfo::new("core-options-display-test-core", "0.0.0")
3881        }
3882
3883        fn av_info(&self) -> SystemAvInfo {
3884            SystemAvInfo::default()
3885        }
3886
3887        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3888
3889        fn core_options_update_display(&mut self, env: &mut Environment<'_>) -> bool {
3890            self.calls
3891                .lock()
3892                .expect("core options display calls mutex poisoned")
3893                .push("update");
3894            env.set_core_option_display(CoreOptionDisplay::new("demo_extra", false))
3895        }
3896    }
3897
3898    impl Core for AudioBufferStatusRecordingCore {
3899        fn system_info(&self) -> SystemInfo {
3900            SystemInfo::new("audio-buffer-status-test-core", "0.0.0")
3901        }
3902
3903        fn av_info(&self) -> SystemAvInfo {
3904            SystemAvInfo::default()
3905        }
3906
3907        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3908
3909        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3910            events.add_audio_buffer_status_listener(Self::audio_buffer_status);
3911        }
3912    }
3913
3914    impl AudioBufferStatusRecordingCore {
3915        fn audio_buffer_status(&mut self, status: AudioBufferStatus) {
3916            self.calls
3917                .lock()
3918                .expect("audio buffer status calls mutex poisoned")
3919                .push(status);
3920        }
3921    }
3922
3923    impl Core for FrameTimeRecordingCore {
3924        fn system_info(&self) -> SystemInfo {
3925            SystemInfo::new("frame-time-test-core", "0.0.0")
3926        }
3927
3928        fn av_info(&self) -> SystemAvInfo {
3929            SystemAvInfo::default()
3930        }
3931
3932        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3933
3934        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3935            events.set_frame_time_callback(FrameTime::from_micros(16_667), Self::frame_time);
3936        }
3937    }
3938
3939    impl FrameTimeRecordingCore {
3940        fn frame_time(&mut self, time: FrameTime) {
3941            self.calls
3942                .lock()
3943                .expect("frame time calls mutex poisoned")
3944                .push(time);
3945        }
3946    }
3947
3948    impl Core for FrameTimeReplacementCore {
3949        fn system_info(&self) -> SystemInfo {
3950            SystemInfo::new("frame-time-replacement-test-core", "0.0.0")
3951        }
3952
3953        fn av_info(&self) -> SystemAvInfo {
3954            SystemAvInfo::default()
3955        }
3956
3957        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3958
3959        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3960            events
3961                .set_frame_time_callback(FrameTime::from_micros(1_000), Self::first_frame_time)
3962                .set_frame_time_callback(FrameTime::from_micros(2_000), Self::second_frame_time);
3963        }
3964    }
3965
3966    impl FrameTimeReplacementCore {
3967        fn first_frame_time(&mut self, _time: FrameTime) {
3968            self.calls
3969                .lock()
3970                .expect("frame time replacement calls mutex poisoned")
3971                .push("first");
3972        }
3973
3974        fn second_frame_time(&mut self, _time: FrameTime) {
3975            self.calls
3976                .lock()
3977                .expect("frame time replacement calls mutex poisoned")
3978                .push("second");
3979        }
3980    }
3981
3982    impl Core for FrameTimeClearedCore {
3983        fn system_info(&self) -> SystemInfo {
3984            SystemInfo::new("frame-time-cleared-test-core", "0.0.0")
3985        }
3986
3987        fn av_info(&self) -> SystemAvInfo {
3988            SystemAvInfo::default()
3989        }
3990
3991        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
3992
3993        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
3994            events
3995                .set_frame_time_callback(FrameTime::from_micros(16_667), Self::frame_time)
3996                .clear_frame_time_callback();
3997        }
3998    }
3999
4000    impl FrameTimeClearedCore {
4001        fn frame_time(&mut self, _time: FrameTime) {}
4002    }
4003
4004    impl Core for ProcAddressRecordingCore {
4005        fn system_info(&self) -> SystemInfo {
4006            SystemInfo::new("proc-address-test-core", "0.0.0")
4007        }
4008
4009        fn av_info(&self) -> SystemAvInfo {
4010            SystemAvInfo::default()
4011        }
4012
4013        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4014
4015        fn proc_address(&mut self, symbol: &CStr) -> Option<CoreProcAddress> {
4016            let symbol = symbol.to_string_lossy().into_owned();
4017            self.calls
4018                .lock()
4019                .expect("proc address calls mutex poisoned")
4020                .push(symbol.clone());
4021            (symbol == "test_extension_proc")
4022                .then_some(CoreProcAddress::from_fn(test_extension_proc))
4023        }
4024    }
4025
4026    impl Core for LocationRecordingCore {
4027        fn system_info(&self) -> SystemInfo {
4028            SystemInfo::new("location-test-core", "0.0.0")
4029        }
4030
4031        fn av_info(&self) -> SystemAvInfo {
4032            SystemAvInfo::default()
4033        }
4034
4035        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4036
4037        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
4038            events
4039                .add_location_initialized_listener(Self::location_initialized)
4040                .add_location_deinitialized_listener(Self::location_deinitialized);
4041        }
4042    }
4043
4044    impl LocationRecordingCore {
4045        fn location_initialized(&mut self) {
4046            self.calls
4047                .lock()
4048                .expect("location lifecycle calls mutex poisoned")
4049                .push(LocationLifecycleEvent::Initialized);
4050        }
4051
4052        fn location_deinitialized(&mut self) {
4053            self.calls
4054                .lock()
4055                .expect("location lifecycle calls mutex poisoned")
4056                .push(LocationLifecycleEvent::Deinitialized);
4057        }
4058    }
4059
4060    impl Core for CameraRecordingCore {
4061        fn system_info(&self) -> SystemInfo {
4062            SystemInfo::new("camera-test-core", "0.0.0")
4063        }
4064
4065        fn av_info(&self) -> SystemAvInfo {
4066            SystemAvInfo::default()
4067        }
4068
4069        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4070
4071        fn configure_events(&mut self, events: &mut CoreEventConfig<Self>) {
4072            events
4073                .add_camera_initialized_listener(Self::camera_initialized)
4074                .add_camera_deinitialized_listener(Self::camera_deinitialized)
4075                .add_camera_raw_frame_listener(Self::camera_raw_frame)
4076                .add_camera_texture_frame_listener(Self::camera_texture_frame);
4077        }
4078    }
4079
4080    impl CameraRecordingCore {
4081        fn camera_initialized(&mut self) {
4082            self.calls
4083                .lock()
4084                .expect("camera calls mutex poisoned")
4085                .push(CameraEvent::Initialized);
4086        }
4087
4088        fn camera_deinitialized(&mut self) {
4089            self.calls
4090                .lock()
4091                .expect("camera calls mutex poisoned")
4092                .push(CameraEvent::Deinitialized);
4093        }
4094
4095        fn camera_raw_frame(&mut self, frame: CameraRawFrame<'_>) {
4096            self.calls
4097                .lock()
4098                .expect("camera calls mutex poisoned")
4099                .push(CameraEvent::Raw {
4100                    width: frame.width,
4101                    height: frame.height,
4102                    pitch: frame.pitch_bytes,
4103                    pixels: frame.pixels.to_vec(),
4104                });
4105        }
4106
4107        fn camera_texture_frame(&mut self, frame: CameraTextureFrame) {
4108            self.calls
4109                .lock()
4110                .expect("camera calls mutex poisoned")
4111                .push(CameraEvent::Texture {
4112                    texture_id: frame.texture_id.get(),
4113                    texture_target: frame.texture_target.get(),
4114                    affine: frame.affine,
4115                });
4116        }
4117    }
4118
4119    impl Core for DiskControlRecordingCore {
4120        fn system_info(&self) -> SystemInfo {
4121            SystemInfo::new("disk-test-core", "0.0.0")
4122        }
4123
4124        fn av_info(&self) -> SystemAvInfo {
4125            SystemAvInfo::default()
4126        }
4127
4128        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4129
4130        fn disk_set_tray_state(&mut self, state: DiskTrayState) -> bool {
4131            self.calls
4132                .lock()
4133                .expect("disk control calls mutex poisoned")
4134                .push(DiskControlEvent::SetTray(state));
4135            true
4136        }
4137
4138        fn disk_tray_state(&mut self) -> DiskTrayState {
4139            DiskTrayState::Ejected
4140        }
4141
4142        fn disk_image_index(&mut self) -> DiskIndex {
4143            DiskIndex::new(2)
4144        }
4145
4146        fn disk_set_image_index(&mut self, index: DiskIndex) -> bool {
4147            self.calls
4148                .lock()
4149                .expect("disk control calls mutex poisoned")
4150                .push(DiskControlEvent::SetImage(index));
4151            true
4152        }
4153
4154        fn disk_image_count(&mut self) -> u32 {
4155            4
4156        }
4157
4158        fn disk_replace_image_index(
4159            &mut self,
4160            index: DiskIndex,
4161            game: Option<GameInfo<'_>>,
4162        ) -> bool {
4163            self.calls
4164                .lock()
4165                .expect("disk control calls mutex poisoned")
4166                .push(DiskControlEvent::ReplaceImage(index, game.is_some()));
4167            true
4168        }
4169
4170        fn disk_add_image_index(&mut self) -> bool {
4171            self.calls
4172                .lock()
4173                .expect("disk control calls mutex poisoned")
4174                .push(DiskControlEvent::AddImage);
4175            true
4176        }
4177
4178        fn disk_set_initial_image(&mut self, index: DiskIndex, path: &CStr) -> bool {
4179            self.calls
4180                .lock()
4181                .expect("disk control calls mutex poisoned")
4182                .push(DiskControlEvent::SetInitialImage(
4183                    index,
4184                    path.to_string_lossy().into_owned(),
4185                ));
4186            true
4187        }
4188
4189        fn disk_image_path(&mut self, index: DiskIndex) -> Option<String> {
4190            (index == DiskIndex::new(2)).then(|| "/games/disc\0two.cue".to_string())
4191        }
4192
4193        fn disk_image_label(&mut self, index: DiskIndex) -> Option<String> {
4194            (index == DiskIndex::new(2)).then(|| "Disc Two".to_string())
4195        }
4196    }
4197
4198    impl Core for NetpacketRecordingCore {
4199        fn system_info(&self) -> SystemInfo {
4200            SystemInfo::new("netpacket-test-core", "0.0.0")
4201        }
4202
4203        fn av_info(&self) -> SystemAvInfo {
4204            SystemAvInfo::default()
4205        }
4206
4207        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4208
4209        fn netpacket_start(&mut self, session: NetpacketSession) {
4210            self.calls
4211                .lock()
4212                .expect("netpacket calls mutex poisoned")
4213                .push(NetpacketEvent::Start(
4214                    session.client_id(),
4215                    session.can_poll_receive(),
4216                ));
4217            session.send(
4218                NetpacketTarget::Broadcast,
4219                NetpacketFlags::reliable(),
4220                b"hello",
4221            );
4222            session.flush(NetpacketTarget::Client(session.client_id()));
4223            assert!(session.poll_receive());
4224        }
4225
4226        fn netpacket_receive(&mut self, packet: Netpacket<'_>) {
4227            self.calls
4228                .lock()
4229                .expect("netpacket calls mutex poisoned")
4230                .push(NetpacketEvent::Receive(
4231                    packet.client_id,
4232                    packet.data.to_vec(),
4233                ));
4234        }
4235
4236        fn netpacket_stop(&mut self) {
4237            self.calls
4238                .lock()
4239                .expect("netpacket calls mutex poisoned")
4240                .push(NetpacketEvent::Stop);
4241        }
4242
4243        fn netpacket_poll(&mut self) {
4244            self.calls
4245                .lock()
4246                .expect("netpacket calls mutex poisoned")
4247                .push(NetpacketEvent::Poll);
4248        }
4249
4250        fn netpacket_connected(&mut self, client_id: NetplayClientId) -> bool {
4251            self.calls
4252                .lock()
4253                .expect("netpacket calls mutex poisoned")
4254                .push(NetpacketEvent::Connected(client_id));
4255            client_id != NetplayClientId::new(9)
4256        }
4257
4258        fn netpacket_disconnected(&mut self, client_id: NetplayClientId) {
4259            self.calls
4260                .lock()
4261                .expect("netpacket calls mutex poisoned")
4262                .push(NetpacketEvent::Disconnected(client_id));
4263        }
4264    }
4265
4266    impl Core for ControllerDeviceRecordingCore {
4267        fn system_info(&self) -> SystemInfo {
4268            SystemInfo::new("controller-device-test-core", "0.0.0")
4269        }
4270
4271        fn av_info(&self) -> SystemAvInfo {
4272            SystemAvInfo::default()
4273        }
4274
4275        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4276
4277        fn set_controller_port_device(&mut self, port: InputPort, device: ControllerDevice) {
4278            self.calls
4279                .lock()
4280                .expect("controller device calls mutex poisoned")
4281                .push((port, device));
4282        }
4283    }
4284
4285    impl Core for CheatRecordingCore {
4286        fn system_info(&self) -> SystemInfo {
4287            SystemInfo::new("cheat-test-core", "0.0.0")
4288        }
4289
4290        fn av_info(&self) -> SystemAvInfo {
4291            SystemAvInfo::default()
4292        }
4293
4294        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4295
4296        fn cheat_set(&mut self, index: CheatIndex, enabled: bool, code: Option<CheatCode<'_>>) {
4297            self.calls
4298                .lock()
4299                .expect("cheat calls mutex poisoned")
4300                .push((
4301                    index,
4302                    enabled,
4303                    code.map(|code| code.to_string_lossy().into_owned()),
4304                ));
4305        }
4306    }
4307
4308    impl Core for MemoryRecordingCore {
4309        fn system_info(&self) -> SystemInfo {
4310            SystemInfo::new("memory-test-core", "0.0.0")
4311        }
4312
4313        fn av_info(&self) -> SystemAvInfo {
4314            SystemAvInfo::default()
4315        }
4316
4317        fn run(&mut self, _runtime: &mut Runtime<'_>) {}
4318
4319        fn memory_region(&mut self, region: MemoryRegion) -> Option<CoreMemory<'_>> {
4320            self.calls
4321                .lock()
4322                .expect("memory calls mutex poisoned")
4323                .push(region);
4324            match region {
4325                MemoryRegion::SaveRam => Some(CoreMemory::read_write(&mut self.save_ram)),
4326                _ => None,
4327            }
4328        }
4329    }
4330
4331    fn captured_hw_render_state() -> &'static Mutex<CapturedHwRenderState> {
4332        CAPTURED_HW_RENDER_STATE.get_or_init(|| Mutex::new(CapturedHwRenderState::default()))
4333    }
4334
4335    fn lifecycle_call_counts() -> &'static Mutex<LifecycleCallCounts> {
4336        CAPTURED_LIFECYCLE_COUNTS.get_or_init(|| Mutex::new(LifecycleCallCounts::default()))
4337    }
4338
4339    fn captured_geometries() -> &'static Mutex<Vec<GameGeometry>> {
4340        CAPTURED_GEOMETRIES.get_or_init(|| Mutex::new(Vec::new()))
4341    }
4342
4343    fn captured_messages() -> &'static Mutex<Vec<CapturedMessage>> {
4344        CAPTURED_MESSAGES.get_or_init(|| Mutex::new(Vec::new()))
4345    }
4346
4347    fn captured_extended_messages() -> &'static Mutex<Vec<CapturedExtendedMessage>> {
4348        CAPTURED_EXTENDED_MESSAGES.get_or_init(|| Mutex::new(Vec::new()))
4349    }
4350
4351    fn captured_video_refreshes() -> &'static Mutex<Vec<CapturedVideoRefresh>> {
4352        CAPTURED_VIDEO_REFRESHES.get_or_init(|| Mutex::new(Vec::new()))
4353    }
4354
4355    fn captured_input_queries() -> &'static Mutex<Vec<CapturedInputQuery>> {
4356        CAPTURED_INPUT_QUERIES.get_or_init(|| Mutex::new(Vec::new()))
4357    }
4358
4359    fn captured_input_descriptors() -> &'static Mutex<Vec<CapturedInputDescriptor>> {
4360        CAPTURED_INPUT_DESCRIPTORS.get_or_init(|| Mutex::new(Vec::new()))
4361    }
4362
4363    fn captured_controller_info() -> &'static Mutex<Vec<Vec<CapturedControllerDescription>>> {
4364        CAPTURED_CONTROLLER_INFO.get_or_init(|| Mutex::new(Vec::new()))
4365    }
4366
4367    fn captured_core_options_version() -> &'static Mutex<Option<u32>> {
4368        CAPTURED_CORE_OPTIONS_VERSION.get_or_init(|| Mutex::new(Some(2)))
4369    }
4370
4371    fn captured_core_options_v2() -> &'static Mutex<Option<CapturedCoreOptionsV2>> {
4372        CAPTURED_CORE_OPTIONS_V2.get_or_init(|| Mutex::new(None))
4373    }
4374
4375    fn captured_core_options_v1() -> &'static Mutex<Vec<CapturedCoreOptionDefinition>> {
4376        CAPTURED_CORE_OPTIONS_V1.get_or_init(|| Mutex::new(Vec::new()))
4377    }
4378
4379    fn captured_core_option_displays() -> &'static Mutex<Vec<CapturedCoreOptionDisplay>> {
4380        CAPTURED_CORE_OPTION_DISPLAYS.get_or_init(|| Mutex::new(Vec::new()))
4381    }
4382
4383    fn captured_variables() -> &'static Mutex<Vec<CapturedVariable>> {
4384        CAPTURED_VARIABLES.get_or_init(|| Mutex::new(Vec::new()))
4385    }
4386
4387    fn captured_core_options_update_display_callback()
4388    -> &'static Mutex<Option<raw::retro_core_options_update_display_callback>> {
4389        CAPTURED_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK.get_or_init(|| Mutex::new(None))
4390    }
4391
4392    fn captured_vfs_interface_requests() -> &'static Mutex<Vec<u32>> {
4393        CAPTURED_VFS_INTERFACE_REQUESTS.get_or_init(|| Mutex::new(Vec::new()))
4394    }
4395
4396    fn captured_vfs_opens() -> &'static Mutex<Vec<CapturedVfsOpen>> {
4397        CAPTURED_VFS_OPENS.get_or_init(|| Mutex::new(Vec::new()))
4398    }
4399
4400    fn captured_vfs_closes() -> &'static Mutex<u32> {
4401        CAPTURED_VFS_CLOSES.get_or_init(|| Mutex::new(0))
4402    }
4403
4404    fn captured_vfs_dir_closes() -> &'static Mutex<u32> {
4405        CAPTURED_VFS_DIR_CLOSES.get_or_init(|| Mutex::new(0))
4406    }
4407
4408    fn captured_vfs_writes() -> &'static Mutex<Vec<Vec<u8>>> {
4409        CAPTURED_VFS_WRITES.get_or_init(|| Mutex::new(Vec::new()))
4410    }
4411
4412    fn captured_vfs_removes() -> &'static Mutex<Vec<String>> {
4413        CAPTURED_VFS_REMOVES.get_or_init(|| Mutex::new(Vec::new()))
4414    }
4415
4416    fn captured_vfs_renames() -> &'static Mutex<Vec<CapturedVfsRename>> {
4417        CAPTURED_VFS_RENAMES.get_or_init(|| Mutex::new(Vec::new()))
4418    }
4419
4420    fn captured_vfs_mkdirs() -> &'static Mutex<Vec<String>> {
4421        CAPTURED_VFS_MKDIRS.get_or_init(|| Mutex::new(Vec::new()))
4422    }
4423
4424    fn captured_vfs_readdirs() -> &'static Mutex<u32> {
4425        CAPTURED_VFS_READDIRS.get_or_init(|| Mutex::new(0))
4426    }
4427
4428    fn captured_memory_descriptors() -> &'static Mutex<Vec<CapturedMemoryDescriptor>> {
4429        CAPTURED_MEMORY_DESCRIPTORS.get_or_init(|| Mutex::new(Vec::new()))
4430    }
4431
4432    fn captured_subsystem_info() -> &'static Mutex<Vec<CapturedSubsystem>> {
4433        CAPTURED_SUBSYSTEM_INFO.get_or_init(|| Mutex::new(Vec::new()))
4434    }
4435
4436    fn software_framebuffer_pixels() -> &'static Mutex<Vec<u32>> {
4437        SOFTWARE_FRAMEBUFFER_PIXELS.get_or_init(|| Mutex::new(vec![0; 4 * 2]))
4438    }
4439
4440    fn extended_game_info_ptr() -> *const raw::retro_game_info_ext {
4441        *EXTENDED_GAME_INFO_PTR.get_or_init(|| {
4442            Box::leak(Box::new([
4443                raw::retro_game_info_ext {
4444                    full_path: c"/games/test.sfc".as_ptr(),
4445                    archive_path: ptr::null(),
4446                    archive_file: ptr::null(),
4447                    dir: c"/games".as_ptr(),
4448                    name: c"test".as_ptr(),
4449                    ext: c"sfc".as_ptr(),
4450                    meta: c"plain".as_ptr(),
4451                    data: EXTENDED_GAME_CONTENT.as_ptr().cast::<c_void>(),
4452                    size: EXTENDED_GAME_CONTENT.len(),
4453                    file_in_archive: false,
4454                    persistent_data: true,
4455                },
4456                raw::retro_game_info_ext {
4457                    full_path: ptr::null(),
4458                    archive_path: c"/games/archive.zip".as_ptr(),
4459                    archive_file: c"inside.bin".as_ptr(),
4460                    dir: c"/games".as_ptr(),
4461                    name: c"archive".as_ptr(),
4462                    ext: c"bin".as_ptr(),
4463                    meta: ptr::null(),
4464                    data: ptr::null(),
4465                    size: 0,
4466                    file_in_archive: true,
4467                    persistent_data: false,
4468                },
4469            ])) as *const [raw::retro_game_info_ext; 2] as usize
4470        }) as *const raw::retro_game_info_ext
4471    }
4472
4473    fn captured_led_states() -> &'static Mutex<Vec<(i32, i32)>> {
4474        CAPTURED_LED_STATES.get_or_init(|| Mutex::new(Vec::new()))
4475    }
4476
4477    fn captured_rumble_states() -> &'static Mutex<Vec<(u32, raw::retro_rumble_effect, u16)>> {
4478        CAPTURED_RUMBLE_STATES.get_or_init(|| Mutex::new(Vec::new()))
4479    }
4480
4481    fn captured_sensor_states() -> &'static Mutex<Vec<(u32, raw::retro_sensor_action, u32)>> {
4482        CAPTURED_SENSOR_STATES.get_or_init(|| Mutex::new(Vec::new()))
4483    }
4484
4485    fn captured_location_intervals() -> &'static Mutex<Vec<(u32, u32)>> {
4486        CAPTURED_LOCATION_INTERVALS.get_or_init(|| Mutex::new(Vec::new()))
4487    }
4488
4489    fn captured_location_starts() -> &'static Mutex<u32> {
4490        CAPTURED_LOCATION_STARTS.get_or_init(|| Mutex::new(0))
4491    }
4492
4493    fn captured_location_stops() -> &'static Mutex<u32> {
4494        CAPTURED_LOCATION_STOPS.get_or_init(|| Mutex::new(0))
4495    }
4496
4497    fn captured_location_callback() -> &'static Mutex<Option<raw::retro_location_callback>> {
4498        CAPTURED_LOCATION_CALLBACK.get_or_init(|| Mutex::new(None))
4499    }
4500
4501    fn captured_camera_callback() -> &'static Mutex<Option<raw::retro_camera_callback>> {
4502        CAPTURED_CAMERA_CALLBACK.get_or_init(|| Mutex::new(None))
4503    }
4504
4505    fn captured_camera_starts() -> &'static Mutex<u32> {
4506        CAPTURED_CAMERA_STARTS.get_or_init(|| Mutex::new(0))
4507    }
4508
4509    fn captured_camera_stops() -> &'static Mutex<u32> {
4510        CAPTURED_CAMERA_STOPS.get_or_init(|| Mutex::new(0))
4511    }
4512
4513    fn captured_disk_control_callback() -> &'static Mutex<Option<raw::retro_disk_control_callback>>
4514    {
4515        CAPTURED_DISK_CONTROL_CALLBACK.get_or_init(|| Mutex::new(None))
4516    }
4517
4518    fn captured_disk_control_ext_callback()
4519    -> &'static Mutex<Option<raw::retro_disk_control_ext_callback>> {
4520        CAPTURED_DISK_CONTROL_EXT_CALLBACK.get_or_init(|| Mutex::new(None))
4521    }
4522
4523    fn captured_netpacket_callback() -> &'static Mutex<Option<CapturedNetpacketCallback>> {
4524        CAPTURED_NETPACKET_CALLBACK.get_or_init(|| Mutex::new(None))
4525    }
4526
4527    fn captured_netpacket_sends() -> &'static Mutex<Vec<CapturedNetpacketSend>> {
4528        CAPTURED_NETPACKET_SENDS.get_or_init(|| Mutex::new(Vec::new()))
4529    }
4530
4531    fn captured_netpacket_polls() -> &'static Mutex<u32> {
4532        CAPTURED_NETPACKET_POLLS.get_or_init(|| Mutex::new(0))
4533    }
4534
4535    fn captured_mic_open_params() -> &'static Mutex<Vec<Option<u32>>> {
4536        CAPTURED_MIC_OPEN_PARAMS.get_or_init(|| Mutex::new(Vec::new()))
4537    }
4538
4539    fn captured_mic_states() -> &'static Mutex<Vec<bool>> {
4540        CAPTURED_MIC_STATES.get_or_init(|| Mutex::new(Vec::new()))
4541    }
4542
4543    fn captured_mic_closes() -> &'static Mutex<u32> {
4544        CAPTURED_MIC_CLOSES.get_or_init(|| Mutex::new(0))
4545    }
4546
4547    fn captured_midi_writes() -> &'static Mutex<Vec<(u8, u32)>> {
4548        CAPTURED_MIDI_WRITES.get_or_init(|| Mutex::new(Vec::new()))
4549    }
4550
4551    fn captured_midi_flushes() -> &'static Mutex<u32> {
4552        CAPTURED_MIDI_FLUSHES.get_or_init(|| Mutex::new(0))
4553    }
4554
4555    fn captured_midi_probes() -> &'static Mutex<u32> {
4556        CAPTURED_MIDI_PROBES.get_or_init(|| Mutex::new(0))
4557    }
4558
4559    fn captured_keyboard_callback() -> &'static Mutex<Option<RawKeyboardCallback>> {
4560        CAPTURED_KEYBOARD_CALLBACK.get_or_init(|| Mutex::new(None))
4561    }
4562
4563    fn captured_audio_latencies() -> &'static Mutex<Vec<Option<u32>>> {
4564        CAPTURED_AUDIO_LATENCIES.get_or_init(|| Mutex::new(Vec::new()))
4565    }
4566
4567    fn captured_audio_buffer_status_callback()
4568    -> &'static Mutex<Option<RawAudioBufferStatusCallback>> {
4569        CAPTURED_AUDIO_BUFFER_STATUS_CALLBACK.get_or_init(|| Mutex::new(None))
4570    }
4571
4572    fn captured_audio_callback() -> &'static Mutex<Option<RawAudioCallback>> {
4573        CAPTURED_AUDIO_CALLBACK.get_or_init(|| Mutex::new(None))
4574    }
4575
4576    fn captured_audio_callback_probes() -> &'static Mutex<u32> {
4577        CAPTURED_AUDIO_CALLBACK_PROBES.get_or_init(|| Mutex::new(0))
4578    }
4579
4580    fn captured_frame_time_callback() -> &'static Mutex<Option<RawFrameTimeCallback>> {
4581        CAPTURED_FRAME_TIME_CALLBACK.get_or_init(|| Mutex::new(None))
4582    }
4583
4584    fn captured_proc_address_interface()
4585    -> &'static Mutex<Option<raw::retro_get_proc_address_interface>> {
4586        CAPTURED_PROC_ADDRESS_INTERFACE.get_or_init(|| Mutex::new(None))
4587    }
4588
4589    fn captured_fastforwarding_overrides()
4590    -> &'static Mutex<Vec<Option<raw::retro_fastforwarding_override>>> {
4591        CAPTURED_FASTFORWARDING_OVERRIDES.get_or_init(|| Mutex::new(Vec::new()))
4592    }
4593
4594    fn captured_achievement_support() -> &'static Mutex<Vec<bool>> {
4595        CAPTURED_ACHIEVEMENT_SUPPORT.get_or_init(|| Mutex::new(Vec::new()))
4596    }
4597
4598    fn captured_performance_levels() -> &'static Mutex<Vec<u32>> {
4599        CAPTURED_PERFORMANCE_LEVELS.get_or_init(|| Mutex::new(Vec::new()))
4600    }
4601
4602    fn captured_perf_logs() -> &'static Mutex<u32> {
4603        CAPTURED_PERF_LOGS.get_or_init(|| Mutex::new(0))
4604    }
4605
4606    fn captured_perf_registered_idents() -> &'static Mutex<Vec<String>> {
4607        CAPTURED_PERF_REGISTERED_IDENTS.get_or_init(|| Mutex::new(Vec::new()))
4608    }
4609
4610    fn captured_rotations() -> &'static Mutex<Vec<u32>> {
4611        CAPTURED_ROTATIONS.get_or_init(|| Mutex::new(Vec::new()))
4612    }
4613
4614    fn captured_system_av_infos() -> &'static Mutex<Vec<SystemAvInfo>> {
4615        CAPTURED_SYSTEM_AV_INFOS.get_or_init(|| Mutex::new(Vec::new()))
4616    }
4617
4618    fn captured_shutdowns() -> &'static Mutex<u32> {
4619        CAPTURED_SHUTDOWNS.get_or_init(|| Mutex::new(0))
4620    }
4621
4622    fn captured_hw_shared_contexts() -> &'static Mutex<u32> {
4623        CAPTURED_HW_SHARED_CONTEXTS.get_or_init(|| Mutex::new(0))
4624    }
4625
4626    fn captured_serialization_quirks() -> &'static Mutex<Vec<u64>> {
4627        CAPTURED_SERIALIZATION_QUIRKS.get_or_init(|| Mutex::new(Vec::new()))
4628    }
4629
4630    fn captured_support_no_game() -> &'static Mutex<Vec<bool>> {
4631        CAPTURED_SUPPORT_NO_GAME.get_or_init(|| Mutex::new(Vec::new()))
4632    }
4633
4634    fn reset_captured_messages() {
4635        captured_messages()
4636            .lock()
4637            .expect("message capture mutex poisoned")
4638            .clear();
4639    }
4640
4641    fn reset_captured_extended_messages() {
4642        captured_extended_messages()
4643            .lock()
4644            .expect("extended message capture mutex poisoned")
4645            .clear();
4646    }
4647
4648    fn reset_captured_video_refreshes() {
4649        captured_video_refreshes()
4650            .lock()
4651            .expect("video refresh capture mutex poisoned")
4652            .clear();
4653    }
4654
4655    fn reset_captured_input_queries() {
4656        captured_input_queries()
4657            .lock()
4658            .expect("input query capture mutex poisoned")
4659            .clear();
4660    }
4661
4662    fn reset_captured_input_descriptors() {
4663        captured_input_descriptors()
4664            .lock()
4665            .expect("input descriptor capture mutex poisoned")
4666            .clear();
4667    }
4668
4669    fn reset_captured_controller_info() {
4670        captured_controller_info()
4671            .lock()
4672            .expect("controller info capture mutex poisoned")
4673            .clear();
4674    }
4675
4676    fn reset_captured_core_options() {
4677        *captured_core_options_version()
4678            .lock()
4679            .expect("core options version capture mutex poisoned") = Some(2);
4680        *captured_core_options_v2()
4681            .lock()
4682            .expect("core options v2 capture mutex poisoned") = None;
4683        captured_core_options_v1()
4684            .lock()
4685            .expect("core options v1 capture mutex poisoned")
4686            .clear();
4687        captured_core_option_displays()
4688            .lock()
4689            .expect("core option display capture mutex poisoned")
4690            .clear();
4691        captured_variables()
4692            .lock()
4693            .expect("variable capture mutex poisoned")
4694            .clear();
4695        *captured_core_options_update_display_callback()
4696            .lock()
4697            .expect("core options update display callback capture mutex poisoned") = None;
4698    }
4699
4700    fn reset_captured_vfs_interface() {
4701        captured_vfs_interface_requests()
4702            .lock()
4703            .expect("VFS request capture mutex poisoned")
4704            .clear();
4705        captured_vfs_opens()
4706            .lock()
4707            .expect("VFS open capture mutex poisoned")
4708            .clear();
4709        *captured_vfs_closes()
4710            .lock()
4711            .expect("VFS close capture mutex poisoned") = 0;
4712        *captured_vfs_dir_closes()
4713            .lock()
4714            .expect("VFS dir close capture mutex poisoned") = 0;
4715        captured_vfs_writes()
4716            .lock()
4717            .expect("VFS write capture mutex poisoned")
4718            .clear();
4719        captured_vfs_removes()
4720            .lock()
4721            .expect("VFS remove capture mutex poisoned")
4722            .clear();
4723        captured_vfs_renames()
4724            .lock()
4725            .expect("VFS rename capture mutex poisoned")
4726            .clear();
4727        captured_vfs_mkdirs()
4728            .lock()
4729            .expect("VFS mkdir capture mutex poisoned")
4730            .clear();
4731        *captured_vfs_readdirs()
4732            .lock()
4733            .expect("VFS readdir capture mutex poisoned") = 0;
4734    }
4735
4736    fn reset_captured_memory_descriptors() {
4737        captured_memory_descriptors()
4738            .lock()
4739            .expect("memory descriptor capture mutex poisoned")
4740            .clear();
4741    }
4742
4743    fn reset_captured_subsystem_info() {
4744        captured_subsystem_info()
4745            .lock()
4746            .expect("subsystem info capture mutex poisoned")
4747            .clear();
4748    }
4749
4750    fn reset_captured_fastforwarding_overrides() {
4751        captured_fastforwarding_overrides()
4752            .lock()
4753            .expect("fastforwarding override capture mutex poisoned")
4754            .clear();
4755    }
4756
4757    fn reset_captured_proc_address_interface() {
4758        *captured_proc_address_interface()
4759            .lock()
4760            .expect("proc address interface capture mutex poisoned") = None;
4761    }
4762
4763    fn reset_software_framebuffer_pixels() {
4764        software_framebuffer_pixels()
4765            .lock()
4766            .expect("software framebuffer pixels mutex poisoned")
4767            .fill(0);
4768    }
4769
4770    fn reset_captured_led_states() {
4771        captured_led_states()
4772            .lock()
4773            .expect("LED state capture mutex poisoned")
4774            .clear();
4775    }
4776
4777    fn reset_captured_rumble_states() {
4778        captured_rumble_states()
4779            .lock()
4780            .expect("rumble state capture mutex poisoned")
4781            .clear();
4782    }
4783
4784    fn reset_captured_sensor_states() {
4785        captured_sensor_states()
4786            .lock()
4787            .expect("sensor state capture mutex poisoned")
4788            .clear();
4789    }
4790
4791    fn reset_captured_location_interface() {
4792        captured_location_intervals()
4793            .lock()
4794            .expect("location interval capture mutex poisoned")
4795            .clear();
4796        *captured_location_starts()
4797            .lock()
4798            .expect("location start capture mutex poisoned") = 0;
4799        *captured_location_stops()
4800            .lock()
4801            .expect("location stop capture mutex poisoned") = 0;
4802        *captured_location_callback()
4803            .lock()
4804            .expect("location callback capture mutex poisoned") = None;
4805    }
4806
4807    fn reset_captured_camera_interface() {
4808        *captured_camera_callback()
4809            .lock()
4810            .expect("camera callback capture mutex poisoned") = None;
4811        *captured_camera_starts()
4812            .lock()
4813            .expect("camera start capture mutex poisoned") = 0;
4814        *captured_camera_stops()
4815            .lock()
4816            .expect("camera stop capture mutex poisoned") = 0;
4817    }
4818
4819    fn reset_captured_disk_control_callbacks() {
4820        *captured_disk_control_callback()
4821            .lock()
4822            .expect("disk control callback capture mutex poisoned") = None;
4823        *captured_disk_control_ext_callback()
4824            .lock()
4825            .expect("disk control ext callback capture mutex poisoned") = None;
4826    }
4827
4828    fn reset_captured_netpacket_interface() {
4829        *captured_netpacket_callback()
4830            .lock()
4831            .expect("netpacket callback capture mutex poisoned") = None;
4832        captured_netpacket_sends()
4833            .lock()
4834            .expect("netpacket sends mutex poisoned")
4835            .clear();
4836        *captured_netpacket_polls()
4837            .lock()
4838            .expect("netpacket polls mutex poisoned") = 0;
4839    }
4840
4841    fn reset_captured_microphone_interface() {
4842        captured_mic_open_params()
4843            .lock()
4844            .expect("microphone open params mutex poisoned")
4845            .clear();
4846        captured_mic_states()
4847            .lock()
4848            .expect("microphone states mutex poisoned")
4849            .clear();
4850        *captured_mic_closes()
4851            .lock()
4852            .expect("microphone closes mutex poisoned") = 0;
4853    }
4854
4855    fn reset_captured_midi_interface() {
4856        captured_midi_writes()
4857            .lock()
4858            .expect("MIDI write capture mutex poisoned")
4859            .clear();
4860        *captured_midi_flushes()
4861            .lock()
4862            .expect("MIDI flush capture mutex poisoned") = 0;
4863        *captured_midi_probes()
4864            .lock()
4865            .expect("MIDI probe capture mutex poisoned") = 0;
4866    }
4867
4868    fn reset_captured_keyboard_callback() {
4869        *captured_keyboard_callback()
4870            .lock()
4871            .expect("keyboard callback capture mutex poisoned") = None;
4872    }
4873
4874    fn reset_captured_audio_latencies() {
4875        captured_audio_latencies()
4876            .lock()
4877            .expect("audio latency capture mutex poisoned")
4878            .clear();
4879    }
4880
4881    fn reset_captured_audio_buffer_status_callback() {
4882        *captured_audio_buffer_status_callback()
4883            .lock()
4884            .expect("audio buffer status callback capture mutex poisoned") = None;
4885    }
4886
4887    fn reset_captured_audio_callback() {
4888        *captured_audio_callback()
4889            .lock()
4890            .expect("audio callback capture mutex poisoned") = None;
4891        *captured_audio_callback_probes()
4892            .lock()
4893            .expect("audio callback probe mutex poisoned") = 0;
4894    }
4895
4896    fn reset_captured_frame_time_callback() {
4897        *captured_frame_time_callback()
4898            .lock()
4899            .expect("frame time callback capture mutex poisoned") = None;
4900    }
4901
4902    fn reset_captured_achievement_support() {
4903        captured_achievement_support()
4904            .lock()
4905            .expect("achievement support capture mutex poisoned")
4906            .clear();
4907    }
4908
4909    fn reset_captured_performance_levels() {
4910        captured_performance_levels()
4911            .lock()
4912            .expect("performance level capture mutex poisoned")
4913            .clear();
4914    }
4915
4916    fn reset_captured_perf_interface() {
4917        *captured_perf_logs()
4918            .lock()
4919            .expect("perf log capture mutex poisoned") = 0;
4920        captured_perf_registered_idents()
4921            .lock()
4922            .expect("perf registered idents mutex poisoned")
4923            .clear();
4924    }
4925
4926    fn reset_captured_rotations() {
4927        captured_rotations()
4928            .lock()
4929            .expect("rotation capture mutex poisoned")
4930            .clear();
4931    }
4932
4933    fn reset_captured_system_av_infos() {
4934        captured_system_av_infos()
4935            .lock()
4936            .expect("system av info capture mutex poisoned")
4937            .clear();
4938    }
4939
4940    fn reset_captured_shutdowns() {
4941        *captured_shutdowns()
4942            .lock()
4943            .expect("shutdown capture mutex poisoned") = 0;
4944    }
4945
4946    fn reset_captured_hw_shared_contexts() {
4947        *captured_hw_shared_contexts()
4948            .lock()
4949            .expect("HW shared context capture mutex poisoned") = 0;
4950    }
4951
4952    fn reset_captured_serialization_quirks() {
4953        captured_serialization_quirks()
4954            .lock()
4955            .expect("serialization quirk capture mutex poisoned")
4956            .clear();
4957    }
4958
4959    fn reset_captured_support_no_game() {
4960        captured_support_no_game()
4961            .lock()
4962            .expect("support-no-game capture mutex poisoned")
4963            .clear();
4964    }
4965
4966    fn reset_captured_geometries() {
4967        captured_geometries()
4968            .lock()
4969            .expect("geometry capture mutex poisoned")
4970            .clear();
4971    }
4972
4973    fn reset_lifecycle_call_counts() {
4974        *lifecycle_call_counts()
4975            .lock()
4976            .expect("lifecycle count mutex poisoned") = LifecycleCallCounts::default();
4977    }
4978
4979    fn snapshot_lifecycle_call_counts() -> LifecycleCallCounts {
4980        *lifecycle_call_counts()
4981            .lock()
4982            .expect("lifecycle count mutex poisoned")
4983    }
4984
4985    fn serial_test_guard() -> MutexGuard<'static, ()> {
4986        TEST_SERIAL_GUARD
4987            .get_or_init(|| Mutex::new(()))
4988            .lock()
4989            .expect("test serial guard mutex poisoned")
4990    }
4991
4992    fn install_global_test_core(core: impl Core) {
4993        with_state(|state| {
4994            state.reset_frontend_state();
4995            let bundle = create_core(core);
4996            state.event_handlers = bundle.event_handlers;
4997            state.core = Some(bundle.core);
4998        });
4999    }
5000
5001    fn clear_global_test_core() {
5002        with_state(|state| {
5003            state.reset_frontend_state();
5004            state.core = None;
5005        });
5006    }
5007
5008    unsafe extern "C" fn capture_input_state(port: u32, device: u32, index: u32, id: u32) -> i16 {
5009        captured_input_queries()
5010            .lock()
5011            .expect("input query capture mutex poisoned")
5012            .push(CapturedInputQuery {
5013                port,
5014                device,
5015                index,
5016                id,
5017            });
5018
5019        match (device, index, id) {
5020            (RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK) => {
5021                ((1u16 << RETRO_DEVICE_ID_JOYPAD_A) | (1u16 << RETRO_DEVICE_ID_JOYPAD_B)) as i16
5022            }
5023            (RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A) => 1,
5024            (RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) => -123,
5025            (RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_R2) => {
5026                123
5027            }
5028            (RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) => 7,
5029            (RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT) => 1,
5030            (RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_WHEELUP) => 1,
5031            (RETRO_DEVICE_POINTER, 1, RETRO_DEVICE_ID_POINTER_Y) => -77,
5032            (RETRO_DEVICE_POINTER, 1, RETRO_DEVICE_ID_POINTER_PRESSED) => 1,
5033            (RETRO_DEVICE_POINTER, 0, raw::RETRO_DEVICE_ID_POINTER_COUNT) => 2,
5034            (RETRO_DEVICE_POINTER, 1, raw::RETRO_DEVICE_ID_POINTER_IS_OFFSCREEN) => 1,
5035            (RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X) => 99,
5036            (RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_TRIGGER) => 1,
5037            (RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) => 1,
5038            _ => 0,
5039        }
5040    }
5041
5042    unsafe extern "C" fn capture_led_state(led: i32, state: i32) {
5043        captured_led_states()
5044            .lock()
5045            .expect("LED state capture mutex poisoned")
5046            .push((led, state));
5047    }
5048
5049    unsafe extern "C" fn capture_rumble_state(
5050        port: u32,
5051        effect: raw::retro_rumble_effect,
5052        strength: u16,
5053    ) -> bool {
5054        captured_rumble_states()
5055            .lock()
5056            .expect("rumble state capture mutex poisoned")
5057            .push((port, effect, strength));
5058        strength != 0
5059    }
5060
5061    unsafe extern "C" fn capture_sensor_state(
5062        port: u32,
5063        action: raw::retro_sensor_action,
5064        rate: u32,
5065    ) -> bool {
5066        captured_sensor_states()
5067            .lock()
5068            .expect("sensor state capture mutex poisoned")
5069            .push((port, action, rate));
5070        action != raw::retro_sensor_action::IlluminanceEnable
5071    }
5072
5073    unsafe extern "C" fn capture_sensor_input(port: u32, id: u32) -> f32 {
5074        port as f32 + id as f32 / 10.0
5075    }
5076
5077    unsafe extern "C" fn capture_location_start() -> bool {
5078        *captured_location_starts()
5079            .lock()
5080            .expect("location start capture mutex poisoned") += 1;
5081        true
5082    }
5083
5084    unsafe extern "C" fn capture_location_stop() {
5085        *captured_location_stops()
5086            .lock()
5087            .expect("location stop capture mutex poisoned") += 1;
5088    }
5089
5090    unsafe extern "C" fn capture_location_get_position(
5091        lat: *mut f64,
5092        lon: *mut f64,
5093        horiz_accuracy: *mut f64,
5094        vert_accuracy: *mut f64,
5095    ) -> bool {
5096        let Some(lat) = (unsafe { lat.as_mut() }) else {
5097            return false;
5098        };
5099        let Some(lon) = (unsafe { lon.as_mut() }) else {
5100            return false;
5101        };
5102        let Some(horiz_accuracy) = (unsafe { horiz_accuracy.as_mut() }) else {
5103            return false;
5104        };
5105        let Some(vert_accuracy) = (unsafe { vert_accuracy.as_mut() }) else {
5106            return false;
5107        };
5108        *lat = 12.5;
5109        *lon = -45.25;
5110        *horiz_accuracy = 3.0;
5111        *vert_accuracy = 8.0;
5112        true
5113    }
5114
5115    unsafe extern "C" fn capture_location_set_interval(interval_ms: u32, interval_distance: u32) {
5116        captured_location_intervals()
5117            .lock()
5118            .expect("location interval capture mutex poisoned")
5119            .push((interval_ms, interval_distance));
5120    }
5121
5122    unsafe extern "C" fn capture_camera_start() -> bool {
5123        *captured_camera_starts()
5124            .lock()
5125            .expect("camera start capture mutex poisoned") += 1;
5126        true
5127    }
5128
5129    unsafe extern "C" fn capture_camera_stop() {
5130        *captured_camera_stops()
5131            .lock()
5132            .expect("camera stop capture mutex poisoned") += 1;
5133    }
5134
5135    unsafe extern "C" fn capture_netpacket_send(
5136        flags: i32,
5137        buf: *const c_void,
5138        len: usize,
5139        client_id: u16,
5140    ) {
5141        let data = if buf.is_null() || len == 0 {
5142            Vec::new()
5143        } else {
5144            unsafe { std::slice::from_raw_parts(buf.cast::<u8>(), len) }.to_vec()
5145        };
5146        captured_netpacket_sends()
5147            .lock()
5148            .expect("netpacket sends mutex poisoned")
5149            .push(CapturedNetpacketSend {
5150                flags,
5151                data,
5152                client_id,
5153            });
5154    }
5155
5156    unsafe extern "C" fn capture_netpacket_poll_receive() {
5157        *captured_netpacket_polls()
5158            .lock()
5159            .expect("netpacket polls mutex poisoned") += 1;
5160    }
5161
5162    unsafe extern "C" fn capture_open_mic(
5163        params: *const raw::retro_microphone_params,
5164    ) -> *mut raw::retro_microphone {
5165        let rate = if params.is_null() {
5166            None
5167        } else {
5168            Some(unsafe { (*params).rate })
5169        };
5170        captured_mic_open_params()
5171            .lock()
5172            .expect("microphone open params mutex poisoned")
5173            .push(rate);
5174        static MIC_HANDLE: u8 = 0;
5175        (&MIC_HANDLE as *const u8).cast_mut().cast()
5176    }
5177
5178    unsafe extern "C" fn capture_close_mic(_microphone: *mut raw::retro_microphone) {
5179        *captured_mic_closes()
5180            .lock()
5181            .expect("microphone closes mutex poisoned") += 1;
5182    }
5183
5184    unsafe extern "C" fn capture_get_mic_params(
5185        microphone: *const raw::retro_microphone,
5186        params: *mut raw::retro_microphone_params,
5187    ) -> bool {
5188        let Some(params) = (unsafe { params.as_mut() }) else {
5189            return false;
5190        };
5191        if microphone.is_null() {
5192            return false;
5193        }
5194        params.rate = 22_050;
5195        true
5196    }
5197
5198    unsafe extern "C" fn capture_set_mic_state(
5199        microphone: *mut raw::retro_microphone,
5200        state: bool,
5201    ) -> bool {
5202        if microphone.is_null() {
5203            return false;
5204        }
5205        captured_mic_states()
5206            .lock()
5207            .expect("microphone states mutex poisoned")
5208            .push(state);
5209        true
5210    }
5211
5212    unsafe extern "C" fn capture_get_mic_state(microphone: *const raw::retro_microphone) -> bool {
5213        !microphone.is_null()
5214    }
5215
5216    unsafe extern "C" fn capture_read_mic(
5217        microphone: *mut raw::retro_microphone,
5218        samples: *mut i16,
5219        num_samples: usize,
5220    ) -> i32 {
5221        if microphone.is_null() || samples.is_null() {
5222            return -1;
5223        }
5224        let samples = unsafe { std::slice::from_raw_parts_mut(samples, num_samples) };
5225        for (index, sample) in samples.iter_mut().enumerate() {
5226            *sample = i16::try_from(index + 1).expect("test sample index fits i16");
5227        }
5228        i32::try_from(num_samples).expect("test sample count fits i32")
5229    }
5230
5231    unsafe extern "C" fn capture_midi_input_enabled() -> bool {
5232        true
5233    }
5234
5235    unsafe extern "C" fn capture_midi_output_enabled() -> bool {
5236        true
5237    }
5238
5239    unsafe extern "C" fn capture_midi_read(byte: *mut u8) -> bool {
5240        let Some(byte) = (unsafe { byte.as_mut() }) else {
5241            return false;
5242        };
5243        *byte = 0x90;
5244        true
5245    }
5246
5247    unsafe extern "C" fn capture_midi_write(byte: u8, delta_time: u32) -> bool {
5248        captured_midi_writes()
5249            .lock()
5250            .expect("MIDI write capture mutex poisoned")
5251            .push((byte, delta_time));
5252        byte != 0
5253    }
5254
5255    unsafe extern "C" fn capture_midi_flush() -> bool {
5256        *captured_midi_flushes()
5257            .lock()
5258            .expect("MIDI flush capture mutex poisoned") += 1;
5259        true
5260    }
5261
5262    unsafe extern "C" fn capture_perf_time_usec() -> raw::retro_time_t {
5263        123_456
5264    }
5265
5266    unsafe extern "C" fn capture_perf_counter() -> raw::retro_perf_tick_t {
5267        9_001
5268    }
5269
5270    unsafe extern "C" fn capture_perf_cpu_features() -> u64 {
5271        raw::RETRO_SIMD_SSE2 | raw::RETRO_SIMD_NEON
5272    }
5273
5274    unsafe extern "C" fn capture_perf_register(counter: *mut raw::retro_perf_counter) {
5275        let Some(counter) = (unsafe { counter.as_mut() }) else {
5276            return;
5277        };
5278        let ident = if counter.ident.is_null() {
5279            String::new()
5280        } else {
5281            unsafe { CStr::from_ptr(counter.ident) }
5282                .to_string_lossy()
5283                .into_owned()
5284        };
5285        captured_perf_registered_idents()
5286            .lock()
5287            .expect("perf registered idents mutex poisoned")
5288            .push(ident);
5289        counter.registered = true;
5290    }
5291
5292    unsafe extern "C" fn capture_perf_start(counter: *mut raw::retro_perf_counter) {
5293        let Some(counter) = (unsafe { counter.as_mut() }) else {
5294            return;
5295        };
5296        counter.start = 9_001;
5297        counter.call_cnt += 1;
5298    }
5299
5300    unsafe extern "C" fn capture_perf_stop(counter: *mut raw::retro_perf_counter) {
5301        let Some(counter) = (unsafe { counter.as_mut() }) else {
5302            return;
5303        };
5304        counter.total += 377;
5305    }
5306
5307    unsafe extern "C" fn capture_perf_log() {
5308        *captured_perf_logs()
5309            .lock()
5310            .expect("perf log capture mutex poisoned") += 1;
5311    }
5312
5313    unsafe fn capture_required_cstr(value: *const c_char) -> String {
5314        assert!(!value.is_null());
5315        unsafe { CStr::from_ptr(value) }
5316            .to_string_lossy()
5317            .into_owned()
5318    }
5319
5320    unsafe fn capture_optional_cstr(value: *const c_char) -> Option<String> {
5321        (!value.is_null()).then(|| unsafe { capture_required_cstr(value) })
5322    }
5323
5324    unsafe fn capture_core_option_values(
5325        values: &[raw::retro_core_option_value; raw::RETRO_NUM_CORE_OPTION_VALUES_MAX],
5326    ) -> Vec<CapturedCoreOptionValue> {
5327        values
5328            .iter()
5329            .take_while(|value| !value.value.is_null())
5330            .map(|value| CapturedCoreOptionValue {
5331                value: unsafe { capture_required_cstr(value.value) },
5332                label: unsafe { capture_optional_cstr(value.label) },
5333            })
5334            .collect()
5335    }
5336
5337    unsafe fn capture_v1_definitions(
5338        mut current: *const raw::retro_core_option_definition,
5339    ) -> Vec<CapturedCoreOptionDefinition> {
5340        let mut definitions = Vec::new();
5341        while !current.is_null() {
5342            let definition = unsafe { &*current };
5343            if definition.key.is_null() {
5344                break;
5345            }
5346            definitions.push(CapturedCoreOptionDefinition {
5347                key: unsafe { capture_required_cstr(definition.key) },
5348                description: unsafe { capture_required_cstr(definition.desc) },
5349                description_categorized: None,
5350                info: unsafe { capture_optional_cstr(definition.info) },
5351                info_categorized: None,
5352                category_key: None,
5353                values: unsafe { capture_core_option_values(&definition.values) },
5354                default_value: unsafe { capture_required_cstr(definition.default_value) },
5355            });
5356            current = unsafe { current.add(1) };
5357        }
5358        definitions
5359    }
5360
5361    unsafe fn capture_v2_options(
5362        options: *const raw::retro_core_options_v2,
5363    ) -> CapturedCoreOptionsV2 {
5364        let options = unsafe { &*options };
5365        let mut categories = Vec::new();
5366        let mut category = options.categories;
5367        while !category.is_null() {
5368            let raw_category = unsafe { &*category };
5369            if raw_category.key.is_null() {
5370                break;
5371            }
5372            categories.push(CapturedCoreOptionCategory {
5373                key: unsafe { capture_required_cstr(raw_category.key) },
5374                description: unsafe { capture_required_cstr(raw_category.desc) },
5375                info: unsafe { capture_optional_cstr(raw_category.info) },
5376            });
5377            category = unsafe { category.add(1) };
5378        }
5379
5380        let mut definitions = Vec::new();
5381        let mut definition = options.definitions;
5382        while !definition.is_null() {
5383            let raw_definition = unsafe { &*definition };
5384            if raw_definition.key.is_null() {
5385                break;
5386            }
5387            definitions.push(CapturedCoreOptionDefinition {
5388                key: unsafe { capture_required_cstr(raw_definition.key) },
5389                description: unsafe { capture_required_cstr(raw_definition.desc) },
5390                description_categorized: unsafe {
5391                    capture_optional_cstr(raw_definition.desc_categorized)
5392                },
5393                info: unsafe { capture_optional_cstr(raw_definition.info) },
5394                info_categorized: unsafe { capture_optional_cstr(raw_definition.info_categorized) },
5395                category_key: unsafe { capture_optional_cstr(raw_definition.category_key) },
5396                values: unsafe { capture_core_option_values(&raw_definition.values) },
5397                default_value: unsafe { capture_required_cstr(raw_definition.default_value) },
5398            });
5399            definition = unsafe { definition.add(1) };
5400        }
5401
5402        CapturedCoreOptionsV2 {
5403            categories,
5404            definitions,
5405        }
5406    }
5407
5408    unsafe extern "C" fn core_options_env(command: u32, data: *mut c_void) -> bool {
5409        match command {
5410            raw::RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION => {
5411                let Some(version) = *captured_core_options_version()
5412                    .lock()
5413                    .expect("core options version capture mutex poisoned")
5414                else {
5415                    return false;
5416                };
5417                unsafe { *data.cast::<u32>() = version };
5418                true
5419            }
5420            raw::RETRO_ENVIRONMENT_SET_VARIABLES => {
5421                *captured_variables()
5422                    .lock()
5423                    .expect("variable capture mutex poisoned") =
5424                    unsafe { capture_variables(data.cast::<RawVariable>()) };
5425                true
5426            }
5427            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS => {
5428                *captured_core_options_v1()
5429                    .lock()
5430                    .expect("core options v1 capture mutex poisoned") =
5431                    unsafe { capture_v1_definitions(data.cast()) };
5432                true
5433            }
5434            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL => {
5435                let raw = unsafe { &*data.cast::<raw::retro_core_options_intl>() };
5436                *captured_core_options_v1()
5437                    .lock()
5438                    .expect("core options v1 capture mutex poisoned") =
5439                    unsafe { capture_v1_definitions(raw.us) };
5440                true
5441            }
5442            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 => {
5443                *captured_core_options_v2()
5444                    .lock()
5445                    .expect("core options v2 capture mutex poisoned") =
5446                    Some(unsafe { capture_v2_options(data.cast()) });
5447                true
5448            }
5449            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL => {
5450                let raw = unsafe { &*data.cast::<raw::retro_core_options_v2_intl>() };
5451                *captured_core_options_v2()
5452                    .lock()
5453                    .expect("core options v2 capture mutex poisoned") =
5454                    Some(unsafe { capture_v2_options(raw.us) });
5455                true
5456            }
5457            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY => {
5458                let raw = unsafe { &*data.cast::<raw::retro_core_option_display>() };
5459                captured_core_option_displays()
5460                    .lock()
5461                    .expect("core option display capture mutex poisoned")
5462                    .push(CapturedCoreOptionDisplay {
5463                        key: unsafe { capture_required_cstr(raw.key) },
5464                        visible: raw.visible,
5465                    });
5466                true
5467            }
5468            raw::RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK => {
5469                *captured_core_options_update_display_callback()
5470                    .lock()
5471                    .expect("core options update display callback capture mutex poisoned") =
5472                    Some(unsafe {
5473                        *data.cast::<raw::retro_core_options_update_display_callback>()
5474                    });
5475                true
5476            }
5477            raw::RETRO_ENVIRONMENT_SET_VARIABLE => {
5478                let raw = unsafe { &*data.cast::<RawVariable>() };
5479                captured_variables()
5480                    .lock()
5481                    .expect("variable capture mutex poisoned")
5482                    .push(CapturedVariable {
5483                        key: unsafe { capture_required_cstr(raw.key) },
5484                        value: unsafe { capture_optional_cstr(raw.value) },
5485                    });
5486                true
5487            }
5488            _ => false,
5489        }
5490    }
5491
5492    unsafe fn capture_variables(mut current: *const RawVariable) -> Vec<CapturedVariable> {
5493        let mut variables = Vec::new();
5494        while !current.is_null() {
5495            let variable = unsafe { &*current };
5496            if variable.key.is_null() {
5497                break;
5498            }
5499            variables.push(CapturedVariable {
5500                key: unsafe { capture_required_cstr(variable.key) },
5501                value: unsafe { capture_optional_cstr(variable.value) },
5502            });
5503            current = unsafe { current.add(1) };
5504        }
5505        variables
5506    }
5507
5508    unsafe extern "C" fn vfs_env(command: u32, data: *mut c_void) -> bool {
5509        if command != raw::RETRO_ENVIRONMENT_GET_VFS_INTERFACE {
5510            return false;
5511        }
5512        let info = unsafe { data.cast::<raw::retro_vfs_interface_info>().as_mut() }
5513            .expect("VFS interface info must be non-null");
5514        captured_vfs_interface_requests()
5515            .lock()
5516            .expect("VFS request capture mutex poisoned")
5517            .push(info.required_interface_version);
5518        if info.required_interface_version > 3 {
5519            return false;
5520        }
5521        info.required_interface_version = 3;
5522        info.iface = (&FRONTEND_VFS_INTERFACE as *const raw::retro_vfs_interface).cast_mut();
5523        true
5524    }
5525
5526    fn fake_vfs_file_handle() -> *mut raw::retro_vfs_file_handle {
5527        std::ptr::dangling_mut::<raw::retro_vfs_file_handle>()
5528    }
5529
5530    fn fake_vfs_dir_handle() -> *mut raw::retro_vfs_dir_handle {
5531        std::ptr::dangling_mut::<raw::retro_vfs_dir_handle>()
5532    }
5533
5534    unsafe extern "C" fn capture_vfs_get_path(
5535        _stream: *mut raw::retro_vfs_file_handle,
5536    ) -> *const c_char {
5537        c"/tmp/test.bin".as_ptr()
5538    }
5539
5540    unsafe extern "C" fn capture_vfs_open(
5541        path: *const c_char,
5542        mode: u32,
5543        hints: u32,
5544    ) -> *mut raw::retro_vfs_file_handle {
5545        captured_vfs_opens()
5546            .lock()
5547            .expect("VFS open capture mutex poisoned")
5548            .push(CapturedVfsOpen {
5549                path: unsafe { capture_required_cstr(path) },
5550                mode,
5551                hints,
5552            });
5553        fake_vfs_file_handle()
5554    }
5555
5556    unsafe extern "C" fn capture_vfs_close(_stream: *mut raw::retro_vfs_file_handle) -> i32 {
5557        *captured_vfs_closes()
5558            .lock()
5559            .expect("VFS close capture mutex poisoned") += 1;
5560        0
5561    }
5562
5563    unsafe extern "C" fn capture_vfs_size(_stream: *mut raw::retro_vfs_file_handle) -> i64 {
5564        8
5565    }
5566
5567    unsafe extern "C" fn capture_vfs_truncate(
5568        _stream: *mut raw::retro_vfs_file_handle,
5569        length: i64,
5570    ) -> i64 {
5571        if length == 4 { 0 } else { -1 }
5572    }
5573
5574    unsafe extern "C" fn capture_vfs_tell(_stream: *mut raw::retro_vfs_file_handle) -> i64 {
5575        3
5576    }
5577
5578    unsafe extern "C" fn capture_vfs_seek(
5579        _stream: *mut raw::retro_vfs_file_handle,
5580        offset: i64,
5581        seek_position: i32,
5582    ) -> i64 {
5583        assert_eq!(offset, -2);
5584        assert_eq!(seek_position, raw::RETRO_VFS_SEEK_POSITION_END);
5585        6
5586    }
5587
5588    unsafe extern "C" fn capture_vfs_read(
5589        _stream: *mut raw::retro_vfs_file_handle,
5590        out: *mut c_void,
5591        len: u64,
5592    ) -> i64 {
5593        let bytes = b"abc";
5594        assert!(len >= bytes.len() as u64);
5595        unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.cast::<u8>(), bytes.len()) };
5596        bytes.len() as i64
5597    }
5598
5599    unsafe extern "C" fn capture_vfs_write(
5600        _stream: *mut raw::retro_vfs_file_handle,
5601        data: *const c_void,
5602        len: u64,
5603    ) -> i64 {
5604        let bytes = unsafe { std::slice::from_raw_parts(data.cast::<u8>(), len as usize) };
5605        captured_vfs_writes()
5606            .lock()
5607            .expect("VFS write capture mutex poisoned")
5608            .push(bytes.to_vec());
5609        len as i64
5610    }
5611
5612    unsafe extern "C" fn capture_vfs_flush(_stream: *mut raw::retro_vfs_file_handle) -> i32 {
5613        0
5614    }
5615
5616    unsafe extern "C" fn capture_vfs_remove(path: *const c_char) -> i32 {
5617        captured_vfs_removes()
5618            .lock()
5619            .expect("VFS remove capture mutex poisoned")
5620            .push(unsafe { capture_required_cstr(path) });
5621        0
5622    }
5623
5624    unsafe extern "C" fn capture_vfs_rename(
5625        old_path: *const c_char,
5626        new_path: *const c_char,
5627    ) -> i32 {
5628        captured_vfs_renames()
5629            .lock()
5630            .expect("VFS rename capture mutex poisoned")
5631            .push(CapturedVfsRename {
5632                old_path: unsafe { capture_required_cstr(old_path) },
5633                new_path: unsafe { capture_required_cstr(new_path) },
5634            });
5635        0
5636    }
5637
5638    unsafe extern "C" fn capture_vfs_stat(path: *const c_char, size: *mut i32) -> i32 {
5639        assert_eq!(unsafe { capture_required_cstr(path) }, "/tmp/test.bin");
5640        unsafe { *size = 8 };
5641        (raw::RETRO_VFS_STAT_IS_VALID | raw::RETRO_VFS_STAT_IS_DIRECTORY) as i32
5642    }
5643
5644    unsafe extern "C" fn capture_vfs_mkdir(path: *const c_char) -> i32 {
5645        captured_vfs_mkdirs()
5646            .lock()
5647            .expect("VFS mkdir capture mutex poisoned")
5648            .push(unsafe { capture_required_cstr(path) });
5649        0
5650    }
5651
5652    unsafe extern "C" fn capture_vfs_opendir(
5653        path: *const c_char,
5654        include_hidden: bool,
5655    ) -> *mut raw::retro_vfs_dir_handle {
5656        assert_eq!(unsafe { capture_required_cstr(path) }, "/tmp");
5657        assert!(include_hidden);
5658        fake_vfs_dir_handle()
5659    }
5660
5661    unsafe extern "C" fn capture_vfs_readdir(_dirstream: *mut raw::retro_vfs_dir_handle) -> bool {
5662        let mut calls = captured_vfs_readdirs()
5663            .lock()
5664            .expect("VFS readdir capture mutex poisoned");
5665        *calls += 1;
5666        *calls == 1
5667    }
5668
5669    unsafe extern "C" fn capture_vfs_dirent_get_name(
5670        _dirstream: *mut raw::retro_vfs_dir_handle,
5671    ) -> *const c_char {
5672        c"entry.bin".as_ptr()
5673    }
5674
5675    unsafe extern "C" fn capture_vfs_dirent_is_dir(
5676        _dirstream: *mut raw::retro_vfs_dir_handle,
5677    ) -> bool {
5678        false
5679    }
5680
5681    unsafe extern "C" fn capture_vfs_closedir(_dirstream: *mut raw::retro_vfs_dir_handle) -> i32 {
5682        *captured_vfs_dir_closes()
5683            .lock()
5684            .expect("VFS dir close capture mutex poisoned") += 1;
5685        0
5686    }
5687
5688    unsafe extern "C" fn frontend_services_env(command: u32, data: *mut c_void) -> bool {
5689        match command {
5690            raw::RETRO_ENVIRONMENT_SET_ROTATION => {
5691                let rotation =
5692                    unsafe { data.cast::<u32>().as_ref() }.expect("rotation data must be non-null");
5693                captured_rotations()
5694                    .lock()
5695                    .expect("rotation capture mutex poisoned")
5696                    .push(*rotation);
5697                true
5698            }
5699            raw::RETRO_ENVIRONMENT_GET_OVERSCAN => {
5700                unsafe { *data.cast::<bool>() = false };
5701                true
5702            }
5703            raw::RETRO_ENVIRONMENT_GET_CAN_DUPE => {
5704                unsafe { *data.cast::<bool>() = true };
5705                true
5706            }
5707            raw::RETRO_ENVIRONMENT_SHUTDOWN => {
5708                *captured_shutdowns()
5709                    .lock()
5710                    .expect("shutdown capture mutex poisoned") += 1;
5711                true
5712            }
5713            raw::RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT => {
5714                assert!(data.is_null());
5715                *captured_hw_shared_contexts()
5716                    .lock()
5717                    .expect("HW shared context capture mutex poisoned") += 1;
5718                true
5719            }
5720            raw::RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY => {
5721                unsafe { *data.cast::<*const c_char>() = c"/system".as_ptr() };
5722                true
5723            }
5724            raw::RETRO_ENVIRONMENT_GET_LIBRETRO_PATH => {
5725                unsafe { *data.cast::<*const c_char>() = c"/cores/test_libretro.so".as_ptr() };
5726                true
5727            }
5728            raw::RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY => {
5729                unsafe { *data.cast::<*const c_char>() = c"/assets".as_ptr() };
5730                true
5731            }
5732            raw::RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY => {
5733                unsafe { *data.cast::<*const c_char>() = c"/saves".as_ptr() };
5734                true
5735            }
5736            raw::RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO => {
5737                let info = unsafe { data.cast::<raw::retro_system_av_info>().as_ref() }
5738                    .expect("system av info data must be non-null");
5739                captured_system_av_infos()
5740                    .lock()
5741                    .expect("system av info capture mutex poisoned")
5742                    .push(SystemAvInfo::from_raw(*info));
5743                true
5744            }
5745            raw::RETRO_ENVIRONMENT_GET_GAME_INFO_EXT => {
5746                unsafe {
5747                    *data.cast::<*const raw::retro_game_info_ext>() = extended_game_info_ptr()
5748                };
5749                true
5750            }
5751            raw::RETRO_ENVIRONMENT_GET_USERNAME => {
5752                unsafe { *data.cast::<*const c_char>() = c"player".as_ptr() };
5753                true
5754            }
5755            raw::RETRO_ENVIRONMENT_GET_PLAYLIST_DIRECTORY => {
5756                unsafe { *data.cast::<*const c_char>() = c"/playlists".as_ptr() };
5757                true
5758            }
5759            raw::RETRO_ENVIRONMENT_GET_FILE_BROWSER_START_DIRECTORY => {
5760                unsafe { *data.cast::<*const c_char>() = c"/browser".as_ptr() };
5761                true
5762            }
5763            raw::RETRO_ENVIRONMENT_GET_LANGUAGE => {
5764                unsafe { *data.cast::<i32>() = Language::PortugueseBrazil.as_raw() };
5765                true
5766            }
5767            raw::RETRO_ENVIRONMENT_GET_JIT_CAPABLE => {
5768                unsafe { *data.cast::<bool>() = true };
5769                true
5770            }
5771            raw::RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS => {
5772                let supported = unsafe { data.cast::<bool>().as_ref() }
5773                    .expect("achievement support data must be non-null");
5774                captured_achievement_support()
5775                    .lock()
5776                    .expect("achievement support capture mutex poisoned")
5777                    .push(*supported);
5778                true
5779            }
5780            raw::RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL => {
5781                let level = unsafe { data.cast::<u32>().as_ref() }
5782                    .expect("performance level data must be non-null");
5783                captured_performance_levels()
5784                    .lock()
5785                    .expect("performance level capture mutex poisoned")
5786                    .push(*level);
5787                true
5788            }
5789            raw::RETRO_ENVIRONMENT_GET_PERF_INTERFACE => {
5790                unsafe {
5791                    *data.cast::<raw::retro_perf_callback>() = raw::retro_perf_callback {
5792                        get_time_usec: Some(capture_perf_time_usec),
5793                        get_cpu_features: Some(capture_perf_cpu_features),
5794                        get_perf_counter: Some(capture_perf_counter),
5795                        perf_register: Some(capture_perf_register),
5796                        perf_start: Some(capture_perf_start),
5797                        perf_stop: Some(capture_perf_stop),
5798                        perf_log: Some(capture_perf_log),
5799                    }
5800                };
5801                true
5802            }
5803            raw::RETRO_ENVIRONMENT_GET_DEVICE_POWER => {
5804                unsafe {
5805                    *data.cast::<raw::retro_device_power>() = raw::retro_device_power {
5806                        state: raw::retro_power_state::Discharging,
5807                        seconds: 3600,
5808                        percent: 72,
5809                    }
5810                };
5811                true
5812            }
5813            raw::RETRO_ENVIRONMENT_GET_NETPLAY_CLIENT_INDEX => {
5814                unsafe { *data.cast::<u32>() = 2 };
5815                true
5816            }
5817            raw::RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION => {
5818                unsafe { *data.cast::<u32>() = 1 };
5819                true
5820            }
5821            raw::RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS => {
5822                let quirks = unsafe { data.cast::<u64>().as_mut() }
5823                    .expect("serialization quirks data must be non-null");
5824                captured_serialization_quirks()
5825                    .lock()
5826                    .expect("serialization quirk capture mutex poisoned")
5827                    .push(*quirks);
5828                *quirks &= raw::RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE
5829                    | raw::RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT;
5830                true
5831            }
5832            raw::RETRO_ENVIRONMENT_SET_MEMORY_MAPS => {
5833                let map = unsafe { data.cast::<raw::retro_memory_map>().as_ref() }
5834                    .expect("memory map data must be non-null");
5835                let descriptors = unsafe {
5836                    std::slice::from_raw_parts(map.descriptors, map.num_descriptors as usize)
5837                };
5838                let captured = descriptors
5839                    .iter()
5840                    .map(|descriptor| CapturedMemoryDescriptor {
5841                        flags: descriptor.flags,
5842                        ptr_is_null: descriptor.ptr.is_null(),
5843                        offset: descriptor.offset,
5844                        start: descriptor.start,
5845                        select: descriptor.select,
5846                        disconnect: descriptor.disconnect,
5847                        len: descriptor.len,
5848                        addrspace: if descriptor.addrspace.is_null() {
5849                            None
5850                        } else {
5851                            Some(
5852                                unsafe { CStr::from_ptr(descriptor.addrspace) }
5853                                    .to_string_lossy()
5854                                    .into_owned(),
5855                            )
5856                        },
5857                    })
5858                    .collect::<Vec<_>>();
5859                *captured_memory_descriptors()
5860                    .lock()
5861                    .expect("memory descriptor capture mutex poisoned") = captured;
5862                true
5863            }
5864            raw::RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO => {
5865                let mut captured = Vec::new();
5866                let mut current = data.cast::<raw::retro_subsystem_info>();
5867                loop {
5868                    let subsystem = unsafe { *current };
5869                    if subsystem.desc.is_null()
5870                        && subsystem.ident.is_null()
5871                        && subsystem.roms.is_null()
5872                        && subsystem.num_roms == 0
5873                        && subsystem.id == 0
5874                    {
5875                        break;
5876                    }
5877                    let roms = if subsystem.roms.is_null() {
5878                        Vec::new()
5879                    } else {
5880                        unsafe {
5881                            std::slice::from_raw_parts(subsystem.roms, subsystem.num_roms as usize)
5882                        }
5883                        .to_vec()
5884                    };
5885                    captured.push(CapturedSubsystem {
5886                        description: unsafe { CStr::from_ptr(subsystem.desc) }
5887                            .to_string_lossy()
5888                            .into_owned(),
5889                        identifier: unsafe { CStr::from_ptr(subsystem.ident) }
5890                            .to_string_lossy()
5891                            .into_owned(),
5892                        id: subsystem.id,
5893                        roms: roms
5894                            .iter()
5895                            .map(|rom| {
5896                                let memory = if rom.memory.is_null() {
5897                                    Vec::new()
5898                                } else {
5899                                    unsafe {
5900                                        std::slice::from_raw_parts(
5901                                            rom.memory,
5902                                            rom.num_memory as usize,
5903                                        )
5904                                    }
5905                                    .iter()
5906                                    .map(|memory| CapturedSubsystemMemory {
5907                                        extension: unsafe { CStr::from_ptr(memory.extension) }
5908                                            .to_string_lossy()
5909                                            .into_owned(),
5910                                        memory_type: memory.memory_type,
5911                                    })
5912                                    .collect()
5913                                };
5914                                CapturedSubsystemRom {
5915                                    description: unsafe { CStr::from_ptr(rom.desc) }
5916                                        .to_string_lossy()
5917                                        .into_owned(),
5918                                    valid_extensions: unsafe {
5919                                        CStr::from_ptr(rom.valid_extensions)
5920                                    }
5921                                    .to_string_lossy()
5922                                    .into_owned(),
5923                                    need_fullpath: rom.need_fullpath,
5924                                    block_extract: rom.block_extract,
5925                                    required: rom.required,
5926                                    memory,
5927                                }
5928                            })
5929                            .collect(),
5930                    });
5931                    current = unsafe { current.add(1) };
5932                }
5933                *captured_subsystem_info()
5934                    .lock()
5935                    .expect("subsystem info capture mutex poisoned") = captured;
5936                true
5937            }
5938            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE => {
5939                *captured_disk_control_callback()
5940                    .lock()
5941                    .expect("disk control callback capture mutex poisoned") = if data.is_null() {
5942                    None
5943                } else {
5944                    Some(unsafe { *data.cast::<raw::retro_disk_control_callback>() })
5945                };
5946                true
5947            }
5948            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE => {
5949                *captured_disk_control_ext_callback()
5950                    .lock()
5951                    .expect("disk control ext callback capture mutex poisoned") = if data.is_null()
5952                {
5953                    None
5954                } else {
5955                    Some(unsafe { *data.cast::<raw::retro_disk_control_ext_callback>() })
5956                };
5957                true
5958            }
5959            raw::RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE => {
5960                *captured_netpacket_callback()
5961                    .lock()
5962                    .expect("netpacket callback capture mutex poisoned") = if data.is_null() {
5963                    None
5964                } else {
5965                    Some(CapturedNetpacketCallback::from_raw(unsafe {
5966                        *data.cast::<raw::retro_netpacket_callback>()
5967                    }))
5968                };
5969                true
5970            }
5971            raw::RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE => {
5972                let interface = unsafe { data.cast::<raw::retro_microphone_interface>().as_mut() }
5973                    .expect("microphone interface data must be non-null");
5974                assert_eq!(
5975                    interface.interface_version,
5976                    raw::RETRO_MICROPHONE_INTERFACE_VERSION
5977                );
5978                *interface = raw::retro_microphone_interface {
5979                    interface_version: raw::RETRO_MICROPHONE_INTERFACE_VERSION,
5980                    open_mic: Some(capture_open_mic),
5981                    close_mic: Some(capture_close_mic),
5982                    get_params: Some(capture_get_mic_params),
5983                    set_mic_state: Some(capture_set_mic_state),
5984                    get_mic_state: Some(capture_get_mic_state),
5985                    read_mic: Some(capture_read_mic),
5986                };
5987                true
5988            }
5989            raw::RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER => {
5990                let framebuffer = unsafe { data.cast::<raw::retro_framebuffer>().as_mut() }
5991                    .expect("software framebuffer data must be non-null");
5992                assert_eq!(framebuffer.width, 4);
5993                assert_eq!(framebuffer.height, 2);
5994                assert_eq!(
5995                    framebuffer.access_flags,
5996                    raw::RETRO_MEMORY_ACCESS_WRITE | raw::RETRO_MEMORY_ACCESS_READ
5997                );
5998
5999                let mut pixels = software_framebuffer_pixels()
6000                    .lock()
6001                    .expect("software framebuffer pixels mutex poisoned");
6002                framebuffer.data = pixels.as_mut_ptr().cast::<c_void>();
6003                framebuffer.pitch = 4 * mem::size_of::<u32>();
6004                framebuffer.format = PixelFormat::Xrgb8888;
6005                framebuffer.memory_flags = raw::RETRO_MEMORY_TYPE_CACHED;
6006                true
6007            }
6008            raw::RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES => {
6009                unsafe {
6010                    *data.cast::<u64>() = (1u64 << raw::RETRO_DEVICE_JOYPAD)
6011                        | (1u64 << raw::RETRO_DEVICE_ANALOG)
6012                        | (1u64 << raw::RETRO_DEVICE_POINTER)
6013                };
6014                true
6015            }
6016            raw::RETRO_ENVIRONMENT_GET_INPUT_BITMASKS => true,
6017            raw::RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS => {
6018                unsafe { *data.cast::<u32>() = 4 };
6019                true
6020            }
6021            raw::RETRO_ENVIRONMENT_SET_CONTROLLER_INFO => {
6022                let mut ports = Vec::new();
6023                let mut current = data.cast::<raw::retro_controller_info>();
6024                loop {
6025                    let port = unsafe { *current };
6026                    if port.types.is_null() && port.num_types == 0 {
6027                        break;
6028                    }
6029                    let types =
6030                        unsafe { std::slice::from_raw_parts(port.types, port.num_types as usize) };
6031                    ports.push(
6032                        types
6033                            .iter()
6034                            .map(|description| CapturedControllerDescription {
6035                                description: unsafe { CStr::from_ptr(description.desc) }
6036                                    .to_string_lossy()
6037                                    .into_owned(),
6038                                id: description.id,
6039                            })
6040                            .collect::<Vec<_>>(),
6041                    );
6042                    current = unsafe { current.add(1) };
6043                }
6044                *captured_controller_info()
6045                    .lock()
6046                    .expect("controller info capture mutex poisoned") = ports;
6047                true
6048            }
6049            raw::RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK => {
6050                *captured_proc_address_interface()
6051                    .lock()
6052                    .expect("proc address interface capture mutex poisoned") =
6053                    Some(unsafe { *data.cast::<raw::retro_get_proc_address_interface>() });
6054                true
6055            }
6056            raw::RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS => {
6057                let mut descriptors = Vec::new();
6058                let mut current = data.cast::<RawInputDescriptor>();
6059                loop {
6060                    let descriptor = unsafe { *current };
6061                    if descriptor.description.is_null() {
6062                        break;
6063                    }
6064                    descriptors.push(CapturedInputDescriptor {
6065                        port: descriptor.port,
6066                        device: descriptor.device,
6067                        index: descriptor.index,
6068                        id: descriptor.id,
6069                        description: unsafe { CStr::from_ptr(descriptor.description) }
6070                            .to_string_lossy()
6071                            .into_owned(),
6072                    });
6073                    current = unsafe { current.add(1) };
6074                }
6075                *captured_input_descriptors()
6076                    .lock()
6077                    .expect("input descriptor capture mutex poisoned") = descriptors;
6078                true
6079            }
6080            raw::RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK => {
6081                *captured_keyboard_callback()
6082                    .lock()
6083                    .expect("keyboard callback capture mutex poisoned") =
6084                    Some(unsafe { *data.cast::<RawKeyboardCallback>() });
6085                true
6086            }
6087            raw::RETRO_ENVIRONMENT_GET_LED_INTERFACE => {
6088                unsafe {
6089                    *data.cast::<raw::retro_led_interface>() = raw::retro_led_interface {
6090                        set_led_state: Some(capture_led_state),
6091                    }
6092                };
6093                true
6094            }
6095            raw::RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE => {
6096                unsafe {
6097                    *data.cast::<raw::retro_rumble_interface>() = raw::retro_rumble_interface {
6098                        set_rumble_state: Some(capture_rumble_state),
6099                    }
6100                };
6101                true
6102            }
6103            raw::RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE => {
6104                unsafe {
6105                    *data.cast::<raw::retro_sensor_interface>() = raw::retro_sensor_interface {
6106                        set_sensor_state: Some(capture_sensor_state),
6107                        get_sensor_input: Some(capture_sensor_input),
6108                    }
6109                };
6110                true
6111            }
6112            raw::RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE => {
6113                let callback = unsafe { data.cast::<raw::retro_camera_callback>().as_mut() }
6114                    .expect("camera callback data must be non-null");
6115                assert_eq!(
6116                    callback.caps,
6117                    (CameraCapabilities::from(CameraCapability::RawFramebuffer)
6118                        | CameraCapability::OpenGlTexture)
6119                        .bits()
6120                );
6121                assert_eq!(callback.width, 320);
6122                assert_eq!(callback.height, 240);
6123                assert!(callback.frame_raw_framebuffer.is_some());
6124                assert!(callback.frame_opengl_texture.is_some());
6125                assert!(callback.initialized.is_some());
6126                assert!(callback.deinitialized.is_some());
6127                callback.start = Some(capture_camera_start);
6128                callback.stop = Some(capture_camera_stop);
6129                callback.caps = CameraCapabilities::from(CameraCapability::RawFramebuffer).bits();
6130                callback.width = 160;
6131                callback.height = 120;
6132                *captured_camera_callback()
6133                    .lock()
6134                    .expect("camera callback capture mutex poisoned") = Some(*callback);
6135                true
6136            }
6137            raw::RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE => {
6138                let callback = unsafe { data.cast::<raw::retro_location_callback>().as_mut() }
6139                    .expect("location callback data must be non-null");
6140                let initialized = callback.initialized;
6141                let deinitialized = callback.deinitialized;
6142                *callback = raw::retro_location_callback {
6143                    start: Some(capture_location_start),
6144                    stop: Some(capture_location_stop),
6145                    get_position: Some(capture_location_get_position),
6146                    set_interval: Some(capture_location_set_interval),
6147                    initialized,
6148                    deinitialized,
6149                };
6150                *captured_location_callback()
6151                    .lock()
6152                    .expect("location callback capture mutex poisoned") = Some(*callback);
6153                true
6154            }
6155            raw::RETRO_ENVIRONMENT_GET_MIDI_INTERFACE => {
6156                if data.is_null() {
6157                    *captured_midi_probes()
6158                        .lock()
6159                        .expect("MIDI probe capture mutex poisoned") += 1;
6160                } else {
6161                    unsafe {
6162                        *data.cast::<raw::retro_midi_interface>() = raw::retro_midi_interface {
6163                            input_enabled: Some(capture_midi_input_enabled),
6164                            output_enabled: Some(capture_midi_output_enabled),
6165                            read: Some(capture_midi_read),
6166                            write: Some(capture_midi_write),
6167                            flush: Some(capture_midi_flush),
6168                        }
6169                    };
6170                }
6171                true
6172            }
6173            raw::RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE => {
6174                unsafe {
6175                    *data.cast::<u32>() =
6176                        raw::RETRO_AV_ENABLE_VIDEO | raw::RETRO_AV_ENABLE_HARD_DISABLE_AUDIO
6177                };
6178                true
6179            }
6180            raw::RETRO_ENVIRONMENT_GET_FASTFORWARDING => {
6181                unsafe { *data.cast::<bool>() = true };
6182                true
6183            }
6184            raw::RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE => {
6185                let captured = if data.is_null() {
6186                    None
6187                } else {
6188                    Some(unsafe { *data.cast::<raw::retro_fastforwarding_override>() })
6189                };
6190                captured_fastforwarding_overrides()
6191                    .lock()
6192                    .expect("fastforwarding override capture mutex poisoned")
6193                    .push(captured);
6194                true
6195            }
6196            raw::RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE => {
6197                unsafe { *data.cast::<f32>() = 59.94 };
6198                true
6199            }
6200            raw::RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE => {
6201                unsafe { *data.cast::<u32>() = 48_000 };
6202                true
6203            }
6204            raw::RETRO_ENVIRONMENT_GET_THROTTLE_STATE => {
6205                unsafe {
6206                    *data.cast::<raw::retro_throttle_state>() = raw::retro_throttle_state {
6207                        mode: raw::RETRO_THROTTLE_FAST_FORWARD,
6208                        rate: 120.0,
6209                    }
6210                };
6211                true
6212            }
6213            raw::RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT => {
6214                unsafe {
6215                    *data.cast::<i32>() = raw::retro_savestate_context::RollbackNetplay as i32
6216                };
6217                true
6218            }
6219            raw::RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY => {
6220                let captured = if data.is_null() {
6221                    None
6222                } else {
6223                    Some(unsafe { *data.cast::<u32>() })
6224                };
6225                captured_audio_latencies()
6226                    .lock()
6227                    .expect("audio latency capture mutex poisoned")
6228                    .push(captured);
6229                true
6230            }
6231            raw::RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK => {
6232                if data.is_null() {
6233                    *captured_audio_callback_probes()
6234                        .lock()
6235                        .expect("audio callback probe mutex poisoned") += 1;
6236                } else {
6237                    *captured_audio_callback()
6238                        .lock()
6239                        .expect("audio callback capture mutex poisoned") =
6240                        Some(unsafe { *data.cast::<RawAudioCallback>() });
6241                }
6242                true
6243            }
6244            raw::RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK => {
6245                let callback = if data.is_null() {
6246                    None
6247                } else {
6248                    Some(unsafe { *data.cast::<RawAudioBufferStatusCallback>() })
6249                };
6250                *captured_audio_buffer_status_callback()
6251                    .lock()
6252                    .expect("audio buffer status callback capture mutex poisoned") = callback;
6253                true
6254            }
6255            raw::RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK => {
6256                let callback = if data.is_null() {
6257                    None
6258                } else {
6259                    Some(unsafe { *data.cast::<RawFrameTimeCallback>() })
6260                };
6261                *captured_frame_time_callback()
6262                    .lock()
6263                    .expect("frame time callback capture mutex poisoned") = callback;
6264                true
6265            }
6266            _ => false,
6267        }
6268    }
6269
6270    unsafe extern "C" fn null_frontend_string_env(command: u32, data: *mut c_void) -> bool {
6271        if command != raw::RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY {
6272            return false;
6273        }
6274        unsafe { *data.cast::<*const c_char>() = ptr::null() };
6275        true
6276    }
6277
6278    unsafe extern "C" fn capture_content_info_overrides(command: u32, data: *mut c_void) -> bool {
6279        if command != RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE {
6280            return false;
6281        }
6282
6283        let storage = CAPTURED_CONTENT_OVERRIDES.get_or_init(|| Mutex::new(Vec::new()));
6284        let mut captured = storage.lock().expect("capture mutex poisoned");
6285        captured.clear();
6286
6287        let mut cursor = data.cast::<RawContentInfoOverride>();
6288        loop {
6289            let item = unsafe { *cursor };
6290            if item.extensions.is_null() {
6291                break;
6292            }
6293            let extensions = unsafe { CStr::from_ptr(item.extensions) }
6294                .to_string_lossy()
6295                .into_owned();
6296            captured.push(CapturedContentOverride {
6297                extensions,
6298                need_fullpath: item.need_fullpath,
6299                persistent_data: item.persistent_data,
6300            });
6301            cursor = unsafe { cursor.add(1) };
6302        }
6303
6304        true
6305    }
6306
6307    unsafe extern "C" fn capture_content_contract_env(command: u32, data: *mut c_void) -> bool {
6308        match command {
6309            RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME => {
6310                captured_support_no_game()
6311                    .lock()
6312                    .expect("support-no-game capture mutex poisoned")
6313                    .push(unsafe { *data.cast::<bool>() });
6314                true
6315            }
6316            RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE => unsafe {
6317                capture_content_info_overrides(command, data)
6318            },
6319            _ => false,
6320        }
6321    }
6322
6323    unsafe extern "C" fn capture_content_contract_env_rejects_support_no_game(
6324        command: u32,
6325        data: *mut c_void,
6326    ) -> bool {
6327        match command {
6328            RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME => {
6329                captured_support_no_game()
6330                    .lock()
6331                    .expect("support-no-game capture mutex poisoned")
6332                    .push(unsafe { *data.cast::<bool>() });
6333                false
6334            }
6335            RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE => unsafe {
6336                capture_content_info_overrides(command, data)
6337            },
6338            _ => false,
6339        }
6340    }
6341
6342    unsafe extern "C" fn capture_message_env(command: u32, data: *mut c_void) -> bool {
6343        match command {
6344            RETRO_ENVIRONMENT_SET_MESSAGE => {
6345                let message = unsafe { *data.cast::<raw::retro_message>() };
6346                let message_text = if message.msg.is_null() {
6347                    String::new()
6348                } else {
6349                    unsafe { CStr::from_ptr(message.msg) }
6350                        .to_string_lossy()
6351                        .into_owned()
6352                };
6353                captured_messages()
6354                    .lock()
6355                    .expect("message capture mutex poisoned")
6356                    .push(CapturedMessage {
6357                        message: message_text,
6358                        frames: message.frames,
6359                    });
6360                true
6361            }
6362            raw::RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION => {
6363                unsafe { *data.cast::<u32>() = 1 };
6364                true
6365            }
6366            raw::RETRO_ENVIRONMENT_SET_MESSAGE_EXT => {
6367                let message = unsafe { *data.cast::<raw::retro_message_ext>() };
6368                let message_text = if message.msg.is_null() {
6369                    String::new()
6370                } else {
6371                    unsafe { CStr::from_ptr(message.msg) }
6372                        .to_string_lossy()
6373                        .into_owned()
6374                };
6375                captured_extended_messages()
6376                    .lock()
6377                    .expect("extended message capture mutex poisoned")
6378                    .push(CapturedExtendedMessage {
6379                        message: message_text,
6380                        duration: message.duration,
6381                        priority: message.priority,
6382                        level: message.level,
6383                        target: message.target,
6384                        kind: message.type_,
6385                        progress: message.progress,
6386                    });
6387                true
6388            }
6389            _ => false,
6390        }
6391    }
6392
6393    unsafe extern "C" fn capture_video_refresh(
6394        data: *const c_void,
6395        width: u32,
6396        height: u32,
6397        pitch: usize,
6398    ) {
6399        let data_kind = if data.is_null() {
6400            CapturedVideoDataKind::Dupe
6401        } else if data == RETRO_HW_FRAME_BUFFER_VALID {
6402            CapturedVideoDataKind::Hardware
6403        } else {
6404            CapturedVideoDataKind::Software
6405        };
6406
6407        captured_video_refreshes()
6408            .lock()
6409            .expect("video refresh capture mutex poisoned")
6410            .push(CapturedVideoRefresh {
6411                data_kind,
6412                data_addr: data as usize,
6413                width,
6414                height,
6415                pitch,
6416            });
6417    }
6418
6419    unsafe extern "C" fn fake_current_framebuffer() -> usize {
6420        99
6421    }
6422
6423    unsafe extern "C" fn fake_zero_current_framebuffer() -> usize {
6424        0
6425    }
6426
6427    unsafe extern "C" fn fake_gl_proc() {}
6428
6429    unsafe extern "C" fn fake_get_proc_address(_sym: *const c_char) -> raw::retro_proc_address_t {
6430        Some(fake_gl_proc)
6431    }
6432
6433    unsafe extern "C" fn missing_get_proc_address(
6434        _sym: *const c_char,
6435    ) -> raw::retro_proc_address_t {
6436        None
6437    }
6438
6439    unsafe extern "C" fn capture_hw_render_env(command: u32, data: *mut c_void) -> bool {
6440        let mut captured = captured_hw_render_state()
6441            .lock()
6442            .expect("hw render capture mutex poisoned");
6443
6444        match command {
6445            RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER => {
6446                let out = data.cast::<HwContextType>();
6447                unsafe { *out = captured.preferred_context_type };
6448                captured.supports_non_preferred_context
6449            }
6450            RETRO_ENVIRONMENT_SET_HW_RENDER => {
6451                let callback = unsafe { &mut *data.cast::<RawHwRenderCallback>() };
6452                captured.record_attempt(callback.context_type);
6453                if !captured.accepts(callback.context_type) {
6454                    return false;
6455                }
6456                if captured.inject_runtime_callbacks {
6457                    callback.get_current_framebuffer = Some(fake_current_framebuffer);
6458                    callback.get_proc_address = Some(fake_get_proc_address);
6459                }
6460                captured.last_callback = Some(*callback);
6461                true
6462            }
6463            raw::RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE => {
6464                unsafe {
6465                    *data.cast::<*const raw::retro_hw_render_interface>() =
6466                        &FRONTEND_HW_RENDER_INTERFACE
6467                };
6468                true
6469            }
6470            raw::RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT => {
6471                let Some(version) = captured.context_negotiation_support_version else {
6472                    return false;
6473                };
6474                let interface = unsafe {
6475                    data.cast::<raw::retro_hw_render_context_negotiation_interface>()
6476                        .as_mut()
6477                }
6478                .expect("HW render context negotiation support data must be non-null");
6479                assert_eq!(
6480                    interface.interface_type,
6481                    raw::retro_hw_render_context_negotiation_interface_type::Vulkan as i32
6482                );
6483                interface.interface_version = version;
6484                true
6485            }
6486            raw::RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE => {
6487                let interface =
6488                    unsafe { *data.cast::<raw::retro_hw_render_context_negotiation_interface>() };
6489                captured.last_context_negotiation =
6490                    Some(HwRenderContextNegotiationInterface::from_raw(interface));
6491                true
6492            }
6493            _ => false,
6494        }
6495    }
6496
6497    unsafe extern "C" fn capture_geometry_env(command: u32, data: *mut c_void) -> bool {
6498        if command != RETRO_ENVIRONMENT_SET_GEOMETRY {
6499            return false;
6500        }
6501
6502        captured_geometries()
6503            .lock()
6504            .expect("geometry capture mutex poisoned")
6505            .push(GameGeometry::from_raw(unsafe {
6506                *data.cast::<raw::retro_game_geometry>()
6507            }));
6508        true
6509    }
6510
6511    #[test]
6512    fn content_info_overrides_are_forwarded_to_frontend() {
6513        let _guard = serial_test_guard();
6514        let mut state = CoreState::default();
6515        state.callbacks.environment = Some(capture_content_info_overrides);
6516
6517        let mut env = Environment { state: &mut state };
6518        let ok = env.set_content_info_overrides(&[ContentInfoOverride {
6519            extensions: "bin".to_string(),
6520            need_fullpath: true,
6521            persistent_data: false,
6522        }]);
6523
6524        assert!(ok);
6525        assert!(env.state.content_info_overrides.is_some());
6526
6527        let captured = CAPTURED_CONTENT_OVERRIDES
6528            .get()
6529            .expect("content overrides were not captured")
6530            .lock()
6531            .expect("capture mutex poisoned")
6532            .clone();
6533        assert_eq!(
6534            captured,
6535            vec![CapturedContentOverride {
6536                extensions: "bin".to_string(),
6537                need_fullpath: true,
6538                persistent_data: false,
6539            }]
6540        );
6541    }
6542
6543    #[test]
6544    fn content_contract_does_not_send_false_support_no_game_command() {
6545        let _guard = serial_test_guard();
6546        reset_captured_support_no_game();
6547        let mut state = CoreState::default();
6548        state.callbacks.environment = Some(capture_content_contract_env_rejects_support_no_game);
6549
6550        let mut env = Environment { state: &mut state };
6551        let ok = ContentContract::new("bin")
6552            .with_need_fullpath(true)
6553            .register_environment(&mut env);
6554
6555        assert!(ok);
6556        assert!(
6557            captured_support_no_game()
6558                .lock()
6559                .expect("support-no-game capture mutex poisoned")
6560                .is_empty()
6561        );
6562        assert!(env.state.content_info_overrides.is_some());
6563    }
6564
6565    #[test]
6566    fn content_contract_sends_true_support_no_game_when_requested() {
6567        let _guard = serial_test_guard();
6568        reset_captured_support_no_game();
6569        let mut state = CoreState::default();
6570        state.callbacks.environment = Some(capture_content_contract_env);
6571
6572        let mut env = Environment { state: &mut state };
6573        let ok = ContentContract::new("bin")
6574            .with_support_no_game(true)
6575            .register_environment(&mut env);
6576
6577        assert!(ok);
6578        assert_eq!(
6579            *captured_support_no_game()
6580                .lock()
6581                .expect("support-no-game capture mutex poisoned"),
6582            vec![true]
6583        );
6584        assert!(env.state.content_info_overrides.is_some());
6585    }
6586
6587    #[test]
6588    fn set_message_is_forwarded_to_frontend() {
6589        let _guard = serial_test_guard();
6590        reset_captured_messages();
6591
6592        let mut state = CoreState::default();
6593        state.callbacks.environment = Some(capture_message_env);
6594
6595        let mut env = Environment { state: &mut state };
6596        assert!(env.set_message("frontend message", 120));
6597
6598        assert_eq!(
6599            *captured_messages()
6600                .lock()
6601                .expect("message capture mutex poisoned"),
6602            vec![CapturedMessage {
6603                message: "frontend message".to_string(),
6604                frames: 120,
6605            }]
6606        );
6607    }
6608
6609    #[test]
6610    fn extended_message_is_forwarded_to_frontend_with_typed_fields() {
6611        let _guard = serial_test_guard();
6612        reset_captured_extended_messages();
6613
6614        let mut state = CoreState::default();
6615        state.callbacks.environment = Some(capture_message_env);
6616
6617        let mut env = Environment { state: &mut state };
6618        assert_eq!(env.message_interface_version(), Some(1));
6619        assert!(
6620            env.set_message_ext(
6621                ExtendedMessage::new("loading assets")
6622                    .with_duration_millis(750)
6623                    .with_priority(3)
6624                    .with_level(LogLevel::Warn)
6625                    .with_target(MessageTarget::All)
6626                    .with_kind(MessageKind::Status)
6627                    .with_progress(MessageProgress::percent(42).expect("valid progress percent"))
6628            )
6629        );
6630
6631        assert_eq!(
6632            *captured_extended_messages()
6633                .lock()
6634                .expect("extended message capture mutex poisoned"),
6635            vec![CapturedExtendedMessage {
6636                message: "loading assets".to_string(),
6637                duration: 750,
6638                priority: 3,
6639                level: LogLevel::Warn,
6640                target: raw::retro_message_target::All,
6641                kind: raw::retro_message_type::Progress,
6642                progress: 42,
6643            }]
6644        );
6645    }
6646
6647    #[test]
6648    fn frontend_service_queries_return_rust_values() {
6649        let _guard = serial_test_guard();
6650        reset_captured_achievement_support();
6651        reset_captured_audio_latencies();
6652        reset_captured_fastforwarding_overrides();
6653        reset_captured_led_states();
6654        reset_captured_midi_interface();
6655        reset_captured_performance_levels();
6656        reset_captured_rumble_states();
6657        reset_captured_sensor_states();
6658        reset_captured_location_interface();
6659        reset_captured_rotations();
6660        reset_captured_serialization_quirks();
6661        reset_captured_shutdowns();
6662        reset_captured_hw_shared_contexts();
6663        reset_captured_system_av_infos();
6664        let mut state = CoreState::default();
6665        state.callbacks.environment = Some(frontend_services_env);
6666
6667        let mut env = Environment { state: &mut state };
6668
6669        assert_eq!(env.overscan(), Some(false));
6670        assert_eq!(env.can_dupe_frames(), Some(true));
6671        assert!(env.set_rotation(VideoRotation::CounterClockwise90));
6672        assert!(env.shutdown());
6673        assert!(env.set_hw_shared_context());
6674        assert!(env.set_system_av_info(system_av_info(game_geometry(256, 224), 60.0, 44_100.0)));
6675        assert_eq!(env.system_directory().as_deref(), Some("/system"));
6676        assert_eq!(
6677            env.libretro_path().as_deref(),
6678            Some("/cores/test_libretro.so")
6679        );
6680        assert_eq!(env.core_assets_directory().as_deref(), Some("/assets"));
6681        assert_eq!(env.content_directory().as_deref(), Some("/assets"));
6682        assert_eq!(env.save_directory().as_deref(), Some("/saves"));
6683        assert_eq!(env.username().as_deref(), Some("player"));
6684        assert_eq!(env.playlist_directory().as_deref(), Some("/playlists"));
6685        assert_eq!(
6686            env.file_browser_start_directory().as_deref(),
6687            Some("/browser")
6688        );
6689        assert_eq!(env.language(), Some(Language::PortugueseBrazil));
6690        assert_eq!(env.jit_capable(), Some(true));
6691        assert_eq!(
6692            env.disk_control_interface_version(),
6693            Some(DiskControlInterfaceVersion::new(1))
6694        );
6695        assert!(
6696            env.disk_control_interface_version()
6697                .expect("disk control version should be available")
6698                .supports_extended()
6699        );
6700        assert!(env.set_support_achievements(true));
6701        assert!(env.set_support_achievements(false));
6702        assert!(env.set_performance_level(PerformanceLevel::new(2)));
6703        assert_eq!(
6704            env.device_power(),
6705            Some(DevicePower {
6706                state: PowerState::Discharging,
6707                seconds_remaining: Some(3600),
6708                percent: Some(72),
6709            })
6710        );
6711        assert_eq!(env.netplay_client_index(), Some(NetplayClientId::new(2)));
6712        let input_capabilities = env
6713            .input_device_capabilities()
6714            .expect("input capabilities query should work");
6715        assert!(input_capabilities.contains(InputDeviceCapability::Joypad));
6716        assert!(input_capabilities.contains(InputDeviceCapability::Analog));
6717        assert!(input_capabilities.contains(InputDeviceCapability::Pointer));
6718        assert!(!input_capabilities.contains(InputDeviceCapability::Mouse));
6719        assert!(env.supports_joypad_bitmasks());
6720        assert_eq!(env.input_max_users(), Some(4));
6721        let led_interface = env
6722            .led_interface()
6723            .expect("LED interface should be available");
6724        assert!(led_interface.is_available());
6725        assert!(led_interface.set_state(LedIndex::new(2), LedState::On));
6726        assert!(led_interface.set_state(2, LedState::Off));
6727        let rumble_interface = env
6728            .rumble_interface()
6729            .expect("rumble interface should be available");
6730        assert!(rumble_interface.is_available());
6731        assert!(rumble_interface.set_state(
6732            InputPort::new(1),
6733            RumbleEffect::Strong,
6734            RumbleStrength::max()
6735        ));
6736        assert!(!rumble_interface.set_state(1, RumbleEffect::Weak, RumbleStrength::off()));
6737        let sensor_interface = env
6738            .sensor_interface()
6739            .expect("sensor interface should be available");
6740        assert!(sensor_interface.is_available());
6741        assert!(sensor_interface.enable(2, Sensor::Accelerometer, SensorRateHz::new(60)));
6742        assert!(sensor_interface.disable(2, Sensor::Gyroscope));
6743        assert!(!sensor_interface.enable(2, Sensor::Illuminance, SensorRateHz::new(15)));
6744        assert_eq!(
6745            sensor_interface.input(2, SensorInput::GyroscopeZ),
6746            Some(2.5)
6747        );
6748        let location_interface = env
6749            .location_interface()
6750            .expect("location interface should be available");
6751        assert!(location_interface.is_available());
6752        assert!(location_interface.set_interval(
6753            LocationIntervalMillis::new(1000),
6754            LocationIntervalMeters::new(25)
6755        ));
6756        assert!(location_interface.start());
6757        assert_eq!(
6758            location_interface.position(),
6759            Some(LocationPosition {
6760                latitude_degrees: 12.5,
6761                longitude_degrees: -45.25,
6762                horizontal_accuracy: 3.0,
6763                vertical_accuracy: 8.0,
6764            })
6765        );
6766        assert!(location_interface.stop());
6767        assert!(env.midi_interface_available());
6768        let midi_interface = env
6769            .midi_interface()
6770            .expect("MIDI interface should be available");
6771        assert!(midi_interface.is_available());
6772        assert!(midi_interface.input_enabled());
6773        assert!(midi_interface.output_enabled());
6774        assert_eq!(midi_interface.read_byte(), Some(0x90));
6775        assert!(midi_interface.write_byte(0x91, MidiDeltaMicros::new(240)));
6776        assert!(!midi_interface.write_byte(0, MidiDeltaMicros::new(480)));
6777        assert!(midi_interface.flush());
6778        let av_enable = env
6779            .audio_video_enable()
6780            .expect("AV enable query should work");
6781        assert!(av_enable.contains(AvEnable::Video));
6782        assert!(av_enable.contains(AvEnable::HardDisableAudio));
6783        assert!(!av_enable.contains(AvEnable::Audio));
6784        assert_eq!(env.fastforwarding(), Some(true));
6785        assert!(env.fastforwarding_override_supported());
6786        assert!(
6787            env.set_fastforwarding_override(
6788                FastForwardingOverride::enable()
6789                    .with_ratio(FastForwardRatio::multiplier(2.5).expect("valid ratio"))
6790                    .with_notification(false)
6791                    .with_inhibit_toggle(true)
6792            )
6793        );
6794        assert_eq!(
6795            env.target_refresh_rate().map(RefreshRateHz::get),
6796            Some(59.94)
6797        );
6798        assert_eq!(
6799            env.target_sample_rate().map(AudioSampleRateHz::get),
6800            Some(48_000)
6801        );
6802        assert_eq!(
6803            env.throttle_state(),
6804            Some(ThrottleState {
6805                mode: ThrottleMode::FastForward,
6806                rate: RunLoopRateHz::new(120.0),
6807            })
6808        );
6809        assert_eq!(
6810            env.savestate_context(),
6811            Some(SavestateContext::RollbackNetplay)
6812        );
6813        assert!(env.set_minimum_audio_latency(Some(AudioLatencyMillis::new(96))));
6814        assert!(env.set_minimum_audio_latency(None));
6815        let requested_quirks = SerializationQuirks::from(SerializationQuirk::MustInitialize)
6816            | SerializationQuirk::CoreVariableSize
6817            | SerializationQuirk::PlatformDependent;
6818        let supported_quirks = env
6819            .set_serialization_quirks(requested_quirks)
6820            .expect("serialization quirks should be supported");
6821        assert!(supported_quirks.contains(SerializationQuirk::MustInitialize));
6822        assert!(supported_quirks.contains(SerializationQuirk::PlatformDependent));
6823        assert!(!supported_quirks.contains(SerializationQuirk::CoreVariableSize));
6824        assert_eq!(
6825            *captured_audio_latencies()
6826                .lock()
6827                .expect("audio latency capture mutex poisoned"),
6828            vec![Some(96), None]
6829        );
6830        assert_eq!(
6831            *captured_fastforwarding_overrides()
6832                .lock()
6833                .expect("fastforwarding override capture mutex poisoned"),
6834            vec![
6835                None,
6836                Some(raw::retro_fastforwarding_override {
6837                    ratio: 2.5,
6838                    fastforward: true,
6839                    notification: false,
6840                    inhibit_toggle: true,
6841                }),
6842            ]
6843        );
6844        assert_eq!(
6845            *captured_led_states()
6846                .lock()
6847                .expect("LED state capture mutex poisoned"),
6848            vec![(2, 1), (2, 0)]
6849        );
6850        assert_eq!(
6851            *captured_rumble_states()
6852                .lock()
6853                .expect("rumble state capture mutex poisoned"),
6854            vec![
6855                (1, raw::retro_rumble_effect::Strong, u16::MAX),
6856                (1, raw::retro_rumble_effect::Weak, 0),
6857            ]
6858        );
6859        assert_eq!(
6860            *captured_sensor_states()
6861                .lock()
6862                .expect("sensor state capture mutex poisoned"),
6863            vec![
6864                (
6865                    2,
6866                    raw::retro_sensor_action::AccelerometerEnable,
6867                    SensorRateHz::new(60).as_raw(),
6868                ),
6869                (2, raw::retro_sensor_action::GyroscopeDisable, 0),
6870                (2, raw::retro_sensor_action::IlluminanceEnable, 15),
6871            ]
6872        );
6873        assert_eq!(
6874            *captured_location_intervals()
6875                .lock()
6876                .expect("location interval capture mutex poisoned"),
6877            vec![(1000, 25)]
6878        );
6879        assert_eq!(
6880            *captured_location_starts()
6881                .lock()
6882                .expect("location start capture mutex poisoned"),
6883            1
6884        );
6885        assert_eq!(
6886            *captured_location_stops()
6887                .lock()
6888                .expect("location stop capture mutex poisoned"),
6889            1
6890        );
6891        assert_eq!(
6892            *captured_midi_probes()
6893                .lock()
6894                .expect("MIDI probe capture mutex poisoned"),
6895            1
6896        );
6897        assert_eq!(
6898            *captured_midi_writes()
6899                .lock()
6900                .expect("MIDI write capture mutex poisoned"),
6901            vec![(0x91, 240), (0, 480)]
6902        );
6903        assert_eq!(
6904            *captured_midi_flushes()
6905                .lock()
6906                .expect("MIDI flush capture mutex poisoned"),
6907            1
6908        );
6909        assert_eq!(
6910            *captured_rotations()
6911                .lock()
6912                .expect("rotation capture mutex poisoned"),
6913            vec![VideoRotation::CounterClockwise90.as_raw()]
6914        );
6915        assert_eq!(
6916            *captured_shutdowns()
6917                .lock()
6918                .expect("shutdown capture mutex poisoned"),
6919            1
6920        );
6921        assert_eq!(
6922            *captured_hw_shared_contexts()
6923                .lock()
6924                .expect("HW shared context capture mutex poisoned"),
6925            1
6926        );
6927        let captured_av_infos = captured_system_av_infos()
6928            .lock()
6929            .expect("system av info capture mutex poisoned")
6930            .clone();
6931        assert_eq!(captured_av_infos.len(), 1);
6932        assert_eq!(captured_av_infos[0].geometry.base_width, 256);
6933        assert_eq!(captured_av_infos[0].geometry.base_height, 224);
6934        assert_eq!(captured_av_infos[0].timing.fps, 60.0);
6935        assert_eq!(captured_av_infos[0].timing.sample_rate, 44_100.0);
6936        assert_eq!(
6937            *captured_achievement_support()
6938                .lock()
6939                .expect("achievement support capture mutex poisoned"),
6940            vec![true, false]
6941        );
6942        assert_eq!(
6943            *captured_performance_levels()
6944                .lock()
6945                .expect("performance level capture mutex poisoned"),
6946            vec![2]
6947        );
6948        assert_eq!(
6949            *captured_serialization_quirks()
6950                .lock()
6951                .expect("serialization quirk capture mutex poisoned"),
6952            vec![requested_quirks.bits()]
6953        );
6954    }
6955
6956    #[test]
6957    fn frontend_string_query_returns_none_for_available_null_value() {
6958        let _guard = serial_test_guard();
6959        let mut state = CoreState::default();
6960        state.callbacks.environment = Some(null_frontend_string_env);
6961
6962        let mut env = Environment { state: &mut state };
6963
6964        assert_eq!(env.save_directory(), None);
6965    }
6966
6967    #[test]
6968    fn core_options_v2_registration_display_and_single_setter_are_forwarded() {
6969        let _guard = serial_test_guard();
6970        reset_captured_core_options();
6971        let mut state = CoreState::default();
6972        state.callbacks.environment = Some(core_options_env);
6973        let mut env = Environment { state: &mut state };
6974
6975        let options = CoreOptions::new([CoreOptionDefinition::new(
6976            "demo_renderer",
6977            "Renderer",
6978            "gl",
6979        )
6980        .with_category("video")
6981        .with_categorized_description("API")
6982        .with_info("Selects the renderer")
6983        .with_categorized_info("Rendering API")
6984        .with_values([
6985            CoreOptionValue::new("gl").with_label("OpenGL"),
6986            CoreOptionValue::new("soft").with_label("Software"),
6987        ])])
6988        .with_categories([CoreOptionCategory::new("video", "Video").with_info("Video settings")]);
6989
6990        assert_eq!(env.core_options_version(), CoreOptionsVersion::V2);
6991        assert!(
6992            env.set_core_options(&options)
6993                .expect("core options should build")
6994        );
6995        assert!(env.set_core_option_display(CoreOptionDisplay::new("demo_renderer", true)));
6996        assert!(env.set_variable("demo_renderer", Some("soft")));
6997
6998        let captured = captured_core_options_v2()
6999            .lock()
7000            .expect("core options v2 capture mutex poisoned")
7001            .clone()
7002            .expect("core options v2 should be captured");
7003        assert_eq!(
7004            captured.categories,
7005            vec![CapturedCoreOptionCategory {
7006                key: "video".to_string(),
7007                description: "Video".to_string(),
7008                info: Some("Video settings".to_string()),
7009            }]
7010        );
7011        assert_eq!(
7012            captured.definitions,
7013            vec![CapturedCoreOptionDefinition {
7014                key: "demo_renderer".to_string(),
7015                description: "Renderer".to_string(),
7016                description_categorized: Some("API".to_string()),
7017                info: Some("Selects the renderer".to_string()),
7018                info_categorized: Some("Rendering API".to_string()),
7019                category_key: Some("video".to_string()),
7020                values: vec![
7021                    CapturedCoreOptionValue {
7022                        value: "gl".to_string(),
7023                        label: Some("OpenGL".to_string()),
7024                    },
7025                    CapturedCoreOptionValue {
7026                        value: "soft".to_string(),
7027                        label: Some("Software".to_string()),
7028                    },
7029                ],
7030                default_value: "gl".to_string(),
7031            }]
7032        );
7033        assert_eq!(
7034            *captured_core_option_displays()
7035                .lock()
7036                .expect("core option display capture mutex poisoned"),
7037            vec![CapturedCoreOptionDisplay {
7038                key: "demo_renderer".to_string(),
7039                visible: true,
7040            }]
7041        );
7042        assert_eq!(
7043            *captured_variables()
7044                .lock()
7045                .expect("variable capture mutex poisoned"),
7046            vec![CapturedVariable {
7047                key: "demo_renderer".to_string(),
7048                value: Some("soft".to_string()),
7049            }]
7050        );
7051    }
7052
7053    #[test]
7054    fn core_options_legacy_v1_and_intl_paths_are_forwarded() {
7055        let _guard = serial_test_guard();
7056        reset_captured_core_options();
7057        let definition = CoreOptionDefinition::new("demo_speed", "Speed", "normal").with_values([
7058            CoreOptionValue::new("slow"),
7059            CoreOptionValue::new("normal"),
7060            CoreOptionValue::new("fast"),
7061        ]);
7062        let options = CoreOptions::new([definition.clone()]);
7063
7064        let mut state = CoreState::default();
7065        state.callbacks.environment = Some(core_options_env);
7066        let mut env = Environment { state: &mut state };
7067
7068        *captured_core_options_version()
7069            .lock()
7070            .expect("core options version capture mutex poisoned") = Some(1);
7071        assert!(
7072            env.set_core_options(&options)
7073                .expect("v1 core options should build")
7074        );
7075        assert!(
7076            env.set_core_options_v1_intl(
7077                &[definition.clone()],
7078                Some(std::slice::from_ref(&definition))
7079            )
7080            .expect("v1 intl core options should build")
7081        );
7082        assert!(
7083            env.set_core_options_v2_intl(&options, Some(&options))
7084                .expect("v2 intl core options should build")
7085        );
7086
7087        *captured_core_options_version()
7088            .lock()
7089            .expect("core options version capture mutex poisoned") = None;
7090        assert_eq!(
7091            env.core_options_version(),
7092            CoreOptionsVersion::LEGACY_VARIABLES
7093        );
7094        assert!(
7095            env.set_core_options_legacy(&options)
7096                .expect("legacy core options should build")
7097        );
7098
7099        assert_eq!(
7100            captured_core_options_v1()
7101                .lock()
7102                .expect("core options v1 capture mutex poisoned")
7103                .first()
7104                .expect("v1 option should be captured")
7105                .key,
7106            "demo_speed"
7107        );
7108        assert_eq!(
7109            *captured_variables()
7110                .lock()
7111                .expect("variable capture mutex poisoned"),
7112            vec![CapturedVariable {
7113                key: "demo_speed".to_string(),
7114                value: Some("Speed; normal|slow|fast".to_string()),
7115            }]
7116        );
7117    }
7118
7119    #[test]
7120    fn core_options_update_display_callback_dispatches_to_core() {
7121        let _guard = serial_test_guard();
7122        reset_captured_core_options();
7123        let calls = Arc::new(Mutex::new(Vec::new()));
7124        install_global_test_core(CoreOptionsDisplayRecordingCore {
7125            calls: Arc::clone(&calls),
7126        });
7127
7128        with_state(|state| {
7129            state.callbacks.environment = Some(core_options_env);
7130            let mut env = Environment { state };
7131            assert!(env.set_core_options_update_display_callback());
7132        });
7133
7134        let callback = captured_core_options_update_display_callback()
7135            .lock()
7136            .expect("core options update display callback capture mutex poisoned")
7137            .expect("core options update display callback should be registered")
7138            .callback
7139            .expect("core options update display function should be set");
7140        assert!(unsafe { callback() });
7141
7142        assert_eq!(
7143            *calls
7144                .lock()
7145                .expect("core options display calls mutex poisoned"),
7146            vec!["update"]
7147        );
7148        assert_eq!(
7149            *captured_core_option_displays()
7150                .lock()
7151                .expect("core option display capture mutex poisoned"),
7152            vec![CapturedCoreOptionDisplay {
7153                key: "demo_extra".to_string(),
7154                visible: false,
7155            }]
7156        );
7157
7158        clear_global_test_core();
7159    }
7160
7161    #[test]
7162    fn vfs_interface_wraps_file_directory_and_path_operations() {
7163        let _guard = serial_test_guard();
7164        reset_captured_vfs_interface();
7165        let mut state = CoreState::default();
7166        state.callbacks.environment = Some(vfs_env);
7167        let mut env = Environment { state: &mut state };
7168
7169        let vfs = env
7170            .vfs_interface(VfsInterfaceVersion::new(3))
7171            .expect("VFS interface should be available");
7172        assert_eq!(vfs.version(), VfsInterfaceVersion::new(3));
7173
7174        let access = VfsFileAccessFlags::from(VfsFileAccess::Read)
7175            | VfsFileAccess::Write
7176            | VfsFileAccess::UpdateExisting;
7177        let hints = VfsFileAccessHints::from(VfsFileAccessHint::FrequentAccess);
7178        let mut file = vfs
7179            .open_file("/tmp/test.bin", access, hints)
7180            .expect("file should open");
7181
7182        assert_eq!(file.path().as_deref(), Some("/tmp/test.bin"));
7183        assert_eq!(file.size(), Some(8));
7184        assert_eq!(file.tell(), Some(3));
7185        assert_eq!(file.seek(-2, VfsSeekPosition::End), Some(6));
7186        let mut bytes = [0u8; 4];
7187        assert_eq!(file.read(&mut bytes), Some(3));
7188        assert_eq!(&bytes[..3], b"abc");
7189        assert_eq!(file.write(b"xyz"), Some(3));
7190        assert!(file.truncate(4));
7191        assert!(file.flush());
7192        assert!(file.close());
7193
7194        let stat = vfs.stat("/tmp/test.bin").expect("path should stat");
7195        assert!(stat.flags.contains(VfsStatFlag::Valid));
7196        assert!(stat.flags.contains(VfsStatFlag::Directory));
7197        assert_eq!(stat.size, Some(8));
7198        assert!(vfs.remove_file("/tmp/remove.bin"));
7199        assert!(vfs.rename("/tmp/old.bin", "/tmp/new.bin"));
7200        assert!(vfs.create_dir("/tmp/new-dir"));
7201
7202        let mut dir = vfs.open_dir("/tmp", true).expect("directory should open");
7203        assert!(dir.read_next());
7204        assert_eq!(dir.entry_name().as_deref(), Some("entry.bin"));
7205        assert!(!dir.entry_is_dir());
7206        assert!(!dir.read_next());
7207        assert!(dir.close());
7208
7209        assert_eq!(
7210            *captured_vfs_interface_requests()
7211                .lock()
7212                .expect("VFS request capture mutex poisoned"),
7213            vec![3]
7214        );
7215        assert_eq!(
7216            *captured_vfs_opens()
7217                .lock()
7218                .expect("VFS open capture mutex poisoned"),
7219            vec![CapturedVfsOpen {
7220                path: "/tmp/test.bin".to_string(),
7221                mode: access.bits(),
7222                hints: hints.bits(),
7223            }]
7224        );
7225        assert_eq!(
7226            *captured_vfs_writes()
7227                .lock()
7228                .expect("VFS write capture mutex poisoned"),
7229            vec![b"xyz".to_vec()]
7230        );
7231        assert_eq!(
7232            *captured_vfs_removes()
7233                .lock()
7234                .expect("VFS remove capture mutex poisoned"),
7235            vec!["/tmp/remove.bin".to_string()]
7236        );
7237        assert_eq!(
7238            *captured_vfs_renames()
7239                .lock()
7240                .expect("VFS rename capture mutex poisoned"),
7241            vec![CapturedVfsRename {
7242                old_path: "/tmp/old.bin".to_string(),
7243                new_path: "/tmp/new.bin".to_string(),
7244            }]
7245        );
7246        assert_eq!(
7247            *captured_vfs_mkdirs()
7248                .lock()
7249                .expect("VFS mkdir capture mutex poisoned"),
7250            vec!["/tmp/new-dir".to_string()]
7251        );
7252        assert_eq!(
7253            *captured_vfs_closes()
7254                .lock()
7255                .expect("VFS close capture mutex poisoned"),
7256            1
7257        );
7258        assert_eq!(
7259            *captured_vfs_dir_closes()
7260                .lock()
7261                .expect("VFS dir close capture mutex poisoned"),
7262            1
7263        );
7264    }
7265
7266    #[test]
7267    fn perf_interface_wraps_frontend_counter_callbacks() {
7268        let _guard = serial_test_guard();
7269        reset_captured_perf_interface();
7270        let mut state = CoreState::default();
7271        state.callbacks.environment = Some(frontend_services_env);
7272
7273        let mut env = Environment { state: &mut state };
7274        let perf = env
7275            .perf_interface()
7276            .expect("perf interface should be available");
7277
7278        assert_eq!(
7279            perf.time_micros(),
7280            Some(PerfTimeMicros::from_micros(123_456))
7281        );
7282        assert_eq!(perf.tick_counter(), Some(PerfTick::from_ticks(9_001)));
7283        let features = perf
7284            .cpu_features()
7285            .expect("CPU features should be available");
7286        assert!(features.contains(CpuFeature::Sse2));
7287        assert!(features.contains(CpuFeature::Neon));
7288        assert!(!features.contains(CpuFeature::Avx2));
7289
7290        let mut counter = PerfCounter::new("hot\0path");
7291        assert_eq!(
7292            counter
7293                .as_ref()
7294                .get_ref()
7295                .ident()
7296                .to_str()
7297                .expect("counter identifier should be utf-8"),
7298            "hotpath"
7299        );
7300        assert!(perf.register_counter(counter.as_mut()));
7301        assert!(counter.as_ref().get_ref().is_registered());
7302        assert!(perf.start_counter(counter.as_mut()));
7303        assert_eq!(
7304            counter.as_ref().get_ref().last_start(),
7305            PerfTick::from_ticks(9_001)
7306        );
7307        assert_eq!(counter.as_ref().get_ref().call_count(), 1);
7308        assert!(perf.stop_counter(counter.as_mut()));
7309        assert_eq!(
7310            counter.as_ref().get_ref().total(),
7311            PerfTick::from_ticks(377)
7312        );
7313        assert!(perf.log());
7314
7315        assert_eq!(
7316            *captured_perf_registered_idents()
7317                .lock()
7318                .expect("perf registered idents mutex poisoned"),
7319            vec!["hotpath".to_string()]
7320        );
7321        assert_eq!(
7322            *captured_perf_logs()
7323                .lock()
7324                .expect("perf log capture mutex poisoned"),
7325            1
7326        );
7327    }
7328
7329    #[test]
7330    fn input_descriptors_are_forwarded_with_retained_strings() {
7331        let _guard = serial_test_guard();
7332        reset_captured_input_descriptors();
7333        let mut state = CoreState::default();
7334        state.callbacks.environment = Some(frontend_services_env);
7335
7336        let mut env = Environment { state: &mut state };
7337        assert!(env.set_input_descriptors(&[
7338            InputDescriptor::joypad(0, JoypadButton::A, "Jump\0button"),
7339            InputDescriptor::analog(1, AnalogStick::Left, AnalogAxis::X, "Move X"),
7340        ]));
7341
7342        assert_eq!(
7343            *captured_input_descriptors()
7344                .lock()
7345                .expect("input descriptor capture mutex poisoned"),
7346            vec![
7347                CapturedInputDescriptor {
7348                    port: 0,
7349                    device: ControllerDevice::Joypad.as_raw(),
7350                    index: 0,
7351                    id: JoypadButton::A.as_raw(),
7352                    description: "Jumpbutton".to_string(),
7353                },
7354                CapturedInputDescriptor {
7355                    port: 1,
7356                    device: ControllerDevice::Analog.as_raw(),
7357                    index: AnalogStick::Left.as_raw(),
7358                    id: AnalogAxis::X.as_raw(),
7359                    description: "Move X".to_string(),
7360                },
7361            ]
7362        );
7363        assert!(env.state.input_descriptors.is_some());
7364    }
7365
7366    #[test]
7367    fn controller_info_is_forwarded_with_typed_devices() {
7368        let _guard = serial_test_guard();
7369        reset_captured_controller_info();
7370        let mut state = CoreState::default();
7371        state.callbacks.environment = Some(frontend_services_env);
7372        let lightgun_scope = ControllerDeviceSubclass::new(ControllerDevice::Lightgun, 0)
7373            .expect("lightgun subclass should be valid");
7374
7375        let mut env = Environment { state: &mut state };
7376        assert!(env.set_controller_info(&[
7377            ControllerInfo::new(vec![
7378                ControllerDescription::new("Gamepad\0Default", ControllerDevice::Joypad),
7379                ControllerDescription::new(
7380                    "Lightgun Scope",
7381                    ControllerDevice::Subclass(lightgun_scope),
7382                ),
7383            ]),
7384            ControllerInfo::new(vec![ControllerDescription::new(
7385                "Mouse",
7386                ControllerDevice::Mouse,
7387            )]),
7388        ]));
7389
7390        assert_eq!(
7391            *captured_controller_info()
7392                .lock()
7393                .expect("controller info capture mutex poisoned"),
7394            vec![
7395                vec![
7396                    CapturedControllerDescription {
7397                        description: "GamepadDefault".to_string(),
7398                        id: ControllerDevice::Joypad.as_raw(),
7399                    },
7400                    CapturedControllerDescription {
7401                        description: "Lightgun Scope".to_string(),
7402                        id: lightgun_scope.as_raw(),
7403                    },
7404                ],
7405                vec![CapturedControllerDescription {
7406                    description: "Mouse".to_string(),
7407                    id: ControllerDevice::Mouse.as_raw(),
7408                }],
7409            ]
7410        );
7411
7412        assert!(!env.set_controller_info(&[ControllerInfo::default()]));
7413    }
7414
7415    #[test]
7416    fn memory_maps_are_forwarded_with_typed_descriptors() {
7417        let _guard = serial_test_guard();
7418        reset_captured_memory_descriptors();
7419        let mut state = CoreState::default();
7420        state.callbacks.environment = Some(frontend_services_env);
7421        let mut wram = [0u8; 16];
7422
7423        let descriptors = [
7424            MemoryMapDescriptor::from_slice(Some("WRAM\0".to_string()), 0x7e0000usize, &mut wram)
7425                .with_flags(MemoryDescriptorFlags::from(MemoryDescriptorFlag::SystemRam))
7426                .with_alignment(MemoryDescriptorAlignment::FourBytes)
7427                .with_min_access_size(MemoryDescriptorMinAccessSize::TwoBytes)
7428                .with_offset(MemoryMapOffset::new(2))
7429                .with_select(MemoryMapMask::new(0xff0000))
7430                .with_disconnect(MemoryMapMask::new(0x80))
7431                .with_len(MemoryMapLen::new(8)),
7432            MemoryMapDescriptor::new_inaccessible(
7433                None,
7434                EmulatedAddress::new(0xffffff),
7435                MemoryMapMask::new(0xffffff),
7436            ),
7437        ];
7438
7439        let mut env = Environment { state: &mut state };
7440        assert!(env.set_memory_maps(&descriptors));
7441
7442        assert_eq!(
7443            *captured_memory_descriptors()
7444                .lock()
7445                .expect("memory descriptor capture mutex poisoned"),
7446            vec![
7447                CapturedMemoryDescriptor {
7448                    flags: raw::RETRO_MEMDESC_SYSTEM_RAM
7449                        | raw::RETRO_MEMDESC_ALIGN_4
7450                        | raw::RETRO_MEMDESC_MINSIZE_2,
7451                    ptr_is_null: false,
7452                    offset: 2,
7453                    start: 0x7e0000,
7454                    select: 0xff0000,
7455                    disconnect: 0x80,
7456                    len: 8,
7457                    addrspace: Some("WRAM".to_string()),
7458                },
7459                CapturedMemoryDescriptor {
7460                    flags: 0,
7461                    ptr_is_null: true,
7462                    offset: 0,
7463                    start: 0xffffff,
7464                    select: 0xffffff,
7465                    disconnect: 0,
7466                    len: 0,
7467                    addrspace: None,
7468                },
7469            ]
7470        );
7471    }
7472
7473    #[test]
7474    fn subsystem_info_is_forwarded_with_retained_nested_descriptors() {
7475        let _guard = serial_test_guard();
7476        reset_captured_subsystem_info();
7477        let mut state = CoreState::default();
7478        state.callbacks.environment = Some(frontend_services_env);
7479
7480        let mut env = Environment { state: &mut state };
7481        assert!(
7482            env.set_subsystem_info(&[SubsystemInfo::new(
7483                "Super Game Boy\0",
7484                "sgb",
7485                SubsystemId::new(7),
7486            )
7487            .with_roms([
7488                SubsystemRomInfo::new("Game Boy ROM", "gb|gbc")
7489                    .with_memory([SubsystemMemoryInfo::new("sav", 0x101)]),
7490                SubsystemRomInfo::new("BIOS", "bin")
7491                    .with_need_fullpath(true)
7492                    .with_block_extract(true)
7493                    .with_required(false),
7494            ])])
7495        );
7496
7497        assert_eq!(
7498            *captured_subsystem_info()
7499                .lock()
7500                .expect("subsystem info capture mutex poisoned"),
7501            vec![CapturedSubsystem {
7502                description: "Super Game Boy".to_string(),
7503                identifier: "sgb".to_string(),
7504                id: 7,
7505                roms: vec![
7506                    CapturedSubsystemRom {
7507                        description: "Game Boy ROM".to_string(),
7508                        valid_extensions: "gb|gbc".to_string(),
7509                        need_fullpath: false,
7510                        block_extract: false,
7511                        required: true,
7512                        memory: vec![CapturedSubsystemMemory {
7513                            extension: "sav".to_string(),
7514                            memory_type: 0x101,
7515                        }],
7516                    },
7517                    CapturedSubsystemRom {
7518                        description: "BIOS".to_string(),
7519                        valid_extensions: "bin".to_string(),
7520                        need_fullpath: true,
7521                        block_extract: true,
7522                        required: false,
7523                        memory: Vec::new(),
7524                    },
7525                ],
7526            }]
7527        );
7528        assert!(env.state.subsystem_info.is_some());
7529    }
7530
7531    #[test]
7532    fn extended_game_info_query_returns_borrowed_typed_views() {
7533        let _guard = serial_test_guard();
7534        let mut state = CoreState::default();
7535        state.callbacks.environment = Some(frontend_services_env);
7536        let mut env = Environment { state: &mut state };
7537
7538        {
7539            let infos = env
7540                .extended_game_infos(2)
7541                .expect("extended game info should be available");
7542            assert_eq!(infos.len(), 2);
7543            assert_eq!(infos[0].full_path, Some(c"/games/test.sfc"));
7544            assert_eq!(infos[0].archive_path, None);
7545            assert_eq!(infos[0].dir, Some(c"/games"));
7546            assert_eq!(infos[0].name, Some(c"test"));
7547            assert_eq!(infos[0].extension, Some(c"sfc"));
7548            assert_eq!(infos[0].meta, Some(c"plain"));
7549            assert_eq!(infos[0].data, Some(EXTENDED_GAME_CONTENT));
7550            assert!(!infos[0].file_in_archive);
7551            assert!(infos[0].persistent_data);
7552
7553            assert_eq!(infos[1].full_path, None);
7554            assert_eq!(infos[1].archive_path, Some(c"/games/archive.zip"));
7555            assert_eq!(infos[1].archive_file, Some(c"inside.bin"));
7556            assert_eq!(infos[1].data, None);
7557            assert!(infos[1].file_in_archive);
7558            assert!(!infos[1].persistent_data);
7559        }
7560
7561        let info = env
7562            .extended_game_info()
7563            .expect("single extended game info should be available");
7564        assert_eq!(info.name, Some(c"test"));
7565    }
7566
7567    #[test]
7568    fn software_framebuffer_request_returns_typed_view_and_submits_frontend_buffer() {
7569        let _guard = serial_test_guard();
7570        reset_software_framebuffer_pixels();
7571        reset_captured_video_refreshes();
7572        let mut state = CoreState::default();
7573        state.callbacks.environment = Some(frontend_services_env);
7574        state.callbacks.video_refresh = Some(capture_video_refresh);
7575
7576        let mut runtime = Runtime { state: &mut state };
7577        let request = SoftwareFramebufferRequest::new(4, 2).with_access(
7578            FramebufferMemoryAccessFlags::from(FramebufferMemoryAccess::Write)
7579                | FramebufferMemoryAccess::Read,
7580        );
7581        let mut framebuffer = runtime
7582            .environment()
7583            .current_software_framebuffer(request)
7584            .expect("frontend should provide a software framebuffer");
7585
7586        assert_eq!(framebuffer.width(), 4);
7587        assert_eq!(framebuffer.height(), 2);
7588        assert_eq!(framebuffer.pitch(), 4 * mem::size_of::<u32>());
7589        assert_eq!(framebuffer.format(), PixelFormat::Xrgb8888);
7590        assert_eq!(
7591            framebuffer.access(),
7592            FramebufferMemoryAccessFlags::from(FramebufferMemoryAccess::Write)
7593                | FramebufferMemoryAccess::Read
7594        );
7595        assert_eq!(
7596            framebuffer.memory(),
7597            FramebufferMemoryTypes::from(FramebufferMemoryType::Cached)
7598        );
7599
7600        framebuffer
7601            .bytes_mut()
7602            .expect("write access should expose writable bytes")
7603            .fill(0x5a);
7604        runtime.video_refresh_software_framebuffer(framebuffer);
7605
7606        let pixels = software_framebuffer_pixels()
7607            .lock()
7608            .expect("software framebuffer pixels mutex poisoned");
7609        assert_eq!(*pixels, vec![0x5a5a5a5a; 8]);
7610
7611        assert_eq!(
7612            *captured_video_refreshes()
7613                .lock()
7614                .expect("video refresh capture mutex poisoned"),
7615            vec![CapturedVideoRefresh {
7616                data_kind: CapturedVideoDataKind::Software,
7617                data_addr: pixels.as_ptr() as usize,
7618                width: 4,
7619                height: 2,
7620                pitch: 4 * mem::size_of::<u32>(),
7621            }]
7622        );
7623    }
7624
7625    #[test]
7626    fn keyboard_callback_dispatches_typed_events_to_core() {
7627        let _guard = serial_test_guard();
7628        reset_captured_keyboard_callback();
7629        let calls = Arc::new(Mutex::new(Vec::new()));
7630        install_global_test_core(KeyboardRecordingCore {
7631            calls: Arc::clone(&calls),
7632        });
7633
7634        __private::retro_set_environment(Some(frontend_services_env));
7635
7636        let callback = captured_keyboard_callback()
7637            .lock()
7638            .expect("keyboard callback capture mutex poisoned")
7639            .expect("keyboard callback should be registered")
7640            .callback
7641            .expect("keyboard callback function should be set");
7642        unsafe {
7643            callback(
7644                true,
7645                KeyboardKey::A.as_raw(),
7646                'a' as u32,
7647                (KeyboardModifiers::from(KeyboardModifier::Shift) | KeyboardModifier::Ctrl).bits(),
7648            )
7649        };
7650        unsafe { callback(false, 65_535, 0, 0) };
7651
7652        assert_eq!(
7653            *calls.lock().expect("keyboard event calls mutex poisoned"),
7654            vec![
7655                KeyboardEvent::new(
7656                    true,
7657                    KeyboardKey::A,
7658                    KeyboardCharacter::from_utf32('a' as u32),
7659                    KeyboardModifiers::from(KeyboardModifier::Shift) | KeyboardModifier::Ctrl,
7660                ),
7661                KeyboardEvent::new(
7662                    false,
7663                    KeyboardKey::UnknownKeycode(65_535),
7664                    KeyboardCharacter::from_utf32(0),
7665                    KeyboardModifiers::empty(),
7666                ),
7667            ]
7668        );
7669
7670        clear_global_test_core();
7671    }
7672
7673    #[test]
7674    fn configured_event_listeners_auto_register_frontend_callbacks() {
7675        let _guard = serial_test_guard();
7676        reset_captured_keyboard_callback();
7677        reset_captured_audio_callback();
7678        reset_captured_audio_buffer_status_callback();
7679        reset_captured_frame_time_callback();
7680        let keyboard_calls = Arc::new(Mutex::new(Vec::new()));
7681        install_global_test_core(ConfiguredEventCore {
7682            keyboard_calls: Arc::clone(&keyboard_calls),
7683        });
7684
7685        __private::retro_set_environment(Some(frontend_services_env));
7686
7687        let keyboard_callback = captured_keyboard_callback()
7688            .lock()
7689            .expect("keyboard callback capture mutex poisoned")
7690            .expect("keyboard callback should be registered")
7691            .callback
7692            .expect("keyboard callback function should be set");
7693        assert!(
7694            captured_audio_callback()
7695                .lock()
7696                .expect("audio callback capture mutex poisoned")
7697                .expect("audio callback should be registered")
7698                .callback
7699                .is_some()
7700        );
7701        assert!(
7702            captured_audio_buffer_status_callback()
7703                .lock()
7704                .expect("audio buffer status callback capture mutex poisoned")
7705                .expect("audio buffer status callback should be registered")
7706                .callback
7707                .is_some()
7708        );
7709        assert_eq!(
7710            captured_frame_time_callback()
7711                .lock()
7712                .expect("frame time callback capture mutex poisoned")
7713                .expect("frame time callback should be registered")
7714                .reference,
7715            16_667
7716        );
7717
7718        unsafe { keyboard_callback(true, KeyboardKey::Return.as_raw(), '\n' as u32, 0) };
7719        assert_eq!(
7720            *keyboard_calls
7721                .lock()
7722                .expect("keyboard event calls mutex poisoned"),
7723            vec![KeyboardEvent::new(
7724                true,
7725                KeyboardKey::Return,
7726                KeyboardCharacter::from_utf32('\n' as u32),
7727                KeyboardModifiers::empty(),
7728            )]
7729        );
7730
7731        clear_global_test_core();
7732    }
7733
7734    #[test]
7735    fn event_listeners_dispatch_in_order_and_remove_by_callback() {
7736        let _guard = serial_test_guard();
7737        reset_captured_keyboard_callback();
7738        let calls = Arc::new(Mutex::new(Vec::new()));
7739        install_global_test_core(MultiKeyboardListenerCore {
7740            calls: Arc::clone(&calls),
7741        });
7742
7743        __private::retro_set_environment(Some(frontend_services_env));
7744
7745        let keyboard_callback = captured_keyboard_callback()
7746            .lock()
7747            .expect("keyboard callback capture mutex poisoned")
7748            .expect("keyboard callback should be registered")
7749            .callback
7750            .expect("keyboard callback function should be set");
7751
7752        unsafe { keyboard_callback(true, KeyboardKey::Return.as_raw(), '\n' as u32, 0) };
7753        assert_eq!(
7754            *calls
7755                .lock()
7756                .expect("multi keyboard listener calls mutex poisoned"),
7757            vec!["first", "third"]
7758        );
7759
7760        clear_global_test_core();
7761    }
7762
7763    #[test]
7764    fn audio_callback_probe_register_clear_and_dispatch_work() {
7765        let _guard = serial_test_guard();
7766        reset_captured_audio_callback();
7767        let calls = Arc::new(Mutex::new(Vec::new()));
7768        install_global_test_core(AudioCallbackRecordingCore {
7769            calls: Arc::clone(&calls),
7770        });
7771
7772        with_state(|state| {
7773            state.callbacks.environment = Some(frontend_services_env);
7774            let mut env = Environment { state };
7775            assert!(env.audio_callback_available());
7776        });
7777        __private::retro_set_environment(Some(frontend_services_env));
7778        assert_eq!(
7779            *captured_audio_callback_probes()
7780                .lock()
7781                .expect("audio callback probe mutex poisoned"),
7782            1
7783        );
7784
7785        let callback = captured_audio_callback()
7786            .lock()
7787            .expect("audio callback capture mutex poisoned")
7788            .expect("audio callback should be registered");
7789
7790        unsafe {
7791            callback
7792                .set_state
7793                .expect("audio callback set_state function should be set")(true);
7794            callback
7795                .callback
7796                .expect("audio callback function should be set")();
7797            callback
7798                .set_state
7799                .expect("audio callback set_state function should be set")(false);
7800        }
7801
7802        assert_eq!(
7803            *calls.lock().expect("audio callback calls mutex poisoned"),
7804            vec![
7805                AudioCallbackEvent::State(AudioCallbackState::Active),
7806                AudioCallbackEvent::Request,
7807                AudioCallbackEvent::State(AudioCallbackState::Inactive),
7808            ]
7809        );
7810
7811        with_state(|state| {
7812            state.callbacks.environment = Some(frontend_services_env);
7813            let mut env = Environment { state };
7814            assert!(env.clear_audio_callback());
7815        });
7816        let callback = captured_audio_callback()
7817            .lock()
7818            .expect("audio callback capture mutex poisoned")
7819            .expect("audio callback clear should send a non-null empty interface");
7820        assert!(callback.callback.is_none());
7821        assert!(callback.set_state.is_none());
7822
7823        clear_global_test_core();
7824    }
7825
7826    #[test]
7827    fn audio_buffer_status_callback_dispatches_to_core() {
7828        let _guard = serial_test_guard();
7829        reset_captured_audio_buffer_status_callback();
7830        let calls = Arc::new(Mutex::new(Vec::new()));
7831        install_global_test_core(AudioBufferStatusRecordingCore {
7832            calls: Arc::clone(&calls),
7833        });
7834
7835        __private::retro_set_environment(Some(frontend_services_env));
7836
7837        let callback = captured_audio_buffer_status_callback()
7838            .lock()
7839            .expect("audio buffer status callback capture mutex poisoned")
7840            .expect("audio buffer status callback should be registered")
7841            .callback
7842            .expect("audio buffer status callback function should be set");
7843        unsafe { callback(true, 75, false) };
7844        unsafe { callback(false, 150, true) };
7845
7846        assert_eq!(
7847            *calls
7848                .lock()
7849                .expect("audio buffer status calls mutex poisoned"),
7850            vec![
7851                AudioBufferStatus::new(
7852                    true,
7853                    AudioBufferOccupancy::from_percent(75).expect("valid occupancy"),
7854                    false,
7855                ),
7856                AudioBufferStatus::from_raw(false, 150, true),
7857            ]
7858        );
7859
7860        with_state(|state| {
7861            state.callbacks.environment = Some(frontend_services_env);
7862            let mut env = Environment { state };
7863            assert!(env.set_audio_buffer_status_callback(false));
7864        });
7865        assert!(
7866            captured_audio_buffer_status_callback()
7867                .lock()
7868                .expect("audio buffer status callback capture mutex poisoned")
7869                .is_none()
7870        );
7871
7872        clear_global_test_core();
7873    }
7874
7875    #[test]
7876    fn frame_time_callback_dispatches_to_core() {
7877        let _guard = serial_test_guard();
7878        reset_captured_frame_time_callback();
7879        let calls = Arc::new(Mutex::new(Vec::new()));
7880        install_global_test_core(FrameTimeRecordingCore {
7881            calls: Arc::clone(&calls),
7882        });
7883
7884        __private::retro_set_environment(Some(frontend_services_env));
7885
7886        let callback = captured_frame_time_callback()
7887            .lock()
7888            .expect("frame time callback capture mutex poisoned")
7889            .expect("frame time callback should be registered");
7890        assert_eq!(callback.reference, 16_667);
7891
7892        let callback = callback
7893            .callback
7894            .expect("frame time callback function should be set");
7895        unsafe { callback(17_000) };
7896        unsafe { callback(-1) };
7897
7898        assert_eq!(
7899            *calls.lock().expect("frame time calls mutex poisoned"),
7900            vec![FrameTime::from_micros(17_000), FrameTime::from_micros(-1)]
7901        );
7902
7903        with_state(|state| {
7904            state.callbacks.environment = Some(frontend_services_env);
7905            let mut env = Environment { state };
7906            assert!(env.clear_frame_time_callback());
7907        });
7908        assert!(
7909            captured_frame_time_callback()
7910                .lock()
7911                .expect("frame time callback capture mutex poisoned")
7912                .is_none()
7913        );
7914
7915        clear_global_test_core();
7916    }
7917
7918    #[test]
7919    fn frame_time_callback_set_replaces_previous_callback() {
7920        let _guard = serial_test_guard();
7921        reset_captured_frame_time_callback();
7922        let calls = Arc::new(Mutex::new(Vec::new()));
7923        install_global_test_core(FrameTimeReplacementCore {
7924            calls: Arc::clone(&calls),
7925        });
7926
7927        __private::retro_set_environment(Some(frontend_services_env));
7928
7929        let callback = captured_frame_time_callback()
7930            .lock()
7931            .expect("frame time callback capture mutex poisoned")
7932            .expect("frame time callback should be registered");
7933        assert_eq!(callback.reference, 2_000);
7934
7935        let callback = callback
7936            .callback
7937            .expect("frame time callback function should be set");
7938        unsafe { callback(2_100) };
7939
7940        assert_eq!(
7941            *calls
7942                .lock()
7943                .expect("frame time replacement calls mutex poisoned"),
7944            vec!["second"]
7945        );
7946
7947        clear_global_test_core();
7948    }
7949
7950    #[test]
7951    fn frame_time_callback_can_be_cleared_during_event_configuration() {
7952        let _guard = serial_test_guard();
7953        reset_captured_frame_time_callback();
7954        install_global_test_core(FrameTimeClearedCore);
7955
7956        __private::retro_set_environment(Some(frontend_services_env));
7957
7958        assert!(
7959            captured_frame_time_callback()
7960                .lock()
7961                .expect("frame time callback capture mutex poisoned")
7962                .is_none()
7963        );
7964
7965        clear_global_test_core();
7966    }
7967
7968    #[test]
7969    fn proc_address_callback_dispatches_symbol_lookup_to_core() {
7970        let _guard = serial_test_guard();
7971        reset_captured_proc_address_interface();
7972        let calls = Arc::new(Mutex::new(Vec::new()));
7973        install_global_test_core(ProcAddressRecordingCore {
7974            calls: Arc::clone(&calls),
7975        });
7976
7977        with_state(|state| {
7978            state.callbacks.environment = Some(frontend_services_env);
7979            let mut env = Environment { state };
7980            assert!(env.set_proc_address_callback());
7981        });
7982
7983        let callback = captured_proc_address_interface()
7984            .lock()
7985            .expect("proc address interface capture mutex poisoned")
7986            .expect("proc address interface should be registered")
7987            .get_proc_address
7988            .expect("proc address callback should be set");
7989
7990        let found = unsafe { callback(c"test_extension_proc".as_ptr()) }
7991            .expect("known extension should be returned");
7992        let missing = unsafe { callback(c"missing_extension".as_ptr()) };
7993        let expected: unsafe extern "C" fn() = test_extension_proc;
7994
7995        assert!(std::ptr::fn_addr_eq(found, expected));
7996        assert!(missing.is_none());
7997        assert_eq!(
7998            *calls.lock().expect("proc address calls mutex poisoned"),
7999            vec![
8000                "test_extension_proc".to_string(),
8001                "missing_extension".to_string(),
8002            ]
8003        );
8004
8005        clear_global_test_core();
8006    }
8007
8008    #[test]
8009    fn location_lifetime_callbacks_dispatch_to_core() {
8010        let _guard = serial_test_guard();
8011        reset_captured_location_interface();
8012        let calls = Arc::new(Mutex::new(Vec::new()));
8013        install_global_test_core(LocationRecordingCore {
8014            calls: Arc::clone(&calls),
8015        });
8016
8017        with_state(|state| {
8018            state.callbacks.environment = Some(frontend_services_env);
8019            let mut env = Environment { state };
8020            assert!(
8021                env.location_interface()
8022                    .expect("location interface should be available")
8023                    .is_available()
8024            );
8025        });
8026
8027        let callback = captured_location_callback()
8028            .lock()
8029            .expect("location callback capture mutex poisoned")
8030            .expect("location callback should be captured");
8031        unsafe {
8032            callback
8033                .initialized
8034                .expect("location initialized callback should be set")();
8035            callback
8036                .deinitialized
8037                .expect("location deinitialized callback should be set")();
8038        }
8039
8040        assert_eq!(
8041            *calls
8042                .lock()
8043                .expect("location lifecycle calls mutex poisoned"),
8044            vec![
8045                LocationLifecycleEvent::Initialized,
8046                LocationLifecycleEvent::Deinitialized,
8047            ]
8048        );
8049
8050        clear_global_test_core();
8051    }
8052
8053    #[test]
8054    fn camera_interface_dispatches_frames_and_lifecycle_to_core() {
8055        let _guard = serial_test_guard();
8056        reset_captured_camera_interface();
8057        let calls = Arc::new(Mutex::new(Vec::new()));
8058        install_global_test_core(CameraRecordingCore {
8059            calls: Arc::clone(&calls),
8060        });
8061
8062        with_state(|state| {
8063            state.callbacks.environment = Some(frontend_services_env);
8064            let mut env = Environment { state };
8065            let interface = env
8066                .camera_interface(CameraRequest {
8067                    capabilities: CameraCapabilities::from(CameraCapability::RawFramebuffer)
8068                        | CameraCapability::OpenGlTexture,
8069                    size: CameraFrameSize::new(320, 240),
8070                })
8071                .expect("camera interface should be available");
8072            assert!(interface.is_available());
8073            assert!(
8074                interface
8075                    .capabilities()
8076                    .contains(CameraCapability::RawFramebuffer)
8077            );
8078            assert!(
8079                !interface
8080                    .capabilities()
8081                    .contains(CameraCapability::OpenGlTexture)
8082            );
8083            assert_eq!(interface.size(), CameraFrameSize::new(160, 120));
8084            assert!(interface.start());
8085            assert!(interface.stop());
8086        });
8087
8088        let callback = captured_camera_callback()
8089            .lock()
8090            .expect("camera callback capture mutex poisoned")
8091            .expect("camera callback should be captured");
8092        let pixels = [0xff00_0001, 0xff00_0002, 0xff00_0003, 0xff00_0004];
8093        let affine = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.25, 0.5, 1.0];
8094        unsafe {
8095            callback
8096                .initialized
8097                .expect("camera initialized callback should be set")();
8098            callback
8099                .frame_raw_framebuffer
8100                .expect("raw frame callback should be set")(
8101                pixels.as_ptr(),
8102                2,
8103                2,
8104                2 * std::mem::size_of::<u32>(),
8105            );
8106            callback
8107                .frame_opengl_texture
8108                .expect("texture frame callback should be set")(
8109                7, 0x0de1, affine.as_ptr()
8110            );
8111            callback
8112                .deinitialized
8113                .expect("camera deinitialized callback should be set")();
8114        }
8115
8116        assert_eq!(
8117            *captured_camera_starts()
8118                .lock()
8119                .expect("camera start capture mutex poisoned"),
8120            1
8121        );
8122        assert_eq!(
8123            *captured_camera_stops()
8124                .lock()
8125                .expect("camera stop capture mutex poisoned"),
8126            1
8127        );
8128        assert_eq!(
8129            *calls.lock().expect("camera calls mutex poisoned"),
8130            vec![
8131                CameraEvent::Initialized,
8132                CameraEvent::Raw {
8133                    width: 2,
8134                    height: 2,
8135                    pitch: 2 * std::mem::size_of::<u32>(),
8136                    pixels: pixels.to_vec(),
8137                },
8138                CameraEvent::Texture {
8139                    texture_id: 7,
8140                    texture_target: 0x0de1,
8141                    affine,
8142                },
8143                CameraEvent::Deinitialized,
8144            ]
8145        );
8146
8147        clear_global_test_core();
8148    }
8149
8150    #[test]
8151    fn disk_control_callbacks_dispatch_to_core() {
8152        let _guard = serial_test_guard();
8153        reset_captured_disk_control_callbacks();
8154        let calls = Arc::new(Mutex::new(Vec::new()));
8155        install_global_test_core(DiskControlRecordingCore {
8156            calls: Arc::clone(&calls),
8157        });
8158
8159        with_state(|state| {
8160            state.callbacks.environment = Some(frontend_services_env);
8161            let mut env = Environment { state };
8162            assert!(env.set_disk_control_interface());
8163            assert!(env.set_disk_control_ext_interface());
8164        });
8165
8166        let legacy = captured_disk_control_callback()
8167            .lock()
8168            .expect("disk control callback capture mutex poisoned")
8169            .expect("legacy disk control callback should be captured");
8170        assert!(legacy.set_eject_state.is_some());
8171        assert!(legacy.get_eject_state.is_some());
8172        assert!(legacy.get_image_index.is_some());
8173        assert!(legacy.set_image_index.is_some());
8174        assert!(legacy.get_num_images.is_some());
8175        assert!(legacy.replace_image_index.is_some());
8176        assert!(legacy.add_image_index.is_some());
8177
8178        let callback = captured_disk_control_ext_callback()
8179            .lock()
8180            .expect("disk control ext callback capture mutex poisoned")
8181            .expect("extended disk control callback should be captured");
8182        unsafe {
8183            assert!(callback
8184                .set_eject_state
8185                .expect("set_eject_state should be set")(
8186                true
8187            ));
8188            assert!(callback
8189                .get_eject_state
8190                .expect("get_eject_state should be set")());
8191            assert_eq!(
8192                callback
8193                    .get_image_index
8194                    .expect("get_image_index should be set")(),
8195                2
8196            );
8197            assert!(callback
8198                .set_image_index
8199                .expect("set_image_index should be set")(3));
8200            assert_eq!(
8201                callback
8202                    .get_num_images
8203                    .expect("get_num_images should be set")(),
8204                4
8205            );
8206            assert!(callback
8207                .replace_image_index
8208                .expect("replace_image_index should be set")(
8209                1, ptr::null()
8210            ));
8211            assert!(callback
8212                .add_image_index
8213                .expect("add_image_index should be set")());
8214            assert!(callback
8215                .set_initial_image
8216                .expect("set_initial_image should be set")(
8217                2,
8218                c"/games/disc2.cue".as_ptr()
8219            ));
8220
8221            let mut path = [0i8; 64];
8222            assert!(callback
8223                .get_image_path
8224                .expect("get_image_path should be set")(
8225                2,
8226                path.as_mut_ptr(),
8227                path.len()
8228            ));
8229            assert_eq!(CStr::from_ptr(path.as_ptr()), c"/games/disctwo.cue");
8230
8231            let mut label = [0i8; 64];
8232            assert!(callback
8233                .get_image_label
8234                .expect("get_image_label should be set")(
8235                2,
8236                label.as_mut_ptr(),
8237                label.len()
8238            ));
8239            assert_eq!(CStr::from_ptr(label.as_ptr()), c"Disc Two");
8240        }
8241
8242        assert_eq!(
8243            *calls.lock().expect("disk control calls mutex poisoned"),
8244            vec![
8245                DiskControlEvent::SetTray(DiskTrayState::Ejected),
8246                DiskControlEvent::SetImage(DiskIndex::new(3)),
8247                DiskControlEvent::ReplaceImage(DiskIndex::new(1), false),
8248                DiskControlEvent::AddImage,
8249                DiskControlEvent::SetInitialImage(
8250                    DiskIndex::new(2),
8251                    "/games/disc2.cue".to_string()
8252                ),
8253            ]
8254        );
8255
8256        with_state(|state| {
8257            state.callbacks.environment = Some(frontend_services_env);
8258            let mut env = Environment { state };
8259            assert!(env.clear_disk_control_interface());
8260            assert!(env.clear_disk_control_ext_interface());
8261        });
8262        assert!(
8263            captured_disk_control_callback()
8264                .lock()
8265                .expect("disk control callback capture mutex poisoned")
8266                .is_none()
8267        );
8268        assert!(
8269            captured_disk_control_ext_callback()
8270                .lock()
8271                .expect("disk control ext callback capture mutex poisoned")
8272                .is_none()
8273        );
8274
8275        clear_global_test_core();
8276    }
8277
8278    #[test]
8279    fn netpacket_callbacks_dispatch_to_core_and_send_through_session() {
8280        let _guard = serial_test_guard();
8281        reset_captured_netpacket_interface();
8282        let calls = Arc::new(Mutex::new(Vec::new()));
8283        install_global_test_core(NetpacketRecordingCore {
8284            calls: Arc::clone(&calls),
8285        });
8286
8287        with_state(|state| {
8288            state.callbacks.environment = Some(frontend_services_env);
8289            let mut env = Environment { state };
8290            assert!(env.set_netpacket_interface(Some("proto\0v1")));
8291        });
8292
8293        let callback = captured_netpacket_callback()
8294            .lock()
8295            .expect("netpacket callback capture mutex poisoned")
8296            .expect("netpacket callback should be captured");
8297        assert_eq!(
8298            unsafe { CStr::from_ptr(callback.protocol_version as *const c_char) },
8299            c"protov1"
8300        );
8301
8302        unsafe {
8303            callback.start.expect("start callback should be set")(
8304                0,
8305                Some(capture_netpacket_send),
8306                Some(capture_netpacket_poll_receive),
8307            );
8308            callback.receive.expect("receive callback should be set")(
8309                b"packet".as_ptr().cast(),
8310                6,
8311                3,
8312            );
8313            callback.poll.expect("poll callback should be set")();
8314            assert!(callback
8315                .connected
8316                .expect("connected callback should be set")(
8317                7
8318            ));
8319            assert!(!callback
8320                .connected
8321                .expect("connected callback should be set")(
8322                9
8323            ));
8324            callback
8325                .disconnected
8326                .expect("disconnected callback should be set")(7);
8327            callback.stop.expect("stop callback should be set")();
8328        }
8329
8330        assert_eq!(
8331            *captured_netpacket_sends()
8332                .lock()
8333                .expect("netpacket sends mutex poisoned"),
8334            vec![
8335                CapturedNetpacketSend {
8336                    flags: NetpacketFlags::reliable().as_raw(),
8337                    data: b"hello".to_vec(),
8338                    client_id: raw::RETRO_NETPACKET_BROADCAST,
8339                },
8340                CapturedNetpacketSend {
8341                    flags: NetpacketFlags::reliable().with_flush_hint(true).as_raw(),
8342                    data: Vec::new(),
8343                    client_id: 0,
8344                },
8345            ]
8346        );
8347        assert_eq!(
8348            *captured_netpacket_polls()
8349                .lock()
8350                .expect("netpacket polls mutex poisoned"),
8351            1
8352        );
8353        assert_eq!(
8354            *calls.lock().expect("netpacket calls mutex poisoned"),
8355            vec![
8356                NetpacketEvent::Start(NetplayClientId::host(), true),
8357                NetpacketEvent::Receive(NetplayClientId::new(3), b"packet".to_vec()),
8358                NetpacketEvent::Poll,
8359                NetpacketEvent::Connected(NetplayClientId::new(7)),
8360                NetpacketEvent::Connected(NetplayClientId::new(9)),
8361                NetpacketEvent::Disconnected(NetplayClientId::new(7)),
8362                NetpacketEvent::Stop,
8363            ]
8364        );
8365
8366        with_state(|state| {
8367            state.callbacks.environment = Some(frontend_services_env);
8368            let mut env = Environment { state };
8369            assert!(env.clear_netpacket_interface());
8370        });
8371        assert!(
8372            captured_netpacket_callback()
8373                .lock()
8374                .expect("netpacket callback capture mutex poisoned")
8375                .is_none()
8376        );
8377
8378        clear_global_test_core();
8379    }
8380
8381    #[test]
8382    fn microphone_interface_opens_reads_and_closes_handles() {
8383        let _guard = serial_test_guard();
8384        reset_captured_microphone_interface();
8385        let mut state = CoreState::default();
8386        state.callbacks.environment = Some(frontend_services_env);
8387
8388        {
8389            let mut env = Environment { state: &mut state };
8390            let interface = env
8391                .microphone_interface()
8392                .expect("microphone interface should be available");
8393            assert!(interface.is_available());
8394            assert_eq!(interface.version(), raw::RETRO_MICROPHONE_INTERFACE_VERSION);
8395
8396            let default_mic = interface
8397                .open_default()
8398                .expect("default microphone should open");
8399            drop(default_mic);
8400
8401            let mut mic = interface
8402                .open(MicrophoneParams::new(MicrophoneRateHz::new(16_000)))
8403                .expect("configured microphone should open");
8404            assert_eq!(
8405                mic.params(),
8406                Some(MicrophoneParams::new(MicrophoneRateHz::new(22_050)))
8407            );
8408            assert!(mic.set_enabled(true));
8409            assert!(mic.enabled());
8410
8411            let mut samples = [0i16; 4];
8412            assert_eq!(mic.read_samples(&mut samples), Ok(4));
8413            assert_eq!(samples, [1, 2, 3, 4]);
8414        }
8415
8416        assert_eq!(
8417            *captured_mic_open_params()
8418                .lock()
8419                .expect("microphone open params mutex poisoned"),
8420            vec![None, Some(16_000)]
8421        );
8422        assert_eq!(
8423            *captured_mic_states()
8424                .lock()
8425                .expect("microphone states mutex poisoned"),
8426            vec![true]
8427        );
8428        assert_eq!(
8429            *captured_mic_closes()
8430                .lock()
8431                .expect("microphone closes mutex poisoned"),
8432            2
8433        );
8434    }
8435
8436    #[test]
8437    fn runtime_video_refresh_frame_forwards_valid_software_frame() {
8438        let _guard = serial_test_guard();
8439        reset_captured_video_refreshes();
8440        let mut state = CoreState::default();
8441        state.callbacks.video_refresh = Some(capture_video_refresh);
8442
8443        let runtime = Runtime { state: &mut state };
8444        let pixels = vec![0u32; 320 * 240];
8445
8446        assert!(runtime.video_refresh_frame(&pixels, 320, 240, 320 * mem::size_of::<u32>()));
8447        assert_eq!(
8448            *captured_video_refreshes()
8449                .lock()
8450                .expect("video refresh capture mutex poisoned"),
8451            vec![CapturedVideoRefresh {
8452                data_kind: CapturedVideoDataKind::Software,
8453                data_addr: pixels.as_ptr() as usize,
8454                width: 320,
8455                height: 240,
8456                pitch: 320 * mem::size_of::<u32>(),
8457            }]
8458        );
8459    }
8460
8461    #[test]
8462    fn runtime_video_refresh_frame_rejects_buffer_smaller_than_pitch_times_height() {
8463        let _guard = serial_test_guard();
8464        reset_captured_video_refreshes();
8465        let mut state = CoreState::default();
8466        state.callbacks.video_refresh = Some(capture_video_refresh);
8467
8468        let runtime = Runtime { state: &mut state };
8469        let pixels = vec![0u16; (320 * 240) - 1];
8470
8471        assert!(!runtime.video_refresh_frame(&pixels, 320, 240, 320 * mem::size_of::<u16>()));
8472        assert!(
8473            captured_video_refreshes()
8474                .lock()
8475                .expect("video refresh capture mutex poisoned")
8476                .is_empty()
8477        );
8478    }
8479
8480    #[test]
8481    fn runtime_video_refresh_frame_rejects_pitch_smaller_than_one_row() {
8482        let _guard = serial_test_guard();
8483        reset_captured_video_refreshes();
8484        let mut state = CoreState::default();
8485        state.callbacks.video_refresh = Some(capture_video_refresh);
8486
8487        let runtime = Runtime { state: &mut state };
8488        let pixels = vec![0u32; 4];
8489
8490        assert!(!runtime.video_refresh_frame(&pixels, 2, 2, mem::size_of::<u32>()));
8491        assert!(
8492            captured_video_refreshes()
8493                .lock()
8494                .expect("video refresh capture mutex poisoned")
8495                .is_empty()
8496        );
8497    }
8498
8499    #[test]
8500    fn hw_render_interface_and_context_negotiation_are_typed() {
8501        let _guard = serial_test_guard();
8502        let mut captured = captured_hw_render_state()
8503            .lock()
8504            .expect("hw render capture mutex poisoned");
8505        captured.reset();
8506        captured.context_negotiation_support_version = Some(3);
8507        drop(captured);
8508
8509        let mut state = CoreState::default();
8510        state.callbacks.environment = Some(capture_hw_render_env);
8511        let mut env = Environment { state: &mut state };
8512
8513        let interface = env
8514            .hw_render_interface()
8515            .expect("HW render interface should be available");
8516        assert_eq!(interface.interface_type(), HwRenderInterfaceType::Vulkan);
8517        assert_eq!(interface.interface_version(), 1);
8518        assert!(!interface.as_base_ptr().is_null());
8519        assert_eq!(
8520            env.hw_render_context_negotiation_interface_support(
8521                HwRenderContextNegotiationInterfaceType::Vulkan
8522            ),
8523            Some(3)
8524        );
8525
8526        let requested = HwRenderContextNegotiationInterface::vulkan(2);
8527        assert!(env.set_hw_render_context_negotiation_interface(requested));
8528
8529        let captured = captured_hw_render_state()
8530            .lock()
8531            .expect("hw render capture mutex poisoned");
8532        assert_eq!(captured.last_context_negotiation, Some(requested));
8533    }
8534
8535    #[test]
8536    fn set_hw_render_from_candidates_prefers_frontend_preferred_context() {
8537        let _guard = serial_test_guard();
8538        let mut captured = captured_hw_render_state()
8539            .lock()
8540            .expect("hw render capture mutex poisoned");
8541        captured.reset();
8542        captured.preferred_context_type = HwContextType::OpenGlEs3;
8543        captured.supports_non_preferred_context = false;
8544        captured.set_accept_contexts(&[HwContextType::OpenGlEs3]);
8545        drop(captured);
8546
8547        let mut state = CoreState::default();
8548        state.callbacks.environment = Some(capture_hw_render_env);
8549        let mut env = Environment { state: &mut state };
8550
8551        let chosen = env.set_hw_render_from_candidates(&[
8552            HwRenderConfig {
8553                context_type: HwContextType::OpenGlCore,
8554                ..HwRenderConfig::default()
8555            },
8556            HwRenderConfig {
8557                context_type: HwContextType::OpenGlEs3,
8558                ..HwRenderConfig::default()
8559            },
8560            HwRenderConfig {
8561                context_type: HwContextType::OpenGlEs2,
8562                ..HwRenderConfig::default()
8563            },
8564        ]);
8565
8566        assert_eq!(
8567            chosen.map(|config| config.context_type),
8568            Some(HwContextType::OpenGlEs3)
8569        );
8570        let captured = captured_hw_render_state()
8571            .lock()
8572            .expect("hw render capture mutex poisoned");
8573        assert_eq!(
8574            captured.attempted_contexts(),
8575            vec![HwContextType::OpenGlEs3]
8576        );
8577    }
8578
8579    #[test]
8580    fn set_hw_render_from_candidates_respects_frontend_nonpreferred_restriction() {
8581        let _guard = serial_test_guard();
8582        let mut captured = captured_hw_render_state()
8583            .lock()
8584            .expect("hw render capture mutex poisoned");
8585        captured.reset();
8586        captured.preferred_context_type = HwContextType::Vulkan;
8587        captured.supports_non_preferred_context = false;
8588        captured.accept_any_context = true;
8589        drop(captured);
8590
8591        let mut state = CoreState::default();
8592        state.callbacks.environment = Some(capture_hw_render_env);
8593        let mut env = Environment { state: &mut state };
8594
8595        let chosen = env.set_hw_render_from_candidates(&[
8596            HwRenderConfig {
8597                context_type: HwContextType::OpenGlCore,
8598                ..HwRenderConfig::default()
8599            },
8600            HwRenderConfig {
8601                context_type: HwContextType::OpenGlEs2,
8602                ..HwRenderConfig::default()
8603            },
8604        ]);
8605
8606        assert!(chosen.is_none());
8607        let captured = captured_hw_render_state()
8608            .lock()
8609            .expect("hw render capture mutex poisoned");
8610        assert!(captured.attempted_contexts().is_empty());
8611    }
8612
8613    #[test]
8614    fn set_hw_render_from_candidates_falls_back_only_when_frontend_allows_it() {
8615        let _guard = serial_test_guard();
8616        let mut captured = captured_hw_render_state()
8617            .lock()
8618            .expect("hw render capture mutex poisoned");
8619        captured.reset();
8620        captured.preferred_context_type = HwContextType::Vulkan;
8621        captured.supports_non_preferred_context = true;
8622        captured.set_accept_contexts(&[HwContextType::OpenGlEs2]);
8623        drop(captured);
8624
8625        let mut state = CoreState::default();
8626        state.callbacks.environment = Some(capture_hw_render_env);
8627        let mut env = Environment { state: &mut state };
8628
8629        let chosen = env.set_hw_render_from_candidates(&[
8630            HwRenderConfig {
8631                context_type: HwContextType::OpenGlCore,
8632                ..HwRenderConfig::default()
8633            },
8634            HwRenderConfig {
8635                context_type: HwContextType::OpenGlEs3,
8636                ..HwRenderConfig::default()
8637            },
8638            HwRenderConfig {
8639                context_type: HwContextType::OpenGlEs2,
8640                ..HwRenderConfig::default()
8641            },
8642        ]);
8643
8644        assert_eq!(
8645            chosen.map(|config| config.context_type),
8646            Some(HwContextType::OpenGlEs2)
8647        );
8648        let captured = captured_hw_render_state()
8649            .lock()
8650            .expect("hw render capture mutex poisoned");
8651        assert_eq!(
8652            captured.attempted_contexts(),
8653            vec![
8654                HwContextType::OpenGlCore,
8655                HwContextType::OpenGlEs3,
8656                HwContextType::OpenGlEs2,
8657            ]
8658        );
8659    }
8660
8661    #[test]
8662    fn set_hw_render_from_candidates_recovers_from_rejected_generic_opengl_with_gles_fallbacks() {
8663        let _guard = serial_test_guard();
8664        let mut captured = captured_hw_render_state()
8665            .lock()
8666            .expect("hw render capture mutex poisoned");
8667        captured.reset();
8668        captured.preferred_context_type = HwContextType::OpenGl;
8669        captured.supports_non_preferred_context = false;
8670        captured.set_accept_contexts(&[HwContextType::OpenGlEs2]);
8671        drop(captured);
8672
8673        let mut state = CoreState::default();
8674        state.callbacks.environment = Some(capture_hw_render_env);
8675        let mut env = Environment { state: &mut state };
8676
8677        let chosen = env.set_hw_render_from_candidates(&[
8678            HwRenderConfig {
8679                context_type: HwContextType::OpenGlCore,
8680                ..HwRenderConfig::default()
8681            },
8682            HwRenderConfig {
8683                context_type: HwContextType::OpenGlEs3,
8684                ..HwRenderConfig::default()
8685            },
8686            HwRenderConfig {
8687                context_type: HwContextType::OpenGl,
8688                ..HwRenderConfig::default()
8689            },
8690            HwRenderConfig {
8691                context_type: HwContextType::OpenGlEs2,
8692                ..HwRenderConfig::default()
8693            },
8694        ]);
8695
8696        assert_eq!(
8697            chosen.map(|config| config.context_type),
8698            Some(HwContextType::OpenGlEs2)
8699        );
8700        let captured = captured_hw_render_state()
8701            .lock()
8702            .expect("hw render capture mutex poisoned");
8703        assert_eq!(
8704            captured.attempted_contexts(),
8705            vec![
8706                HwContextType::OpenGl,
8707                HwContextType::OpenGlEs3,
8708                HwContextType::OpenGlEs2,
8709            ]
8710        );
8711    }
8712
8713    #[test]
8714    fn set_hw_render_preserves_frontend_injected_runtime_callbacks() {
8715        let _guard = serial_test_guard();
8716        let mut captured = captured_hw_render_state()
8717            .lock()
8718            .expect("hw render capture mutex poisoned");
8719        captured.reset();
8720        captured.accept_any_context = true;
8721        captured.inject_runtime_callbacks = true;
8722        drop(captured);
8723
8724        let mut state = CoreState::default();
8725        state.callbacks.environment = Some(capture_hw_render_env);
8726        let mut env = Environment { state: &mut state };
8727
8728        let ok = env.set_hw_render(HwRenderConfig {
8729            context_type: HwContextType::OpenGlEs2,
8730            ..HwRenderConfig::default()
8731        });
8732
8733        assert!(ok);
8734        let stored = env
8735            .state
8736            .hw_render
8737            .expect("accepted hardware-render callback should be stored on state");
8738        assert!(stored.context_reset.is_some());
8739        assert!(stored.context_destroy.is_some());
8740        assert_eq!(
8741            stored
8742                .get_current_framebuffer
8743                .map(|callback| callback as usize),
8744            Some(fake_current_framebuffer as usize)
8745        );
8746        assert_eq!(
8747            stored.get_proc_address.map(|callback| callback as usize),
8748            Some(fake_get_proc_address as usize)
8749        );
8750    }
8751
8752    #[test]
8753    fn runtime_exposes_hardware_proc_addresses_without_raw_abi_types() {
8754        let _guard = serial_test_guard();
8755        let mut captured = captured_hw_render_state()
8756            .lock()
8757            .expect("hw render capture mutex poisoned");
8758        captured.reset();
8759        captured.accept_any_context = true;
8760        captured.inject_runtime_callbacks = true;
8761        drop(captured);
8762
8763        let mut state = CoreState::default();
8764        state.callbacks.environment = Some(capture_hw_render_env);
8765        {
8766            let mut env = Environment { state: &mut state };
8767            assert!(env.set_hw_render(HwRenderConfig {
8768                context_type: HwContextType::OpenGlEs2,
8769                ..HwRenderConfig::default()
8770            }));
8771        }
8772
8773        let runtime = Runtime { state: &mut state };
8774        assert_eq!(
8775            runtime.hw_proc_address("glClear").unwrap() as usize,
8776            fake_gl_proc as usize
8777        );
8778    }
8779
8780    #[test]
8781    fn runtime_treats_zero_hardware_framebuffer_as_unavailable() {
8782        let mut state = CoreState {
8783            hw_render: Some(RawHwRenderCallback {
8784                get_current_framebuffer: Some(fake_zero_current_framebuffer),
8785                ..RawHwRenderCallback::default()
8786            }),
8787            ..CoreState::default()
8788        };
8789
8790        let runtime = Runtime { state: &mut state };
8791
8792        assert_eq!(runtime.current_framebuffer(), None);
8793    }
8794
8795    #[cfg(unix)]
8796    #[test]
8797    fn runtime_falls_back_to_process_global_symbols_when_frontend_proc_lookup_fails() {
8798        let mut state = CoreState {
8799            hw_render: Some(RawHwRenderCallback {
8800                context_type: HwContextType::OpenGlEs2,
8801                get_proc_address: Some(missing_get_proc_address),
8802                ..RawHwRenderCallback::default()
8803            }),
8804            ..CoreState::default()
8805        };
8806
8807        let runtime = Runtime { state: &mut state };
8808
8809        assert!(runtime.hw_proc_address("malloc").is_ok());
8810        assert!(
8811            runtime
8812                .hw_proc_address("__libretro_core_missing_symbol")
8813                .is_err()
8814        );
8815    }
8816
8817    #[test]
8818    fn set_geometry_is_forwarded_to_frontend() {
8819        let _guard = serial_test_guard();
8820        reset_captured_geometries();
8821
8822        let mut state = CoreState::default();
8823        state.callbacks.environment = Some(capture_geometry_env);
8824        let mut env = Environment { state: &mut state };
8825
8826        assert!(env.set_geometry(GameGeometry {
8827            base_width: 320,
8828            base_height: 240,
8829            max_width: 2048,
8830            max_height: 2048,
8831            aspect_ratio: 4.0 / 3.0,
8832        }));
8833
8834        let captured = captured_geometries()
8835            .lock()
8836            .expect("geometry capture mutex poisoned")
8837            .clone();
8838        assert_eq!(captured.len(), 1);
8839        assert_eq!(captured[0].base_width, 320);
8840        assert_eq!(captured[0].base_height, 240);
8841        assert_eq!(captured[0].max_width, 2048);
8842        assert_eq!(captured[0].max_height, 2048);
8843        assert_eq!(captured[0].aspect_ratio, 4.0 / 3.0);
8844    }
8845
8846    #[test]
8847    fn runtime_input_helpers_use_typed_device_queries() {
8848        let _guard = serial_test_guard();
8849        reset_captured_input_queries();
8850
8851        let mut state = CoreState::default();
8852        state.callbacks.input_state = Some(capture_input_state);
8853        let runtime = Runtime { state: &mut state };
8854
8855        assert!(runtime.joypad_pressed(0, JoypadButton::A));
8856        let buttons = runtime.joypad_buttons(0);
8857        assert!(buttons.contains(JoypadButton::A));
8858        assert!(buttons.contains(JoypadButton::B));
8859        assert!(!buttons.contains(JoypadButton::X));
8860        assert_eq!(
8861            runtime.analog_axis(0, AnalogStick::Left, AnalogAxis::X),
8862            -123
8863        );
8864        assert_eq!(runtime.analog_button(0, JoypadButton::R2), 123);
8865        assert_eq!(runtime.mouse_axis(0, MouseAxis::X), 7);
8866        assert!(runtime.mouse_button_pressed(0, MouseButton::Left));
8867        assert!(runtime.mouse_wheel_moved(0, MouseWheel::Up));
8868        assert_eq!(runtime.pointer_axis(0, 1, PointerAxis::Y), -77);
8869        assert!(runtime.pointer_pressed(0, 1));
8870        assert_eq!(runtime.pointer_count(0), 2);
8871        assert!(runtime.pointer_is_offscreen(0, 1));
8872        assert_eq!(runtime.lightgun_axis(0, LightgunAxis::ScreenX), 99);
8873        assert!(runtime.lightgun_button_pressed(0, LightgunButton::Trigger));
8874        assert!(runtime.lightgun_is_offscreen(0));
8875
8876        let captured = captured_input_queries()
8877            .lock()
8878            .expect("input query capture mutex poisoned")
8879            .clone();
8880        assert!(captured.contains(&CapturedInputQuery {
8881            port: 0,
8882            device: RETRO_DEVICE_ANALOG,
8883            index: RETRO_DEVICE_INDEX_ANALOG_LEFT,
8884            id: RETRO_DEVICE_ID_ANALOG_X,
8885        }));
8886        assert!(captured.contains(&CapturedInputQuery {
8887            port: 0,
8888            device: RETRO_DEVICE_POINTER,
8889            index: 1,
8890            id: RETRO_DEVICE_ID_POINTER_PRESSED,
8891        }));
8892        assert!(captured.contains(&CapturedInputQuery {
8893            port: 0,
8894            device: RETRO_DEVICE_LIGHTGUN,
8895            index: 0,
8896            id: RETRO_DEVICE_ID_LIGHTGUN_TRIGGER,
8897        }));
8898    }
8899
8900    #[test]
8901    fn stored_hw_render_lifecycle_trampolines_dispatch_to_core_methods() {
8902        let _guard = serial_test_guard();
8903        reset_lifecycle_call_counts();
8904
8905        let mut captured = captured_hw_render_state()
8906            .lock()
8907            .expect("hw render capture mutex poisoned");
8908        captured.reset();
8909        captured.accept_any_context = true;
8910        drop(captured);
8911
8912        let callbacks = with_state(|state| {
8913            state.reset_frontend_state();
8914            state.core = Some(Box::new(LifecycleRecordingCore));
8915            state.callbacks.environment = Some(capture_hw_render_env);
8916
8917            let mut env = Environment { state };
8918            let ok = env.set_hw_render(HwRenderConfig {
8919                context_type: HwContextType::OpenGlEs2,
8920                ..HwRenderConfig::default()
8921            });
8922            assert!(ok);
8923
8924            env.state
8925                .hw_render
8926                .expect("accepted hardware-render callback should be stored on state")
8927        });
8928
8929        unsafe {
8930            callbacks
8931                .context_reset
8932                .expect("context_reset trampoline should be present")();
8933            callbacks
8934                .context_destroy
8935                .expect("context_destroy trampoline should be present")();
8936        }
8937
8938        assert_eq!(
8939            snapshot_lifecycle_call_counts(),
8940            LifecycleCallCounts {
8941                resets: 1,
8942                destroys: 1,
8943            }
8944        );
8945
8946        with_state(|state| {
8947            state.reset_frontend_state();
8948            state.core = None;
8949        });
8950    }
8951
8952    #[test]
8953    fn retro_get_system_info_catches_core_panic_and_returns_default_info() {
8954        let _guard = serial_test_guard();
8955        install_global_test_core(PanickingCore::new(PanicAt::SystemInfo));
8956        let stale = c"stale".as_ptr();
8957
8958        let mut info = RawSystemInfo {
8959            library_name: stale,
8960            library_version: stale,
8961            valid_extensions: stale,
8962            need_fullpath: true,
8963            block_extract: true,
8964        };
8965
8966        __private::retro_get_system_info(&mut info);
8967
8968        assert!(info.library_name.is_null());
8969        assert!(info.library_version.is_null());
8970        assert!(info.valid_extensions.is_null());
8971        assert!(!info.need_fullpath);
8972        assert!(!info.block_extract);
8973
8974        clear_global_test_core();
8975    }
8976
8977    #[test]
8978    fn retro_get_system_info_reuses_owned_strings_across_calls() {
8979        let _guard = serial_test_guard();
8980        let calls = Arc::new(AtomicUsize::new(0));
8981        install_global_test_core(ChangingSystemInfoCore {
8982            calls: Arc::clone(&calls),
8983        });
8984
8985        let mut first = RawSystemInfo::default();
8986        let mut second = RawSystemInfo::default();
8987
8988        __private::retro_get_system_info(&mut first);
8989        __private::retro_get_system_info(&mut second);
8990
8991        assert_eq!(calls.load(Ordering::SeqCst), 1);
8992        assert_eq!(first.library_name, second.library_name);
8993        assert_eq!(first.library_version, second.library_version);
8994        assert_eq!(first.valid_extensions, second.valid_extensions);
8995        assert_eq!(
8996            unsafe { CStr::from_ptr(first.library_name) }
8997                .to_str()
8998                .unwrap(),
8999            "cached-test-core"
9000        );
9001        assert_eq!(
9002            unsafe { CStr::from_ptr(first.library_version) }
9003                .to_str()
9004                .unwrap(),
9005            "first"
9006        );
9007        assert_eq!(
9008            unsafe { CStr::from_ptr(first.valid_extensions) }
9009                .to_str()
9010                .unwrap(),
9011            "first"
9012        );
9013
9014        clear_global_test_core();
9015    }
9016
9017    #[test]
9018    fn retro_init_catches_core_panic() {
9019        let _guard = serial_test_guard();
9020        install_global_test_core(PanickingCore::new(PanicAt::Init));
9021
9022        __private::retro_init();
9023
9024        clear_global_test_core();
9025    }
9026
9027    #[test]
9028    fn retro_load_game_catches_core_panic_and_returns_false() {
9029        let _guard = serial_test_guard();
9030        install_global_test_core(PanickingCore::new(PanicAt::LoadGame));
9031
9032        assert!(!__private::retro_load_game(ptr::null()));
9033
9034        clear_global_test_core();
9035    }
9036
9037    #[test]
9038    fn retro_memory_callbacks_receive_typed_regions() {
9039        let _guard = serial_test_guard();
9040        let calls = Arc::new(Mutex::new(Vec::new()));
9041        install_global_test_core(MemoryRecordingCore::new(Arc::clone(&calls)));
9042
9043        let save_ram = __private::retro_get_memory_data(RETRO_MEMORY_SAVE_RAM);
9044        let save_ram_size = __private::retro_get_memory_size(RETRO_MEMORY_SAVE_RAM);
9045        let unknown = __private::retro_get_memory_data(99);
9046        let unknown_size = __private::retro_get_memory_size(99);
9047
9048        assert!(!save_ram.is_null());
9049        assert_eq!(save_ram_size, 4);
9050        assert!(unknown.is_null());
9051        assert_eq!(unknown_size, 0);
9052        assert_eq!(
9053            *calls.lock().expect("memory calls mutex poisoned"),
9054            vec![
9055                MemoryRegion::SaveRam,
9056                MemoryRegion::SaveRam,
9057                MemoryRegion::Unknown(99),
9058                MemoryRegion::Unknown(99),
9059            ]
9060        );
9061
9062        clear_global_test_core();
9063    }
9064
9065    #[test]
9066    fn retro_set_controller_port_device_converts_to_typed_values() {
9067        let _guard = serial_test_guard();
9068        let calls = Arc::new(Mutex::new(Vec::new()));
9069        install_global_test_core(ControllerDeviceRecordingCore {
9070            calls: Arc::clone(&calls),
9071        });
9072
9073        __private::retro_set_controller_port_device(2, RETRO_DEVICE_MOUSE);
9074        __private::retro_set_controller_port_device(3, 123);
9075
9076        assert_eq!(
9077            *calls
9078                .lock()
9079                .expect("controller device calls mutex poisoned"),
9080            vec![
9081                (InputPort::new(2), ControllerDevice::Mouse),
9082                (InputPort::new(3), ControllerDevice::Unknown(123)),
9083            ]
9084        );
9085
9086        clear_global_test_core();
9087    }
9088
9089    #[test]
9090    fn retro_cheat_set_converts_to_typed_values() {
9091        let _guard = serial_test_guard();
9092        let calls = Arc::new(Mutex::new(Vec::new()));
9093        install_global_test_core(CheatRecordingCore {
9094            calls: Arc::clone(&calls),
9095        });
9096
9097        __private::retro_cheat_set(7, true, c"ABCD-EFGH".as_ptr());
9098        __private::retro_cheat_set(8, false, ptr::null());
9099
9100        assert_eq!(
9101            *calls.lock().expect("cheat calls mutex poisoned"),
9102            vec![
9103                (CheatIndex::new(7), true, Some("ABCD-EFGH".to_owned())),
9104                (CheatIndex::new(8), false, None),
9105            ]
9106        );
9107
9108        clear_global_test_core();
9109    }
9110
9111    #[test]
9112    fn retro_run_panic_keeps_core_available_for_later_callbacks() {
9113        let _guard = serial_test_guard();
9114        let reset_calls = Arc::new(AtomicUsize::new(0));
9115        install_global_test_core(RunPanicThenResetCore {
9116            reset_calls: Arc::clone(&reset_calls),
9117        });
9118
9119        __private::retro_run();
9120        __private::retro_reset();
9121
9122        assert_eq!(reset_calls.load(Ordering::SeqCst), 1);
9123
9124        clear_global_test_core();
9125    }
9126}