common.rsannotatedcommon.rssource435 lines · 13.8 KB · raw
1use genesis_config::{GenesisAspectRatio, GenesisEmulatorConfig};
2use jgenesis_common::frontend::TimingMode;
3use jgenesis_proc_macros::{EnumAll, EnumDisplay, deserialize_default_on_error};
4use jgenesis_renderer::config::{
5    AntiDitherShader, FilterMode, FrameRotation, NtscShaderConfig, PreprocessShader,
6    PrescaleFactor, VSyncMode, WgpuBackend, WgpuPowerPreference,
7};
8use nes_config::NesAspectRatio;
9use pce_config::PceAspectRatio;
10use serde::{Deserialize, Serialize};
11use smsgg_config::{GgAspectRatio, SmsAspectRatio};
12use snes_config::SnesAspectRatio;
13use std::fmt::{Display, Formatter};
14use std::num::{NonZeroU8, NonZeroU32};
15use std::path::PathBuf;
16
17const DEFAULT_OUTPUT_FREQUENCY: u64 = jgenesis_common::audio::DEFAULT_OUTPUT_FREQUENCY;
18const DEFAULT_AUDIO_BUFFER_SIZE: u32 = 2048;
19const DEFAULT_AUDIO_HW_QUEUE_SIZE: u32 = 256;
20const DEFAULT_INITIAL_WINDOW_SIZE: NonZeroU8 = NonZeroU8::new(3).unwrap();
21const DEFAULT_SCANLINES_BRIGHTNESS: f64 = 0.5;
22const DEFAULT_FAST_FORWARD_MULTIPLIER: u64 = 2;
23const DEFAULT_REWIND_BUFFER_LENGTH: u64 = 10;
24
25#[derive(Debug, Clone, Copy)]
26pub struct WindowSize {
27    pub width: u32,
28    pub height: u32,
29}
30
31impl WindowSize {
32    const SMS_HEIGHT: f64 = 192.0;
33    const SMS_WIDTH: f64 = 256.0;
34
35    const GG_HEIGHT: f64 = 144.0;
36    const GG_WIDTH: f64 = 160.0;
37
38    const GENESIS_HEIGHT: f64 = 224.0;
39    const GENESIS_WIDTH_H40: f64 = 320.0;
40    const GENESIS_H_BORDER_H40: f64 = 27.0;
41    const GENESIS_NTSC_V_BORDER: f64 = 19.0;
42    const GENESIS_PAL_V_BORDER: f64 = 70.0;
43
44    const NES_NTSC_HEIGHT: f64 = 224.0;
45    const NES_PAL_HEIGHT: f64 = 240.0;
46    const NES_WIDTH: f64 = 256.0;
47
48    const SNES_HEIGHT: f64 = 224.0;
49    const SNES_WIDTH: f64 = 256.0;
50
51    const GB_HEIGHT: f64 = 144.0;
52    const GB_WIDTH: f64 = 160.0;
53
54    const GBA_HEIGHT: f64 = 160.0;
55    const GBA_WIDTH: f64 = 240.0;
56
57    const PCE_WIDTH_WITH_OVERSCAN: f64 = 282.0;
58    const PCE_WIDTH_WITHOUT_OVERSCAN: f64 = 256.0;
59    const PCE_HEIGHT_WITH_OVERSCAN: f64 = 242.0;
60    const PCE_HEIGHT_WITHOUT_OVERSCAN: f64 = 224.0;
61
62    #[must_use]
63    pub fn new(native_width: f64, native_height: f64, size: NonZeroU8) -> Self {
64        let size: f64 = size.get().into();
65
66        let width = (native_width * size).ceil() as u32;
67        let height = (native_height * size).ceil() as u32;
68        Self { width, height }
69    }
70
71    #[must_use]
72    pub fn new_sms(size: NonZeroU8, aspect_ratio: SmsAspectRatio) -> Self {
73        let pixel_aspect_ratio = aspect_ratio.to_pixel_aspect_ratio_f64().unwrap_or_else(|| {
74            SmsAspectRatio::default().to_pixel_aspect_ratio_f64().unwrap_or(1.0)
75        });
76        let width = Self::SMS_WIDTH * pixel_aspect_ratio;
77
78        Self::new(width, Self::SMS_HEIGHT, size)
79    }
80
81    #[must_use]
82    pub fn new_game_gear(size: NonZeroU8, aspect_ratio: GgAspectRatio) -> Self {
83        let pixel_aspect_ratio = aspect_ratio
84            .to_pixel_aspect_ratio_f64()
85            .unwrap_or_else(|| GgAspectRatio::default().to_pixel_aspect_ratio_f64().unwrap_or(1.0));
86        let width = Self::GG_WIDTH * pixel_aspect_ratio;
87
88        Self::new(width, Self::GG_HEIGHT, size)
89    }
90
91    #[must_use]
92    pub fn new_genesis(
93        size: NonZeroU8,
94        timing_mode: TimingMode,
95        config: &GenesisEmulatorConfig,
96    ) -> Self {
97        Self::new(
98            Self::genesis_width(timing_mode, config),
99            Self::genesis_height(timing_mode, config),
100            size,
101        )
102    }
103
104    fn genesis_width(timing_mode: TimingMode, config: &GenesisEmulatorConfig) -> f64 {
105        let mut h40_width = Self::GENESIS_WIDTH_H40;
106
107        if config.render_horizontal_border {
108            h40_width += Self::GENESIS_H_BORDER_H40;
109        }
110
111        if !config.force_square_pixels_in_h40 {
112            let h40_par =
113                config.aspect_ratio.to_h40_pixel_aspect_ratio(timing_mode).unwrap_or_else(|| {
114                    GenesisAspectRatio::default()
115                        .to_h40_pixel_aspect_ratio(timing_mode)
116                        .unwrap_or(1.0)
117                });
118            h40_width *= h40_par;
119        }
120
121        if config.anamorphic_widescreen {
122            h40_width *= 4.0 / 3.0;
123        }
124
125        h40_width
126    }
127
128    fn genesis_height(timing_mode: TimingMode, config: &GenesisEmulatorConfig) -> f64 {
129        let mut height = Self::GENESIS_HEIGHT;
130
131        if config.render_vertical_border {
132            height += match timing_mode {
133                TimingMode::Ntsc => Self::GENESIS_NTSC_V_BORDER,
134                TimingMode::Pal => Self::GENESIS_PAL_V_BORDER,
135            };
136        }
137
138        height
139    }
140
141    #[must_use]
142    pub fn new_32x(
143        size: NonZeroU8,
144        timing_mode: TimingMode,
145        config: &GenesisEmulatorConfig,
146    ) -> Self {
147        // Make 32X window a little wider than Genesis by default so that the frame won't shrink if a
148        // game switches to H32 mode while the renderer has forced integer height scaling enabled
149        let genesis_width = Self::genesis_width(timing_mode, config);
150        let width = genesis_width * 323.25 / 320.0;
151
152        let height = Self::genesis_height(timing_mode, config);
153
154        Self::new(width, height, size)
155    }
156
157    #[must_use]
158    pub fn new_nes(
159        size: NonZeroU8,
160        aspect_ratio: NesAspectRatio,
161        timing_mode: TimingMode,
162        ntsc_crop_v_overscan: bool,
163    ) -> Self {
164        let pixel_aspect_ratio = aspect_ratio.to_pixel_aspect_ratio_f64().unwrap_or_else(|| {
165            NesAspectRatio::default().to_pixel_aspect_ratio_f64().unwrap_or(1.0)
166        });
167        let width = Self::NES_WIDTH * pixel_aspect_ratio;
168
169        let height = match timing_mode {
170            TimingMode::Ntsc if ntsc_crop_v_overscan => Self::NES_NTSC_HEIGHT,
171            _ => Self::NES_PAL_HEIGHT,
172        };
173
174        Self::new(width, height, size)
175    }
176
177    #[must_use]
178    pub fn new_snes(size: NonZeroU8, aspect_ratio: SnesAspectRatio) -> Self {
179        let pixel_aspect_ratio = aspect_ratio.to_pixel_aspect_ratio_f64().unwrap_or_else(|| {
180            SnesAspectRatio::default().to_pixel_aspect_ratio_f64().unwrap_or(1.0)
181        });
182        let width = Self::SNES_WIDTH * pixel_aspect_ratio;
183
184        Self::new(width, Self::SNES_HEIGHT, size)
185    }
186
187    #[must_use]
188    pub fn new_gb(size: NonZeroU8) -> Self {
189        // Only GB aspect ratio options are square pixels and stretched
190        Self::new(Self::GB_WIDTH, Self::GB_HEIGHT, size)
191    }
192
193    #[must_use]
194    pub fn new_gba(size: NonZeroU8) -> Self {
195        Self::new(Self::GBA_WIDTH, Self::GBA_HEIGHT, size)
196    }
197
198    #[must_use]
199    pub fn new_pce(size: NonZeroU8, aspect_ratio: PceAspectRatio, crop_overscan: bool) -> Self {
200        let (mut width, height) = if crop_overscan {
201            (Self::PCE_WIDTH_WITHOUT_OVERSCAN, Self::PCE_HEIGHT_WITHOUT_OVERSCAN)
202        } else {
203            (Self::PCE_WIDTH_WITH_OVERSCAN, Self::PCE_HEIGHT_WITH_OVERSCAN)
204        };
205
206        if aspect_ratio == PceAspectRatio::Ntsc {
207            width *= 8.0 / 7.0;
208        }
209
210        Self::new(width, height, size)
211    }
212
213    #[must_use]
214    pub fn scale(self, scale_factor: f32) -> Self {
215        Self {
216            width: (self.width as f32 * scale_factor).round() as u32,
217            height: (self.height as f32 * scale_factor).round() as u32,
218        }
219    }
220}
221
222#[derive(Debug, Clone)]
223pub enum SavePath {
224    RomFolder,
225    EmulatorFolder,
226    Custom(PathBuf),
227}
228
229impl SavePath {
230    pub const SAVE_SUBDIR: &'static str = "saves";
231    pub const STATE_SUBDIR: &'static str = "states";
232}
233
234impl Display for SavePath {
235    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::RomFolder => write!(f, "ROM Folder"),
238            Self::EmulatorFolder => write!(f, "Emulator Folder"),
239            Self::Custom(path) => write!(f, "{}", path.display()),
240        }
241    }
242}
243
244#[derive(
245    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumDisplay, EnumAll,
246)]
247#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
248pub enum PauseEmulator {
249    #[default]
250    Never,
251    EmulatorLosesFocus,
252    ApplicationLosesFocus,
253}
254
255impl PauseEmulator {
256    #[inline]
257    #[must_use]
258    pub fn should_pause(self, emulator_focused: bool, any_focused: bool) -> bool {
259        match self {
260            Self::Never => false,
261            Self::EmulatorLosesFocus => !emulator_focused,
262            Self::ApplicationLosesFocus => !any_focused,
263        }
264    }
265}
266
267#[derive(
268    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumDisplay, EnumAll,
269)]
270#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
271pub enum HideMouseCursor {
272    #[default]
273    Fullscreen,
274    Never,
275    Always,
276}
277
278impl HideMouseCursor {
279    #[inline]
280    #[must_use]
281    pub fn should_hide(self, fullscreen: bool) -> bool {
282        self == Self::Always || (fullscreen && self == Self::Fullscreen)
283    }
284}
285
286#[derive(
287    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumDisplay, EnumAll,
288)]
289#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
290pub enum ConfigSavePath {
291    #[default]
292    RomFolder,
293    EmulatorFolder,
294    Custom,
295}
296
297#[derive(
298    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumDisplay, EnumAll,
299)]
300#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
301pub enum CheatPath {
302    #[default]
303    SettingsFolder,
304    EmulatorFolder,
305    Custom,
306}
307
308#[deserialize_default_on_error]
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310#[serde(default)]
311pub struct CommonAppConfig {
312    pub audio_output_frequency: u64,
313    pub mute_audio: bool,
314    pub audio_sync: bool,
315    pub audio_dynamic_resampling_ratio: bool,
316    pub audio_buffer_size: u32,
317    pub audio_hardware_queue_size: u32,
318    pub audio_gain_db: f64,
319    pub save_path: ConfigSavePath,
320    pub custom_save_path: PathBuf,
321    pub state_path: ConfigSavePath,
322    pub custom_state_path: PathBuf,
323    pub window_width: Option<u32>,
324    pub window_height: Option<u32>,
325    pub launch_in_fullscreen: bool,
326    pub borderless_window: bool,
327    pub initial_window_size: NonZeroU8,
328    pub wgpu_backend: WgpuBackend,
329    pub wgpu_power_preference: WgpuPowerPreference,
330    pub vsync_mode: VSyncMode,
331    pub frame_time_sync: bool,
332    pub auto_prescale: bool,
333    pub prescale_width: PrescaleFactor,
334    pub prescale_height: PrescaleFactor,
335    pub scanlines_enabled: bool,
336    pub scanlines_brightness: f64,
337    pub force_integer_height_scaling: bool,
338    pub filter_mode: FilterMode,
339    pub supersample_minification: bool,
340    pub preprocess_shader: PreprocessShader,
341    pub anti_dither_shader: AntiDitherShader,
342    pub frame_rotation: FrameRotation,
343    pub ntsc: NtscShaderConfig,
344    pub load_recent_state_at_launch: bool,
345    pub fast_forward_multiplier: u64,
346    pub rewind_buffer_length_seconds: u64,
347    pub pause_emulator: PauseEmulator,
348    pub hide_mouse_cursor: HideMouseCursor,
349    pub cheats_enabled: bool,
350    pub cheats_path: CheatPath,
351    pub cheats_custom_path: PathBuf,
352}
353
354impl CommonAppConfig {
355    #[inline]
356    #[must_use]
357    pub fn window_size(&self) -> Option<WindowSize> {
358        match (self.window_width, self.window_height) {
359            (Some(width), Some(height)) => Some(WindowSize { width, height }),
360            _ => None,
361        }
362    }
363}
364
365impl Default for CommonAppConfig {
366    fn default() -> Self {
367        Self {
368            audio_output_frequency: DEFAULT_OUTPUT_FREQUENCY,
369            mute_audio: false,
370            audio_sync: true,
371            audio_dynamic_resampling_ratio: true,
372            audio_buffer_size: DEFAULT_AUDIO_BUFFER_SIZE,
373            audio_hardware_queue_size: DEFAULT_AUDIO_HW_QUEUE_SIZE,
374            audio_gain_db: 0.0,
375            save_path: ConfigSavePath::default(),
376            custom_save_path: default_custom_save_path(),
377            state_path: ConfigSavePath::default(),
378            custom_state_path: default_custom_state_path(),
379            window_width: None,
380            window_height: None,
381            launch_in_fullscreen: false,
382            borderless_window: false,
383            initial_window_size: DEFAULT_INITIAL_WINDOW_SIZE,
384            wgpu_backend: WgpuBackend::default(),
385            wgpu_power_preference: WgpuPowerPreference::default(),
386            vsync_mode: VSyncMode::default(),
387            frame_time_sync: true,
388            auto_prescale: true,
389            prescale_width: default_prescale_factor(),
390            prescale_height: default_prescale_factor(),
391            scanlines_enabled: false,
392            scanlines_brightness: DEFAULT_SCANLINES_BRIGHTNESS,
393            force_integer_height_scaling: false,
394            filter_mode: FilterMode::default(),
395            supersample_minification: true,
396            preprocess_shader: PreprocessShader::default(),
397            anti_dither_shader: AntiDitherShader::default(),
398            frame_rotation: FrameRotation::default(),
399            ntsc: NtscShaderConfig::default(),
400            load_recent_state_at_launch: false,
401            fast_forward_multiplier: DEFAULT_FAST_FORWARD_MULTIPLIER,
402            rewind_buffer_length_seconds: DEFAULT_REWIND_BUFFER_LENGTH,
403            pause_emulator: PauseEmulator::default(),
404            hide_mouse_cursor: HideMouseCursor::default(),
405            cheats_enabled: true,
406            cheats_path: CheatPath::default(),
407            cheats_custom_path: default_custom_cheats_path(),
408        }
409    }
410}
411
412fn default_custom_path(subdir: &str) -> PathBuf {
413    let Some(base_dirs) = directories::BaseDirs::new() else {
414        log::error!("Unable to determine user base directories for default custom paths");
415        return PathBuf::default();
416    };
417
418    base_dirs.data_local_dir().join("jgenesis").join(subdir)
419}
420
421fn default_custom_save_path() -> PathBuf {
422    default_custom_path(SavePath::SAVE_SUBDIR)
423}
424
425fn default_custom_state_path() -> PathBuf {
426    default_custom_path(SavePath::STATE_SUBDIR)
427}
428
429fn default_custom_cheats_path() -> PathBuf {
430    default_custom_path("cheats")
431}
432
433fn default_prescale_factor() -> PrescaleFactor {
434    PrescaleFactor::from(NonZeroU32::new(3).unwrap())
435}