1mod ntsc;
2mod shaders;
3
4use crate::config;
5use crate::config::{FrameRotation, PreprocessShader, RendererConfig, VSyncMode};
6use crate::renderer::ntsc::{NtscShader, NtscShaderVariant};
7use crate::renderer::shaders::{
8    BlurShader, ColorCorrectionShader, FrameBlendShader, PrescaleShader, UpscaleShader,
9};
10#[cfg(feature = "ttf")]
11use crate::ttf;
12use jgenesis_common::frontend::{
13    Color, DisplayArea, DisplayInfo, FiniteF64, FrameSize, RenderFrameOptions,
14    RenderFrameOptionsHashable, Renderer,
15};
16use jgenesis_common::timeutils;
17use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle};
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::fmt::Debug;
21use std::sync::{Arc, LazyLock};
22use std::{cmp, iter};
23use thiserror::Error;
24use wgpu::util::DeviceExt;
25
26// Texture usages that some shaders require on input texture
27static REQUIRED_TEXTURE_USAGES: LazyLock<wgpu::TextureUsages> = LazyLock::new(|| {
28    wgpu::TextureUsages::COPY_SRC
29        | wgpu::TextureUsages::TEXTURE_BINDING
30        | wgpu::TextureUsages::STORAGE_BINDING
31});
32
33#[repr(C)]
34#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
35struct Vertex {
36    position: [f32; 2],
37    texture_coords: [f32; 2],
38}
39
40impl Vertex {
41    const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
42        wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
43
44    fn buffer_layout() -> wgpu::VertexBufferLayout<'static> {
45        wgpu::VertexBufferLayout {
46            array_stride: size_of::<Vertex>() as u64,
47            step_mode: wgpu::VertexStepMode::Vertex,
48            attributes: &Self::ATTRIBUTES,
49        }
50    }
51}
52
53const VERTICES: [Vertex; 4] = [
54    Vertex { position: [-1.0, -1.0], texture_coords: [0.0, 1.0] },
55    Vertex { position: [1.0, -1.0], texture_coords: [1.0, 1.0] },
56    Vertex { position: [-1.0, 1.0], texture_coords: [0.0, 0.0] },
57    Vertex { position: [1.0, 1.0], texture_coords: [1.0, 0.0] },
58];
59
60trait PipelineShader {
61    #[allow(unused_variables)]
62    fn prepare(&mut self, device: &wgpu::Device, options: RenderFrameOptions) {}
63
64    fn draw(&mut self, encoder: &mut wgpu::CommandEncoder);
65
66    fn output_texture(&self) -> &Arc<wgpu::Texture>;
67
68    fn reset_interframe_state(&mut self) {}
69}
70
71impl FrameRotation {
72    fn rotate_frame_size_and_aspect_ratio(
73        self,
74        size: FrameSize,
75        pixel_aspect_ratio: Option<FiniteF64>,
76    ) -> (FrameSize, Option<FiniteF64>) {
77        match self {
78            Self::None | Self::OneEighty => (size, pixel_aspect_ratio),
79            Self::Clockwise | Self::Counterclockwise => {
80                let rotated_size = FrameSize { width: size.height, height: size.width };
81                let rotated_par =
82                    pixel_aspect_ratio.and_then(|par| FiniteF64::try_from(1.0 / par.get()).ok());
83
84                (rotated_size, rotated_par)
85            }
86        }
87    }
88
89    fn rotate_display_area_size(self, area: DisplayArea) -> (u32, u32) {
90        match self {
91            Self::None | Self::OneEighty => (area.width, area.height),
92            Self::Clockwise | Self::Counterclockwise => (area.height, area.width),
93        }
94    }
95
96    fn rotate_texture_coords(self, [x, y]: [f32; 2]) -> [f32; 2] {
97        // Rotation is reversed because input coordinates are position in the rotated frame, and
98        // return value should be position in the original frame
99        match self {
100            Self::None => [x, y],
101            Self::Clockwise => [y, 1.0 - x],
102            Self::OneEighty => [1.0 - x, 1.0 - y],
103            Self::Counterclockwise => [1.0 - y, x],
104        }
105    }
106}
107
108struct RenderingPipeline {
109    frame_size: FrameSize,
110    display_area: DisplayArea,
111    rotation: FrameRotation,
112    input_texture: Arc<wgpu::Texture>,
113    shader_pipeline: Vec<Box<dyn PipelineShader>>,
114    vertex_buffer: wgpu::Buffer,
115    render_bind_group: wgpu::BindGroup,
116    render_pipeline: wgpu::RenderPipeline,
117    multisample_output: Option<wgpu::Texture>,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121enum RenderResult {
122    None,
123    SuboptimalSurface,
124}
125
126impl RenderingPipeline {
127    #[allow(clippy::too_many_arguments)]
128    fn create(
129        device: &wgpu::Device,
130        limits: &wgpu::Limits,
131        shaders: &Shaders,
132        window_size: WindowSize,
133        frame_size: FrameSize,
134        options: RenderFrameOptions,
135        surface_config: &wgpu::SurfaceConfiguration,
136        renderer_config: RendererConfig,
137    ) -> Self {
138        fn current_output_texture(
139            pipeline: &[Box<dyn PipelineShader>],
140            input: &Arc<wgpu::Texture>,
141        ) -> Arc<wgpu::Texture> {
142            Arc::clone(pipeline.last().map_or(input, |shader| shader.output_texture()))
143        }
144
145        let input_texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor {
146            label: "input_texture".into(),
147            size: wgpu::Extent3d {
148                width: frame_size.width,
149                height: frame_size.height,
150                depth_or_array_layers: 1,
151            },
152            mip_level_count: 1,
153            sample_count: 1,
154            dimension: wgpu::TextureDimension::D2,
155            format: wgpu::TextureFormat::Rgba8Unorm,
156            usage: *REQUIRED_TEXTURE_USAGES | wgpu::TextureUsages::COPY_DST,
157            view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
158        }));
159
160        // For 90/270 degree rotations, compute display area based on swapped frame width/height and
161        // inverted aspect ratio
162        let (rotated_frame_size, rotated_aspect_ratio) = renderer_config
163            .frame_rotation
164            .rotate_frame_size_and_aspect_ratio(frame_size, options.pixel_aspect_ratio);
165        let display_area = determine_display_area(
166            window_size,
167            rotated_frame_size,
168            rotated_aspect_ratio,
169            renderer_config.force_integer_height_scaling,
170        );
171
172        // Pipeline shaders (all optional):
173        //   1. Color correction
174        //   2. Anti-dither
175        //   3. NTSC composite / Upscaling / Horizontal blur
176        //   4. Frame blending
177        //   5. Prescale / Scanlines
178        let mut shader_pipeline: Vec<Box<dyn PipelineShader>> = Vec::new();
179
180        macro_rules! current_output_texture {
181            () => {
182                current_output_texture(&shader_pipeline, &input_texture)
183            };
184        }
185
186        // GBC/GBA color correction
187        if let Some(color_correction_shader) = ColorCorrectionShader::create(
188            options.color_correction,
189            &current_output_texture!(),
190            device,
191            shaders,
192        ) {
193            log::debug!("Adding color correction shader");
194            shader_pipeline.push(Box::new(color_correction_shader));
195        }
196
197        // Anti-dither
198        if !renderer_config.preprocess_shader.exclude_anti_dither()
199            && let Some(anti_dither_shader) = BlurShader::create_anti_dither(
200                renderer_config.anti_dither_shader,
201                device,
202                &current_output_texture!(),
203                shaders,
204            )
205        {
206            log::debug!("Adding anti-dither shader");
207            shader_pipeline.push(Box::new(anti_dither_shader));
208        }
209
210        // NTSC composite
211        if renderer_config.preprocess_shader == PreprocessShader::NtscComposite
212            && let Some(params) = options.composite_params
213        {
214            log::debug!("Adding NTSC composite shader");
215
216            let variant = if options.emulate_nes_ntsc_output {
217                NtscShaderVariant::NesPpu
218            } else {
219                NtscShaderVariant::Rgb
220            };
221            shader_pipeline.push(Box::new(NtscShader::create(
222                device,
223                shaders,
224                &current_output_texture!(),
225                params,
226                renderer_config.ntsc_config,
227                variant,
228            )));
229        }
230
231        // Horizontal blur
232        if let Some(blur_shader) = BlurShader::create_horizontal_blur(
233            renderer_config.preprocess_shader,
234            device,
235            &current_output_texture!(),
236            shaders,
237        ) {
238            log::debug!("Adding blur shader");
239            shader_pipeline.push(Box::new(blur_shader));
240        }
241
242        // xBRZ upscaling
243        if let Some(scale_factor) = renderer_config.preprocess_shader.xbrz_scale_factor()
244            && let Some(xbrz_shader) = UpscaleShader::create_xbrz(
245                device,
246                shaders,
247                &current_output_texture!(),
248                scale_factor,
249            )
250        {
251            log::debug!("Adding xBRZ shader");
252            shader_pipeline.push(Box::new(xbrz_shader));
253        }
254
255        // MMPX upscaling
256        if renderer_config.preprocess_shader == PreprocessShader::Mmpx
257            && let Some(mmpx_shader) =
258                UpscaleShader::create_mmpx(device, shaders, &current_output_texture!())
259        {
260            log::debug!("Adding MMPX shader");
261            shader_pipeline.push(Box::new(mmpx_shader));
262        }
263
264        if renderer_config.preprocess_shader == PreprocessShader::MmpxEnhanced
265            && let Some(mmpx_enhanced_shader) =
266                UpscaleShader::create_mmpx_enhanced(device, shaders, &current_output_texture!())
267        {
268            log::debug!("Adding MMPX-Enhanced shader");
269            shader_pipeline.push(Box::new(mmpx_enhanced_shader));
270        }
271
272        // Frame blending
273        if options.frame_blending {
274            log::debug!("Adding frame blending shader");
275            shader_pipeline.push(Box::new(FrameBlendShader::create(
276                current_output_texture!(),
277                device,
278                shaders,
279            )));
280        }
281
282        // Prescaling / Scanlines
283        if let Some(prescale_shader) = PrescaleShader::create(
284            renderer_config,
285            frame_size,
286            display_area,
287            options.pixel_aspect_ratio,
288            &current_output_texture!(),
289            device,
290            limits,
291            shaders,
292        ) {
293            log::debug!("Adding prescale/scanlines shader");
294            shader_pipeline.push(Box::new(prescale_shader));
295        }
296
297        let render_input_texture = current_output_texture!();
298        let render_input_view = render_input_texture.create_view(&wgpu::TextureViewDescriptor {
299            format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
300            usage: Some(wgpu::TextureUsages::TEXTURE_BINDING),
301            ..wgpu::TextureViewDescriptor::default()
302        });
303
304        // Use multisampled rendering only when the final frame texture is at least twice as large
305        // as the display area in at least 1 dimension; otherwise it's just a waste of compute
306        let multisample = renderer_config.supersample_minification
307            && (render_input_texture.width() > 2 * display_area.width
308                || render_input_texture.height() > 2 * display_area.height);
309
310        let multisample_output = multisample.then(|| {
311            device.create_texture(&wgpu::TextureDescriptor {
312                label: "multisample_output_texture".into(),
313                size: wgpu::Extent3d {
314                    width: surface_config.width,
315                    height: surface_config.height,
316                    depth_or_array_layers: 1,
317                },
318                mip_level_count: 1,
319                sample_count: 4,
320                dimension: wgpu::TextureDimension::D2,
321                format: surface_config.format,
322                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
323                view_formats: &[],
324            })
325        });
326
327        // If surface format is not sRGB-aware, fragment shader needs to perform gamma encoding
328        // since the frame texture view is always sRGB-aware
329        let encode_to_srgb = !surface_config.format.is_srgb();
330
331        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
332            label: "render_pipeline".into(),
333            layout: None,
334            vertex: wgpu::VertexState {
335                module: &shaders.render,
336                entry_point: None,
337                compilation_options: wgpu::PipelineCompilationOptions::default(),
338                buffers: &[Vertex::buffer_layout()],
339            },
340            primitive: wgpu::PrimitiveState {
341                topology: wgpu::PrimitiveTopology::TriangleStrip,
342                strip_index_format: None,
343                front_face: wgpu::FrontFace::Ccw,
344                cull_mode: None,
345                unclipped_depth: false,
346                polygon_mode: wgpu::PolygonMode::Fill,
347                conservative: false,
348            },
349            depth_stencil: None,
350            multisample: wgpu::MultisampleState {
351                count: if multisample { 4 } else { 1 },
352                mask: !0,
353                alpha_to_coverage_enabled: false,
354            },
355            fragment: Some(wgpu::FragmentState {
356                module: &shaders.render,
357                entry_point: None,
358                compilation_options: wgpu::PipelineCompilationOptions {
359                    constants: &[("encode_to_srgb", encode_to_srgb.into())],
360                    ..wgpu::PipelineCompilationOptions::default()
361                },
362                targets: &[Some(wgpu::ColorTargetState {
363                    format: surface_config.format,
364                    blend: Some(wgpu::BlendState::REPLACE),
365                    write_mask: wgpu::ColorWrites::ALL,
366                })],
367            }),
368            multiview_mask: None,
369            cache: None,
370        });
371
372        let filter_mode = renderer_config.filter_mode.to_wgpu_filter_mode();
373        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
374            label: "sampler".into(),
375            address_mode_u: wgpu::AddressMode::ClampToEdge,
376            address_mode_v: wgpu::AddressMode::ClampToEdge,
377            address_mode_w: wgpu::AddressMode::ClampToEdge,
378            mag_filter: filter_mode,
379            min_filter: if renderer_config.supersample_minification {
380                wgpu::FilterMode::Linear
381            } else {
382                filter_mode
383            },
384            ..wgpu::SamplerDescriptor::default()
385        });
386
387        let render_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
388            label: "render_bind_group".into(),
389            layout: &render_pipeline.get_bind_group_layout(0),
390            entries: &[
391                wgpu::BindGroupEntry {
392                    binding: 0,
393                    resource: wgpu::BindingResource::TextureView(&render_input_view),
394                },
395                wgpu::BindGroupEntry {
396                    binding: 1,
397                    resource: wgpu::BindingResource::Sampler(&sampler),
398                },
399            ],
400        });
401
402        let mut vertices = match options.pixel_aspect_ratio {
403            Some(_) => compute_vertices(window_size.width, window_size.height, display_area),
404            None => VERTICES.into(),
405        };
406
407        apply_frame_rotation(&mut vertices, renderer_config.frame_rotation);
408
409        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
410            label: "vertex_buffer".into(),
411            contents: bytemuck::cast_slice(&vertices),
412            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::VERTEX,
413        });
414
415        Self {
416            frame_size,
417            display_area,
418            rotation: renderer_config.frame_rotation,
419            input_texture,
420            shader_pipeline,
421            vertex_buffer,
422            render_bind_group,
423            render_pipeline,
424            multisample_output,
425        }
426    }
427
428    #[allow(clippy::too_many_arguments)]
429    fn render(
430        &mut self,
431        device: &wgpu::Device,
432        queue: &wgpu::Queue,
433        surface: &wgpu::Surface<'_>,
434        frame_buffer: &[Color],
435        options: RenderFrameOptions,
436        #[cfg(feature = "ttf")] surface_config: &wgpu::SurfaceConfiguration,
437        #[cfg(feature = "ttf")] modal_renderer: &mut ttf::ModalRenderer,
438        frame_time_tracker: &mut FrameTimeTracker,
439    ) -> Result<RenderResult, RendererError> {
440        let mut suboptimal_surface = false;
441        let output = match surface.get_current_texture() {
442            wgpu::CurrentSurfaceTexture::Success(texture) => texture,
443            wgpu::CurrentSurfaceTexture::Suboptimal(texture) => {
444                suboptimal_surface = true;
445                texture
446            }
447            wgpu::CurrentSurfaceTexture::Timeout => {
448                return Err(RendererError::WgpuSurfaceTimeout);
449            }
450            wgpu::CurrentSurfaceTexture::Occluded => {
451                return Err(RendererError::WgpuSurfaceOccluded);
452            }
453            wgpu::CurrentSurfaceTexture::Outdated => {
454                log::warn!("Skipping frame because wgpu surface is outdated");
455                return Ok(RenderResult::SuboptimalSurface);
456            }
457            wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Validation => {
458                return Err(RendererError::WgpuSurfaceLost);
459            }
460        };
461
462        queue.write_texture(
463            wgpu::TexelCopyTextureInfo {
464                texture: &self.input_texture,
465                mip_level: 0,
466                origin: wgpu::Origin3d::ZERO,
467                aspect: wgpu::TextureAspect::All,
468            },
469            bytemuck::cast_slice(frame_buffer),
470            wgpu::TexelCopyBufferLayout {
471                offset: 0,
472                bytes_per_row: Some(self.frame_size.width * 4),
473                rows_per_image: Some(self.frame_size.height),
474            },
475            self.input_texture.size(),
476        );
477
478        for shader in &mut self.shader_pipeline {
479            shader.prepare(device, options);
480        }
481
482        let mut encoder = device
483            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: "encoder".into() });
484
485        for shader in &mut self.shader_pipeline {
486            shader.draw(&mut encoder);
487        }
488
489        #[cfg(feature = "ttf")]
490        let ttf_multisampled = match self.multisample_output {
491            Some(_) => ttf::Multisampled::Yes,
492            None => ttf::Multisampled::No,
493        };
494
495        #[cfg(feature = "ttf")]
496        let modal_vertex_buffer = modal_renderer.prepare_modals(
497            device,
498            queue,
499            ttf_multisampled,
500            surface_config.width,
501            surface_config.height,
502        )?;
503
504        let surface_output_view =
505            output.texture.create_view(&wgpu::TextureViewDescriptor::default());
506
507        let (output_view, resolve_target) = match &self.multisample_output {
508            Some(multisample_output) => (
509                multisample_output.create_view(&wgpu::TextureViewDescriptor::default()),
510                Some(surface_output_view),
511            ),
512            None => (surface_output_view, None),
513        };
514
515        {
516            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
517                label: "surface_render_pass".into(),
518                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
519                    view: &output_view,
520                    depth_slice: None,
521                    resolve_target: resolve_target.as_ref(),
522                    ops: wgpu::Operations {
523                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
524                        store: wgpu::StoreOp::Store,
525                    },
526                })],
527                ..wgpu::RenderPassDescriptor::default()
528            });
529
530            render_pass.set_bind_group(0, &self.render_bind_group, &[]);
531            render_pass.set_pipeline(&self.render_pipeline);
532            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
533
534            render_pass.draw(0..VERTICES.len() as u32, 0..1);
535
536            #[cfg(feature = "ttf")]
537            if let Some(modal_vertex_buffer) = &modal_vertex_buffer {
538                modal_renderer.render(ttf_multisampled, modal_vertex_buffer, &mut render_pass)?;
539            }
540        }
541
542        queue.submit(iter::once(encoder.finish()));
543
544        frame_time_tracker.sync();
545        output.present();
546
547        Ok(if suboptimal_surface { RenderResult::SuboptimalSurface } else { RenderResult::None })
548    }
549}
550
551fn compute_vertices(
552    window_width: u32,
553    window_height: u32,
554    display_area: DisplayArea,
555) -> Vec<Vertex> {
556    log::info!(
557        "Display area: width={}, height={}, left={}, top={}",
558        display_area.width,
559        display_area.height,
560        display_area.x,
561        display_area.y
562    );
563
564    VERTICES
565        .into_iter()
566        .map(|vertex| Vertex {
567            position: [
568                scale_vertex_position(
569                    vertex.position[0],
570                    window_width,
571                    display_area.width,
572                    display_area.x,
573                ),
574                scale_vertex_position(
575                    vertex.position[1],
576                    window_height,
577                    display_area.height,
578                    display_area.y,
579                ),
580            ],
581            texture_coords: vertex.texture_coords,
582        })
583        .collect()
584}
585
586fn determine_display_area(
587    WindowSize { width: window_width, height: window_height, pixel_density }: WindowSize,
588    frame_size: FrameSize,
589    pixel_aspect_ratio: Option<FiniteF64>,
590    force_integer_height_scaling: bool,
591) -> DisplayArea {
592    let Some(pixel_aspect_ratio) = pixel_aspect_ratio else {
593        return DisplayArea {
594            width: window_width,
595            height: window_height,
596            x: 0,
597            y: 0,
598            pixel_density,
599        };
600    };
601
602    let pixel_aspect_ratio: f64 = pixel_aspect_ratio.into();
603
604    let frame_aspect_ratio = f64::from(frame_size.width) / f64::from(frame_size.height);
605    let screen_aspect_ratio = pixel_aspect_ratio * frame_aspect_ratio;
606
607    let screen_width =
608        cmp::min(window_width, (f64::from(window_height) * screen_aspect_ratio).round() as u32);
609    let screen_height =
610        cmp::min(window_height, (f64::from(screen_width) / screen_aspect_ratio).round() as u32);
611
612    // Apply integer height scaling
613    let (screen_width, screen_height) =
614        if force_integer_height_scaling && screen_height >= frame_size.height {
615            let scale_factor = screen_height / frame_size.height;
616            let scaled_height = scale_factor * frame_size.height;
617            let scaled_width = (f64::from(scaled_height) * screen_aspect_ratio).round() as u32;
618            (scaled_width, scaled_height)
619        } else {
620            (screen_width, screen_height)
621        };
622
623    let x = (window_width - screen_width) / 2;
624    let y = (window_height - screen_height) / 2;
625
626    DisplayArea { width: screen_width, height: screen_height, x, y, pixel_density }
627}
628
629fn scale_vertex_position(
630    position: f32,
631    window_dimension: u32,
632    screen_dimension: u32,
633    offset: u32,
634) -> f32 {
635    let position = if position.is_sign_positive() {
636        f64::from(screen_dimension + offset) / f64::from(window_dimension) * 2.0 - 1.0
637    } else {
638        f64::from(offset) / f64::from(window_dimension) * 2.0 - 1.0
639    };
640    position as f32
641}
642
643fn apply_frame_rotation(vertices: &mut Vec<Vertex>, rotation: FrameRotation) {
644    // Rotate frame by rotating the texture coordinates of each vertex
645    for vertex in vertices {
646        vertex.texture_coords = rotation.rotate_texture_coords(vertex.texture_coords);
647    }
648}
649
650#[derive(Debug, Error)]
651pub enum RendererError {
652    #[error(
653        "Frame buffer of len {buffer_len} is too small for specified frame size of {frame_width}x{frame_height}"
654    )]
655    FrameBufferTooSmall { frame_width: u32, frame_height: u32, buffer_len: usize },
656    #[error("Invalid target fps value, must be finite and positive: {0}")]
657    InvalidTargetFps(f64),
658    #[error("Error creating surface from window: {0}")]
659    WindowHandleError(#[from] HandleError),
660    #[error("Error creating wgpu surface: {0}")]
661    WgpuCreateSurface(#[from] wgpu::CreateSurfaceError),
662    #[error("Error requesting wgpu device: {0}")]
663    WgpuRequestDevice(#[from] wgpu::RequestDeviceError),
664    #[error("Timeout obtaining wgpu surface texture")]
665    WgpuSurfaceTimeout,
666    #[error("wgpu surface is occluded")]
667    WgpuSurfaceOccluded,
668    #[error("wgpu surface was lost or failed validation")]
669    WgpuSurfaceLost,
670    #[error("Failed to obtain wgpu adapter")]
671    WgpuRequestAdapter(#[from] wgpu::RequestAdapterError),
672    #[error(
673        "wgpu adapter does not support present mode {desired:?} (from VSync mode {vsync_mode:?}); supported modes are {available:?}"
674    )]
675    UnsupportedPresentMode {
676        desired: wgpu::PresentMode,
677        available: Vec<wgpu::PresentMode>,
678        vsync_mode: VSyncMode,
679    },
680    #[cfg(feature = "ttf")]
681    #[error("Error preparing text to render: {0}")]
682    GlyphonPrepare(#[from] glyphon::PrepareError),
683    #[cfg(feature = "ttf")]
684    #[error("Error rendering text: {0}")]
685    GlyphonRender(#[from] glyphon::RenderError),
686}
687
688struct Shaders {
689    render: wgpu::ShaderModule,
690    prescale: wgpu::ShaderModule,
691    identity: wgpu::ShaderModule,
692    hblur: wgpu::ShaderModule,
693    frame_blend: wgpu::ShaderModule,
694    gb_color: wgpu::ShaderModule,
695    ntsc: wgpu::ShaderModule,
696    xbrz: wgpu::ShaderModule,
697    mmpx: wgpu::ShaderModule,
698    mmpx_enhanced: wgpu::ShaderModule,
699}
700
701impl Shaders {
702    fn create(device: &wgpu::Device) -> Self {
703        let render = device.create_shader_module(wgpu::include_wgsl!("wgsl/render.wgsl"));
704        let prescale = device.create_shader_module(wgpu::include_wgsl!("wgsl/prescale.wgsl"));
705        let identity = device.create_shader_module(wgpu::include_wgsl!("wgsl/identity.wgsl"));
706        let hblur = device.create_shader_module(wgpu::include_wgsl!("wgsl/hblur.wgsl"));
707        let frame_blend = device.create_shader_module(wgpu::include_wgsl!("wgsl/frameblend.wgsl"));
708        let gb_color = device.create_shader_module(wgpu::include_wgsl!("wgsl/gb_color.wgsl"));
709        let ntsc = device.create_shader_module(wgpu::include_wgsl!("wgsl/ntsc.wgsl"));
710        let xbrz = device.create_shader_module(wgpu::include_wgsl!("wgsl/xbrz.wgsl"));
711        let mmpx = device.create_shader_module(wgpu::include_wgsl!("wgsl/mmpx.wgsl"));
712        let mmpx_enhanced = device.create_shader_module(wgpu::include_wgsl!(concat!(
713            env!("OUT_DIR"),
714            "/mmpx_enhanced.wgsl"
715        )));
716
717        Self {
718            render,
719            prescale,
720            identity,
721            hblur,
722            frame_blend,
723            gb_color,
724            ntsc,
725            xbrz,
726            mmpx,
727            mmpx_enhanced,
728        }
729    }
730}
731
732#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
733struct PipelineKey {
734    frame_size: FrameSize,
735    options: RenderFrameOptionsHashable,
736}
737
738impl PipelineKey {
739    fn new(frame_size: FrameSize, options: RenderFrameOptions) -> Self {
740        Self { frame_size, options: options.to_hashable() }
741    }
742}
743
744struct RenderingPipelines {
745    pipelines: HashMap<PipelineKey, RenderingPipeline>,
746    last_display_info: Option<DisplayInfo>,
747}
748
749impl RenderingPipelines {
750    fn new() -> Self {
751        Self { pipelines: HashMap::new(), last_display_info: None }
752    }
753
754    fn clear(&mut self) {
755        self.pipelines.clear();
756        self.last_display_info = None;
757    }
758
759    fn get_or_insert(
760        &mut self,
761        frame_size: FrameSize,
762        options: RenderFrameOptions,
763        create_fn: impl FnOnce() -> RenderingPipeline,
764    ) -> &mut RenderingPipeline {
765        let pipeline =
766            self.pipelines.entry(PipelineKey::new(frame_size, options)).or_insert_with(create_fn);
767
768        self.last_display_info = Some(DisplayInfo {
769            frame_size,
770            display_area: pipeline.display_area,
771            rotation: pipeline.rotation.into(),
772        });
773
774        pipeline
775    }
776}
777
778#[derive(Debug, Clone)]
779struct FrameTimeTracker {
780    sync_enabled: bool,
781    last_frame_time_nanos: u128,
782    frame_interval_nanos: u128,
783}
784
785impl FrameTimeTracker {
786    fn new(sync_enabled: bool) -> Self {
787        Self {
788            sync_enabled,
789            last_frame_time_nanos: timeutils::current_time_nanos(),
790            frame_interval_nanos: (1_000_000_000.0_f64 / 60.0).round() as u128,
791        }
792    }
793
794    fn set_target_fps(&mut self, fps: f64) {
795        self.frame_interval_nanos = (1_000_000_000.0_f64 / fps).round() as u128;
796    }
797
798    fn sync(&mut self) {
799        if !self.sync_enabled {
800            return;
801        }
802
803        let next_frame_time = self.last_frame_time_nanos + self.frame_interval_nanos;
804        let now = timeutils::sleep_until(next_frame_time);
805        self.last_frame_time_nanos += self.frame_interval_nanos;
806
807        if now > self.last_frame_time_nanos
808            && (now - self.last_frame_time_nanos) > 5 * self.frame_interval_nanos
809        {
810            log::warn!("Frame time sync is more than 5 frames behind; catching up frame time");
811            self.last_frame_time_nanos = now;
812        }
813    }
814}
815
816#[derive(Debug, Clone, Copy, PartialEq)]
817pub struct WindowSize {
818    pub width: u32,
819    pub height: u32,
820    pub pixel_density: f32,
821}
822
823trait PresentModeExt: Copy {
824    fn always_supported(self) -> bool;
825}
826
827impl PresentModeExt for wgpu::PresentMode {
828    fn always_supported(self) -> bool {
829        // AutoVsync and AutoNoVsync present modes are guaranteed to always work due to fallback behavior
830        matches!(self, Self::AutoVsync | Self::AutoNoVsync)
831    }
832}
833
834pub struct WgpuRenderer<Window> {
835    surface: wgpu::Surface<'static>,
836    surface_config: wgpu::SurfaceConfiguration,
837    surface_capabilities: wgpu::SurfaceCapabilities,
838    device: wgpu::Device,
839    device_limits: wgpu::Limits,
840    queue: wgpu::Queue,
841    shaders: Shaders,
842    renderer_config: RendererConfig,
843    pipelines: RenderingPipelines,
844    #[cfg(feature = "ttf")]
845    modal_renderer: ttf::ModalRenderer,
846    frame_count: u64,
847    speed_multiplier: u64,
848    force_sync_off: bool,
849    frame_time_tracker: FrameTimeTracker,
850    // SAFETY: The surface must not outlive the window it was created from, thus the window must be
851    // declared after the surface
852    window: Window,
853    window_size: WindowSize,
854}
855
856impl<Window: HasDisplayHandle + HasWindowHandle> WgpuRenderer<Window> {
857    /// Construct a wgpu renderer from the given window and config.
858    ///
859    /// # Errors
860    ///
861    /// This function will return any errors encountered while initializing wgpu.
862    pub async fn new(
863        window: Window,
864        window_size: WindowSize,
865        config: RendererConfig,
866    ) -> Result<Self, RendererError> {
867        let backends = config.wgpu_backend.to_wgpu();
868
869        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
870            backends,
871            backend_options: wgpu::BackendOptions {
872                dx12: config::dx12_backend_options(),
873                ..wgpu::BackendOptions::default()
874            },
875            ..wgpu::InstanceDescriptor::new_without_display_handle()
876        });
877
878        // SAFETY: The surface must not outlive the window it was created from
879        let surface = unsafe {
880            instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_display_and_window(
881                &window, &window,
882            )?)
883        }?;
884
885        let adapter = instance
886            .request_adapter(&wgpu::RequestAdapterOptions {
887                power_preference: config.wgpu_power_preference.to_wgpu(),
888                compatible_surface: Some(&surface),
889                force_fallback_adapter: false,
890            })
891            .await?;
892
893        let adapter_info = adapter.get_info();
894        log::info!(
895            "Obtained wgpu adapter with backend {:?}: {}",
896            adapter_info.backend,
897            adapter_info.name
898        );
899
900        let (device, queue) = adapter
901            .request_device(&wgpu::DeviceDescriptor {
902                label: "device".into(),
903                required_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
904                required_limits: wgpu::Limits {
905                    // Default texture dimension limit is 8K but basically every modern device
906                    // supports 16K textures; use whatever the device supports
907                    max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d,
908                    ..wgpu::Limits::default()
909                },
910                experimental_features: wgpu::ExperimentalFeatures::default(),
911                memory_hints: wgpu::MemoryHints::default(),
912                trace: wgpu::Trace::Off,
913            })
914            .await?;
915
916        let surface_capabilities = surface.get_capabilities(&adapter);
917
918        let present_mode = config.vsync_mode.to_wgpu_present_mode();
919        if !present_mode.always_supported()
920            && !surface_capabilities.present_modes.contains(&present_mode)
921        {
922            return Err(RendererError::UnsupportedPresentMode {
923                desired: present_mode,
924                available: surface_capabilities.present_modes.clone(),
925                vsync_mode: config.vsync_mode,
926            });
927        }
928
929        // On Windows, using the Vulkan backend with an AMD GPU can seemingly cause incorrect colors
930        // when rendering to a surface with an sRGB-aware texture format; prefer non-sRGB-aware for
931        // Windows+Vulkan
932        //
933        // Possibly related: https://github.com/gfx-rs/wgpu/issues/8354
934        let prefer_srgb_format =
935            cfg_select! {
936                target_os = "windows" => adapter_info.backend != wgpu::Backend::Vulkan,
937                _ => true,
938            };
939
940        let surface_format = surface_capabilities
941            .formats
942            .iter()
943            .copied()
944            .find(|format| format.is_srgb() == prefer_srgb_format)
945            .unwrap_or_else(|| {
946                log::warn!("wgpu adapter does not support any surface formats with is_srgb={prefer_srgb_format}; defaulting to first format in this list: {:?}", surface_capabilities.formats);
947                surface_capabilities.formats[0]
948            });
949
950        log::info!("Configuring wgpu surface with texture format {surface_format:?}");
951
952        let surface_config = wgpu::SurfaceConfiguration {
953            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
954            format: surface_format,
955            width: window_size.width,
956            height: window_size.height,
957            present_mode,
958            desired_maximum_frame_latency: 1,
959            alpha_mode: surface_capabilities.alpha_modes[0],
960            view_formats: vec![],
961        };
962        surface.configure(&device, &surface_config);
963
964        let device_limits = device.limits();
965        let shaders = Shaders::create(&device);
966
967        #[cfg(feature = "ttf")]
968        let modal_renderer = ttf::ModalRenderer::new(&device, &queue, surface_format);
969
970        Ok(Self {
971            surface,
972            surface_config,
973            surface_capabilities,
974            device,
975            device_limits,
976            queue,
977            shaders,
978            renderer_config: config,
979            pipelines: RenderingPipelines::new(),
980            #[cfg(feature = "ttf")]
981            modal_renderer,
982            frame_count: 0,
983            speed_multiplier: 1,
984            force_sync_off: false,
985            frame_time_tracker: FrameTimeTracker::new(config.frame_time_sync),
986            window,
987            window_size,
988        })
989    }
990}
991
992impl<Window> WgpuRenderer<Window> {
993    pub fn reload_config(&mut self, mut config: RendererConfig) {
994        let prev_surface_config = self.surface_config.clone();
995
996        let present_mode = config.vsync_mode.to_wgpu_present_mode();
997        if present_mode.always_supported()
998            || self.surface_capabilities.present_modes.contains(&present_mode)
999        {
1000            self.surface_config.present_mode = present_mode;
1001        } else {
1002            log::error!(
1003                "wgpu adapter does not support requested present mode '{present_mode:?}' for VSync mode '{:?}'; leaving VSync mode set to '{:?}'",
1004                config.vsync_mode,
1005                self.renderer_config.vsync_mode
1006            );
1007            config.vsync_mode = self.renderer_config.vsync_mode;
1008        }
1009
1010        if !self.frame_time_tracker.sync_enabled && config.frame_time_sync {
1011            // Reset last frame time if frame time sync was just enabled
1012            self.frame_time_tracker.last_frame_time_nanos = timeutils::current_time_nanos();
1013        }
1014        self.frame_time_tracker.sync_enabled = config.frame_time_sync && !self.force_sync_off;
1015
1016        self.renderer_config = config;
1017
1018        // Firefox Nightly on Linux crashes if Surface::configure() is called with an unchanged config
1019        if prev_surface_config != self.surface_config {
1020            self.surface.configure(&self.device, &self.surface_config);
1021        }
1022
1023        // Force render pipeline to be recreated on the next render_frame() call
1024        self.pipelines.clear();
1025    }
1026
1027    pub fn set_force_sync_off(&mut self, force_sync_off: bool) {
1028        let prev_force_sync_off = self.force_sync_off;
1029        self.force_sync_off = force_sync_off;
1030
1031        if prev_force_sync_off != force_sync_off
1032            && self.renderer_config.vsync_mode == VSyncMode::Enabled
1033        {
1034            let desired_present_mode = if force_sync_off {
1035                VSyncMode::Disabled.to_wgpu_present_mode()
1036            } else {
1037                self.renderer_config.vsync_mode.to_wgpu_present_mode()
1038            };
1039            log::debug!("Changing wgpu present mode to {desired_present_mode:?}");
1040
1041            if desired_present_mode.always_supported()
1042                || self.surface_capabilities.present_modes.contains(&desired_present_mode)
1043            {
1044                self.surface.configure(
1045                    &self.device,
1046                    &wgpu::SurfaceConfiguration {
1047                        present_mode: desired_present_mode,
1048                        ..self.surface_config.clone()
1049                    },
1050                );
1051            }
1052        }
1053
1054        self.frame_time_tracker.sync_enabled =
1055            self.renderer_config.frame_time_sync && !force_sync_off;
1056    }
1057
1058    pub fn handle_resize(&mut self, size: WindowSize) {
1059        if self.window_size == size {
1060            // No change
1061            return;
1062        }
1063
1064        self.window_size = size;
1065
1066        self.surface_config.width = size.width;
1067        self.surface_config.height = size.height;
1068        self.surface.configure(&self.device, &self.surface_config);
1069
1070        // Force render pipeline to be recreated on the next render_frame() call
1071        self.pipelines.clear();
1072    }
1073
1074    /// Obtain a shared reference to the window.
1075    pub fn window(&self) -> &Window {
1076        &self.window
1077    }
1078
1079    /// Obtain a mutable reference to the window.
1080    ///
1081    /// # Safety
1082    ///
1083    /// You must not reassign the window. You can freely mutate it and call any methods
1084    /// that require `&mut self`, but you must not do anything that will deallocate the existing
1085    /// window.
1086    pub unsafe fn window_mut(&mut self) -> &mut Window {
1087        &mut self.window
1088    }
1089
1090    /// Set the speed multiplier. For a multiplier of N, only 1 out of every N frames will be rendered.
1091    ///
1092    /// # Panics
1093    ///
1094    /// This method will panic if `speed_multiplier` is 0.
1095    pub fn set_speed_multiplier(&mut self, speed_multiplier: u64) {
1096        assert_ne!(speed_multiplier, 0, "speed multiplier must be non-zero");
1097        self.speed_multiplier = speed_multiplier;
1098    }
1099
1100    pub fn config(&self) -> &RendererConfig {
1101        &self.renderer_config
1102    }
1103
1104    /// Obtain the last rendered frame size and the current display area within the window.
1105    ///
1106    /// May return None if rendering config was just changed or initialized and a frame has not yet been rendered with
1107    /// the new config.
1108    #[must_use]
1109    pub fn current_display_info(&self) -> Option<DisplayInfo> {
1110        self.pipelines.last_display_info
1111    }
1112
1113    pub fn reset_interframe_state(&mut self) {
1114        for pipeline in self.pipelines.pipelines.values_mut() {
1115            for shader in &mut pipeline.shader_pipeline {
1116                shader.reset_interframe_state();
1117            }
1118        }
1119    }
1120
1121    #[cfg(feature = "ttf")]
1122    pub fn add_modal(&mut self, text: String, duration: std::time::Duration) {
1123        self.add_or_update_modal(None, text, duration);
1124    }
1125
1126    #[cfg(feature = "ttf")]
1127    pub fn add_or_update_modal(
1128        &mut self,
1129        id: Option<Cow<'static, str>>,
1130        text: String,
1131        duration: std::time::Duration,
1132    ) {
1133        self.modal_renderer.add_or_update_modal(id, text, duration);
1134    }
1135
1136    pub fn reload(&mut self) {
1137        self.reload_config(self.renderer_config);
1138    }
1139}
1140
1141impl<Window> Renderer for WgpuRenderer<Window> {
1142    type Err = RendererError;
1143
1144    fn render_frame(
1145        &mut self,
1146        frame_buffer: &[Color],
1147        frame_size: FrameSize,
1148        target_fps: f64,
1149        options: RenderFrameOptions,
1150    ) -> Result<(), Self::Err> {
1151        if frame_size.len() > frame_buffer.len() as u32 {
1152            return Err(RendererError::FrameBufferTooSmall {
1153                frame_width: frame_size.width,
1154                frame_height: frame_size.height,
1155                buffer_len: frame_buffer.len(),
1156            });
1157        }
1158
1159        if !target_fps.is_finite() || target_fps <= 0.0 {
1160            return Err(RendererError::InvalidTargetFps(target_fps));
1161        }
1162
1163        self.frame_count += 1;
1164        if !self.frame_count.is_multiple_of(self.speed_multiplier) {
1165            return Ok(());
1166        }
1167
1168        self.frame_time_tracker.set_target_fps(target_fps);
1169
1170        let pipeline = self.pipelines.get_or_insert(frame_size, options, || {
1171            log::info!(
1172                "Creating render pipeline for frame size {frame_size:?} and pixel aspect ratio {}",
1173                pixel_aspect_ratio_display(options.pixel_aspect_ratio)
1174            );
1175
1176            RenderingPipeline::create(
1177                &self.device,
1178                &self.device_limits,
1179                &self.shaders,
1180                self.window_size,
1181                frame_size,
1182                options,
1183                &self.surface_config,
1184                self.renderer_config,
1185            )
1186        });
1187
1188        match pipeline.render(
1189            &self.device,
1190            &self.queue,
1191            &self.surface,
1192            frame_buffer,
1193            options,
1194            #[cfg(feature = "ttf")]
1195            &self.surface_config,
1196            #[cfg(feature = "ttf")]
1197            &mut self.modal_renderer,
1198            &mut self.frame_time_tracker,
1199        ) {
1200            Ok(RenderResult::None) => {}
1201            Ok(RenderResult::SuboptimalSurface) => {
1202                log::debug!("Reconfiguring surface because graphics API reported it as suboptimal");
1203                self.surface.configure(&self.device, &self.surface_config);
1204            }
1205            Err(RendererError::WgpuSurfaceTimeout) => {
1206                log::warn!("Skipping frame because wgpu surface timed out");
1207                self.surface.configure(&self.device, &self.surface_config);
1208            }
1209            Err(RendererError::WgpuSurfaceOccluded) => {
1210                log::debug!("Skipping frame because surface is occluded");
1211            }
1212            Err(err) => return Err(err),
1213        }
1214
1215        Ok(())
1216    }
1217}
1218
1219fn pixel_aspect_ratio_display(par: Option<FiniteF64>) -> Cow<'static, str> {
1220    par.map_or("None".into(), |par| par.to_string().into())
1221}