use crate::descriptors::DescriptorsTargetData; use crate::frame::Frame; use crate::target::RenderTargetFrame; use crate::target::TextureTarget; use crate::uniform_buffer::BufferStorage; use crate::{ create_buffer_with_data, format_list, get_backend_names, target, BufferDimensions, Descriptors, Draw, DrawType, Error, Globals, GradientStorage, GradientUniforms, Mesh, RegistryData, RenderTarget, SwapChainTarget, Texture, TextureOffscreen, TextureTransforms, Transforms, UniformBuffer, Vertex, }; use fnv::FnvHashMap; use ruffle_render::backend::{RenderBackend, ShapeHandle, ViewportDimensions}; use ruffle_render::bitmap::{Bitmap, BitmapHandle, BitmapSource}; use ruffle_render::commands::CommandList; use ruffle_render::error::Error as BitmapError; use ruffle_render::shape_utils::DistilledShape; use ruffle_render::tessellator::{DrawType as TessDrawType, ShapeTessellator}; use std::num::NonZeroU32; use std::path::Path; use std::sync::Arc; use swf::Color; const DEFAULT_SAMPLE_COUNT: u32 = 4; pub struct WgpuRenderBackend { descriptors: Arc, globals: Globals, uniform_buffers_storage: BufferStorage, target: T, frame_buffer_view: Option, depth_texture_view: wgpu::TextureView, copy_srgb_view: Option, copy_srgb_bind_group: Option, meshes: Vec, shape_tessellator: ShapeTessellator, quad_vbo: wgpu::Buffer, quad_ibo: wgpu::Buffer, quad_tex_transforms: wgpu::Buffer, bitmap_registry: FnvHashMap, next_bitmap_handle: BitmapHandle, // This is currently unused - we just store it to report in // `get_viewport_dimensions` viewport_scale_factor: f64, offscreen: bool, } impl WgpuRenderBackend { #[cfg(target_family = "wasm")] pub async fn for_canvas(canvas: &web_sys::HtmlCanvasElement) -> Result { let instance = wgpu::Instance::new(wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL); let surface = instance.create_surface_from_canvas(canvas); let descriptors = Self::build_descriptors( wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, instance, Some(&surface), wgpu::PowerPreference::HighPerformance, None, ) .await?; let target = SwapChainTarget::new( surface, descriptors.onscreen.surface_format, (1, 1), &descriptors.device, ); Self::new(Arc::new(descriptors), target) } #[cfg(not(target_family = "wasm"))] pub fn for_window( window: &W, size: (u32, u32), backend: wgpu::Backends, power_preference: wgpu::PowerPreference, trace_path: Option<&Path>, ) -> Result { if wgpu::Backends::SECONDARY.contains(backend) { log::warn!( "{} graphics backend support may not be fully supported.", format_list(&get_backend_names(backend), "and") ); } let instance = wgpu::Instance::new(backend); let surface = unsafe { instance.create_surface(window) }; let descriptors = futures::executor::block_on(Self::build_descriptors( backend, instance, Some(&surface), power_preference, trace_path, ))?; let target = SwapChainTarget::new( surface, descriptors.onscreen.surface_format, size, &descriptors.device, ); Self::new(Arc::new(descriptors), target) } } #[cfg(not(target_family = "wasm"))] impl WgpuRenderBackend { pub fn for_offscreen( size: (u32, u32), backend: wgpu::Backends, power_preference: wgpu::PowerPreference, trace_path: Option<&Path>, ) -> Result { if wgpu::Backends::SECONDARY.contains(backend) { log::warn!( "{} graphics backend support may not be fully supported.", format_list(&get_backend_names(backend), "and") ); } let instance = wgpu::Instance::new(backend); let descriptors = futures::executor::block_on(Self::build_descriptors( backend, instance, None, power_preference, trace_path, ))?; let target = target::TextureTarget::new(&descriptors.device, size)?; Self::new(Arc::new(descriptors), target) } pub fn capture_frame(&self, premultiplied_alpha: bool) -> Option { self.target .capture(&self.descriptors.device, premultiplied_alpha) } } #[macro_export] macro_rules! target_data { ($this:expr) => {{ if $this.offscreen { &$this.descriptors.offscreen } else { &$this.descriptors.onscreen } }}; } impl WgpuRenderBackend { pub fn new(descriptors: Arc, target: T) -> Result { if target.width() > descriptors.limits.max_texture_dimension_2d || target.height() > descriptors.limits.max_texture_dimension_2d { return Err(format!( "Render target texture cannot be larger than {}px on either dimension (requested {} x {})", descriptors.limits.max_texture_dimension_2d, target.width(), target.height() ) .into()); } let extent = wgpu::Extent3d { width: target.width(), height: target.height(), depth_or_array_layers: 1, }; let frame_buffer_view = if descriptors.onscreen.msaa_sample_count > 1 { let frame_buffer = descriptors.device.create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Framebuffer texture").as_deref(), size: extent, mip_level_count: 1, sample_count: descriptors.onscreen.msaa_sample_count, dimension: wgpu::TextureDimension::D2, format: descriptors.onscreen.frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); Some(frame_buffer.create_view(&Default::default())) } else { None }; let depth_texture = descriptors.device.create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Depth texture").as_deref(), size: extent, mip_level_count: 1, sample_count: descriptors.onscreen.msaa_sample_count, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth24PlusStencil8, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); let depth_texture_view = depth_texture.create_view(&Default::default()); let (quad_vbo, quad_ibo, quad_tex_transforms) = create_quad_buffers(&descriptors.device); let (copy_srgb_view, copy_srgb_bind_group) = if descriptors.onscreen.frame_buffer_format != descriptors.offscreen.surface_format { let copy_srgb_buffer = descriptors.device.create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Copy sRGB framebuffer texture").as_deref(), size: extent, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: descriptors.onscreen.frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, }); let copy_srgb_view = copy_srgb_buffer.create_view(&Default::default()); let copy_srgb_bind_group = descriptors .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &descriptors.onscreen.pipelines.bitmap_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &quad_tex_transforms, offset: 0, size: wgpu::BufferSize::new( std::mem::size_of::() as u64, ), }), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(©_srgb_view), }, ], label: create_debug_label!("Copy sRGB bind group").as_deref(), }); (Some(copy_srgb_view), Some(copy_srgb_bind_group)) } else { (None, None) }; let mut globals = Globals::new(&descriptors.device, &descriptors.globals_layout); globals.set_resolution(target.width(), target.height()); let uniform_buffers_storage = BufferStorage::new(descriptors.limits.min_uniform_buffer_offset_alignment); Ok(Self { descriptors, globals, uniform_buffers_storage, target, frame_buffer_view, depth_texture_view, copy_srgb_view, copy_srgb_bind_group, meshes: Vec::new(), shape_tessellator: ShapeTessellator::new(), quad_vbo, quad_ibo, quad_tex_transforms, bitmap_registry: Default::default(), next_bitmap_handle: BitmapHandle(0), viewport_scale_factor: 1.0, offscreen: false, }) } pub async fn build_descriptors( backend: wgpu::Backends, instance: wgpu::Instance, surface: Option<&wgpu::Surface>, power_preference: wgpu::PowerPreference, trace_path: Option<&Path>, ) -> Result { let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference, compatible_surface: surface, force_fallback_adapter: false, }).await .ok_or_else(|| { let names = get_backend_names(backend); if names.is_empty() { "Ruffle requires hardware acceleration, but no compatible graphics device was found (no backend provided?)".to_string() } else if cfg!(any(windows, target_os = "macos")) { format!("Ruffle does not support OpenGL on {}.", if cfg!(windows) { "Windows" } else { "macOS" }) } else { format!("Ruffle requires hardware acceleration, but no compatible graphics device was found supporting {}", format_list(&names, "or")) } })?; let (device, queue) = request_device(&adapter, trace_path).await?; let info = adapter.get_info(); // Ideally we want to use an RGBA non-sRGB surface format, because Flash colors and // blending are done in sRGB space -- we don't want the GPU to adjust the colors. // Some platforms may only support an sRGB surface, in which case we will draw to an // intermediate linear buffer and then copy to the sRGB surface. let surface_format = surface .and_then(|surface| { let formats = surface.get_supported_formats(&adapter); formats .iter() .find(|format| { matches!( format, wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm ) }) .or_else(|| formats.first()) .cloned() }) // No surface (rendering to texture), default to linear RBGA. .unwrap_or(wgpu::TextureFormat::Rgba8Unorm); // TODO: Allow the sample count to be set from command line/settings file. Ok(Descriptors::new( device, queue, info, surface_format, DEFAULT_SAMPLE_COUNT, )) } fn register_shape_internal( &mut self, shape: DistilledShape, bitmap_source: &dyn BitmapSource, ) -> Mesh { let shape_id = shape.id; // TODO: remove? let lyon_mesh = self .shape_tessellator .tessellate_shape(shape, bitmap_source); let mut draws = Vec::with_capacity(lyon_mesh.len()); for draw in lyon_mesh { let vertices: Vec<_> = draw.vertices.into_iter().map(Vertex::from).collect(); let vertex_buffer = create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&vertices), wgpu::BufferUsages::VERTEX, create_debug_label!("Shape {} ({}) vbo", shape_id, draw.draw_type.name()), ); let index_buffer = create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&draw.indices), wgpu::BufferUsages::INDEX, create_debug_label!("Shape {} ({}) ibo", shape_id, draw.draw_type.name()), ); let index_count = draw.indices.len() as u32; let draw_id = draws.len(); draws.push(match draw.draw_type { TessDrawType::Color => Draw { draw_type: DrawType::Color, vertex_buffer, index_buffer, num_indices: index_count, num_mask_indices: draw.mask_index_count, }, TessDrawType::Gradient(gradient) => { // TODO: Extract to function? let mut texture_transform = [[0.0; 4]; 4]; texture_transform[0][..3].copy_from_slice(&gradient.matrix[0]); texture_transform[1][..3].copy_from_slice(&gradient.matrix[1]); texture_transform[2][..3].copy_from_slice(&gradient.matrix[2]); let tex_transforms_ubo = create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&[texture_transform]), wgpu::BufferUsages::UNIFORM, create_debug_label!( "Shape {} draw {} textransforms ubo transfer buffer", shape_id, draw_id ), ); let (gradient_ubo, buffer_size) = if self .descriptors .limits .max_storage_buffers_per_shader_stage > 0 { ( create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&[GradientStorage::from(gradient)]), wgpu::BufferUsages::STORAGE, create_debug_label!( "Shape {} draw {} gradient ubo transfer buffer", shape_id, draw_id ), ), wgpu::BufferSize::new(std::mem::size_of::() as u64), ) } else { ( create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&[GradientUniforms::from(gradient)]), wgpu::BufferUsages::UNIFORM, create_debug_label!( "Shape {} draw {} gradient ubo transfer buffer", shape_id, draw_id ), ), wgpu::BufferSize::new(std::mem::size_of::() as u64), ) }; let bind_group_label = create_debug_label!( "Shape {} (gradient) draw {} bindgroup", shape_id, draw_id ); let bind_group = self.descriptors .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &target_data!(self).pipelines.gradient_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer( wgpu::BufferBinding { buffer: &tex_transforms_ubo, offset: 0, size: wgpu::BufferSize::new(std::mem::size_of::< TextureTransforms, >( ) as u64), }, ), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Buffer( wgpu::BufferBinding { buffer: &gradient_ubo, offset: 0, size: buffer_size, }, ), }, ], label: bind_group_label.as_deref(), }); Draw { draw_type: DrawType::Gradient { texture_transforms: tex_transforms_ubo, gradient: gradient_ubo, bind_group, }, vertex_buffer, index_buffer, num_indices: index_count, num_mask_indices: draw.mask_index_count, } } TessDrawType::Bitmap(bitmap) => { let entry = self.bitmap_registry.get(&bitmap.bitmap).unwrap(); let texture_view = entry .texture_wrapper .texture .create_view(&Default::default()); // TODO: Extract to function? let mut texture_transform = [[0.0; 4]; 4]; texture_transform[0][..3].copy_from_slice(&bitmap.matrix[0]); texture_transform[1][..3].copy_from_slice(&bitmap.matrix[1]); texture_transform[2][..3].copy_from_slice(&bitmap.matrix[2]); let tex_transforms_ubo = create_buffer_with_data( &self.descriptors.device, bytemuck::cast_slice(&[texture_transform]), wgpu::BufferUsages::UNIFORM, create_debug_label!( "Shape {} draw {} textransforms ubo transfer buffer", shape_id, draw_id ), ); let bind_group_label = create_debug_label!( "Shape {} (bitmap) draw {} bindgroup", shape_id, draw_id ); let bind_group = self.descriptors .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &target_data!(self).pipelines.bitmap_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer( wgpu::BufferBinding { buffer: &tex_transforms_ubo, offset: 0, size: wgpu::BufferSize::new(std::mem::size_of::< TextureTransforms, >( ) as u64), }, ), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&texture_view), }, ], label: bind_group_label.as_deref(), }); Draw { draw_type: DrawType::Bitmap { texture_transforms: tex_transforms_ubo, texture_view, is_smoothed: bitmap.is_smoothed, is_repeating: bitmap.is_repeating, bind_group, }, vertex_buffer, index_buffer, num_indices: index_count, num_mask_indices: draw.mask_index_count, } } }); } Mesh { draws } } pub fn descriptors(&self) -> &Arc { &self.descriptors } pub fn target(&self) -> &T { &self.target } pub fn device(&self) -> &wgpu::Device { &self.descriptors.device } fn render_offscreen_internal( mut self, handle: BitmapHandle, width: u32, height: u32, commands: CommandList, clear_color: Color, ) -> Result<(Self, Bitmap), ruffle_render::error::Error> { // We need ownership of `Texture` to access the non-`Clone` // `wgpu` fields. At the end of this method, we re-insert // `texture` into the map. // // This means that the target texture will be inaccessible // while the callback `f` is a problem. This would only be // an issue if a caller tried to render the target texture // to itself, which probably isn't supported by Flash. If it // is, then we could change `TextureTarget` to use an `Rc` let mut texture = self.bitmap_registry.remove(&handle).unwrap(); let extent = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; if self.offscreen { panic!("Nested render_onto_bitmap is not supported!") } let descriptors = self.descriptors.clone(); // We will (presumably) never render to the majority of textures, so // we lazily create the buffer and depth texture. // Once created, we never destroy this data, under the assumption // that the SWF will try to render to this more than once. // // If we end up hitting wgpu device limits due to having too // many buffers / depth textures rendered at once, we could // try storing this data in an LRU cache, evicting entries // as needed. let mut texture_offscreen = texture .texture_wrapper .texture_offscreen .unwrap_or_else(|| { let depth_texture_view = create_depth_texture_view(&descriptors, &descriptors.offscreen, extent); let buffer_dimensions = BufferDimensions::new(width as usize, height as usize); let buffer_label = create_debug_label!("Render target buffer"); let buffer = descriptors.device.create_buffer(&wgpu::BufferDescriptor { label: buffer_label.as_deref(), size: (buffer_dimensions.padded_bytes_per_row.get() as u64 * buffer_dimensions.height as u64), usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); TextureOffscreen { depth_texture_view, buffer, buffer_dimensions, } }); let target = TextureTarget { size: extent, texture: texture.texture_wrapper.texture, format: wgpu::TextureFormat::Rgba8Unorm, buffer: texture_offscreen.buffer, buffer_dimensions: texture_offscreen.buffer_dimensions, }; let (old_width, old_height) = self.globals.resolution(); // Is it worth caching this? self.globals.set_resolution(width, height); let mut texture_backend = WgpuRenderBackend { descriptors, target, // FIXME - Enable MSAA for textures frame_buffer_view: None, depth_texture_view: texture_offscreen.depth_texture_view, // We explicitly request a non-SRGB format for textures copy_srgb_view: None, copy_srgb_bind_group: None, meshes: self.meshes, shape_tessellator: self.shape_tessellator, quad_vbo: self.quad_vbo, quad_ibo: self.quad_ibo, quad_tex_transforms: self.quad_tex_transforms, bitmap_registry: self.bitmap_registry, offscreen: true, next_bitmap_handle: self.next_bitmap_handle, globals: self.globals, uniform_buffers_storage: self.uniform_buffers_storage, viewport_scale_factor: self.viewport_scale_factor, }; texture_backend.submit_frame(clear_color, commands); // Capture with premultiplied alpha, which is what we use for all textures let image = texture_backend .target .capture(&texture_backend.descriptors.device, true); let image = image.map(|image| { Bitmap::new( image.dimensions().0, image.dimensions().1, ruffle_render::bitmap::BitmapFormat::Rgba, image.into_raw(), ) }); self.offscreen = false; self.meshes = texture_backend.meshes; self.shape_tessellator = texture_backend.shape_tessellator; self.bitmap_registry = texture_backend.bitmap_registry; self.quad_tex_transforms = texture_backend.quad_tex_transforms; self.quad_ibo = texture_backend.quad_ibo; self.quad_vbo = texture_backend.quad_vbo; self.globals = texture_backend.globals; self.uniform_buffers_storage = texture_backend.uniform_buffers_storage; self.next_bitmap_handle = texture_backend.next_bitmap_handle; self.globals.set_resolution(old_width, old_height); texture_offscreen.buffer = texture_backend.target.buffer; texture_offscreen.buffer_dimensions = texture_backend.target.buffer_dimensions; texture_offscreen.depth_texture_view = texture_backend.depth_texture_view; texture.texture_wrapper.texture_offscreen = Some(texture_offscreen); texture.texture_wrapper.texture = texture_backend.target.texture; self.bitmap_registry.insert(handle, texture); Ok((self, image.unwrap())) } } impl RenderBackend for WgpuRenderBackend { fn set_viewport_dimensions(&mut self, dimensions: ViewportDimensions) { // Avoid panics from creating 0-sized framebuffers. // TODO: find a way to bubble an error when the size is too large let width = std::cmp::max( std::cmp::min( dimensions.width, self.descriptors.limits.max_texture_dimension_2d, ), 1, ); let height = std::cmp::max( std::cmp::min( dimensions.height, self.descriptors.limits.max_texture_dimension_2d, ), 1, ); self.target.resize(&self.descriptors.device, width, height); let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; self.frame_buffer_view = if target_data!(self).msaa_sample_count > 1 { let frame_buffer = self .descriptors .device .create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Framebuffer texture").as_deref(), size, mip_level_count: 1, sample_count: target_data!(self).msaa_sample_count, dimension: wgpu::TextureDimension::D2, format: target_data!(self).frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); Some(frame_buffer.create_view(&Default::default())) } else { None }; let depth_texture = self .descriptors .device .create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Depth texture").as_deref(), size, mip_level_count: 1, sample_count: target_data!(self).msaa_sample_count, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth24PlusStencil8, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); self.depth_texture_view = depth_texture.create_view(&Default::default()); (self.copy_srgb_view, self.copy_srgb_bind_group) = if target_data!(self).frame_buffer_format != target_data!(self).surface_format { let copy_srgb_buffer = self.descriptors .device .create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Copy sRGB framebuffer texture").as_deref(), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: target_data!(self).frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, }); let copy_srgb_view = copy_srgb_buffer.create_view(&Default::default()); let copy_srgb_bind_group = self.descriptors .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &target_data!(self).pipelines.bitmap_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &self.quad_tex_transforms, offset: 0, size: wgpu::BufferSize::new( std::mem::size_of::() as u64, ), }), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(©_srgb_view), }, ], label: create_debug_label!("Copy sRGB bind group").as_deref(), }); (Some(copy_srgb_view), Some(copy_srgb_bind_group)) } else { (None, None) }; self.globals.set_resolution(width, height); self.viewport_scale_factor = dimensions.scale_factor; } fn viewport_dimensions(&self) -> ViewportDimensions { ViewportDimensions { width: self.target.width(), height: self.target.height(), scale_factor: self.viewport_scale_factor, } } fn register_shape( &mut self, shape: DistilledShape, bitmap_source: &dyn BitmapSource, ) -> ShapeHandle { let handle = ShapeHandle(self.meshes.len()); let mesh = self.register_shape_internal(shape, bitmap_source); self.meshes.push(mesh); handle } fn replace_shape( &mut self, shape: DistilledShape, bitmap_source: &dyn BitmapSource, handle: ShapeHandle, ) { let mesh = self.register_shape_internal(shape, bitmap_source); self.meshes[handle.0] = mesh; } fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { let shape = ruffle_render::shape_utils::swf_glyph_to_shape(glyph); let handle = ShapeHandle(self.meshes.len()); let mesh = self.register_shape_internal( (&shape).into(), &ruffle_render::backend::null::NullBitmapSource, ); self.meshes.push(mesh); handle } fn submit_frame(&mut self, clear: Color, commands: CommandList) { let frame_output = match self.target.get_next_texture() { Ok(frame) => frame, Err(e) => { log::warn!("Couldn't begin new render frame: {}", e); // Attempt to recreate the swap chain in this case. self.target.resize( &self.descriptors.device, self.target.width(), self.target.height(), ); return; } }; let label = create_debug_label!("Draw encoder"); let mut draw_encoder = self.descriptors .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: label.as_deref(), }); let uniform_encoder_label = create_debug_label!("Uniform upload command encoder"); let mut uniform_encoder = self.descriptors .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: uniform_encoder_label.as_deref(), }); self.globals .update_uniform(&self.descriptors.device, &mut draw_encoder); // Use intermediate render targets when resolving MSAA or copying from linear-to-sRGB texture. let (color_view, resolve_target) = match (&self.frame_buffer_view, &self.copy_srgb_view) { (None, None) => (frame_output.view(), None), (None, Some(copy)) => (copy, None), (Some(frame_buffer), None) => (frame_buffer, Some(frame_output.view())), (Some(frame_buffer), Some(copy)) => (frame_buffer, Some(copy)), }; let render_pass = draw_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: color_view, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: f64::from(clear.r) / 255.0, g: f64::from(clear.g) / 255.0, b: f64::from(clear.b) / 255.0, a: f64::from(clear.a) / 255.0, }), store: true, }, resolve_target, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &self.depth_texture_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), store: false, }), stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: true, }), }), label: None, }); let mut frame = Frame::new( &self.descriptors, &self.globals, UniformBuffer::new(&mut self.uniform_buffers_storage), &frame_output, &self.quad_vbo, &self.quad_ibo, &self.meshes, render_pass, &mut uniform_encoder, &self.bitmap_registry, self.offscreen, ); commands.execute(&mut frame); // If we have an sRGB surface, copy from our linear intermediate buffer to the sRGB surface. let copy_encoder = if let Some(copy_srgb_bind_group) = &self.copy_srgb_bind_group { debug_assert!(self.copy_srgb_view.is_some()); Some(frame.swap_srgb( copy_srgb_bind_group, self.target.width() as f32, self.target.height() as f32, )) } else { None }; frame.finish(); let mut command_buffers = vec![uniform_encoder.finish(), draw_encoder.finish()]; if let Some(copy_encoder) = copy_encoder { command_buffers.push(copy_encoder); } self.target.submit( &self.descriptors.device, &self.descriptors.queue, command_buffers, frame_output, ); } fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> Option { self.bitmap_registry.get(&bitmap).map(|e| e.bitmap.clone()) } fn register_bitmap(&mut self, bitmap: Bitmap) -> Result { if bitmap.width() > self.descriptors.limits.max_texture_dimension_2d || bitmap.height() > self.descriptors.limits.max_texture_dimension_2d { return Err(BitmapError::TooLarge); } let bitmap = bitmap.to_rgba(); let extent = wgpu::Extent3d { width: bitmap.width(), height: bitmap.height(), depth_or_array_layers: 1, }; let texture_label = create_debug_label!("Bitmap"); let texture = self .descriptors .device .create_texture(&wgpu::TextureDescriptor { label: texture_label.as_deref(), size: extent, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, }); self.descriptors.queue.write_texture( wgpu::ImageCopyTexture { texture: &texture, mip_level: 0, origin: Default::default(), aspect: wgpu::TextureAspect::All, }, bitmap.data(), wgpu::ImageDataLayout { offset: 0, bytes_per_row: NonZeroU32::new(4 * extent.width), rows_per_image: None, }, extent, ); let handle = self.next_bitmap_handle; self.next_bitmap_handle = BitmapHandle(self.next_bitmap_handle.0 + 1); let width = bitmap.width(); let height = bitmap.height(); // Make bind group for bitmap quad. let texture_view = texture.create_view(&Default::default()); let bind_group = self .descriptors .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &target_data!(self).pipelines.bitmap_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &self.quad_tex_transforms, offset: 0, size: wgpu::BufferSize::new( std::mem::size_of::() as u64 ), }), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&texture_view), }, ], label: create_debug_label!("Bitmap {} bind group", handle.0).as_deref(), }); if self .bitmap_registry .insert( handle, RegistryData { bitmap, texture_wrapper: Texture { width, height, texture, bind_group, texture_offscreen: None, }, }, ) .is_some() { panic!("Overwrote existing bitmap {:?}", handle); } Ok(handle) } fn unregister_bitmap(&mut self, handle: BitmapHandle) { self.bitmap_registry.remove(&handle); } fn update_texture( &mut self, handle: BitmapHandle, width: u32, height: u32, rgba: Vec, ) -> Result { let texture = if let Some(entry) = self.bitmap_registry.get(&handle) { &entry.texture_wrapper.texture } else { log::warn!("Tried to replace nonexistent texture"); return Ok(handle); }; let extent = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; self.descriptors.queue.write_texture( wgpu::ImageCopyTexture { texture, mip_level: 0, origin: Default::default(), aspect: wgpu::TextureAspect::All, }, &rgba, wgpu::ImageDataLayout { offset: 0, bytes_per_row: NonZeroU32::new(4 * extent.width), rows_per_image: None, }, extent, ); Ok(handle) } fn render_offscreen( &mut self, handle: BitmapHandle, width: u32, height: u32, commands: CommandList, clear_color: Color, ) -> Result { // Rendering to a texture backend requires us to use non-`Clone` // wgpu resources (e.g. `wgpu::Device`, `wgpu::Queue`. // // We expect that in the majority of SWFs, we will spend most // of our time performing 'normal' (non-offscreen) renders. // Therefore, we want to avoid penalizing this case by adding // in a check for 'normal' or 'offscreen' mode in the main // rendering code. // // To accomplish this, we use `take_mut` to temporarily // move out of `self`. This allows us to construct a new // `WgpuRenderBackend` with a `TextureTarget` corresponding to // `handle`. This allows us to re-use many of the fields from // our normal `WgpuRenderBackend` without wrapping in an `Rc` // or other indirection. // // Note that `take_mut` causes the process to abort if the // `with_offscreen_render_backend_internal` panics, since // the `&mut self` reference would be logically uninitialized. // However, we normally compile Ruffle with `panic=abort`, // so this shouldn't actually have an effect in practice. // Even with `panic=unwind`, we would still get a backtrace // printed, and there's not really much point in attempting // to recover from a partially failed render operation, anyway. Ok(take_mut(self, |this| { this.render_offscreen_internal(handle, width, height, commands, clear_color) .expect("Failed to render to offscreen backend") })) } } fn create_quad_buffers(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer, wgpu::Buffer) { let vertices = [ Vertex { position: [0.0, 0.0], color: [1.0, 1.0, 1.0, 1.0], }, Vertex { position: [1.0, 0.0], color: [1.0, 1.0, 1.0, 1.0], }, Vertex { position: [1.0, 1.0], color: [1.0, 1.0, 1.0, 1.0], }, Vertex { position: [0.0, 1.0], color: [1.0, 1.0, 1.0, 1.0], }, ]; let indices: [u32; 6] = [0, 1, 2, 0, 2, 3]; let vbo = create_buffer_with_data( device, bytemuck::cast_slice(&vertices), wgpu::BufferUsages::VERTEX, create_debug_label!("Quad vbo"), ); let ibo = create_buffer_with_data( device, bytemuck::cast_slice(&indices), wgpu::BufferUsages::INDEX, create_debug_label!("Quad ibo"), ); let tex_transforms = create_buffer_with_data( device, bytemuck::cast_slice(&[TextureTransforms { u_matrix: [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ], }]), wgpu::BufferUsages::UNIFORM, create_debug_label!("Quad tex transforms"), ); (vbo, ibo, tex_transforms) } fn create_depth_texture_view( descriptors: &Descriptors, target_data: &DescriptorsTargetData, extent: wgpu::Extent3d, ) -> wgpu::TextureView { let depth_texture = descriptors.device.create_texture(&wgpu::TextureDescriptor { label: create_debug_label!("Depth texture").as_deref(), size: extent, mip_level_count: 1, sample_count: target_data.msaa_sample_count, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth24PlusStencil8, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); depth_texture.create_view(&Default::default()) } // Based on https://github.com/Sgeo/take_mut fn take_mut(mut_ref: &mut T, closure: F) -> R where F: FnOnce(T) -> (T, R), { unsafe { let old_t = std::ptr::read(mut_ref); let (new_t, ret) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| closure(old_t))) .unwrap_or_else(|e| { eprintln!("Caught panic: {:?}", e); ::std::process::abort() }); std::ptr::write(mut_ref, new_t); ret } } // We try to request the highest limits we can get away with async fn request_device( adapter: &wgpu::Adapter, trace_path: Option<&Path>, ) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { // We start off with the lowest limits we actually need - basically GL-ES 3.0 let mut limits = wgpu::Limits::downlevel_webgl2_defaults(); // Then we increase parts of it to the maximum supported by the adapter, to take advantage of // more powerful hardware or capabilities limits = limits.using_resolution(adapter.limits()); limits = limits.using_alignment(adapter.limits()); limits.max_storage_buffers_per_shader_stage = adapter.limits().max_storage_buffers_per_shader_stage; limits.max_storage_buffer_binding_size = adapter.limits().max_storage_buffer_binding_size; adapter .request_device( &wgpu::DeviceDescriptor { label: None, features: wgpu::Features::empty(), limits, }, trace_path, ) .await }