1mod 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#[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#[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#[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 unsafe { callback(level, FORMAT.as_ptr().cast::<c_char>(), message.as_ptr()) };
365 } else {
366 eprintln!("{message}");
367 }
368 }
369}
370
371#[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 let raw = unsafe { &*raw };
522 let path = if raw.path.is_null() {
523 None
524 } else {
525 Some(unsafe { CStr::from_ptr(raw.path) })
527 };
528 let data = if raw.data.is_null() {
529 None
530 } else {
531 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 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
723pub 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
1034pub 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
1132pub 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 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 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 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 unsafe { callback(command, data) }
1674 }
1675}
1676
1677pub 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 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 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 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 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 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 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 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 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#[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 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 state.system_info = Some(OwnedSystemInfo::new(system_info));
2163 }
2164 if let Some(storage) = &state.system_info {
2165 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 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 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 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 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 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 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 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#[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 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 unsafe { CString::from_vec_unchecked(bytes) }
2685 } else {
2686 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 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}