Skip to main content

libretro_core/
av.rs

1//! Typed video geometry, timing, and audio pacing helpers.
2//!
3//! These wrappers keep libretro's AV information explicit at the call site:
4//! dimensions are grouped as `GameGeometry`, timing as `SystemTiming`, and
5//! common fixed-rate cores can use helpers instead of hand-building raw structs.
6
7use crate::raw;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub struct GameGeometry {
11    pub base_width: u32,
12    pub base_height: u32,
13    pub max_width: u32,
14    pub max_height: u32,
15    pub aspect_ratio: f32,
16}
17
18impl GameGeometry {
19    pub(crate) fn as_raw(self) -> raw::retro_game_geometry {
20        raw::retro_game_geometry {
21            base_width: self.base_width,
22            base_height: self.base_height,
23            max_width: self.max_width,
24            max_height: self.max_height,
25            aspect_ratio: self.aspect_ratio,
26        }
27    }
28
29    #[cfg(test)]
30    pub(crate) fn from_raw(raw: raw::retro_game_geometry) -> Self {
31        Self {
32            base_width: raw.base_width,
33            base_height: raw.base_height,
34            max_width: raw.max_width,
35            max_height: raw.max_height,
36            aspect_ratio: raw.aspect_ratio,
37        }
38    }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct SystemTiming {
43    pub fps: f64,
44    pub sample_rate: f64,
45}
46
47impl SystemTiming {
48    pub(crate) fn as_raw(self) -> raw::retro_system_timing {
49        raw::retro_system_timing {
50            fps: self.fps,
51            sample_rate: self.sample_rate,
52        }
53    }
54
55    #[cfg(test)]
56    pub(crate) fn from_raw(raw: raw::retro_system_timing) -> Self {
57        Self {
58            fps: raw.fps,
59            sample_rate: raw.sample_rate,
60        }
61    }
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq)]
65pub struct SystemAvInfo {
66    pub geometry: GameGeometry,
67    pub timing: SystemTiming,
68}
69
70impl SystemAvInfo {
71    pub(crate) fn as_raw(self) -> raw::retro_system_av_info {
72        raw::retro_system_av_info {
73            geometry: self.geometry.as_raw(),
74            timing: self.timing.as_raw(),
75        }
76    }
77
78    #[cfg(test)]
79    pub(crate) fn from_raw(raw: raw::retro_system_av_info) -> Self {
80        Self {
81            geometry: GameGeometry::from_raw(raw.geometry),
82            timing: SystemTiming::from_raw(raw.timing),
83        }
84    }
85}
86
87pub fn game_geometry(width: u32, height: u32) -> GameGeometry {
88    bounded_game_geometry(width, height, width, height)
89}
90
91pub fn bounded_game_geometry(
92    width: u32,
93    height: u32,
94    max_width: u32,
95    max_height: u32,
96) -> GameGeometry {
97    GameGeometry {
98        base_width: width,
99        base_height: height,
100        max_width,
101        max_height,
102        aspect_ratio: width as f32 / height as f32,
103    }
104}
105
106pub fn system_av_info(geometry: GameGeometry, fps: f64, sample_rate: f64) -> SystemAvInfo {
107    SystemAvInfo {
108        geometry,
109        timing: SystemTiming { fps, sample_rate },
110    }
111}
112
113pub fn fixed_system_av_info(width: u32, height: u32, fps: f64, sample_rate: f64) -> SystemAvInfo {
114    system_av_info(game_geometry(width, height), fps, sample_rate)
115}
116
117pub const fn exact_audio_frames_per_video_frame(sample_rate_hz: u32, fps_hz: u32) -> usize {
118    if fps_hz == 0 {
119        panic!("libretro video frame rate must be non-zero");
120    }
121    if !sample_rate_hz.is_multiple_of(fps_hz) {
122        panic!("libretro audio sample rate must divide evenly by video frame rate");
123    }
124
125    (sample_rate_hz / fps_hz) as usize
126}
127
128pub fn silent_stereo_frames(frame_count: usize) -> Vec<[i16; 2]> {
129    vec![[0, 0]; frame_count]
130}
131
132pub fn silent_stereo_frames_for_video_frame(sample_rate_hz: u32, fps_hz: u32) -> Vec<[i16; 2]> {
133    silent_stereo_frames(exact_audio_frames_per_video_frame(sample_rate_hz, fps_hz))
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn fixed_av_info_uses_matching_base_and_max_geometry() {
142        let av = fixed_system_av_info(320, 240, 60.0, 48_000.0);
143
144        assert_eq!(av.geometry.base_width, 320);
145        assert_eq!(av.geometry.base_height, 240);
146        assert_eq!(av.geometry.max_width, 320);
147        assert_eq!(av.geometry.max_height, 240);
148        assert_eq!(av.geometry.aspect_ratio, 4.0 / 3.0);
149        assert_eq!(av.timing.fps, 60.0);
150        assert_eq!(av.timing.sample_rate, 48_000.0);
151    }
152
153    #[test]
154    fn bounded_geometry_preserves_requested_maximums() {
155        let geometry = bounded_game_geometry(320, 240, 640, 480);
156
157        assert_eq!(geometry.base_width, 320);
158        assert_eq!(geometry.base_height, 240);
159        assert_eq!(geometry.max_width, 640);
160        assert_eq!(geometry.max_height, 480);
161    }
162
163    #[test]
164    fn silent_stereo_frames_are_zeroed() {
165        let frames = silent_stereo_frames(3);
166
167        assert_eq!(frames, vec![[0, 0], [0, 0], [0, 0]]);
168    }
169
170    #[test]
171    fn exact_audio_frames_per_video_frame_matches_libretro_sixty_hz_pacing() {
172        assert_eq!(exact_audio_frames_per_video_frame(48_000, 60), 800);
173        assert_eq!(
174            silent_stereo_frames_for_video_frame(48_000, 60),
175            vec![[0, 0]; 800]
176        );
177    }
178
179    #[test]
180    #[should_panic(expected = "libretro audio sample rate must divide evenly")]
181    fn exact_audio_frames_per_video_frame_rejects_fractional_batches() {
182        let _ = exact_audio_frames_per_video_frame(44_100, 64);
183    }
184}