From 55fe7ac8b8fa4a067b5e4a4110af756d450073ea Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Tue, 13 Jun 2023 18:14:03 -0500 Subject: [PATCH] wgpu: Cache Program3D bind group layout The bind group layout only depends on the texture registers (and 2D/cubemap type) accessed by the fragment shader, not on the runtime texture bound with Context3D. This means that we can build and cache it when we compile the AGAL program to a Naga module. Since the bind group layout is used for the overall pipeline, I've refactored the shader caching code into `ShaderPairAgal`, which holds both the vertex and fragment shader bytecode, and compiles both in the `compile` function. --- core/src/avm2/object/context3d_object.rs | 20 +- core/src/avm2/object/program_3d_object.rs | 15 +- render/src/backend.rs | 6 +- render/wgpu/src/context3d/current_pipeline.rs | 249 ++++++------------ render/wgpu/src/context3d/mod.rs | 92 ++----- render/wgpu/src/context3d/shader_pair.rs | 209 +++++++++++++++ 6 files changed, 310 insertions(+), 281 deletions(-) create mode 100644 render/wgpu/src/context3d/shader_pair.rs diff --git a/core/src/avm2/object/context3d_object.rs b/core/src/avm2/object/context3d_object.rs index 78254e02bdab..8cda2811da0e 100644 --- a/core/src/avm2/object/context3d_object.rs +++ b/core/src/avm2/object/context3d_object.rs @@ -249,9 +249,8 @@ impl<'gc> Context3DObject<'gc> { .unwrap() .process_command( Context3DCommand::UploadShaders { - vertex_shader: program.vertex_shader_handle(), + module: program.shader_module_handle(), vertex_shader_agal, - fragment_shader: program.fragment_shader_handle(), fragment_shader_agal, }, activation.context.gc_context, @@ -263,15 +262,9 @@ impl<'gc> Context3DObject<'gc> { activation: &mut Activation<'_, 'gc>, program: Option>, ) { - let (vertex_shader, fragment_shader) = match program { - Some(program) => ( - program.vertex_shader_handle(), - program.fragment_shader_handle(), - ), - None => ( - GcCell::allocate(activation.context.gc_context, None), - GcCell::allocate(activation.context.gc_context, None), - ), + let module = match program { + Some(program) => program.shader_module_handle(), + None => GcCell::allocate(activation.context.gc_context, None), }; self.0 @@ -280,10 +273,7 @@ impl<'gc> Context3DObject<'gc> { .as_mut() .unwrap() .process_command( - Context3DCommand::SetShaders { - vertex_shader, - fragment_shader, - }, + Context3DCommand::SetShaders { module }, activation.context.gc_context, ); } diff --git a/core/src/avm2/object/program_3d_object.rs b/core/src/avm2/object/program_3d_object.rs index c0dd181676c2..a0df825e2ea3 100644 --- a/core/src/avm2/object/program_3d_object.rs +++ b/core/src/avm2/object/program_3d_object.rs @@ -33,8 +33,7 @@ impl<'gc> Program3DObject<'gc> { Program3DObjectData { base, context3d, - vertex_shader_handle: GcCell::allocate(activation.context.gc_context, None), - fragment_shader_handle: GcCell::allocate(activation.context.gc_context, None), + shader_module_handle: GcCell::allocate(activation.context.gc_context, None), }, )) .into(); @@ -45,12 +44,8 @@ impl<'gc> Program3DObject<'gc> { Ok(this) } - pub fn vertex_shader_handle(&self) -> GcCell<'gc, Option>> { - self.0.read().vertex_shader_handle - } - - pub fn fragment_shader_handle(&self) -> GcCell<'gc, Option>> { - self.0.read().fragment_shader_handle + pub fn shader_module_handle(&self) -> GcCell<'gc, Option>> { + self.0.read().shader_module_handle } pub fn context3d(&self) -> Context3DObject<'gc> { @@ -66,9 +61,7 @@ pub struct Program3DObjectData<'gc> { context3d: Context3DObject<'gc>, - vertex_shader_handle: GcCell<'gc, Option>>, - - fragment_shader_handle: GcCell<'gc, Option>>, + shader_module_handle: GcCell<'gc, Option>>, } impl<'gc> TObject<'gc> for Program3DObject<'gc> { diff --git a/render/src/backend.rs b/render/src/backend.rs index 463e1bc22382..fccf886527e8 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -429,15 +429,13 @@ pub enum Context3DCommand<'a, 'gc> { }, UploadShaders { - vertex_shader: GcCell<'gc, Option>>, + module: GcCell<'gc, Option>>, vertex_shader_agal: Vec, - fragment_shader: GcCell<'gc, Option>>, fragment_shader_agal: Vec, }, SetShaders { - vertex_shader: GcCell<'gc, Option>>, - fragment_shader: GcCell<'gc, Option>>, + module: GcCell<'gc, Option>>, }, SetProgramConstantsFromVector { program_type: ProgramType, diff --git a/render/wgpu/src/context3d/current_pipeline.rs b/render/wgpu/src/context3d/current_pipeline.rs index e5740951ce7b..ac6187a938ae 100644 --- a/render/wgpu/src/context3d/current_pipeline.rs +++ b/render/wgpu/src/context3d/current_pipeline.rs @@ -6,20 +6,21 @@ use ruffle_render::backend::{ }; use wgpu::{ - BindGroupEntry, BindingResource, BufferDescriptor, BufferUsages, FrontFace, SamplerBindingType, - TextureView, + BindGroupEntry, BindingResource, BufferDescriptor, BufferUsages, FrontFace, TextureView, }; use wgpu::{Buffer, DepthStencilState, StencilFaceState}; use wgpu::{ColorTargetState, RenderPipelineDescriptor, TextureFormat, VertexState}; use std::cell::Cell; +use std::hash::{Hash, Hasher}; use std::num::NonZeroU64; use std::rc::Rc; -use crate::context3d::{ShaderCompileData, VertexBufferWrapper}; +use crate::context3d::shader_pair::ShaderCompileData; +use crate::context3d::VertexBufferWrapper; use crate::descriptors::Descriptors; -use super::{ShaderModuleAgal, VertexAttributeInfo, MAX_VERTEX_ATTRIBUTES}; +use super::{ShaderPairAgal, VertexAttributeInfo, MAX_VERTEX_ATTRIBUTES}; const AGAL_NUM_VERTEX_CONSTANTS: u64 = 128; const AGAL_NUM_FRAGMENT_CONSTANTS: u64 = 28; @@ -30,16 +31,16 @@ const VERTEX_SHADER_UNIFORMS_BUFFER_SIZE: u64 = const FRAGMENT_SHADER_UNIFORMS_BUFFER_SIZE: u64 = AGAL_NUM_FRAGMENT_CONSTANTS * AGAL_FLOATS_PER_REGISTER * std::mem::size_of::() as u64; -const SAMPLER_REPEAT_LINEAR: u32 = 2; -const SAMPLER_REPEAT_NEAREST: u32 = 3; -const SAMPLER_CLAMP_LINEAR: u32 = 4; -const SAMPLER_CLAMP_NEAREST: u32 = 5; -const SAMPLER_CLAMP_U_REPEAT_V_LINEAR: u32 = 6; -const SAMPLER_CLAMP_U_REPEAT_V_NEAREST: u32 = 7; -const SAMPLER_REPEAT_U_CLAMP_V_LINEAR: u32 = 8; -const SAMPLER_REPEAT_U_CLAMP_V_NEAREST: u32 = 9; +pub(super) const SAMPLER_REPEAT_LINEAR: u32 = 2; +pub(super) const SAMPLER_REPEAT_NEAREST: u32 = 3; +pub(super) const SAMPLER_CLAMP_LINEAR: u32 = 4; +pub(super) const SAMPLER_CLAMP_NEAREST: u32 = 5; +pub(super) const SAMPLER_CLAMP_U_REPEAT_V_LINEAR: u32 = 6; +pub(super) const SAMPLER_CLAMP_U_REPEAT_V_NEAREST: u32 = 7; +pub(super) const SAMPLER_REPEAT_U_CLAMP_V_LINEAR: u32 = 8; +pub(super) const SAMPLER_REPEAT_U_CLAMP_V_NEAREST: u32 = 9; -const TEXTURE_START_BIND_INDEX: u32 = 10; +pub(super) const TEXTURE_START_BIND_INDEX: u32 = 10; // The flash Context3D API is similar to OpenGL - it has many methods // which modify the current state (`setVertexBufferAt`, `setCulling`, etc.) @@ -58,8 +59,7 @@ const TEXTURE_START_BIND_INDEX: u32 = 10; // we don't actually store the `wgpu::RenderPipeline` in `CurrentPipeline` - it's // instead stored in `WgpuContext3D`. pub struct CurrentPipeline { - vertex_shader: Option>, - fragment_shader: Option>, + shaders: Option>, culling: Context3DTriangleFace, @@ -85,15 +85,42 @@ pub struct CurrentPipeline { sampler_override: [Option; 8], } +#[derive(Clone)] pub struct BoundTextureData { /// This is used to allow us to remove a bound texture when /// it's used with `setRenderToTexture`. The actual shader binding /// uses `view` pub id: Rc, - pub view: TextureView, + pub view: Rc, pub cube: bool, } +impl Hash for BoundTextureData { + fn hash(&self, state: &mut H) { + // We can't hash 'view', but we can hash the pointer of the 'Rc', + // which is unique to the TextureView + let BoundTextureData { id, cube, view: _ } = self; + (Rc::as_ptr(id) as *const ()).hash(state); + cube.hash(state); + } +} + +impl PartialEq for BoundTextureData { + fn eq(&self, other: &Self) -> bool { + let BoundTextureData { id, cube, view: _ } = self; + let BoundTextureData { + id: other_id, + cube: other_cube, + view: _, + } = other; + std::ptr::eq( + Rc::as_ptr(id) as *const (), + Rc::as_ptr(other_id) as *const (), + ) && cube == other_cube + } +} +impl Eq for BoundTextureData {} + impl CurrentPipeline { pub fn new(descriptors: &Descriptors) -> Self { let vertex_shader_uniforms = descriptors.device.create_buffer(&BufferDescriptor { @@ -111,8 +138,7 @@ impl CurrentPipeline { }); CurrentPipeline { - vertex_shader: None, - fragment_shader: None, + shaders: None, bound_textures: std::array::from_fn(|_| None), vertex_shader_uniforms, fragment_shader_uniforms, @@ -132,14 +158,9 @@ impl CurrentPipeline { sampler_override: [None; 8], } } - pub fn set_vertex_shader(&mut self, shader: Option>) { + pub fn set_shaders(&mut self, shaders: Option>) { self.dirty.set(true); - self.vertex_shader = shader; - } - - pub fn set_fragment_shader(&mut self, shader: Option>) { - self.dirty.set(true); - self.fragment_shader = shader; + self.shaders = shaders; } pub fn update_texture_at(&mut self, index: usize, texture: Option) { @@ -209,111 +230,6 @@ impl CurrentPipeline { self.dirty.set(false); - let mut layout_entries = vec![ - // Vertex shader program constants - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - // Fragment shader program constants - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - // One sampler per filter/wrapping combination - see BitmapFilters - // An AGAL shader can use any of these samplers, so - // we need to bind them all. - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_REPEAT_LINEAR, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_REPEAT_NEAREST, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_CLAMP_LINEAR, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_CLAMP_NEAREST, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_CLAMP_U_REPEAT_V_LINEAR, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_CLAMP_U_REPEAT_V_NEAREST, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_REPEAT_U_CLAMP_V_LINEAR, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: SAMPLER_REPEAT_U_CLAMP_V_NEAREST, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ]; - - for (i, bound_texture) in self.bound_textures.iter().enumerate() { - if let Some(bound_texture) = bound_texture { - let dimension = if bound_texture.cube { - wgpu::TextureViewDimension::Cube - } else { - wgpu::TextureViewDimension::D2 - }; - layout_entries.push(wgpu::BindGroupLayoutEntry { - binding: TEXTURE_START_BIND_INDEX + i as u32, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: dimension, - multisampled: false, - }, - count: None, - }); - } - } - - let globals_layout_label = create_debug_label!("Globals bind group layout"); - let bind_group_layout = - descriptors - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: globals_layout_label.as_deref(), - entries: &layout_entries, - }); - let bind_group_label = create_debug_label!("Bind group"); let mut bind_group_entries = vec![ @@ -384,24 +300,6 @@ impl CurrentPipeline { } } - let bind_group = descriptors - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: bind_group_label.as_deref(), - layout: &bind_group_layout, - entries: &bind_group_entries, - }); - - let pipeline_layout_label = create_debug_label!("Pipeline layout"); - let pipeline_layout = - descriptors - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: pipeline_layout_label.as_deref(), - bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], - }); - let agal_attributes = vertex_attributes.clone().map(|attr| { attr.map(|attr| match attr.format { Context3DVertexBufferFormat::Float4 => naga_agal::VertexAttributeFormat::Float4, @@ -412,31 +310,32 @@ impl CurrentPipeline { }) }); - let vertex_module = self - .vertex_shader - .as_ref() - .expect("Missing vertex shader!") - .compile( - descriptors, - ShaderCompileData { - vertex_attributes: agal_attributes, - // Vertex shaders do not use sampler overrides - sampler_overrides: [None; 8], - }, - ); - - let fragment_module = self - .fragment_shader - .as_ref() - .expect("Missing fragment shader!") - .compile( - descriptors, - ShaderCompileData { - // Fragment shaders do not use vertex attributes - vertex_attributes: [None; 8], - sampler_overrides: self.sampler_override, - }, - ); + let compiled_shaders = self.shaders.as_ref().expect("Missing shaders!").compile( + descriptors, + ShaderCompileData { + vertex_attributes: agal_attributes, + sampler_overrides: self.sampler_override, + bound_textures: self.bound_textures.clone(), + }, + ); + + let pipeline_layout_label = create_debug_label!("Pipeline layout"); + let pipeline_layout = + descriptors + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: pipeline_layout_label.as_deref(), + bind_group_layouts: &[&compiled_shaders.bind_group_layout], + push_constant_ranges: &[], + }); + + let bind_group = descriptors + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: bind_group_label.as_deref(), + layout: &compiled_shaders.bind_group_layout, + entries: &bind_group_entries, + }); struct BufferData { buffer: Rc, @@ -557,12 +456,12 @@ impl CurrentPipeline { label: create_debug_label!("RenderPipeline").as_deref(), layout: Some(&pipeline_layout), vertex: VertexState { - module: &vertex_module, + module: &compiled_shaders.vertex_module, entry_point: naga_agal::SHADER_ENTRY_POINT, buffers: &wgpu_vertex_buffers, }, fragment: Some(wgpu::FragmentState { - module: &fragment_module, + module: &compiled_shaders.fragment_module, entry_point: naga_agal::SHADER_ENTRY_POINT, targets: &[Some(ColorTargetState { format: TextureFormat::Rgba8Unorm, diff --git a/render/wgpu/src/context3d/mod.rs b/render/wgpu/src/context3d/mod.rs index 4598937ec8cf..60bf10bc1768 100644 --- a/render/wgpu/src/context3d/mod.rs +++ b/render/wgpu/src/context3d/mod.rs @@ -1,14 +1,11 @@ -use lru::LruCache; -use naga_agal::{SamplerOverride, VertexAttributeFormat}; use ruffle_render::backend::{ Context3D, Context3DBlendFactor, Context3DCommand, Context3DCompareMode, - Context3DTextureFormat, Context3DVertexBufferFormat, IndexBuffer, ProgramType, ShaderModule, - VertexBuffer, + Context3DTextureFormat, Context3DVertexBufferFormat, IndexBuffer, ProgramType, VertexBuffer, }; use ruffle_render::bitmap::{BitmapFormat, BitmapHandle}; use ruffle_render::error::Error; use std::borrow::Cow; -use std::cell::{Cell, RefCell, RefMut}; +use std::cell::Cell; use swf::{Rectangle, Twips}; use wgpu::util::StagingBelt; @@ -23,14 +20,17 @@ use crate::descriptors::Descriptors; use crate::Texture; use gc_arena::{Collect, MutationContext}; -use std::num::{NonZeroU64, NonZeroUsize}; +use std::num::NonZeroU64; use std::rc::Rc; use std::sync::Arc; mod current_pipeline; +mod shader_pair; use current_pipeline::CurrentPipeline; +use self::shader_pair::ShaderPairAgal; + const COLOR_MASK: u32 = 1 << 0; const DEPTH_MASK: u32 = 1 << 1; const STENCIL_MASK: u32 = 1 << 2; @@ -351,56 +351,6 @@ pub struct VertexBufferWrapper { pub data_32_per_vertex: u8, } -#[derive(Collect)] -#[collect(require_static)] -pub struct ShaderModuleAgal { - bytecode: Vec, - // Caches compiled wgpu shader modules. The cache key represents all of the data - // that we need to pass to `naga_agal::agal_to_naga` to compile a shader. - compiled: RefCell>, -} - -impl ShaderModuleAgal { - pub fn new(bytecode: Vec) -> Self { - Self { - bytecode, - // TODO - figure out a good size for this cache. - compiled: RefCell::new(LruCache::new(NonZeroUsize::new(2).unwrap())), - } - } - - pub fn compile( - &self, - descriptors: &Descriptors, - data: ShaderCompileData, - ) -> RefMut<'_, wgpu::ShaderModule> { - let compiled = self.compiled.borrow_mut(); - RefMut::map(compiled, |compiled| { - // TODO: Figure out a way to avoid the clone when we have a cache hit - compiled.get_or_insert_mut(data.clone(), || { - let naga_module = naga_agal::agal_to_naga( - &self.bytecode, - &data.vertex_attributes, - &data.sampler_overrides, - ) - .unwrap(); - descriptors - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("AGAL shader"), - source: wgpu::ShaderSource::Naga(Cow::Owned(naga_module)), - }) - }) - }) - } -} - -#[derive(Hash, Eq, PartialEq, Clone)] -pub struct ShaderCompileData { - sampler_overrides: [Option; 8], - vertex_attributes: [Option; MAX_VERTEX_ATTRIBUTES], -} - #[derive(Collect)] #[collect(require_static)] pub struct TextureWrapper { @@ -409,7 +359,6 @@ pub struct TextureWrapper { impl IndexBuffer for IndexBufferWrapper {} impl VertexBuffer for VertexBufferWrapper {} -impl ShaderModule for ShaderModuleAgal {} impl ruffle_render::backend::Texture for TextureWrapper {} // Context3D.setVertexBufferAt supports up to 8 vertex buffer attributes @@ -965,32 +914,23 @@ impl Context3D for WgpuContext3D { } Context3DCommand::UploadShaders { - vertex_shader, + module, vertex_shader_agal, - fragment_shader, fragment_shader_agal, } => { - *vertex_shader.write(mc) = Some(Rc::new(ShaderModuleAgal::new(vertex_shader_agal))); - *fragment_shader.write(mc) = - Some(Rc::new(ShaderModuleAgal::new(fragment_shader_agal))); + *module.write(mc) = Some(Rc::new(ShaderPairAgal::new( + vertex_shader_agal, + fragment_shader_agal, + ))); } - Context3DCommand::SetShaders { - vertex_shader, - fragment_shader, - } => { - let vertex_module = vertex_shader - .read() - .clone() - .map(|shader| shader.into_any_rc().downcast::().unwrap()); - - let fragment_module = fragment_shader + Context3DCommand::SetShaders { module } => { + let shaders = module .read() .clone() - .map(|shader| shader.into_any_rc().downcast::().unwrap()); + .map(|shader| shader.into_any_rc().downcast::().unwrap()); - self.current_pipeline.set_vertex_shader(vertex_module); - self.current_pipeline.set_fragment_shader(fragment_module); + self.current_pipeline.set_shaders(shaders) } Context3DCommand::SetProgramConstantsFromVector { program_type, @@ -1120,7 +1060,7 @@ impl Context3D for WgpuContext3D { Some(BoundTextureData { id: texture.clone(), - view: texture_wrapper.texture.create_view(&view), + view: Rc::new(texture_wrapper.texture.create_view(&view)), cube, }) } else { diff --git a/render/wgpu/src/context3d/shader_pair.rs b/render/wgpu/src/context3d/shader_pair.rs new file mode 100644 index 000000000000..2bd8d64f2b7e --- /dev/null +++ b/render/wgpu/src/context3d/shader_pair.rs @@ -0,0 +1,209 @@ +use gc_arena::Collect; +use lru::LruCache; +use naga_agal::{SamplerOverride, VertexAttributeFormat}; +use ruffle_render::backend::ShaderModule; +use std::{ + borrow::Cow, + cell::{RefCell, RefMut}, + num::NonZeroUsize, +}; +use wgpu::SamplerBindingType; + +use super::{ + current_pipeline::{ + BoundTextureData, SAMPLER_CLAMP_LINEAR, SAMPLER_CLAMP_NEAREST, + SAMPLER_CLAMP_U_REPEAT_V_LINEAR, SAMPLER_CLAMP_U_REPEAT_V_NEAREST, SAMPLER_REPEAT_LINEAR, + SAMPLER_REPEAT_NEAREST, SAMPLER_REPEAT_U_CLAMP_V_LINEAR, SAMPLER_REPEAT_U_CLAMP_V_NEAREST, + TEXTURE_START_BIND_INDEX, + }, + MAX_VERTEX_ATTRIBUTES, +}; + +use crate::descriptors::Descriptors; + +#[derive(Collect)] +#[collect(require_static)] +pub struct ShaderPairAgal { + vertex_bytecode: Vec, + fragment_bytecode: Vec, + // Caches compiled wgpu shader modules. The cache key represents all of the data + // that we need to pass to `naga_agal::agal_to_naga` to compile a shader. + compiled: RefCell>, +} + +impl ShaderModule for ShaderPairAgal {} + +pub struct CompiledShaderProgram { + pub vertex_module: wgpu::ShaderModule, + pub fragment_module: wgpu::ShaderModule, + pub bind_group_layout: wgpu::BindGroupLayout, +} + +impl ShaderPairAgal { + pub fn new(vertex_bytecode: Vec, fragment_bytecode: Vec) -> Self { + Self { + vertex_bytecode, + fragment_bytecode, + // TODO - figure out a good size for this cache. + compiled: RefCell::new(LruCache::new(NonZeroUsize::new(2).unwrap())), + } + } + + pub fn compile( + &self, + descriptors: &Descriptors, + data: ShaderCompileData, + ) -> RefMut<'_, CompiledShaderProgram> { + let compiled = self.compiled.borrow_mut(); + RefMut::map(compiled, |compiled| { + // TODO: Figure out a way to avoid the clone when we have a cache hit + compiled.get_or_insert_mut(data.clone(), || { + let vertex_naga_module = naga_agal::agal_to_naga( + &self.vertex_bytecode, + &data.vertex_attributes, + &data.sampler_overrides, + ) + .unwrap(); + let vertex_module = + descriptors + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("AGAL vertex shader"), + source: wgpu::ShaderSource::Naga(Cow::Owned(vertex_naga_module)), + }); + + let fragment_naga_module = naga_agal::agal_to_naga( + &self.fragment_bytecode, + &data.vertex_attributes, + &data.sampler_overrides, + ) + .unwrap(); + let fragment_module = + descriptors + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("AGAL fragment shader"), + source: wgpu::ShaderSource::Naga(Cow::Owned(fragment_naga_module)), + }); + + let mut layout_entries = vec![ + // Vertex shader program constants + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // Fragment shader program constants + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // One sampler per filter/wrapping combination - see BitmapFilters + // An AGAL shader can use any of these samplers, so + // we need to bind them all. + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_REPEAT_LINEAR, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_REPEAT_NEAREST, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_CLAMP_LINEAR, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_CLAMP_NEAREST, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_CLAMP_U_REPEAT_V_LINEAR, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_CLAMP_U_REPEAT_V_NEAREST, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_REPEAT_U_CLAMP_V_LINEAR, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: SAMPLER_REPEAT_U_CLAMP_V_NEAREST, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ]; + + for (i, bound_texture) in data.bound_textures.iter().enumerate() { + if let Some(bound_texture) = bound_texture { + let dimension = if bound_texture.cube { + wgpu::TextureViewDimension::Cube + } else { + wgpu::TextureViewDimension::D2 + }; + layout_entries.push(wgpu::BindGroupLayoutEntry { + binding: TEXTURE_START_BIND_INDEX + i as u32, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: dimension, + multisampled: false, + }, + count: None, + }); + } + } + + let globals_layout_label = create_debug_label!("Globals bind group layout"); + let bind_group_layout = + descriptors + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: globals_layout_label.as_deref(), + entries: &layout_entries, + }); + + CompiledShaderProgram { + vertex_module, + fragment_module, + bind_group_layout, + } + }) + }) + } +} + +#[derive(Hash, Eq, PartialEq, Clone)] +pub struct ShaderCompileData { + pub sampler_overrides: [Option; 8], + pub vertex_attributes: [Option; MAX_VERTEX_ATTRIBUTES], + pub bound_textures: [Option; 8], +}