1use jgenesis_common::frontend::Rotation;
2use jgenesis_proc_macros::{ConfigDisplay, EnumAll, EnumDisplay, EnumFromStr};
3use std::fmt::{Display, Formatter};
4use std::num::NonZeroU32;
5
6pub const DXCOMPILER_PATH: &str = "dxcompiler.dll";
7
8#[must_use]
9pub fn dx12_backend_options() -> wgpu::Dx12BackendOptions {
10    wgpu::Dx12BackendOptions {
11        shader_compiler: wgpu::Dx12Compiler::DynamicDxc { dxc_path: DXCOMPILER_PATH.into() },
12        ..wgpu::Dx12BackendOptions::default()
13    }
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumAll)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
19pub enum WgpuBackend {
20    #[default]
21    Auto,
22    Vulkan,
23    DirectX12,
24}
25
26impl WgpuBackend {
27    #[must_use]
28    pub fn to_wgpu(self) -> wgpu::Backends {
29        #[cfg(target_os = "windows")]
30        if self == WgpuBackend::Auto && supports_dx12() {
31            // Prefer DX12 on Windows if supported (necessary because wgpu prefers Vulkan over DX12)
32            // AMD GPUs seem to sometimes have color space bugs on Windows w/ Vulkan
33            return wgpu::Backends::DX12;
34        }
35
36        match self {
37            WgpuBackend::Auto => wgpu::Backends::PRIMARY,
38            WgpuBackend::Vulkan => wgpu::Backends::VULKAN,
39            WgpuBackend::DirectX12 => wgpu::Backends::DX12,
40        }
41    }
42}
43
44#[cfg(target_os = "windows")]
45fn supports_dx12() -> bool {
46    use std::sync::LazyLock;
47
48    static SUPPORTS_DX12: LazyLock<bool> = LazyLock::new(|| {
49        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
50            backends: wgpu::Backends::DX12,
51            backend_options: wgpu::BackendOptions {
52                dx12: dx12_backend_options(),
53                ..wgpu::BackendOptions::default()
54            },
55            ..wgpu::InstanceDescriptor::new_without_display_handle()
56        });
57
58        let adapter =
59            pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()));
60        adapter.is_ok()
61    });
62
63    *SUPPORTS_DX12
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumAll)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
69pub enum WgpuPowerPreference {
70    #[default]
71    HighPerformance,
72    LowPower,
73    None,
74}
75
76impl WgpuPowerPreference {
77    #[must_use]
78    pub fn to_wgpu(self) -> wgpu::PowerPreference {
79        match self {
80            Self::HighPerformance => wgpu::PowerPreference::HighPerformance,
81            Self::LowPower => wgpu::PowerPreference::LowPower,
82            Self::None => wgpu::PowerPreference::None,
83        }
84    }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumAll)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
90pub enum VSyncMode {
91    Enabled,
92    #[default]
93    Disabled,
94    Fast,
95}
96
97impl VSyncMode {
98    pub(crate) fn to_wgpu_present_mode(self) -> wgpu::PresentMode {
99        match self {
100            Self::Enabled => wgpu::PresentMode::Fifo,
101            Self::Disabled => wgpu::PresentMode::AutoNoVsync,
102            Self::Fast => wgpu::PresentMode::Mailbox,
103        }
104    }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct PrescaleFactor(u32);
110
111impl PrescaleFactor {
112    pub const ONE: Self = Self(1);
113
114    #[must_use]
115    pub fn get(self) -> u32 {
116        self.0
117    }
118}
119
120impl From<PrescaleFactor> for u32 {
121    fn from(value: PrescaleFactor) -> Self {
122        value.0
123    }
124}
125
126impl TryFrom<u32> for PrescaleFactor {
127    type Error = String;
128
129    fn try_from(value: u32) -> Result<Self, Self::Error> {
130        match value {
131            0 => Err(format!("invalid prescale factor: {value}")),
132            _ => Ok(Self(value)),
133        }
134    }
135}
136
137impl From<NonZeroU32> for PrescaleFactor {
138    fn from(value: NonZeroU32) -> Self {
139        Self(value.get())
140    }
141}
142
143impl Default for PrescaleFactor {
144    fn default() -> Self {
145        Self::ONE
146    }
147}
148
149impl Display for PrescaleFactor {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}x", self.0)
152    }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PrescaleMode {
157    Auto,
158    Manual { width: PrescaleFactor, height: PrescaleFactor },
159}
160
161impl Display for PrescaleMode {
162    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Self::Auto => write!(f, "Auto"),
165            Self::Manual { width, height } => write!(f, "Manual(width={width}, height={height})"),
166        }
167    }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumFromStr, EnumAll)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
173pub enum FilterMode {
174    Nearest,
175    #[default]
176    Linear,
177}
178
179impl FilterMode {
180    pub(crate) fn to_wgpu_filter_mode(self) -> wgpu::FilterMode {
181        match self {
182            Self::Nearest => wgpu::FilterMode::Nearest,
183            Self::Linear => wgpu::FilterMode::Linear,
184        }
185    }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumFromStr, EnumAll)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
191pub enum AntiDitherShader {
192    #[default]
193    None,
194    Weak,
195    Strong,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumFromStr, EnumAll)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
201pub enum PreprocessShader {
202    #[default]
203    None,
204    HorizontalBlurTwoPixels,
205    HorizontalBlurThreePixels,
206    HorizontalBlurSnesAdaptive,
207    NtscComposite,
208    Xbrz2x,
209    Xbrz3x,
210    Xbrz4x,
211    Xbrz5x,
212    Xbrz6x,
213    Mmpx,
214    MmpxEnhanced,
215}
216
217impl PreprocessShader {
218    // Certain shaders don't make sense to use in combination with an anti-dither filter
219    #[must_use]
220    pub fn exclude_anti_dither(self) -> bool {
221        matches!(
222            self,
223            Self::NtscComposite
224                | Self::HorizontalBlurTwoPixels
225                | Self::HorizontalBlurThreePixels
226                | Self::HorizontalBlurSnesAdaptive
227        )
228    }
229
230    pub(crate) fn xbrz_scale_factor(self) -> Option<u32> {
231        match self {
232            Self::Xbrz2x => Some(2),
233            Self::Xbrz3x => Some(3),
234            Self::Xbrz4x => Some(4),
235            Self::Xbrz5x => Some(5),
236            Self::Xbrz6x => Some(6),
237            _ => None,
238        }
239    }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumFromStr, EnumAll)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))]
245pub enum FrameRotation {
246    #[default]
247    None,
248    Clockwise,
249    OneEighty,
250    Counterclockwise,
251}
252
253impl From<FrameRotation> for Rotation {
254    fn from(value: FrameRotation) -> Self {
255        match value {
256            FrameRotation::None => Self::None,
257            FrameRotation::Clockwise => Self::Clockwise,
258            FrameRotation::OneEighty => Self::OneEighty,
259            FrameRotation::Counterclockwise => Self::Counterclockwise,
260        }
261    }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, ConfigDisplay)]
265#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
266pub struct NtscShaderConfig {
267    pub brightness: f64,
268    pub saturation: f64,
269    pub gamma: f64,
270}
271
272impl Default for NtscShaderConfig {
273    fn default() -> Self {
274        Self { brightness: 1.0, saturation: 1.0, gamma: 2.2 }
275    }
276}
277
278#[derive(Debug, Clone, Copy, ConfigDisplay)]
279pub struct RendererConfig {
280    pub wgpu_backend: WgpuBackend,
281    pub wgpu_power_preference: WgpuPowerPreference,
282    pub vsync_mode: VSyncMode,
283    pub frame_time_sync: bool,
284    pub prescale_mode: PrescaleMode,
285    pub scanlines_enabled: bool,
286    pub scanlines_brightness: f64,
287    pub force_integer_height_scaling: bool,
288    pub filter_mode: FilterMode,
289    pub supersample_minification: bool,
290    pub preprocess_shader: PreprocessShader,
291    pub anti_dither_shader: AntiDitherShader,
292    pub frame_rotation: FrameRotation,
293    #[cfg_display(indent_nested)]
294    pub ntsc_config: NtscShaderConfig,
295}