use crate::{renderer::WgpuRenderResourceContext, wgpu_type_converter::WgpuInto}; use bevy_asset::{Handle, HandleUntyped}; use bevy_render::{ pipeline::{BindGroupDescriptorId, PipelineDescriptor}, render_resource::{BufferInfo, RenderResource, RenderResourceSetId, ResourceInfo}, renderer::RenderResourceContext, shader::Shader, texture::{Extent3d, SamplerDescriptor, TextureDescriptor}, }; use bevy_window::{Window, WindowId}; use std::{ collections::HashMap, sync::{Arc, RwLock, RwLockReadGuard}, }; #[derive(Default)] pub struct WgpuBindGroupInfo { pub bind_groups: HashMap, } /// Grabs a read lock on all wgpu resources. When paired with WgpuResourceRefs, this allows /// you to pass in wgpu resources to wgpu::RenderPass<'a> with the appropriate lifetime. This is accomplished by /// grabbing a WgpuResourcesReadLock _before_ creating a wgpu::RenderPass, getting a WgpuResourcesRefs, and storing that /// in the pass. /// /// This is only a problem because RwLockReadGuard.read() erases the guard's lifetime and creates a new anonymous lifetime. If /// you call RwLockReadGuard.read() during a pass, the reference will have an anonymous lifetime that lives for less than the /// pass, which violates the lifetime constraints in place. /// /// The biggest implication of this design (other than the additional boilerplate here) is that beginning a render pass /// blocks writes to these resources. This means that if the pass attempts to write any resource, a deadlock will occur. WgpuResourceRefs /// only has immutable references, so the only way to make a deadlock happen is to access WgpuResources directly in the pass. It also means /// that other threads attempting to write resources will need to wait for pass encoding to finish. Almost all writes should occur before /// passes start, so this hopefully won't be a problem. /// /// It is worth comparing the performance of this to transactional / copy-based approaches. This lock based design guarantees /// consistency, doesn't perform redundant allocations, and only blocks when a write is occurring. A copy based approach would /// never block, but would require more allocations / state-synchronization, which I expect will be more expensive. It would also be /// "eventually consistent" instead of "strongly consistent". /// /// Single threaded implementations don't need to worry about these lifetimes constraints at all. RenderPasses can use a RenderContext's /// WgpuResources directly. RenderContext already has a lifetime greater than the RenderPass. pub struct WgpuResourcesReadLock<'a> { pub buffers: RwLockReadGuard<'a, HashMap>, pub textures: RwLockReadGuard<'a, HashMap>, pub swap_chain_outputs: RwLockReadGuard<'a, HashMap>, pub render_pipelines: RwLockReadGuard<'a, HashMap, wgpu::RenderPipeline>>, pub bind_groups: RwLockReadGuard<'a, HashMap>, } impl<'a> WgpuResourcesReadLock<'a> { pub fn refs(&'a self) -> WgpuResourceRefs<'a> { WgpuResourceRefs { buffers: &self.buffers, textures: &self.textures, swap_chain_outputs: &self.swap_chain_outputs, render_pipelines: &self.render_pipelines, bind_groups: &self.bind_groups, } } } /// Stores read only references to WgpuResource collections. See WgpuResourcesReadLock docs for context on why this exists pub struct WgpuResourceRefs<'a> { pub buffers: &'a HashMap, pub textures: &'a HashMap, pub swap_chain_outputs: &'a HashMap, pub render_pipelines: &'a HashMap, wgpu::RenderPipeline>, pub bind_groups: &'a HashMap, } #[derive(Default, Clone)] pub struct WgpuResources { // TODO: remove this from WgpuResources. it doesn't need to be here pub window_surfaces: Arc>>, pub window_swap_chains: Arc>>, pub swap_chain_outputs: Arc>>, pub buffers: Arc>>, pub texture_views: Arc>>, pub textures: Arc>>, pub samplers: Arc>>, pub resource_info: Arc>>, pub shader_modules: Arc, wgpu::ShaderModule>>>, pub render_pipelines: Arc, wgpu::RenderPipeline>>>, pub bind_groups: Arc>>, pub bind_group_layouts: Arc>>, pub asset_resources: Arc>>, } impl WgpuResources { pub fn read(&self) -> WgpuResourcesReadLock { WgpuResourcesReadLock { buffers: self.buffers.read().unwrap(), textures: self.texture_views.read().unwrap(), swap_chain_outputs: self.swap_chain_outputs.read().unwrap(), render_pipelines: self.render_pipelines.read().unwrap(), bind_groups: self.bind_groups.read().unwrap(), } } pub fn set_window_surface(&self, window_id: WindowId, surface: wgpu::Surface) { self.window_surfaces .write() .unwrap() .insert(window_id, surface); } pub fn next_swap_chain_texture(&self, window_id: WindowId) -> RenderResource { let mut swap_chain_outputs = self.window_swap_chains.write().unwrap(); let swap_chain_output = swap_chain_outputs.get_mut(&window_id).unwrap(); let next_texture = swap_chain_output.get_next_texture().unwrap(); let render_resource = RenderResource::new(); // TODO: Add ResourceInfo self.swap_chain_outputs .write() .unwrap() .insert(render_resource, next_texture); render_resource } pub fn remove_swap_chain_texture(&self, render_resource: RenderResource) { self.swap_chain_outputs .write() .unwrap() .remove(&render_resource); } pub fn remove_all_swap_chain_textures(&self) { self.swap_chain_outputs.write().unwrap().clear(); } pub fn create_window_swap_chain(&self, device: &wgpu::Device, window: &Window) { let swap_chain_descriptor: wgpu::SwapChainDescriptor = window.wgpu_into(); let surfaces = self.window_surfaces.read().unwrap(); let surface = surfaces .get(&window.id) .expect("No surface found for window"); let swap_chain = device.create_swap_chain(surface, &swap_chain_descriptor); self.window_swap_chains .write() .unwrap() .insert(window.id, swap_chain); } pub fn add_resource_info(&self, resource: RenderResource, resource_info: ResourceInfo) { self.resource_info .write() .unwrap() .insert(resource, resource_info); } pub fn has_bind_group( &self, bind_group_descriptor_id: BindGroupDescriptorId, render_resource_set_id: RenderResourceSetId, ) -> bool { if let Some(bind_group_info) = self .bind_groups .read() .unwrap() .get(&bind_group_descriptor_id) { bind_group_info .bind_groups .get(&render_resource_set_id) .is_some() } else { false } } pub fn create_bind_group( &self, device: &wgpu::Device, render_resource_set_id: RenderResourceSetId, bind_group_descriptor: &wgpu::BindGroupDescriptor, ) -> wgpu::BindGroup { log::trace!( "created bind group for RenderResourceSet {:?}", render_resource_set_id ); log::trace!("{:#?}", bind_group_descriptor); device.create_bind_group(bind_group_descriptor) } pub fn set_bind_group( &self, bind_group_descriptor_id: BindGroupDescriptorId, render_resource_set_id: RenderResourceSetId, bind_group: wgpu::BindGroup, ) { let mut bind_groups = self.bind_groups.write().unwrap(); let bind_group_info = bind_groups .entry(bind_group_descriptor_id) .or_insert_with(|| WgpuBindGroupInfo::default()); bind_group_info .bind_groups .insert(render_resource_set_id, bind_group); } pub fn create_buffer(&self, device: &wgpu::Device, buffer_info: BufferInfo) -> RenderResource { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: buffer_info.size as u64, usage: buffer_info.buffer_usage.wgpu_into(), }); let resource = RenderResource::new(); self.add_resource_info(resource, ResourceInfo::Buffer(buffer_info)); self.buffers.write().unwrap().insert(resource, buffer); resource } pub fn create_buffer_with_data( &self, device: &wgpu::Device, mut buffer_info: BufferInfo, data: &[u8], ) -> RenderResource { buffer_info.size = data.len(); let buffer = device.create_buffer_with_data(data, buffer_info.buffer_usage.wgpu_into()); self.assign_buffer(buffer, buffer_info) } // TODO: taking a closure isn't fantastic. is there any way to make this better without exposing the lock in the interface? pub fn get_resource_info( &self, resource: RenderResource, handle_info: &mut dyn FnMut(Option<&ResourceInfo>), ) { let resource_info = self.resource_info.read().unwrap(); let info = resource_info.get(&resource); handle_info(info); } pub fn remove_buffer(&self, resource: RenderResource) { self.buffers.write().unwrap().remove(&resource); self.resource_info.write().unwrap().remove(&resource); } pub fn assign_buffer(&self, buffer: wgpu::Buffer, buffer_info: BufferInfo) -> RenderResource { let resource = RenderResource::new(); self.add_resource_info(resource, ResourceInfo::Buffer(buffer_info)); self.buffers.write().unwrap().insert(resource, buffer); resource } // TODO: clean this up pub fn begin_create_buffer_mapped_render_context( buffer_info: &BufferInfo, render_resources: &WgpuRenderResourceContext, setup_data: &mut dyn FnMut(&mut [u8], &dyn RenderResourceContext), ) -> wgpu::Buffer { let device = render_resources.device.clone(); let mut mapped = device.create_buffer_mapped(&wgpu::BufferDescriptor { size: buffer_info.size as u64, usage: buffer_info.buffer_usage.wgpu_into(), label: None, }); setup_data(&mut mapped.data, render_resources); mapped.finish() } pub fn copy_buffer_to_buffer( &self, encoder: &mut wgpu::CommandEncoder, source_buffer: RenderResource, source_offset: u64, destination_buffer: RenderResource, destination_offset: u64, size: u64, ) { let buffers = self.buffers.read().unwrap(); let source = buffers.get(&source_buffer).unwrap(); let destination = buffers.get(&destination_buffer).unwrap(); encoder.copy_buffer_to_buffer(source, source_offset, destination, destination_offset, size); } pub fn copy_buffer_to_texture( &self, encoder: &mut wgpu::CommandEncoder, source_buffer: RenderResource, source_offset: u64, source_bytes_per_row: u32, destination_texture: RenderResource, destination_origin: [u32; 3], // TODO: replace with math type destination_mip_level: u32, destination_array_layer: u32, size: Extent3d, ) { let buffers = self.buffers.read().unwrap(); let textures = self.textures.read().unwrap(); let source = buffers.get(&source_buffer).unwrap(); let destination = textures.get(&destination_texture).unwrap(); encoder.copy_buffer_to_texture( wgpu::BufferCopyView { buffer: source, offset: source_offset, bytes_per_row: source_bytes_per_row, rows_per_image: 0, // NOTE: Example sets this to 0, but should it be height? }, wgpu::TextureCopyView { texture: destination, mip_level: destination_mip_level, array_layer: destination_array_layer, origin: wgpu::Origin3d { x: destination_origin[0], y: destination_origin[1], z: destination_origin[2], }, }, size.wgpu_into(), ); } pub fn create_shader_module( &self, device: &wgpu::Device, shader_handle: Handle, shader: &Shader, ) { let shader_module = device.create_shader_module(&shader.get_spirv(None)); self.shader_modules .write() .unwrap() .insert(shader_handle, shader_module); } pub fn create_sampler( &self, device: &wgpu::Device, sampler_descriptor: &SamplerDescriptor, ) -> RenderResource { let descriptor: wgpu::SamplerDescriptor = (*sampler_descriptor).wgpu_into(); let sampler = device.create_sampler(&descriptor); let resource = RenderResource::new(); self.samplers.write().unwrap().insert(resource, sampler); self.add_resource_info(resource, ResourceInfo::Sampler); resource } pub fn create_texture( &self, device: &wgpu::Device, texture_descriptor: &TextureDescriptor, ) -> RenderResource { let descriptor: wgpu::TextureDescriptor = (*texture_descriptor).wgpu_into(); let texture = device.create_texture(&descriptor); let texture_view = texture.create_default_view(); let resource = RenderResource::new(); self.add_resource_info(resource, ResourceInfo::Texture); self.texture_views .write() .unwrap() .insert(resource, texture_view); self.textures.write().unwrap().insert(resource, texture); resource } pub fn create_texture_with_data( &self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, texture_descriptor: &TextureDescriptor, bytes: &[u8], ) -> RenderResource { let descriptor: wgpu::TextureDescriptor = (*texture_descriptor).wgpu_into(); let texture = device.create_texture(&descriptor); let texture_view = texture.create_default_view(); let temp_buf = device.create_buffer_with_data(bytes, wgpu::BufferUsage::COPY_SRC); encoder.copy_buffer_to_texture( wgpu::BufferCopyView { buffer: &temp_buf, offset: 0, bytes_per_row: 4 * descriptor.size.width, rows_per_image: 0, // NOTE: Example sets this to 0, but should it be height? }, wgpu::TextureCopyView { texture: &texture, mip_level: 0, array_layer: 0, origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, }, descriptor.size, ); let resource = RenderResource::new(); self.add_resource_info(resource, ResourceInfo::Texture); self.texture_views .write() .unwrap() .insert(resource, texture_view); resource } pub fn remove_texture(&self, resource: RenderResource) { self.texture_views.write().unwrap().remove(&resource); self.textures.write().unwrap().remove(&resource); self.resource_info.write().unwrap().remove(&resource); } pub fn remove_sampler(&self, resource: RenderResource) { self.samplers.write().unwrap().remove(&resource); self.resource_info.write().unwrap().remove(&resource); } pub fn set_asset_resource( &mut self, handle: Handle, render_resource: RenderResource, index: usize, ) where T: 'static, { self.asset_resources .write() .unwrap() .insert((handle.into(), index), render_resource); } pub fn get_asset_resource( &mut self, handle: Handle, index: usize, ) -> Option where T: 'static, { self.asset_resources .write() .unwrap() .get(&(handle.into(), index)) .cloned() } pub fn set_asset_resource_untyped( &self, handle: HandleUntyped, render_resource: RenderResource, index: usize, ) { self.asset_resources .write() .unwrap() .insert((handle, index), render_resource); } pub fn get_asset_resource_untyped( &self, handle: HandleUntyped, index: usize, ) -> Option { self.asset_resources .write() .unwrap() .get(&(handle, index)) .cloned() } pub fn create_bind_group_layout( &self, device: &wgpu::Device, bind_group_id: BindGroupDescriptorId, descriptor: &wgpu::BindGroupLayoutDescriptor, ) { let wgpu_bind_group_layout = device.create_bind_group_layout(descriptor); self.bind_group_layouts .write() .unwrap() .insert(bind_group_id, wgpu_bind_group_layout); } pub fn create_render_pipeline( &self, device: &wgpu::Device, descriptor: &wgpu::RenderPipelineDescriptor, ) -> wgpu::RenderPipeline { device.create_render_pipeline(&descriptor) } pub fn set_render_pipeline( &self, pipeline_handle: Handle, render_pipeline: wgpu::RenderPipeline, ) { self.render_pipelines .write() .unwrap() .insert(pipeline_handle, render_pipeline); } }