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}