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;
Texture usages that some shaders require on input texture
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 ¤t_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 ¤t_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 ¤t_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 ¤t_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 ¤t_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, ¤t_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, ¤t_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 ¤t_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> {
Construct a wgpu renderer from the given window and config.
Errors
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}
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 }
Obtain a shared reference to the window.
Obtain a mutable reference to the window.
Safety
You must not reassign the window. You can freely mutate it and call any methods
that require &mut self, but you must not do anything that will deallocate the existing
window.
Set the speed multiplier. For a multiplier of N, only 1 out of every N frames will be rendered.
Panics
This method will panic if speed_multiplier is 0.
Obtain the last rendered frame size and the current display area within the window.
May return None if rendering config was just changed or initialized and a frame has not yet been rendered with the new config.
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}