1use 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}