Skip to main content

libretro_core/
environment.rs

1//! Typed values for libretro environment commands and frontend state.
2//!
3//! `Environment` methods in `lib.rs` use these enums and newtypes to expose
4//! command-specific semantics for messages, language, power, throttling,
5//! fast-forward, latency, rates, rotation, and AV enable hints.
6
7use std::ffi::{CStr, c_char, c_void};
8use std::ptr;
9
10use enumflags2::{BitFlags, bitflags};
11
12use crate::{
13    CameraInterface, CameraRequest, ControllerInfo, DiskControlInterfaceVersion, Environment,
14    ExtendedGameInfo, FrameTime, InputDescriptor, InputDescriptorStorage, InputDeviceCapabilities,
15    LedInterface, LocationInterface, LogLevel, MemoryMapDescriptor, MicrophoneInterface,
16    MidiInterface, NetplayClientId, PerfInterface, RumbleInterface, SavestateContext,
17    SensorInterface, SerializationQuirks, SoftwareFramebuffer, SoftwareFramebufferRequest,
18    SubsystemInfo, SystemAvInfo, raw,
19};
20
21#[bitflags]
22#[repr(u32)]
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum AvEnable {
25    Video = raw::RETRO_AV_ENABLE_VIDEO,
26    Audio = raw::RETRO_AV_ENABLE_AUDIO,
27    FastSavestates = raw::RETRO_AV_ENABLE_FAST_SAVESTATES,
28    HardDisableAudio = raw::RETRO_AV_ENABLE_HARD_DISABLE_AUDIO,
29}
30
31pub type AvEnableFlags = BitFlags<AvEnable>;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum VideoRotation {
35    #[default]
36    Clockwise0,
37    CounterClockwise90,
38    CounterClockwise180,
39    CounterClockwise270,
40}
41
42impl VideoRotation {
43    pub const fn as_raw(self) -> u32 {
44        match self {
45            Self::Clockwise0 => 0,
46            Self::CounterClockwise90 => 1,
47            Self::CounterClockwise180 => 2,
48            Self::CounterClockwise270 => 3,
49        }
50    }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub struct PerformanceLevel(u32);
55
56impl PerformanceLevel {
57    pub const fn new(level: u32) -> Self {
58        Self(level)
59    }
60
61    pub const fn get(self) -> u32 {
62        self.0
63    }
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
67pub enum ThrottleMode {
68    #[default]
69    None,
70    FrameStepping,
71    FastForward,
72    SlowMotion,
73    Rewinding,
74    Vsync,
75    Unblocked,
76    Unknown(u32),
77}
78
79impl ThrottleMode {
80    pub const fn from_raw(mode: u32) -> Self {
81        match mode {
82            raw::RETRO_THROTTLE_NONE => Self::None,
83            raw::RETRO_THROTTLE_FRAME_STEPPING => Self::FrameStepping,
84            raw::RETRO_THROTTLE_FAST_FORWARD => Self::FastForward,
85            raw::RETRO_THROTTLE_SLOW_MOTION => Self::SlowMotion,
86            raw::RETRO_THROTTLE_REWINDING => Self::Rewinding,
87            raw::RETRO_THROTTLE_VSYNC => Self::Vsync,
88            raw::RETRO_THROTTLE_UNBLOCKED => Self::Unblocked,
89            other => Self::Unknown(other),
90        }
91    }
92
93    pub const fn as_raw(self) -> u32 {
94        match self {
95            Self::None => raw::RETRO_THROTTLE_NONE,
96            Self::FrameStepping => raw::RETRO_THROTTLE_FRAME_STEPPING,
97            Self::FastForward => raw::RETRO_THROTTLE_FAST_FORWARD,
98            Self::SlowMotion => raw::RETRO_THROTTLE_SLOW_MOTION,
99            Self::Rewinding => raw::RETRO_THROTTLE_REWINDING,
100            Self::Vsync => raw::RETRO_THROTTLE_VSYNC,
101            Self::Unblocked => raw::RETRO_THROTTLE_UNBLOCKED,
102            Self::Unknown(mode) => mode,
103        }
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct RunLoopRateHz(f32);
109
110impl RunLoopRateHz {
111    pub fn new(hz: f32) -> Option<Self> {
112        (hz.is_finite() && hz > 0.0).then_some(Self(hz))
113    }
114
115    pub const fn get(self) -> f32 {
116        self.0
117    }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct ThrottleState {
122    pub mode: ThrottleMode,
123    pub rate: Option<RunLoopRateHz>,
124}
125
126impl ThrottleState {
127    fn from_raw(state: raw::retro_throttle_state) -> Self {
128        Self {
129            mode: ThrottleMode::from_raw(state.mode),
130            rate: RunLoopRateHz::new(state.rate),
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct FastForwardRatio(f32);
137
138impl FastForwardRatio {
139    pub const fn frontend_default() -> Self {
140        Self(-1.0)
141    }
142
143    pub const fn normal_speed() -> Self {
144        Self(1.0)
145    }
146
147    pub const fn unlimited() -> Self {
148        Self(0.0)
149    }
150
151    pub fn multiplier(ratio: f32) -> Option<Self> {
152        (ratio.is_finite() && ratio > 1.0).then_some(Self(ratio))
153    }
154
155    pub const fn get(self) -> f32 {
156        self.0
157    }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq)]
161pub struct FastForwardingOverride {
162    pub ratio: FastForwardRatio,
163    pub fastforward: bool,
164    pub notification: bool,
165    pub inhibit_toggle: bool,
166}
167
168impl FastForwardingOverride {
169    pub fn enable() -> Self {
170        Self {
171            ratio: FastForwardRatio::frontend_default(),
172            fastforward: true,
173            notification: true,
174            inhibit_toggle: false,
175        }
176    }
177
178    pub fn disable() -> Self {
179        Self {
180            fastforward: false,
181            ..Self::enable()
182        }
183    }
184
185    pub fn with_ratio(mut self, ratio: FastForwardRatio) -> Self {
186        self.ratio = ratio;
187        self
188    }
189
190    pub fn with_notification(mut self, notification: bool) -> Self {
191        self.notification = notification;
192        self
193    }
194
195    pub fn with_inhibit_toggle(mut self, inhibit_toggle: bool) -> Self {
196        self.inhibit_toggle = inhibit_toggle;
197        self
198    }
199
200    fn into_raw(self) -> raw::retro_fastforwarding_override {
201        raw::retro_fastforwarding_override {
202            ratio: self.ratio.get(),
203            fastforward: self.fastforward,
204            notification: self.notification,
205            inhibit_toggle: self.inhibit_toggle,
206        }
207    }
208}
209
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
211pub enum PowerState {
212    #[default]
213    Unknown,
214    Discharging,
215    Charging,
216    Charged,
217    PluggedIn,
218}
219
220impl PowerState {
221    const fn from_raw(state: raw::retro_power_state) -> Self {
222        match state {
223            raw::retro_power_state::Unknown => Self::Unknown,
224            raw::retro_power_state::Discharging => Self::Discharging,
225            raw::retro_power_state::Charging => Self::Charging,
226            raw::retro_power_state::Charged => Self::Charged,
227            raw::retro_power_state::PluggedIn => Self::PluggedIn,
228        }
229    }
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
233pub struct DevicePower {
234    pub state: PowerState,
235    pub seconds_remaining: Option<u32>,
236    pub percent: Option<u8>,
237}
238
239impl DevicePower {
240    fn from_raw(power: raw::retro_device_power) -> Self {
241        Self {
242            state: PowerState::from_raw(power.state),
243            seconds_remaining: u32::try_from(power.seconds).ok(),
244            percent: u8::try_from(power.percent)
245                .ok()
246                .filter(|percent| *percent <= 100),
247        }
248    }
249}
250
251#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
252pub enum MessageTarget {
253    All,
254    #[default]
255    Osd,
256    Log,
257}
258
259impl MessageTarget {
260    const fn as_raw(self) -> raw::retro_message_target {
261        match self {
262            Self::All => raw::retro_message_target::All,
263            Self::Osd => raw::retro_message_target::Osd,
264            Self::Log => raw::retro_message_target::Log,
265        }
266    }
267}
268
269#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
270pub enum MessageKind {
271    #[default]
272    Notification,
273    NotificationAlt,
274    Status,
275    Progress,
276}
277
278impl MessageKind {
279    const fn as_raw(self) -> raw::retro_message_type {
280        match self {
281            Self::Notification => raw::retro_message_type::Notification,
282            Self::NotificationAlt => raw::retro_message_type::NotificationAlt,
283            Self::Status => raw::retro_message_type::Status,
284            Self::Progress => raw::retro_message_type::Progress,
285        }
286    }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
290pub enum MessageProgress {
291    Indeterminate,
292    Percent(u8),
293}
294
295impl MessageProgress {
296    pub fn percent(percent: u8) -> Option<Self> {
297        (percent <= 100).then_some(Self::Percent(percent))
298    }
299
300    const fn as_raw(self) -> i8 {
301        match self {
302            Self::Indeterminate => -1,
303            Self::Percent(percent) => percent as i8,
304        }
305    }
306}
307
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub struct ExtendedMessage {
310    message: String,
311    duration_millis: u32,
312    priority: u32,
313    level: LogLevel,
314    target: MessageTarget,
315    kind: MessageKind,
316    progress: MessageProgress,
317}
318
319impl ExtendedMessage {
320    pub fn new(message: impl Into<String>) -> Self {
321        Self {
322            message: message.into(),
323            duration_millis: 0,
324            priority: 0,
325            level: LogLevel::Info,
326            target: MessageTarget::Osd,
327            kind: MessageKind::Notification,
328            progress: MessageProgress::Indeterminate,
329        }
330    }
331
332    pub fn with_duration_millis(mut self, duration_millis: u32) -> Self {
333        self.duration_millis = duration_millis;
334        self
335    }
336
337    pub fn with_priority(mut self, priority: u32) -> Self {
338        self.priority = priority;
339        self
340    }
341
342    pub fn with_level(mut self, level: LogLevel) -> Self {
343        self.level = level;
344        self
345    }
346
347    pub fn with_target(mut self, target: MessageTarget) -> Self {
348        self.target = target;
349        self
350    }
351
352    pub fn with_kind(mut self, kind: MessageKind) -> Self {
353        self.kind = kind;
354        self
355    }
356
357    pub fn with_progress(mut self, progress: MessageProgress) -> Self {
358        self.progress = progress;
359        self.kind = MessageKind::Progress;
360        self
361    }
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
365pub struct AudioLatencyMillis(u32);
366
367impl AudioLatencyMillis {
368    pub const fn new(milliseconds: u32) -> Self {
369        Self(milliseconds)
370    }
371
372    pub const fn as_millis(self) -> u32 {
373        self.0
374    }
375}
376
377#[derive(Clone, Copy, Debug, PartialEq)]
378pub struct RefreshRateHz(f32);
379
380impl RefreshRateHz {
381    pub const fn new(hz: f32) -> Self {
382        Self(hz)
383    }
384
385    pub const fn get(self) -> f32 {
386        self.0
387    }
388}
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
391pub struct AudioSampleRateHz(u32);
392
393impl AudioSampleRateHz {
394    pub const fn new(hz: u32) -> Self {
395        Self(hz)
396    }
397
398    pub const fn get(self) -> u32 {
399        self.0
400    }
401}
402
403#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
404pub enum Language {
405    #[default]
406    English,
407    Japanese,
408    French,
409    Spanish,
410    German,
411    Italian,
412    Dutch,
413    PortugueseBrazil,
414    PortuguesePortugal,
415    Russian,
416    Korean,
417    ChineseTraditional,
418    ChineseSimplified,
419    Esperanto,
420    Polish,
421    Vietnamese,
422    Arabic,
423    Greek,
424    Turkish,
425    Slovak,
426    Persian,
427    Hebrew,
428    Asturian,
429    Finnish,
430    Indonesian,
431    Swedish,
432    Ukrainian,
433    Czech,
434    CatalanValencia,
435    Catalan,
436    BritishEnglish,
437    Hungarian,
438    Belarusian,
439    Galician,
440    Norwegian,
441    Irish,
442    Unknown(i32),
443}
444
445impl Language {
446    pub const fn from_raw(language: i32) -> Self {
447        match language {
448            0 => Self::English,
449            1 => Self::Japanese,
450            2 => Self::French,
451            3 => Self::Spanish,
452            4 => Self::German,
453            5 => Self::Italian,
454            6 => Self::Dutch,
455            7 => Self::PortugueseBrazil,
456            8 => Self::PortuguesePortugal,
457            9 => Self::Russian,
458            10 => Self::Korean,
459            11 => Self::ChineseTraditional,
460            12 => Self::ChineseSimplified,
461            13 => Self::Esperanto,
462            14 => Self::Polish,
463            15 => Self::Vietnamese,
464            16 => Self::Arabic,
465            17 => Self::Greek,
466            18 => Self::Turkish,
467            19 => Self::Slovak,
468            20 => Self::Persian,
469            21 => Self::Hebrew,
470            22 => Self::Asturian,
471            23 => Self::Finnish,
472            24 => Self::Indonesian,
473            25 => Self::Swedish,
474            26 => Self::Ukrainian,
475            27 => Self::Czech,
476            28 => Self::CatalanValencia,
477            29 => Self::Catalan,
478            30 => Self::BritishEnglish,
479            31 => Self::Hungarian,
480            32 => Self::Belarusian,
481            33 => Self::Galician,
482            34 => Self::Norwegian,
483            35 => Self::Irish,
484            other => Self::Unknown(other),
485        }
486    }
487
488    pub const fn as_raw(self) -> i32 {
489        match self {
490            Self::English => 0,
491            Self::Japanese => 1,
492            Self::French => 2,
493            Self::Spanish => 3,
494            Self::German => 4,
495            Self::Italian => 5,
496            Self::Dutch => 6,
497            Self::PortugueseBrazil => 7,
498            Self::PortuguesePortugal => 8,
499            Self::Russian => 9,
500            Self::Korean => 10,
501            Self::ChineseTraditional => 11,
502            Self::ChineseSimplified => 12,
503            Self::Esperanto => 13,
504            Self::Polish => 14,
505            Self::Vietnamese => 15,
506            Self::Arabic => 16,
507            Self::Greek => 17,
508            Self::Turkish => 18,
509            Self::Slovak => 19,
510            Self::Persian => 20,
511            Self::Hebrew => 21,
512            Self::Asturian => 22,
513            Self::Finnish => 23,
514            Self::Indonesian => 24,
515            Self::Swedish => 25,
516            Self::Ukrainian => 26,
517            Self::Czech => 27,
518            Self::CatalanValencia => 28,
519            Self::Catalan => 29,
520            Self::BritishEnglish => 30,
521            Self::Hungarian => 31,
522            Self::Belarusian => 32,
523            Self::Galician => 33,
524            Self::Norwegian => 34,
525            Self::Irish => 35,
526            Self::Unknown(language) => language,
527        }
528    }
529}
530
531impl Environment<'_> {
532    pub fn set_rotation(&mut self, rotation: VideoRotation) -> bool {
533        let mut raw_rotation = rotation.as_raw();
534        self.call_env(
535            raw::RETRO_ENVIRONMENT_SET_ROTATION,
536            (&mut raw_rotation as *mut u32).cast::<c_void>(),
537        )
538    }
539
540    pub fn overscan(&mut self) -> Option<bool> {
541        let mut overscan = false;
542        if self.call_env(
543            raw::RETRO_ENVIRONMENT_GET_OVERSCAN,
544            (&mut overscan as *mut bool).cast::<c_void>(),
545        ) {
546            Some(overscan)
547        } else {
548            None
549        }
550    }
551
552    pub fn can_dupe_frames(&mut self) -> Option<bool> {
553        let mut can_dupe = false;
554        if self.call_env(
555            raw::RETRO_ENVIRONMENT_GET_CAN_DUPE,
556            (&mut can_dupe as *mut bool).cast::<c_void>(),
557        ) {
558            Some(can_dupe)
559        } else {
560            None
561        }
562    }
563
564    pub fn shutdown(&mut self) -> bool {
565        self.call_env(raw::RETRO_ENVIRONMENT_SHUTDOWN, ptr::null_mut())
566    }
567
568    pub fn set_system_av_info(&mut self, info: SystemAvInfo) -> bool {
569        let mut info = info.as_raw();
570        self.call_env(
571            raw::RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO,
572            (&mut info as *mut raw::retro_system_av_info).cast::<c_void>(),
573        )
574    }
575
576    pub fn extended_game_info(&mut self) -> Option<ExtendedGameInfo<'_>> {
577        self.extended_game_infos(1)
578            .and_then(|mut infos| infos.pop())
579    }
580
581    pub fn extended_game_infos(&mut self, count: usize) -> Option<Vec<ExtendedGameInfo<'_>>> {
582        if count == 0 {
583            return Some(Vec::new());
584        }
585
586        let mut infos = ptr::null::<raw::retro_game_info_ext>();
587        if !self.call_env(
588            raw::RETRO_ENVIRONMENT_GET_GAME_INFO_EXT,
589            (&mut infos as *mut *const raw::retro_game_info_ext).cast::<c_void>(),
590        ) || infos.is_null()
591        {
592            return None;
593        }
594
595        // SAFETY: On success, libretro returns an array whose length is one
596        // during retro_load_game or `num_info` during retro_load_game_special.
597        let infos = unsafe { std::slice::from_raw_parts(infos, count) };
598        Some(
599            infos
600                .iter()
601                .map(|info| unsafe { ExtendedGameInfo::from_raw(info) })
602                .collect(),
603        )
604    }
605
606    pub fn set_performance_level(&mut self, level: PerformanceLevel) -> bool {
607        let mut raw_level = level.get();
608        self.call_env(
609            raw::RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL,
610            (&mut raw_level as *mut u32).cast::<c_void>(),
611        )
612    }
613
614    pub fn perf_interface(&mut self) -> Option<PerfInterface> {
615        let mut callback = raw::retro_perf_callback::default();
616        if self.call_env(
617            raw::RETRO_ENVIRONMENT_GET_PERF_INTERFACE,
618            (&mut callback as *mut raw::retro_perf_callback).cast::<c_void>(),
619        ) {
620            Some(PerfInterface::from_raw(callback))
621        } else {
622            None
623        }
624    }
625
626    pub fn system_directory(&mut self) -> Option<String> {
627        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
628    }
629
630    pub fn libretro_path(&mut self) -> Option<String> {
631        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_LIBRETRO_PATH)
632    }
633
634    pub fn core_assets_directory(&mut self) -> Option<String> {
635        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY)
636    }
637
638    pub fn content_directory(&mut self) -> Option<String> {
639        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY)
640    }
641
642    pub fn save_directory(&mut self) -> Option<String> {
643        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
644    }
645
646    pub fn username(&mut self) -> Option<String> {
647        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_USERNAME)
648    }
649
650    pub fn playlist_directory(&mut self) -> Option<String> {
651        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_PLAYLIST_DIRECTORY)
652    }
653
654    pub fn file_browser_start_directory(&mut self) -> Option<String> {
655        self.frontend_string(raw::RETRO_ENVIRONMENT_GET_FILE_BROWSER_START_DIRECTORY)
656    }
657
658    pub fn language(&mut self) -> Option<Language> {
659        let mut language = raw::retro_language::English as i32;
660        if self.call_env(
661            raw::RETRO_ENVIRONMENT_GET_LANGUAGE,
662            (&mut language as *mut i32).cast::<c_void>(),
663        ) {
664            Some(Language::from_raw(language))
665        } else {
666            None
667        }
668    }
669
670    pub fn jit_capable(&mut self) -> Option<bool> {
671        let mut capable = false;
672        if self.call_env(
673            raw::RETRO_ENVIRONMENT_GET_JIT_CAPABLE,
674            (&mut capable as *mut bool).cast::<c_void>(),
675        ) {
676            Some(capable)
677        } else {
678            None
679        }
680    }
681
682    pub fn set_support_achievements(&mut self, supported: bool) -> bool {
683        let mut raw_supported = supported;
684        self.call_env(
685            raw::RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS,
686            (&mut raw_supported as *mut bool).cast::<c_void>(),
687        )
688    }
689
690    pub fn audio_video_enable(&mut self) -> Option<AvEnableFlags> {
691        let mut flags = 0u32;
692        if self.call_env(
693            raw::RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE,
694            (&mut flags as *mut u32).cast::<c_void>(),
695        ) {
696            Some(AvEnableFlags::from_bits_truncate(flags))
697        } else {
698            None
699        }
700    }
701
702    pub fn fastforwarding(&mut self) -> Option<bool> {
703        let mut fastforwarding = false;
704        if self.call_env(
705            raw::RETRO_ENVIRONMENT_GET_FASTFORWARDING,
706            (&mut fastforwarding as *mut bool).cast::<c_void>(),
707        ) {
708            Some(fastforwarding)
709        } else {
710            None
711        }
712    }
713
714    pub fn fastforwarding_override_supported(&mut self) -> bool {
715        self.call_env(
716            raw::RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE,
717            ptr::null_mut(),
718        )
719    }
720
721    pub fn set_fastforwarding_override(&mut self, override_: FastForwardingOverride) -> bool {
722        let mut override_ = override_.into_raw();
723        self.call_env(
724            raw::RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE,
725            (&mut override_ as *mut raw::retro_fastforwarding_override).cast::<c_void>(),
726        )
727    }
728
729    pub fn target_refresh_rate(&mut self) -> Option<RefreshRateHz> {
730        let mut hz = 0.0f32;
731        if self.call_env(
732            raw::RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE,
733            (&mut hz as *mut f32).cast::<c_void>(),
734        ) {
735            Some(RefreshRateHz::new(hz))
736        } else {
737            None
738        }
739    }
740
741    pub fn target_sample_rate(&mut self) -> Option<AudioSampleRateHz> {
742        let mut hz = 0u32;
743        if self.call_env(
744            raw::RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE,
745            (&mut hz as *mut u32).cast::<c_void>(),
746        ) {
747            Some(AudioSampleRateHz::new(hz))
748        } else {
749            None
750        }
751    }
752
753    pub fn throttle_state(&mut self) -> Option<ThrottleState> {
754        let mut state = raw::retro_throttle_state::default();
755        if self.call_env(
756            raw::RETRO_ENVIRONMENT_GET_THROTTLE_STATE,
757            (&mut state as *mut raw::retro_throttle_state).cast::<c_void>(),
758        ) {
759            Some(ThrottleState::from_raw(state))
760        } else {
761            None
762        }
763    }
764
765    pub fn savestate_context(&mut self) -> Option<SavestateContext> {
766        let mut context = SavestateContext::Normal.as_raw();
767        if self.call_env(
768            raw::RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT,
769            (&mut context as *mut i32).cast::<c_void>(),
770        ) {
771            Some(SavestateContext::from_raw(context))
772        } else {
773            None
774        }
775    }
776
777    pub fn set_memory_maps(&mut self, descriptors: &[MemoryMapDescriptor<'_>]) -> bool {
778        let Ok(num_descriptors) = u32::try_from(descriptors.len()) else {
779            return false;
780        };
781
782        let mut addrspaces = Vec::with_capacity(descriptors.len());
783        let mut raw_descriptors = Vec::with_capacity(descriptors.len());
784        for descriptor in descriptors {
785            let addrspace = descriptor.addrspace.as_ref().map(crate::sanitize_cstring);
786            let addrspace_ptr = addrspace
787                .as_ref()
788                .map(|addrspace| addrspace.as_ptr())
789                .unwrap_or(ptr::null());
790            raw_descriptors.push(raw::retro_memory_descriptor {
791                flags: descriptor.raw_flags(),
792                ptr: descriptor
793                    .ptr
794                    .map(|ptr| ptr.as_ptr().cast::<c_void>())
795                    .unwrap_or(ptr::null_mut()),
796                offset: descriptor.offset.as_usize(),
797                start: descriptor.start.as_usize(),
798                select: descriptor.select.as_usize(),
799                disconnect: descriptor.disconnect.as_usize(),
800                len: descriptor.len.as_usize(),
801                addrspace: addrspace_ptr,
802            });
803            addrspaces.push(addrspace);
804        }
805
806        let mut map = raw::retro_memory_map {
807            descriptors: raw_descriptors.as_ptr(),
808            num_descriptors,
809        };
810        self.call_env(
811            raw::RETRO_ENVIRONMENT_SET_MEMORY_MAPS,
812            (&mut map as *mut raw::retro_memory_map).cast::<c_void>(),
813        )
814    }
815
816    pub fn set_controller_info(&mut self, ports: &[ControllerInfo]) -> bool {
817        let mut descriptions = Vec::with_capacity(ports.len());
818        let mut raw_description_groups = Vec::with_capacity(ports.len());
819        for port in ports {
820            if port.types.is_empty() {
821                return false;
822            }
823            let Ok(num_types) = u32::try_from(port.types.len()) else {
824                return false;
825            };
826
827            let mut port_descriptions = Vec::with_capacity(port.types.len());
828            let mut raw_descriptions = Vec::with_capacity(port.types.len());
829            for controller in &port.types {
830                let description = crate::sanitize_cstring(&controller.description);
831                raw_descriptions.push(raw::retro_controller_description {
832                    desc: description.as_ptr(),
833                    id: controller.device.as_raw(),
834                });
835                port_descriptions.push(description);
836            }
837
838            descriptions.push(port_descriptions);
839            raw_description_groups.push((raw_descriptions, num_types));
840        }
841
842        let mut raw_ports = raw_description_groups
843            .iter()
844            .map(|(raw_descriptions, num_types)| raw::retro_controller_info {
845                types: raw_descriptions.as_ptr(),
846                num_types: *num_types,
847            })
848            .collect::<Vec<_>>();
849        raw_ports.push(raw::retro_controller_info::default());
850
851        let ok = self.call_env(
852            raw::RETRO_ENVIRONMENT_SET_CONTROLLER_INFO,
853            raw_ports.as_mut_ptr().cast::<c_void>(),
854        );
855        drop(descriptions);
856        ok
857    }
858
859    pub fn set_proc_address_callback(&mut self) -> bool {
860        let mut interface = raw::retro_get_proc_address_interface {
861            get_proc_address: Some(crate::proc_address_trampoline),
862        };
863        self.call_env(
864            raw::RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK,
865            (&mut interface as *mut raw::retro_get_proc_address_interface).cast::<c_void>(),
866        )
867    }
868
869    pub fn set_subsystem_info(&mut self, subsystems: &[SubsystemInfo]) -> bool {
870        let mut storage = crate::subsystem::SubsystemInfoStorage::new(subsystems);
871        let ok = self.call_env(
872            raw::RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO,
873            storage.raw.as_mut_ptr().cast::<c_void>(),
874        );
875        if ok {
876            self.state.subsystem_info = Some(storage);
877        }
878        ok
879    }
880
881    pub fn disk_control_interface_version(&mut self) -> Option<DiskControlInterfaceVersion> {
882        let mut version = 0u32;
883        if self.call_env(
884            raw::RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION,
885            (&mut version as *mut u32).cast::<c_void>(),
886        ) {
887            Some(DiskControlInterfaceVersion::new(version))
888        } else {
889            None
890        }
891    }
892
893    pub fn set_disk_control_interface(&mut self) -> bool {
894        let mut callback = raw::retro_disk_control_callback {
895            set_eject_state: Some(crate::disk_set_eject_state_trampoline),
896            get_eject_state: Some(crate::disk_get_eject_state_trampoline),
897            get_image_index: Some(crate::disk_get_image_index_trampoline),
898            set_image_index: Some(crate::disk_set_image_index_trampoline),
899            get_num_images: Some(crate::disk_get_num_images_trampoline),
900            replace_image_index: Some(crate::disk_replace_image_index_trampoline),
901            add_image_index: Some(crate::disk_add_image_index_trampoline),
902        };
903        self.call_env(
904            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE,
905            (&mut callback as *mut raw::retro_disk_control_callback).cast::<c_void>(),
906        )
907    }
908
909    pub fn clear_disk_control_interface(&mut self) -> bool {
910        self.call_env(
911            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE,
912            ptr::null_mut(),
913        )
914    }
915
916    pub fn set_disk_control_ext_interface(&mut self) -> bool {
917        let mut callback = raw::retro_disk_control_ext_callback {
918            set_eject_state: Some(crate::disk_set_eject_state_trampoline),
919            get_eject_state: Some(crate::disk_get_eject_state_trampoline),
920            get_image_index: Some(crate::disk_get_image_index_trampoline),
921            set_image_index: Some(crate::disk_set_image_index_trampoline),
922            get_num_images: Some(crate::disk_get_num_images_trampoline),
923            replace_image_index: Some(crate::disk_replace_image_index_trampoline),
924            add_image_index: Some(crate::disk_add_image_index_trampoline),
925            set_initial_image: Some(crate::disk_set_initial_image_trampoline),
926            get_image_path: Some(crate::disk_get_image_path_trampoline),
927            get_image_label: Some(crate::disk_get_image_label_trampoline),
928        };
929        self.call_env(
930            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE,
931            (&mut callback as *mut raw::retro_disk_control_ext_callback).cast::<c_void>(),
932        )
933    }
934
935    pub fn clear_disk_control_ext_interface(&mut self) -> bool {
936        self.call_env(
937            raw::RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE,
938            ptr::null_mut(),
939        )
940    }
941
942    pub fn set_netpacket_interface(&mut self, protocol_version: Option<&str>) -> bool {
943        let mut storage = crate::netplay::NetpacketInterfaceStorage::new(protocol_version);
944        let ok = self.call_env(
945            raw::RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE,
946            (&mut storage.raw as *mut raw::retro_netpacket_callback).cast::<c_void>(),
947        );
948        if ok {
949            self.state.netpacket_interface = Some(storage);
950        }
951        ok
952    }
953
954    pub fn clear_netpacket_interface(&mut self) -> bool {
955        let ok = self.call_env(
956            raw::RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE,
957            ptr::null_mut(),
958        );
959        if ok {
960            self.state.netpacket_interface = None;
961        }
962        ok
963    }
964
965    pub fn current_software_framebuffer(
966        &mut self,
967        request: SoftwareFramebufferRequest,
968    ) -> Option<SoftwareFramebuffer> {
969        let mut framebuffer = request.into_raw();
970        if self.call_env(
971            raw::RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER,
972            (&mut framebuffer as *mut raw::retro_framebuffer).cast::<c_void>(),
973        ) {
974            SoftwareFramebuffer::from_raw(framebuffer)
975        } else {
976            None
977        }
978    }
979
980    pub fn set_minimum_audio_latency(&mut self, latency: Option<AudioLatencyMillis>) -> bool {
981        match latency {
982            Some(latency) => {
983                let mut milliseconds = latency.as_millis();
984                self.call_env(
985                    raw::RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY,
986                    (&mut milliseconds as *mut u32).cast::<c_void>(),
987                )
988            }
989            None => self.call_env(
990                raw::RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY,
991                ptr::null_mut(),
992            ),
993        }
994    }
995
996    pub fn set_audio_buffer_status_callback(&mut self, enabled: bool) -> bool {
997        if enabled {
998            let mut callback = raw::retro_audio_buffer_status_callback {
999                callback: Some(crate::audio_buffer_status_trampoline),
1000            };
1001            self.call_env(
1002                raw::RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK,
1003                (&mut callback as *mut raw::retro_audio_buffer_status_callback).cast::<c_void>(),
1004            )
1005        } else {
1006            self.call_env(
1007                raw::RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK,
1008                ptr::null_mut(),
1009            )
1010        }
1011    }
1012
1013    pub fn audio_callback_available(&mut self) -> bool {
1014        self.call_env(raw::RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, ptr::null_mut())
1015    }
1016
1017    pub fn set_audio_callback(&mut self) -> bool {
1018        let mut callback = raw::retro_audio_callback {
1019            callback: Some(crate::audio_callback_trampoline),
1020            set_state: Some(crate::audio_set_state_trampoline),
1021        };
1022        self.call_env(
1023            raw::RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK,
1024            (&mut callback as *mut raw::retro_audio_callback).cast::<c_void>(),
1025        )
1026    }
1027
1028    pub fn clear_audio_callback(&mut self) -> bool {
1029        let mut callback = raw::retro_audio_callback::default();
1030        self.call_env(
1031            raw::RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK,
1032            (&mut callback as *mut raw::retro_audio_callback).cast::<c_void>(),
1033        )
1034    }
1035
1036    pub fn set_frame_time_callback(&mut self, reference: FrameTime) -> bool {
1037        let mut callback = raw::retro_frame_time_callback {
1038            callback: Some(crate::frame_time_trampoline),
1039            reference: reference.as_micros(),
1040        };
1041        self.call_env(
1042            raw::RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK,
1043            (&mut callback as *mut raw::retro_frame_time_callback).cast::<c_void>(),
1044        )
1045    }
1046
1047    pub fn clear_frame_time_callback(&mut self) -> bool {
1048        self.call_env(
1049            raw::RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK,
1050            ptr::null_mut(),
1051        )
1052    }
1053
1054    pub fn message_interface_version(&mut self) -> Option<u32> {
1055        let mut version = 0u32;
1056        if self.call_env(
1057            raw::RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION,
1058            (&mut version as *mut u32).cast::<c_void>(),
1059        ) {
1060            Some(version)
1061        } else {
1062            None
1063        }
1064    }
1065
1066    pub fn set_message_ext(&mut self, message: ExtendedMessage) -> bool {
1067        let text = crate::sanitize_cstring(&message.message);
1068        let mut raw_message = raw::retro_message_ext {
1069            msg: text.as_ptr(),
1070            duration: message.duration_millis,
1071            priority: message.priority,
1072            level: message.level,
1073            target: message.target.as_raw(),
1074            type_: message.kind.as_raw(),
1075            progress: message.progress.as_raw(),
1076        };
1077        self.call_env(
1078            raw::RETRO_ENVIRONMENT_SET_MESSAGE_EXT,
1079            (&mut raw_message as *mut raw::retro_message_ext).cast::<c_void>(),
1080        )
1081    }
1082
1083    pub fn input_device_capabilities(&mut self) -> Option<InputDeviceCapabilities> {
1084        let mut capabilities = 0u64;
1085        if self.call_env(
1086            raw::RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES,
1087            (&mut capabilities as *mut u64).cast::<c_void>(),
1088        ) {
1089            Some(InputDeviceCapabilities::from_bits_truncate(capabilities))
1090        } else {
1091            None
1092        }
1093    }
1094
1095    pub fn supports_joypad_bitmasks(&mut self) -> bool {
1096        self.call_env(raw::RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, ptr::null_mut())
1097    }
1098
1099    pub fn set_keyboard_callback(&mut self) -> bool {
1100        let mut callback = raw::retro_keyboard_callback {
1101            callback: Some(crate::keyboard_event_trampoline),
1102        };
1103        self.call_env(
1104            raw::RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK,
1105            (&mut callback as *mut raw::retro_keyboard_callback).cast::<c_void>(),
1106        )
1107    }
1108
1109    pub fn set_input_descriptors(&mut self, descriptors: &[InputDescriptor]) -> bool {
1110        let mut descriptions = Vec::with_capacity(descriptors.len());
1111        let mut raw_descriptors = Vec::with_capacity(descriptors.len() + 1);
1112
1113        for descriptor in descriptors {
1114            let description = crate::sanitize_cstring(&descriptor.description);
1115            raw_descriptors.push(raw::retro_input_descriptor {
1116                port: descriptor.port.as_raw(),
1117                device: descriptor.device.as_raw(),
1118                index: descriptor.index.as_raw(),
1119                id: descriptor.id.as_raw(),
1120                description: description.as_ptr(),
1121            });
1122            descriptions.push(description);
1123        }
1124        raw_descriptors.push(raw::retro_input_descriptor::default());
1125
1126        let ok = self.call_env(
1127            raw::RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS,
1128            raw_descriptors.as_mut_ptr().cast::<c_void>(),
1129        );
1130        if ok {
1131            self.state.input_descriptors = Some(InputDescriptorStorage {
1132                _descriptions: descriptions,
1133                _raw: raw_descriptors,
1134            });
1135        }
1136        ok
1137    }
1138
1139    pub fn input_max_users(&mut self) -> Option<u32> {
1140        let mut users = 0u32;
1141        if self.call_env(
1142            raw::RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS,
1143            (&mut users as *mut u32).cast::<c_void>(),
1144        ) {
1145            Some(users)
1146        } else {
1147            None
1148        }
1149    }
1150
1151    pub fn led_interface(&mut self) -> Option<LedInterface> {
1152        let mut interface = raw::retro_led_interface::default();
1153        if self.call_env(
1154            raw::RETRO_ENVIRONMENT_GET_LED_INTERFACE,
1155            (&mut interface as *mut raw::retro_led_interface).cast::<c_void>(),
1156        ) {
1157            Some(LedInterface::from_raw(interface))
1158        } else {
1159            None
1160        }
1161    }
1162
1163    pub fn rumble_interface(&mut self) -> Option<RumbleInterface> {
1164        let mut interface = raw::retro_rumble_interface::default();
1165        if self.call_env(
1166            raw::RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE,
1167            (&mut interface as *mut raw::retro_rumble_interface).cast::<c_void>(),
1168        ) {
1169            Some(RumbleInterface::from_raw(interface))
1170        } else {
1171            None
1172        }
1173    }
1174
1175    pub fn device_power(&mut self) -> Option<DevicePower> {
1176        let mut power = raw::retro_device_power::default();
1177        if self.call_env(
1178            raw::RETRO_ENVIRONMENT_GET_DEVICE_POWER,
1179            (&mut power as *mut raw::retro_device_power).cast::<c_void>(),
1180        ) {
1181            Some(DevicePower::from_raw(power))
1182        } else {
1183            None
1184        }
1185    }
1186
1187    pub fn netplay_client_index(&mut self) -> Option<NetplayClientId> {
1188        let mut client_id = 0u32;
1189        if self.call_env(
1190            raw::RETRO_ENVIRONMENT_GET_NETPLAY_CLIENT_INDEX,
1191            (&mut client_id as *mut u32).cast::<c_void>(),
1192        ) {
1193            u16::try_from(client_id).ok().map(NetplayClientId::new)
1194        } else {
1195            None
1196        }
1197    }
1198
1199    pub fn sensor_interface(&mut self) -> Option<SensorInterface> {
1200        let mut interface = raw::retro_sensor_interface::default();
1201        if self.call_env(
1202            raw::RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE,
1203            (&mut interface as *mut raw::retro_sensor_interface).cast::<c_void>(),
1204        ) {
1205            Some(SensorInterface::from_raw(interface))
1206        } else {
1207            None
1208        }
1209    }
1210
1211    pub fn camera_interface(&mut self, request: CameraRequest) -> Option<CameraInterface> {
1212        let mut interface = raw::retro_camera_callback {
1213            caps: request.capabilities.bits(),
1214            width: request.size.width,
1215            height: request.size.height,
1216            ..raw::retro_camera_callback::default()
1217        };
1218        if !self.state.event_handlers.camera_raw_frame.is_empty() {
1219            interface.frame_raw_framebuffer = Some(crate::camera_frame_raw_trampoline);
1220        }
1221        if !self.state.event_handlers.camera_texture_frame.is_empty() {
1222            interface.frame_opengl_texture = Some(crate::camera_frame_opengl_texture_trampoline);
1223        }
1224        if !self.state.event_handlers.camera_initialized.is_empty() {
1225            interface.initialized = Some(crate::camera_initialized_trampoline);
1226        }
1227        if !self.state.event_handlers.camera_deinitialized.is_empty() {
1228            interface.deinitialized = Some(crate::camera_deinitialized_trampoline);
1229        }
1230        if self.call_env(
1231            raw::RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE,
1232            (&mut interface as *mut raw::retro_camera_callback).cast::<c_void>(),
1233        ) {
1234            Some(CameraInterface::from_raw(interface))
1235        } else {
1236            None
1237        }
1238    }
1239
1240    pub fn location_interface(&mut self) -> Option<LocationInterface> {
1241        let mut interface = raw::retro_location_callback::default();
1242        if !self.state.event_handlers.location_initialized.is_empty() {
1243            interface.initialized = Some(crate::location_initialized_trampoline);
1244        }
1245        if !self.state.event_handlers.location_deinitialized.is_empty() {
1246            interface.deinitialized = Some(crate::location_deinitialized_trampoline);
1247        }
1248        if self.call_env(
1249            raw::RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE,
1250            (&mut interface as *mut raw::retro_location_callback).cast::<c_void>(),
1251        ) {
1252            Some(LocationInterface::from_raw(interface))
1253        } else {
1254            None
1255        }
1256    }
1257
1258    pub fn microphone_interface(&mut self) -> Option<MicrophoneInterface> {
1259        let mut interface = raw::retro_microphone_interface {
1260            interface_version: raw::RETRO_MICROPHONE_INTERFACE_VERSION,
1261            ..raw::retro_microphone_interface::default()
1262        };
1263        if self.call_env(
1264            raw::RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE,
1265            (&mut interface as *mut raw::retro_microphone_interface).cast::<c_void>(),
1266        ) {
1267            Some(MicrophoneInterface::from_raw(interface))
1268        } else {
1269            None
1270        }
1271    }
1272
1273    pub fn midi_interface_available(&mut self) -> bool {
1274        self.call_env(raw::RETRO_ENVIRONMENT_GET_MIDI_INTERFACE, ptr::null_mut())
1275    }
1276
1277    pub fn midi_interface(&mut self) -> Option<MidiInterface> {
1278        let mut interface = raw::retro_midi_interface::default();
1279        if self.call_env(
1280            raw::RETRO_ENVIRONMENT_GET_MIDI_INTERFACE,
1281            (&mut interface as *mut raw::retro_midi_interface).cast::<c_void>(),
1282        ) {
1283            Some(MidiInterface::from_raw(interface))
1284        } else {
1285            None
1286        }
1287    }
1288
1289    pub fn set_serialization_quirks(
1290        &mut self,
1291        quirks: SerializationQuirks,
1292    ) -> Option<SerializationQuirks> {
1293        let mut raw_quirks = quirks.bits();
1294        if self.call_env(
1295            raw::RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS,
1296            (&mut raw_quirks as *mut u64).cast::<c_void>(),
1297        ) {
1298            Some(SerializationQuirks::from_bits_truncate(raw_quirks))
1299        } else {
1300            None
1301        }
1302    }
1303
1304    fn frontend_string(&mut self, command: u32) -> Option<String> {
1305        let mut value: *const c_char = ptr::null();
1306        let ok = self.call_env(command, (&mut value as *mut *const c_char).cast::<c_void>());
1307        if !ok || value.is_null() {
1308            return None;
1309        }
1310
1311        // SAFETY: On success with a non-null pointer, the frontend returns a
1312        // valid NUL-terminated, frontend-owned string for this immediate read.
1313        Some(
1314            unsafe { CStr::from_ptr(value) }
1315                .to_string_lossy()
1316                .into_owned(),
1317        )
1318    }
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323    use super::{
1324        FastForwardRatio, Language, MessageProgress, RunLoopRateHz, ThrottleMode, VideoRotation,
1325    };
1326
1327    #[test]
1328    fn known_languages_round_trip_to_libretro_ids() {
1329        let languages = [
1330            Language::English,
1331            Language::Japanese,
1332            Language::French,
1333            Language::Spanish,
1334            Language::German,
1335            Language::Italian,
1336            Language::Dutch,
1337            Language::PortugueseBrazil,
1338            Language::PortuguesePortugal,
1339            Language::Russian,
1340            Language::Korean,
1341            Language::ChineseTraditional,
1342            Language::ChineseSimplified,
1343            Language::Esperanto,
1344            Language::Polish,
1345            Language::Vietnamese,
1346            Language::Arabic,
1347            Language::Greek,
1348            Language::Turkish,
1349            Language::Slovak,
1350            Language::Persian,
1351            Language::Hebrew,
1352            Language::Asturian,
1353            Language::Finnish,
1354            Language::Indonesian,
1355            Language::Swedish,
1356            Language::Ukrainian,
1357            Language::Czech,
1358            Language::CatalanValencia,
1359            Language::Catalan,
1360            Language::BritishEnglish,
1361            Language::Hungarian,
1362            Language::Belarusian,
1363            Language::Galician,
1364            Language::Norwegian,
1365            Language::Irish,
1366        ];
1367
1368        for language in languages {
1369            assert_eq!(Language::from_raw(language.as_raw()), language);
1370        }
1371    }
1372
1373    #[test]
1374    fn unknown_language_preserves_original_id() {
1375        assert_eq!(Language::from_raw(99), Language::Unknown(99));
1376        assert_eq!(Language::Unknown(99).as_raw(), 99);
1377    }
1378
1379    #[test]
1380    fn progress_percent_rejects_values_outside_libretro_range() {
1381        assert_eq!(
1382            MessageProgress::percent(100),
1383            Some(MessageProgress::Percent(100))
1384        );
1385        assert_eq!(MessageProgress::percent(101), None);
1386    }
1387
1388    #[test]
1389    fn video_rotations_encode_libretro_values() {
1390        assert_eq!(VideoRotation::Clockwise0.as_raw(), 0);
1391        assert_eq!(VideoRotation::CounterClockwise90.as_raw(), 1);
1392        assert_eq!(VideoRotation::CounterClockwise180.as_raw(), 2);
1393        assert_eq!(VideoRotation::CounterClockwise270.as_raw(), 3);
1394    }
1395
1396    #[test]
1397    fn throttle_modes_round_trip_to_libretro_ids() {
1398        let modes = [
1399            ThrottleMode::None,
1400            ThrottleMode::FrameStepping,
1401            ThrottleMode::FastForward,
1402            ThrottleMode::SlowMotion,
1403            ThrottleMode::Rewinding,
1404            ThrottleMode::Vsync,
1405            ThrottleMode::Unblocked,
1406        ];
1407
1408        for mode in modes {
1409            assert_eq!(ThrottleMode::from_raw(mode.as_raw()), mode);
1410        }
1411    }
1412
1413    #[test]
1414    fn throttle_rate_rejects_unknown_or_invalid_rates() {
1415        assert_eq!(RunLoopRateHz::new(60.0).map(RunLoopRateHz::get), Some(60.0));
1416        assert_eq!(RunLoopRateHz::new(0.0), None);
1417        assert_eq!(RunLoopRateHz::new(f32::NAN), None);
1418    }
1419
1420    #[test]
1421    fn fastforward_ratio_rejects_invalid_explicit_multipliers() {
1422        assert_eq!(
1423            FastForwardRatio::multiplier(2.0).map(FastForwardRatio::get),
1424            Some(2.0)
1425        );
1426        assert_eq!(FastForwardRatio::multiplier(1.0), None);
1427        assert_eq!(FastForwardRatio::multiplier(f32::INFINITY), None);
1428        assert_eq!(FastForwardRatio::multiplier(f32::NAN), None);
1429    }
1430}