mister_fpga/
config.rs

1use merg::Merge;
2use num_traits::FloatConst;
3use serde::Deserialize;
4use serde_with::{serde_as, DeserializeFromStr, DurationSeconds};
5use std::collections::HashMap;
6use std::ffi::{c_char, c_int};
7use std::io;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::time::Duration;
11use thiserror::Error;
12use tracing::info;
13use validator::Validate;
14use video::aspect::AspectRatio;
15use video::resolution::Resolution;
16
17mod bootcore;
18mod fb_size;
19mod hdmi_limited;
20mod hdr;
21mod ini; // Internal module.
22mod ntsc_mode;
23mod osd_rotate;
24mod reset_combo;
25mod vga_mode;
26pub mod video;
27mod vrr_mode;
28mod vscale_mode;
29mod vsync_adjust;
30
31pub use bootcore::*;
32pub use fb_size::*;
33pub use hdmi_limited::*;
34pub use hdr::*;
35pub use ntsc_mode::*;
36pub use osd_rotate::*;
37pub use reset_combo::*;
38pub use vga_mode::*;
39pub use video::*;
40pub use vrr_mode::*;
41pub use vscale_mode::*;
42pub use vsync_adjust::*;
43
44mod cpp {
45    use libc::*;
46
47    #[repr(C)]
48    pub struct CppCfg {
49        pub keyrah_mode: u32,
50        pub forced_scandoubler: u8,
51        pub key_menu_as_rgui: u8,
52        pub reset_combo: u8,
53        pub csync: u8,
54        pub vga_scaler: u8,
55        pub vga_sog: u8,
56        pub hdmi_audio_96k: u8,
57        pub dvi_mode: u8,
58        pub hdmi_limited: u8,
59        pub direct_video: u8,
60        pub video_info: u8,
61        pub refresh_min: c_float,
62        pub refresh_max: c_float,
63        pub controller_info: u8,
64        pub vsync_adjust: u8,
65        pub kbd_nomouse: u8,
66        pub mouse_throttle: u8,
67        pub bootscreen: u8,
68        pub vscale_mode: u8,
69        pub vscale_border: u16,
70        pub rbf_hide_datecode: u8,
71        pub menu_pal: u8,
72        pub bootcore_timeout: i16,
73        pub fb_size: u8,
74        pub fb_terminal: u8,
75        pub osd_rotate: u8,
76        pub osd_timeout: u16,
77        pub gamepad_defaults: u8,
78        pub recents: u8,
79        pub jamma_vid: u16,
80        pub jamma_pid: u16,
81        pub no_merge_vid: u16,
82        pub no_merge_pid: u16,
83        pub no_merge_vidpid: [u32; 256],
84        pub spinner_vid: u16,
85        pub spinner_pid: u16,
86        pub spinner_throttle: c_int,
87        pub spinner_axis: u8,
88        pub sniper_mode: u8,
89        pub browse_expand: u8,
90        pub logo: u8,
91        pub log_file_entry: u8,
92        pub shmask_mode_default: u8,
93        pub bt_auto_disconnect: c_int,
94        pub bt_reset_before_pair: c_int,
95        pub bootcore: [c_char; 256],
96        pub video_conf: [c_char; 1024],
97        pub video_conf_pal: [c_char; 1024],
98        pub video_conf_ntsc: [c_char; 1024],
99        pub font: [c_char; 1024],
100        pub shared_folder: [c_char; 1024],
101        pub waitmount: [c_char; 1024],
102        pub custom_aspect_ratio: [[c_char; 16]; 2],
103        pub afilter_default: [c_char; 1023],
104        pub vfilter_default: [c_char; 1023],
105        pub vfilter_vertical_default: [c_char; 1023],
106        pub vfilter_scanlines_default: [c_char; 1023],
107        pub shmask_default: [c_char; 1023],
108        pub preset_default: [c_char; 1023],
109        pub player_controller: [[[c_char; 256]; 8]; 6],
110        pub rumble: u8,
111        pub wheel_force: u8,
112        pub wheel_range: u16,
113        pub hdmi_game_mode: u8,
114        pub vrr_mode: u8,
115        pub vrr_min_framerate: u8,
116        pub vrr_max_framerate: u8,
117        pub vrr_vesa_framerate: u8,
118        pub video_off: u16,
119        pub disable_autofire: u8,
120        pub video_brightness: u8,
121        pub video_contrast: u8,
122        pub video_saturation: u8,
123        pub video_hue: u16,
124        pub video_gain_offset: [c_char; 256],
125        pub hdr: u8,
126        pub hdr_max_nits: u16,
127        pub hdr_avg_nits: u16,
128        pub vga_mode: [c_char; 16],
129        pub vga_mode_int: c_char,
130        pub ntsc_mode: c_char,
131        pub controller_unique_mapping: [u32; 256],
132    }
133}
134
135#[derive(Error, Debug)]
136#[error(transparent)]
137pub enum ConfigError {
138    #[error("Invalid config file: {0}")]
139    Io(#[from] io::Error),
140
141    #[error("Could not read INI file: {0}")]
142    IniError(#[from] ini::Error),
143
144    #[error("Could not read JSON file: {0}")]
145    JsonError(#[from] json5::Error),
146}
147
148/// A helper function to read minutes into durations from the config.
149struct DurationMinutes;
150
151impl<'de> serde_with::DeserializeAs<'de, Duration> for DurationMinutes {
152    fn deserialize_as<D>(deserializer: D) -> Result<Duration, D::Error>
153    where
154        D: serde::Deserializer<'de>,
155    {
156        let dur: Duration = DurationSeconds::<u64>::deserialize_as(deserializer)?;
157        let secs = dur.as_secs();
158        Ok(Duration::from_secs(secs * 60))
159    }
160}
161
162/// A struct representing the `video=123x456` string for sections in the config.
163#[derive(DeserializeFromStr, Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
164pub struct VideoResolutionString(Resolution);
165
166impl FromStr for VideoResolutionString {
167    type Err = &'static str;
168
169    fn from_str(s: &str) -> Result<Self, Self::Err> {
170        if let Some(s) = s.strip_prefix("video=") {
171            Ok(VideoResolutionString(Resolution::from_str(s)?))
172        } else {
173            Err("Invalid video section string.")
174        }
175    }
176}
177
178/// Serde helper for supporting mister booleans (can be 0 or 1) used in the current config.
179mod mister_bool {
180    use serde::{Deserialize, Deserializer};
181
182    /// Transforms a mister string into a boolean.
183    fn bool_from_str(s: impl AsRef<str>) -> bool {
184        match s.as_ref() {
185            "enabled" | "true" | "1" => true,
186            "0" => false,
187            _ => false,
188        }
189    }
190
191    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
192    where
193        D: Deserializer<'de>,
194    {
195        Option::deserialize(deserializer).map(|v: Option<String>| v.map(bool_from_str))
196    }
197}
198
199/// Serde helper for supporting mister hexa values (can be 0x1234) used in the current config.
200mod mister_hexa {
201    use serde::{Deserialize, Deserializer};
202
203    pub fn deserialize<'de, D, T: num_traits::Num>(deserializer: D) -> Result<Option<T>, D::Error>
204    where
205        D: Deserializer<'de>,
206    {
207        Option::deserialize(deserializer).and_then(|v: Option<String>| {
208            if let Some(s) = v {
209                if let Some(hs) = s.strip_prefix("0x") {
210                    T::from_str_radix(hs, 16)
211                        .map_err(|_| {
212                            serde::de::Error::custom(format!("Invalid hexadecimal value: {s}"))
213                        })
214                        .map(Some)
215                } else if s == "0" {
216                    Ok(Some(T::zero()))
217                } else {
218                    Err(serde::de::Error::custom(format!(
219                        "Invalid hexadecimal value: {}",
220                        s
221                    )))
222                }
223            } else {
224                Ok(None)
225            }
226        })
227    }
228}
229
230/// Serde helper for supporting mister hexa values (can be 0x1234) used in the current config.
231mod mister_hexa_seq {
232    use serde::{Deserialize, Deserializer};
233
234    pub fn deserialize<'de, D, T: num_traits::Num>(deserializer: D) -> Result<Vec<T>, D::Error>
235    where
236        D: Deserializer<'de>,
237    {
238        Vec::deserialize(deserializer).and_then(|v: Vec<String>| {
239            v.into_iter()
240                .map(|s| {
241                    if let Some(hs) = s.strip_prefix("0x") {
242                        T::from_str_radix(hs, 16).map_err(|_| {
243                            serde::de::Error::custom(format!("Invalid hexadecimal value: {s}"))
244                        })
245                    } else if s == "0" {
246                        Ok(T::zero())
247                    } else {
248                        Err(serde::de::Error::custom(format!(
249                            "Invalid hexadecimal value: {}",
250                            s
251                        )))
252                    }
253                })
254                .collect()
255        })
256    }
257}
258
259mod validate {
260    use std::time::Duration;
261    use validator::ValidationError;
262
263    pub fn video_info(video_info: &Duration) -> Result<(), ValidationError> {
264        if video_info.as_secs() > 10 || video_info.as_secs() < 1 {
265            return Err(ValidationError::new("video_info must be between 1 and 10."));
266        }
267
268        Ok(())
269    }
270
271    pub fn controller_info(controller_info: &Duration) -> Result<(), ValidationError> {
272        if controller_info.as_secs() > 10 {
273            return Err(ValidationError::new(
274                "controller_info must be between 0 and 10.",
275            ));
276        }
277
278        Ok(())
279    }
280
281    pub fn osd_timeout(osd_timeout: &Duration) -> Result<(), ValidationError> {
282        if osd_timeout.as_secs() > 3600 || osd_timeout.as_secs() < 5 {
283            return Err(ValidationError::new(
284                "osd_timeout must be between 5 and 3600.",
285            ));
286        }
287
288        Ok(())
289    }
290
291    pub fn bootcore_timeout(bootcore_timeout: &Duration) -> Result<(), ValidationError> {
292        if bootcore_timeout.as_secs() > 30 {
293            return Err(ValidationError::new("bootcore_timeout must be 30 or less."));
294        }
295
296        Ok(())
297    }
298
299    pub fn video_off(video_off: &Duration) -> Result<(), ValidationError> {
300        if video_off.as_secs() > 3600 {
301            return Err(ValidationError::new("video_off must be 3600 or less."));
302        }
303
304        Ok(())
305    }
306}
307
308/// The `[MiSTer]` section of the configuration file. This represents a single MiSTer section,
309/// which contains most configurations.
310///
311/// The `Mister.ini` file specifies that one can override the default values by specifying the
312/// `[MiSTer]` section with resolution specific configurations, e.g. `[video=320x240]`.
313/// Because of this, we pretty much set everything as optional and defines the default in the
314/// getters on the [`Config`] struct.
315///
316/// This allows us to overwrite only options which are defined in the subsections.
317#[serde_as]
318#[derive(Default, Debug, Clone, Deserialize, Merge, Validate)]
319#[serde(default)]
320pub struct MisterConfig {
321    #[merge(strategy = merg::option::overwrite_some)]
322    pub bootcore: Option<BootCoreConfig>,
323
324    #[serde(alias = "ypbpr")]
325    #[merge(strategy = merg::option::overwrite_some)]
326    pub vga_mode: Option<VgaMode>,
327
328    #[merge(strategy = merg::option::overwrite_some)]
329    pub ntsc_mode: Option<NtscModeConfig>,
330
331    #[merge(strategy = merg::option::overwrite_some)]
332    pub reset_combo: Option<ResetComboConfig>,
333
334    #[merge(strategy = merg::option::overwrite_some)]
335    pub hdmi_limited: Option<HdmiLimitedConfig>,
336
337    #[merge(strategy = merg::option::overwrite_some)]
338    #[validate(range(max = 100))]
339    pub mouse_throttle: Option<u8>,
340
341    #[serde(with = "mister_hexa")]
342    #[merge(strategy = merg::option::overwrite_some)]
343    pub keyrah_mode: Option<u32>,
344
345    /// Specify a custom aspect ratio in the format `a:b`. This can be repeated.
346    /// They are applied in order, so the first one matching will be the one used.
347    #[merge(strategy = merg::vec::append)]
348    custom_aspect_ratio: Vec<AspectRatio>,
349
350    /// Specify a custom aspect ratio, allowing for backward compatibility with older
351    /// MiSTer config files. We only need 2 as that's what the previous version supported.
352    #[merge(strategy = merg::option::overwrite_some)]
353    pub custom_aspect_ratio_1: Option<AspectRatio>,
354
355    /// Specify a custom aspect ratio, allowing for backward compatibility with older
356    /// MiSTer config files. We only need 2 as that's what the previous version supported.
357    #[merge(strategy = merg::option::overwrite_some)]
358    pub custom_aspect_ratio_2: Option<AspectRatio>,
359
360    /// Set to 1 to run scandoubler on VGA output always (depends on core).
361    #[serde(with = "mister_bool")]
362    #[merge(strategy = merg::option::overwrite_some)]
363    pub forced_scandoubler: Option<bool>,
364
365    /// Set to true to make the MENU key map to RGUI in Minimig (e.g. for Right Amiga).
366    #[serde(with = "mister_bool")]
367    #[merge(strategy = merg::option::overwrite_some)]
368    pub key_menu_as_rgui: Option<bool>,
369
370    /// Set to true for composite sync on HSync signal of VGA output.
371    #[serde(with = "mister_bool", alias = "csync")]
372    #[merge(strategy = merg::option::overwrite_some)]
373    pub composite_sync: Option<bool>,
374
375    /// Set to true to connect VGA to scaler output.
376    #[serde(with = "mister_bool")]
377    #[merge(strategy = merg::option::overwrite_some)]
378    pub vga_scaler: Option<bool>,
379
380    /// Set to true to enable sync on green (needs analog I/O board v6.0 or newer).
381    #[serde(with = "mister_bool")]
382    #[merge(strategy = merg::option::overwrite_some)]
383    pub vga_sog: Option<bool>,
384
385    /// Set to true for 96khz/16bit HDMI audio (48khz/16bit otherwise)
386    #[serde(with = "mister_bool")]
387    #[merge(strategy = merg::option::overwrite_some)]
388    pub hdmi_audio_96k: Option<bool>,
389
390    /// Set to true for DVI mode. Audio won't be transmitted through HDMI in DVI mode.
391    #[serde(with = "mister_bool")]
392    #[merge(strategy = merg::option::overwrite_some)]
393    pub dvi_mode: Option<bool>,
394
395    /// Set to true to enable core video timing over HDMI, use only with VGA converters.
396    #[serde(with = "mister_bool")]
397    #[merge(strategy = merg::option::overwrite_some)]
398    pub direct_video: Option<bool>,
399
400    /// Set to 0-10 (seconds) to display video info on startup/change
401    #[serde_as(as = "Option<DurationSeconds<u64>>")]
402    #[merge(strategy = merg::option::overwrite_some)]
403    #[validate(custom(function = validate::video_info))]
404    pub video_info: Option<Duration>,
405
406    /// 1-10 (seconds) to display controller's button map upon first time key press
407    /// 0 - disable
408    #[serde_as(as = "Option<DurationSeconds<u64>>")]
409    #[validate(custom(function = validate::controller_info))]
410    #[merge(strategy = merg::option::overwrite_some)]
411    pub controller_info: Option<Duration>,
412
413    /// If you monitor doesn't support either very low (NTSC monitors may not support PAL) or
414    /// very high (PAL monitors may not support NTSC) then you can set refresh_min and/or refresh_max
415    /// parameters, so vsync_adjust won't be applied for refreshes outside specified.
416    /// These parameters are valid only when vsync_adjust is non-zero.
417    #[validate(range(min = 0.0, max = 150.0))]
418    #[merge(strategy = merg::option::overwrite_some)]
419    pub refresh_min: Option<f32>,
420
421    /// If you monitor doesn't support either very low (NTSC monitors may not support PAL) or
422    /// very high (PAL monitors may not support NTSC) then you can set refresh_min and/or refresh_max
423    /// parameters, so vsync_adjust won't be applied for refreshes outside specified.
424    /// These parameters are valid only when vsync_adjust is non-zero.
425    #[validate(range(min = 0.0, max = 150.0))]
426    #[merge(strategy = merg::option::overwrite_some)]
427    refresh_max: Option<f32>,
428
429    /// Set to 1 for automatic HDMI VSync rate adjust to match original VSync.
430    /// Set to 2 for low latency mode (single buffer).
431    /// This option makes video butter smooth like on original emulated system.
432    /// Adjusting is done by changing pixel clock. Not every display supports variable pixel clock.
433    /// For proper adjusting and to reduce possible out of range pixel clock, use 60Hz HDMI video
434    /// modes as a base even for 50Hz systems.
435    #[merge(strategy = merg::option::overwrite_some)]
436    vsync_adjust: Option<VsyncAdjustConfig>,
437
438    // TODO: figure this out.
439    #[serde(with = "mister_bool")]
440    #[merge(strategy = merg::option::overwrite_some)]
441    kbd_nomouse: Option<bool>,
442
443    #[serde(with = "mister_bool")]
444    #[merge(strategy = merg::option::overwrite_some)]
445    bootscreen: Option<bool>,
446
447    /// 0 - scale to fit the screen height.
448    /// 1 - use integer scale only.
449    /// 2 - use 0.5 steps of scale.
450    /// 3 - use 0.25 steps of scale.
451    /// 4 - integer resolution scaling, use core aspect ratio
452    /// 5 - integer resolution scaling, maintain display aspect ratio
453    #[merge(strategy = merg::option::overwrite_some)]
454    vscale_mode: Option<VideoScaleModeConfig>,
455
456    /// Set vertical border for TVs cutting the upper/bottom parts of screen (1-399)
457    #[validate(range(min = 0, max = 399))]
458    #[merge(strategy = merg::option::overwrite_some)]
459    pub vscale_border: Option<u16>,
460
461    /// true - hides datecodes from rbf file names. Press F2 for quick temporary toggle
462    #[serde(with = "mister_bool")]
463    #[merge(strategy = merg::option::overwrite_some)]
464    rbf_hide_datecode: Option<bool>,
465
466    /// 1 - PAL mode for menu core
467    #[serde(with = "mister_bool")]
468    #[merge(strategy = merg::option::overwrite_some)]
469    menu_pal: Option<bool>,
470
471    /// 10-30 timeout before autoboot, comment for autoboot without timeout.
472    #[serde_as(as = "Option<DurationSeconds<u64>>")]
473    #[validate(custom(function = validate::bootcore_timeout))]
474    #[merge(strategy = merg::option::overwrite_some)]
475    bootcore_timeout: Option<Duration>,
476
477    /// 0 - automatic, 1 - full size, 2 - 1/2 of resolution, 4 - 1/4 of resolution.
478    #[merge(strategy = merg::option::overwrite_some)]
479    pub fb_size: Option<FramebufferSizeConfig>,
480
481    /// TODO: figure this out.
482    #[serde(with = "mister_bool")]
483    #[merge(strategy = merg::option::overwrite_some)]
484    fb_terminal: Option<bool>,
485
486    /// Display OSD menu rotated,  0 - no rotation, 1 - rotate right (+90°), 2 - rotate left (-90°)
487    #[merge(strategy = merg::option::overwrite_some)]
488    osd_rotate: Option<OsdRotateConfig>,
489
490    /// 5-3600 timeout (in seconds) for OSD to disappear in Menu core. 0 - never timeout.
491    /// Background picture will get darker after double timeout.
492    #[serde_as(as = "Option<DurationSeconds<u64>>")]
493    #[validate(custom(function = validate::osd_timeout))]
494    #[merge(strategy = merg::option::overwrite_some)]
495    osd_timeout: Option<Duration>,
496
497    /// Defines internal joypad mapping from virtual SNES mapping in main to core mapping
498    /// Set to 0 for name mapping (jn) (e.g. A button in SNES core = A button on controller regardless of position on pad)
499    /// Set to 1 for positional mapping (jp) (e.g. A button in SNES core = East button on controller regardless of button name)
500    #[serde(with = "mister_bool")]
501    #[merge(strategy = merg::option::overwrite_some)]
502    gamepad_defaults: Option<bool>,
503
504    /// 1 - enables the recent file loaded/mounted.
505    /// WARNING: This option will enable write to SD card on every load/mount which may wear the SD card after many writes to the same place
506    ///          There is also higher chance to corrupt the File System if MiSTer will be reset or powered off while writing.
507    #[serde(with = "mister_bool")]
508    #[merge(strategy = merg::option::overwrite_some)]
509    recents: Option<bool>,
510
511    /// JammaSD/J-PAC/I-PAC keys to joysticks translation
512    /// You have to provide correct VID and PID of your input device
513    /// Examples: Legacy J-PAC with Mini-USB or USB capable I-PAC with PS/2 connectors VID=0xD209/PID=0x0301
514    /// USB Capable J-PAC with only PS/2 connectors VID=0x04B4/PID=0x0101
515    /// JammaSD: VID=0x04D8/PID=0xF3AD
516    #[serde(with = "mister_hexa")]
517    #[merge(strategy = merg::option::overwrite_some)]
518    jamma_vid: Option<u16>,
519
520    #[serde(with = "mister_hexa")]
521    #[merge(strategy = merg::option::overwrite_some)]
522    jamma_pid: Option<u16>,
523
524    /// Disable merging input devices. Use if only Player1 works.
525    /// Leave no_merge_pid empty to apply this to all devices with the same VID.
526    #[serde(with = "mister_hexa")]
527    #[merge(strategy = merg::option::overwrite_some)]
528    no_merge_vid: Option<u16>,
529
530    #[serde(with = "mister_hexa")]
531    #[merge(strategy = merg::option::overwrite_some)]
532    no_merge_pid: Option<u16>,
533
534    #[serde(with = "mister_hexa_seq")]
535    #[merge(strategy = merg::vec::append)]
536    no_merge_vidpid: Vec<u32>,
537
538    /// use specific (VID/PID) mouse X movement as a spinner and paddle. Use VID=0xFFFF/PID=0xFFFF to use all mice as spinners.
539    #[serde(with = "mister_hexa")]
540    #[merge(strategy = merg::option::overwrite_some)]
541    spinner_vid: Option<u16>,
542
543    #[serde(with = "mister_hexa")]
544    #[merge(strategy = merg::option::overwrite_some)]
545    spinner_pid: Option<u16>,
546
547    // I WAS HERE.
548    #[validate(range(min = -10000, max = 10000))]
549    #[merge(strategy = merg::option::overwrite_some)]
550    spinner_throttle: Option<i32>,
551
552    #[merge(strategy = merg::option::overwrite_some)]
553    spinner_axis: Option<u8>,
554
555    #[serde(with = "mister_bool")]
556    #[merge(strategy = merg::option::overwrite_some)]
557    sniper_mode: Option<bool>,
558
559    #[serde(with = "mister_bool")]
560    #[merge(strategy = merg::option::overwrite_some)]
561    browse_expand: Option<bool>,
562
563    /// 0 - disable MiSTer logo in Menu core
564    #[serde(with = "mister_bool")]
565    #[merge(strategy = merg::option::overwrite_some)]
566    logo: Option<bool>,
567
568    #[serde(with = "mister_bool")]
569    #[merge(strategy = merg::option::overwrite_some)]
570    log_file_entry: Option<bool>,
571
572    #[merge(strategy = merg::option::overwrite_some)]
573    shmask_mode_default: Option<u8>,
574
575    /// Automatically disconnect (and shutdown) Bluetooth input device if not use specified amount of time.
576    /// Some controllers have no automatic shutdown built in and will keep connection till battery dry out.
577    /// 0 - don't disconnect automatically, otherwise it's amount of minutes.
578    #[serde_as(as = "Option<DurationMinutes>")]
579    #[merge(strategy = merg::option::overwrite_some)]
580    bt_auto_disconnect: Option<Duration>,
581
582    #[serde(with = "mister_bool")]
583    #[merge(strategy = merg::option::overwrite_some)]
584    bt_reset_before_pair: Option<bool>,
585
586    #[serde(alias = "video_mode")]
587    #[merge(strategy = merg::option::overwrite_some)]
588    video_conf: Option<String>,
589
590    #[serde(alias = "video_mode_pal")]
591    #[merge(strategy = merg::option::overwrite_some)]
592    video_conf_pal: Option<String>,
593
594    #[serde(alias = "video_mode_ntsc")]
595    #[merge(strategy = merg::option::overwrite_some)]
596    video_conf_ntsc: Option<String>,
597
598    #[merge(strategy = merg::option::overwrite_some)]
599    font: Option<String>,
600
601    #[merge(strategy = merg::option::overwrite_some)]
602    shared_folder: Option<String>,
603
604    #[merge(strategy = merg::option::overwrite_some)]
605    waitmount: Option<String>,
606
607    #[merge(strategy = merg::option::overwrite_some)]
608    afilter_default: Option<String>,
609
610    #[merge(strategy = merg::option::overwrite_some)]
611    vfilter_default: Option<String>,
612
613    #[merge(strategy = merg::option::overwrite_some)]
614    vfilter_vertical_default: Option<String>,
615
616    #[merge(strategy = merg::option::overwrite_some)]
617    vfilter_scanlines_default: Option<String>,
618
619    #[merge(strategy = merg::option::overwrite_some)]
620    shmask_default: Option<String>,
621
622    #[merge(strategy = merg::option::overwrite_some)]
623    preset_default: Option<String>,
624
625    #[serde(default)]
626    #[merge(strategy = merg::vec::append)]
627    player_controller: Vec<Vec<String>>,
628
629    #[serde(default)]
630    #[merge(strategy = merg::vec::append)]
631    player_1_controller: Vec<String>,
632    #[serde(default)]
633    #[merge(strategy = merg::vec::append)]
634    player_2_controller: Vec<String>,
635    #[serde(default)]
636    #[merge(strategy = merg::vec::append)]
637    player_3_controller: Vec<String>,
638    #[serde(default)]
639    #[merge(strategy = merg::vec::append)]
640    player_4_controller: Vec<String>,
641    #[serde(default)]
642    #[merge(strategy = merg::vec::append)]
643    player_5_controller: Vec<String>,
644    #[serde(default)]
645    #[merge(strategy = merg::vec::append)]
646    player_6_controller: Vec<String>,
647
648    #[serde(with = "mister_bool")]
649    #[merge(strategy = merg::option::overwrite_some)]
650    rumble: Option<bool>,
651
652    #[merge(strategy = merg::option::overwrite_some)]
653    #[validate(range(min = 0, max = 100))]
654    wheel_force: Option<u8>,
655
656    #[merge(strategy = merg::option::overwrite_some)]
657    #[validate(range(min = 0, max = 1000))]
658    wheel_range: Option<u16>,
659
660    #[serde(with = "mister_bool")]
661    #[merge(strategy = merg::option::overwrite_some)]
662    hdmi_game_mode: Option<bool>,
663
664    /// Variable Refresh Rate control
665    /// 0 - Do not enable VRR (send no VRR control frames)
666    /// 1 - Auto Detect VRR from display EDID.
667    /// 2 - Force Enable Freesync
668    /// 3 - Force Enable Vesa HDMI Forum VRR
669    #[merge(strategy = merg::option::overwrite_some)]
670    vrr_mode: Option<VrrModeConfig>,
671
672    #[merge(strategy = merg::option::overwrite_some)]
673    vrr_min_framerate: Option<u8>,
674
675    #[merge(strategy = merg::option::overwrite_some)]
676    vrr_max_framerate: Option<u8>,
677
678    #[merge(strategy = merg::option::overwrite_some)]
679    vrr_vesa_framerate: Option<u8>,
680
681    /// output black frame in Menu core after timeout (is seconds). Valid only if osd_timout is non-zero.
682    #[serde_as(as = "Option<DurationSeconds<u64>>")]
683    #[merge(strategy = merg::option::overwrite_some)]
684    #[validate(custom(function = validate::video_off))]
685    video_off: Option<Duration>,
686
687    #[serde(with = "mister_bool")]
688    #[merge(strategy = merg::option::overwrite_some)]
689    disable_autofire: Option<bool>,
690
691    #[merge(strategy = merg::option::overwrite_some)]
692    #[validate(range(min = 0, max = 100))]
693    video_brightness: Option<u8>,
694
695    #[merge(strategy = merg::option::overwrite_some)]
696    #[validate(range(min = 0, max = 100))]
697    video_contrast: Option<u8>,
698
699    #[merge(strategy = merg::option::overwrite_some)]
700    #[validate(range(min = 0, max = 100))]
701    video_saturation: Option<u8>,
702
703    #[merge(strategy = merg::option::overwrite_some)]
704    #[validate(range(min = 0, max = 360))]
705    video_hue: Option<u16>,
706
707    #[merge(strategy = merg::option::overwrite_some)]
708    video_gain_offset: Option<VideoGainOffsets>,
709
710    #[merge(strategy = merg::option::overwrite_some)]
711    hdr: Option<HdrConfig>,
712
713    #[merge(strategy = merg::option::overwrite_some)]
714    #[validate(range(min = 100, max = 10000))]
715    hdr_max_nits: Option<u16>,
716
717    #[merge(strategy = merg::option::overwrite_some)]
718    #[validate(range(min = 100, max = 10000))]
719    hdr_avg_nits: Option<u16>,
720
721    #[serde(with = "mister_hexa_seq")]
722    #[merge(strategy = merg::vec::append)]
723    controller_unique_mapping: Vec<u32>,
724}
725
726impl MisterConfig {
727    pub fn new() -> Self {
728        Default::default()
729    }
730
731    pub fn new_defaults() -> Self {
732        let mut config = Self::new();
733        config.set_defaults();
734        config
735    }
736
737    /// Set the default values for this config.
738    pub fn set_defaults(&mut self) {
739        self.bootscreen.get_or_insert(true);
740        self.fb_terminal.get_or_insert(true);
741        self.controller_info.get_or_insert(Duration::from_secs(6));
742        self.browse_expand.get_or_insert(true);
743        self.logo.get_or_insert(true);
744        self.rumble.get_or_insert(true);
745        self.wheel_force.get_or_insert(50);
746        self.hdr.get_or_insert(HdrConfig::default());
747        self.hdr_avg_nits.get_or_insert(250);
748        self.hdr_max_nits.get_or_insert(1000);
749        self.video_brightness.get_or_insert(50);
750        self.video_contrast.get_or_insert(50);
751        self.video_saturation.get_or_insert(100);
752        self.video_hue.get_or_insert(0);
753        self.video_gain_offset
754            .get_or_insert("1, 0, 1, 0, 1, 0".parse().unwrap());
755        self.video_conf.get_or_insert("6".to_string());
756    }
757
758    pub fn custom_aspect_ratio(&self) -> Vec<AspectRatio> {
759        if self.custom_aspect_ratio.is_empty() {
760            self.custom_aspect_ratio_1
761                .iter()
762                .chain(self.custom_aspect_ratio_2.iter())
763                .copied()
764                .collect()
765        } else {
766            self.custom_aspect_ratio.clone()
767        }
768    }
769
770    pub fn player_controller(&self) -> Vec<Vec<String>> {
771        if self.player_controller.is_empty() {
772            let mut vec = Vec::new();
773            if !self.player_1_controller.is_empty() {
774                vec.push(self.player_1_controller.clone());
775            }
776            if !self.player_2_controller.is_empty() {
777                vec.push(self.player_2_controller.clone());
778            }
779            if !self.player_3_controller.is_empty() {
780                vec.push(self.player_3_controller.clone());
781            }
782            if !self.player_4_controller.is_empty() {
783                vec.push(self.player_4_controller.clone());
784            }
785            if !self.player_5_controller.is_empty() {
786                vec.push(self.player_5_controller.clone());
787            }
788            if !self.player_6_controller.is_empty() {
789                vec.push(self.player_6_controller.clone());
790            }
791
792            vec
793        } else {
794            self.player_controller.clone()
795        }
796    }
797
798    #[inline]
799    pub fn hdmi_limited(&self) -> HdmiLimitedConfig {
800        self.hdmi_limited.unwrap_or_default()
801    }
802
803    #[inline]
804    pub fn hdmi_game_mode(&self) -> bool {
805        self.hdmi_game_mode.unwrap_or_default()
806    }
807
808    #[inline]
809    pub fn hdr(&self) -> HdrConfig {
810        self.hdr.unwrap_or_default()
811    }
812
813    #[inline]
814    pub fn hdr_max_nits(&self) -> u16 {
815        self.hdr_max_nits.unwrap_or(1000)
816    }
817
818    #[inline]
819    pub fn hdr_avg_nits(&self) -> u16 {
820        self.hdr_avg_nits.unwrap_or(250)
821    }
822
823    #[inline]
824    pub fn dvi_mode(&self) -> bool {
825        self.dvi_mode.unwrap_or_default()
826    }
827
828    #[inline]
829    pub fn dvi_mode_raw(&self) -> Option<bool> {
830        self.dvi_mode
831    }
832
833    #[inline]
834    pub fn hdmi_audio_96k(&self) -> bool {
835        self.hdmi_audio_96k.unwrap_or_default()
836    }
837
838    /// The video brightness, between [-0.5..0.5]
839    #[inline]
840    pub fn video_brightness(&self) -> f32 {
841        (self.video_brightness.unwrap_or(50).clamp(0, 100) as f32 / 100.0) - 0.5
842    }
843
844    /// The video contrast, between [0..2]
845    #[inline]
846    pub fn video_contrast(&self) -> f32 {
847        ((self.video_contrast.unwrap_or(50).clamp(0, 100) as f32 / 100.0) - 0.5) * 2. + 1.
848    }
849
850    /// The video saturation, between [0..1]
851    #[inline]
852    pub fn video_saturation(&self) -> f32 {
853        self.video_saturation.unwrap_or(100).clamp(0, 100) as f32 / 100.
854    }
855
856    /// The video hue.
857    #[inline]
858    pub fn video_hue_radian(&self) -> f32 {
859        (self.video_hue.unwrap_or_default() as f32) * f32::PI() / 180.
860    }
861
862    /// The video gains and offets.
863    #[inline]
864    pub fn video_gain_offset(&self) -> VideoGainOffsets {
865        self.video_gain_offset.unwrap_or_default()
866    }
867
868    /// The VGA mode.
869    #[inline]
870    pub fn vga_mode(&self) -> VgaMode {
871        self.vga_mode.unwrap_or_default()
872    }
873
874    /// Direct Video?
875    #[inline]
876    pub fn direct_video(&self) -> bool {
877        self.direct_video.unwrap_or_default()
878    }
879
880    /// Whether to use vsync adjust.
881    #[inline]
882    pub fn vsync_adjust(&self) -> VsyncAdjustConfig {
883        if self.direct_video() {
884            VsyncAdjustConfig::Disabled
885        } else {
886            self.vsync_adjust.unwrap_or_default()
887        }
888    }
889
890    /// Whether to use PAL in the menu.
891    #[inline]
892    pub fn menu_pal(&self) -> bool {
893        self.menu_pal.unwrap_or_default()
894    }
895
896    /// Whether to force the scan doubler.
897    #[inline]
898    pub fn forced_scandoubler(&self) -> bool {
899        self.forced_scandoubler.unwrap_or_default()
900    }
901}
902
903#[cfg(test)]
904pub mod testing {
905    use std::path::PathBuf;
906
907    pub(super) static mut ROOT: Option<PathBuf> = None;
908
909    #[cfg(test)]
910    pub fn set_config_root(root: impl Into<PathBuf>) {
911        unsafe {
912            ROOT = Some(root.into());
913        }
914    }
915}
916
917#[derive(Default, Debug, Clone, Deserialize, Merge)]
918#[serde(default)]
919pub struct Config {
920    /// The MiSTer section which should be 99% of config files.
921    #[serde(rename = "MiSTer")]
922    mister: MisterConfig,
923
924    /// The `[video=123x456@78]` sections, or core section.
925    #[serde(flatten)]
926    #[merge(strategy = merg::hashmap::recurse)]
927    overrides: HashMap<String, MisterConfig>,
928}
929
930impl Config {
931    fn root() -> PathBuf {
932        #[cfg(test)]
933        {
934            unsafe { testing::ROOT.clone().unwrap() }
935        }
936
937        #[cfg(not(test))]
938        PathBuf::from("/media/fat")
939    }
940
941    pub fn into_inner(self) -> MisterConfig {
942        self.mister
943    }
944
945    pub fn into_inner_with_overrides(self, overrides: &[&str]) -> MisterConfig {
946        let mut mister = self.mister;
947        for o in overrides {
948            if let Some(override_config) = self.overrides.get(&o.to_string()) {
949                mister.merge(override_config.clone());
950            }
951        }
952        mister
953    }
954
955    pub fn base() -> Self {
956        let path = Self::root().join("MiSTer.ini");
957        Self::load(&path).unwrap_or_else(|_| {
958            info!(?path, "Failed to load MiSTer.ini, using defaults.");
959            let mut c = Self::default();
960            c.mister.set_defaults();
961            c
962        })
963    }
964
965    pub fn cores_root() -> PathBuf {
966        Self::root()
967    }
968
969    pub fn config_root() -> PathBuf {
970        Self::root().join("config")
971    }
972
973    pub fn last_core_data() -> Option<String> {
974        std::fs::read_to_string(Self::config_root().join("lastcore.dat")).ok()
975    }
976
977    pub fn merge_core_override(&mut self, corename: &str) {
978        if let Some(o) = self.overrides.get(corename) {
979            self.mister.merge(o.clone());
980        }
981    }
982
983    pub fn merge_video_override(&mut self, resolution: Resolution) {
984        // Try to get `123x456@78` format first.
985        let video_str = format!("video={}", resolution);
986        if let Some(o) = self.overrides.get(&video_str) {
987            self.mister.merge(o.clone());
988        }
989    }
990
991    /// Read INI config using our custom parser, then output the JSON, then parse that into
992    /// the config struct. This is surprisingly fast, solid and byte compatible with the
993    /// original CPP code (checked manually on various files).
994    pub fn from_ini<R: io::Read>(mut content: R) -> Result<Self, ConfigError> {
995        let mut s = String::new();
996        content.read_to_string(&mut s)?;
997
998        if s.is_empty() {
999            return Ok(Default::default());
1000        }
1001
1002        let json = ini::parse(&s).unwrap().to_json_string(
1003            |name, value: &str| match name {
1004                "mouse_throttle"
1005                | "video_info"
1006                | "controller_info"
1007                | "refresh_min"
1008                | "refresh_max"
1009                | "vscale_border"
1010                | "bootcore_timeout"
1011                | "osd_timeout"
1012                | "spinner_throttle"
1013                | "spinner_axis"
1014                | "shmask_mode_default"
1015                | "bt_auto_disconnect"
1016                | "wheel_force"
1017                | "wheel_range"
1018                | "vrr_min_framerate"
1019                | "vrr_max_framerate"
1020                | "vrr_vesa_framerate"
1021                | "video_off"
1022                | "video_brightness"
1023                | "video_contrast"
1024                | "video_saturation"
1025                | "video_hue"
1026                | "hdr_max_nits"
1027                | "hdr_avg_nits" => Some(value.to_string()),
1028                _ => None,
1029            },
1030            |name| {
1031                [
1032                    "custom_aspect_ratio",
1033                    "no_merge_vidpid",
1034                    "player_controller",
1035                    "player_1_controller",
1036                    "player_2_controller",
1037                    "player_3_controller",
1038                    "player_4_controller",
1039                    "player_5_controller",
1040                    "player_6_controller",
1041                    "controller_unique_mapping",
1042                ]
1043                .contains(&name)
1044            },
1045            |name: &str| -> Option<&str> {
1046                if name == "ypbpr" {
1047                    Some("vga_mode")
1048                } else {
1049                    None
1050                }
1051            },
1052        );
1053
1054        Config::from_json(json.as_bytes())
1055    }
1056
1057    pub fn from_json<R: io::Read>(mut content: R) -> Result<Self, ConfigError> {
1058        let mut c = String::new();
1059        content.read_to_string(&mut c)?;
1060        Ok(json5::from_str(&c)?)
1061    }
1062
1063    pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
1064        let path = path.as_ref();
1065        match path.extension().and_then(|ext| ext.to_str()) {
1066            Some("ini") => Self::from_ini(std::fs::File::open(path)?),
1067            Some("json") => Self::from_json(std::fs::File::open(path)?),
1068            _ => Err(
1069                io::Error::new(io::ErrorKind::InvalidInput, "Invalid config file extension").into(),
1070            ),
1071        }
1072    }
1073
1074    /// Copy this configuration to the C++ side.
1075    ///
1076    /// # Safety
1077    /// This function is unsafe because it writes to a C++ struct.
1078    pub unsafe fn copy_to_cfg_cpp(self, dest: &mut cpp::CppCfg) {
1079        unsafe fn copy_string<const N: usize>(dest: &mut [c_char; N], src: &str) {
1080            let src = src.as_bytes();
1081            let l = src.len().min(N);
1082            for i in 0..l {
1083                let c = src[i] as c_char;
1084                dest[i] = c;
1085            }
1086            dest[l] = 0;
1087        }
1088
1089        let m = &self.mister;
1090
1091        dest.keyrah_mode = m.keyrah_mode.unwrap_or_default();
1092        dest.forced_scandoubler = m.forced_scandoubler.unwrap_or_default() as u8;
1093        dest.key_menu_as_rgui = m.key_menu_as_rgui.unwrap_or_default() as u8;
1094        dest.reset_combo = m.reset_combo.unwrap_or_default() as u8;
1095        dest.csync = m.composite_sync.unwrap_or_default() as u8;
1096        dest.vga_scaler = m.vga_scaler.unwrap_or_default() as u8;
1097        dest.vga_sog = m.vga_sog.unwrap_or_default() as u8;
1098        dest.hdmi_audio_96k = m.hdmi_audio_96k.unwrap_or_default() as u8;
1099        // For some reason, dvi_mode is set to 2 by default in the C++ code, instead of false.
1100        dest.dvi_mode = m.dvi_mode.map(|x| x as u8).unwrap_or(2);
1101        dest.hdmi_limited = m.hdmi_limited.unwrap_or_default() as u8;
1102        dest.direct_video = m.direct_video.unwrap_or_default() as u8;
1103        dest.video_info = m.video_info.unwrap_or_default().as_secs() as u8;
1104        dest.refresh_min = m.refresh_min.unwrap_or_default();
1105        dest.refresh_max = m.refresh_max.unwrap_or_default();
1106        dest.controller_info = m.controller_info.unwrap_or_default().as_secs() as u8;
1107        dest.vsync_adjust = m.vsync_adjust.unwrap_or_default() as u8;
1108        dest.kbd_nomouse = m.kbd_nomouse.unwrap_or_default() as u8;
1109        dest.mouse_throttle = m.mouse_throttle.unwrap_or_default();
1110        dest.bootscreen = m.bootscreen.unwrap_or_default() as u8;
1111        dest.vscale_mode = m.vscale_mode.unwrap_or_default() as u8;
1112        dest.vscale_border = m.vscale_border.unwrap_or_default();
1113        dest.rbf_hide_datecode = m.rbf_hide_datecode.unwrap_or_default() as u8;
1114        dest.menu_pal = m.menu_pal.unwrap_or_default() as u8;
1115        dest.bootcore_timeout = m.bootcore_timeout.unwrap_or_default().as_secs() as i16;
1116        dest.fb_size = m.fb_size.unwrap_or_default() as u8;
1117        dest.fb_terminal = m.fb_terminal.unwrap_or_default() as u8;
1118        dest.osd_rotate = m.osd_rotate.unwrap_or_default() as u8;
1119        dest.osd_timeout = m.osd_timeout.unwrap_or_default().as_secs() as u16;
1120        dest.gamepad_defaults = m.gamepad_defaults.unwrap_or_default() as u8;
1121        dest.recents = m.recents.unwrap_or_default() as u8;
1122        dest.jamma_vid = m.jamma_vid.unwrap_or_default();
1123        dest.jamma_pid = m.jamma_pid.unwrap_or_default();
1124        dest.no_merge_vid = m.no_merge_vid.unwrap_or_default();
1125        dest.no_merge_pid = m.no_merge_pid.unwrap_or_default();
1126        dest.no_merge_vidpid = [0; 256];
1127        for (i, vidpid) in m.no_merge_vidpid.iter().enumerate() {
1128            dest.no_merge_vidpid[i] = *vidpid;
1129        }
1130        dest.spinner_vid = m.spinner_vid.unwrap_or_default();
1131        dest.spinner_pid = m.spinner_pid.unwrap_or_default();
1132        dest.spinner_throttle = m.spinner_throttle.unwrap_or_default() as c_int;
1133        dest.spinner_axis = m.spinner_axis.unwrap_or_default();
1134        dest.sniper_mode = m.sniper_mode.unwrap_or_default() as u8;
1135        dest.browse_expand = m.browse_expand.unwrap_or_default() as u8;
1136        dest.logo = m.logo.unwrap_or_default() as u8;
1137        dest.log_file_entry = m.log_file_entry.unwrap_or_default() as u8;
1138        dest.shmask_mode_default = m.shmask_mode_default.unwrap_or_default();
1139        dest.bt_auto_disconnect =
1140            (m.bt_auto_disconnect.unwrap_or_default().as_secs() / 60) as c_int;
1141        dest.bt_reset_before_pair = m.bt_reset_before_pair.unwrap_or_default() as c_int;
1142        copy_string(
1143            &mut dest.bootcore,
1144            &m.bootcore.clone().unwrap_or_default().to_string(),
1145        );
1146        copy_string(
1147            &mut dest.video_conf,
1148            &m.video_conf.clone().unwrap_or_default(),
1149        );
1150        copy_string(
1151            &mut dest.video_conf_pal,
1152            &m.video_conf_pal.clone().unwrap_or_default(),
1153        );
1154        copy_string(
1155            &mut dest.video_conf_ntsc,
1156            &m.video_conf_ntsc.clone().unwrap_or_default(),
1157        );
1158        copy_string(&mut dest.font, &m.font.clone().unwrap_or_default());
1159        copy_string(
1160            &mut dest.shared_folder,
1161            &m.shared_folder.clone().unwrap_or_default(),
1162        );
1163        copy_string(
1164            &mut dest.waitmount,
1165            &m.waitmount.clone().unwrap_or_default(),
1166        );
1167        let ar = m.custom_aspect_ratio();
1168        let aspect_ratio_1 = ar.first();
1169        let aspect_ratio_2 = ar.get(1);
1170        if let Some(a1) = aspect_ratio_1 {
1171            copy_string(&mut dest.custom_aspect_ratio[0], &a1.to_string());
1172        }
1173        if let Some(a2) = aspect_ratio_2 {
1174            copy_string(&mut dest.custom_aspect_ratio[1], &a2.to_string());
1175        }
1176        copy_string(
1177            &mut dest.afilter_default,
1178            &m.afilter_default.clone().unwrap_or_default(),
1179        );
1180        copy_string(
1181            &mut dest.vfilter_default,
1182            &m.vfilter_default.clone().unwrap_or_default(),
1183        );
1184        copy_string(
1185            &mut dest.vfilter_vertical_default,
1186            &m.vfilter_vertical_default.clone().unwrap_or_default(),
1187        );
1188        copy_string(
1189            &mut dest.vfilter_scanlines_default,
1190            &m.vfilter_scanlines_default.clone().unwrap_or_default(),
1191        );
1192        copy_string(
1193            &mut dest.shmask_default,
1194            &m.shmask_default.clone().unwrap_or_default(),
1195        );
1196        copy_string(
1197            &mut dest.preset_default,
1198            &m.preset_default.clone().unwrap_or_default(),
1199        );
1200        let player_controller = m.player_controller();
1201        for (i, pc) in player_controller.iter().enumerate().take(6) {
1202            for (j, p) in pc.iter().enumerate() {
1203                copy_string(&mut dest.player_controller[i][j], p);
1204            }
1205        }
1206        dest.rumble = m.rumble.unwrap_or_default() as u8;
1207        dest.wheel_force = m.wheel_force.unwrap_or_default();
1208        dest.wheel_range = m.wheel_range.unwrap_or_default();
1209        dest.hdmi_game_mode = m.hdmi_game_mode.unwrap_or_default() as u8;
1210        dest.vrr_mode = m.vrr_mode.unwrap_or_default() as u8;
1211        dest.vrr_min_framerate = m.vrr_min_framerate.unwrap_or_default();
1212        dest.vrr_max_framerate = m.vrr_max_framerate.unwrap_or_default();
1213        dest.vrr_vesa_framerate = m.vrr_vesa_framerate.unwrap_or_default();
1214        dest.video_off = m.video_off.unwrap_or_default().as_secs() as u16;
1215        dest.disable_autofire = m.disable_autofire.unwrap_or_default() as u8;
1216        dest.video_brightness = m.video_brightness.unwrap_or_default();
1217        dest.video_contrast = m.video_contrast.unwrap_or_default();
1218        dest.video_saturation = m.video_saturation.unwrap_or_default();
1219        dest.video_hue = m.video_hue.unwrap_or_default();
1220        copy_string(
1221            &mut dest.video_gain_offset,
1222            &match m.video_gain_offset.as_ref() {
1223                Some(v) => v.to_string(),
1224                None => "1, 0, 1, 0, 1, 0".to_string(),
1225            },
1226        );
1227        dest.hdr = m.hdr.unwrap_or_default() as u8;
1228        dest.hdr_max_nits = m.hdr_max_nits.unwrap_or_default();
1229        dest.hdr_avg_nits = m.hdr_avg_nits.unwrap_or_default();
1230        copy_string(
1231            &mut dest.vga_mode,
1232            &m.vga_mode.unwrap_or_default().to_string(),
1233        );
1234        dest.vga_mode_int = m.vga_mode.unwrap_or_default() as u8 as c_char;
1235        dest.ntsc_mode = m.ntsc_mode.unwrap_or_default() as u8 as c_char;
1236        for (i, mapping) in m.controller_unique_mapping.iter().enumerate().take(256) {
1237            dest.controller_unique_mapping[i] = *mapping;
1238        }
1239    }
1240
1241    /// Merge a configuration file with another.
1242    pub fn merge(&mut self, other: Config) {
1243        Merge::merge(self, other);
1244    }
1245}
1246
1247#[test]
1248fn works_with_empty_file() {
1249    unsafe {
1250        let mut cpp_cfg: cpp::CppCfg = std::mem::zeroed();
1251        let config = Config::from_ini(io::empty()).unwrap();
1252        config.copy_to_cfg_cpp(&mut cpp_cfg);
1253    }
1254}
1255
1256#[cfg(test)]
1257mod examples {
1258    use crate::config::*;
1259
1260    #[rstest::rstest]
1261    fn works_with_example(#[files("tests/assets/config/*.ini")] p: PathBuf) {
1262        unsafe {
1263            let mut cpp_cfg: cpp::CppCfg = std::mem::zeroed();
1264            let config = Config::load(p).unwrap();
1265            config.copy_to_cfg_cpp(&mut cpp_cfg);
1266        }
1267    }
1268}