Rip base files from another project
This commit is contained in:
		
							parent
							
								
									a708985c85
								
							
						
					
					
						commit
						8f56358415
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -6,3 +6,7 @@ edition = "2021" | |||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| bytemuck = "1.15.0" | ||||
| log = "0.4.21" | ||||
| wgpu     = "0.19.4" | ||||
| winit    = "0.29.15" | ||||
|  |  | |||
|  | @ -0,0 +1,137 @@ | |||
| use std::num::NonZeroU32; | ||||
| 
 | ||||
| use anyhow::{Context as _, Result}; | ||||
| 
 | ||||
| use super::Context; | ||||
| 
 | ||||
| #[derive(Debug, Default)] | ||||
| pub struct BindGroupLayoutBuilder<'a> { | ||||
|     next_binding: u32, | ||||
|     entries: Vec<wgpu::BindGroupLayoutEntry>, | ||||
|     label: Option<&'a str>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> BindGroupLayoutBuilder<'a> { | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_label(mut self, label: &'a str) -> Self { | ||||
|         self.label = Some(label); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_entry( | ||||
|         mut self, | ||||
|         visibility: wgpu::ShaderStages, | ||||
|         ty: wgpu::BindingType, | ||||
|         count: Option<NonZeroU32>, | ||||
|     ) -> Self { | ||||
|         self.entries.push(wgpu::BindGroupLayoutEntry { | ||||
|             binding: self.next_binding, | ||||
|             visibility, | ||||
|             ty, | ||||
|             count, | ||||
|         }); | ||||
|         self.next_binding += 1; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_uniform_entry(self, visibility: wgpu::ShaderStages) -> Self { | ||||
|         self.with_entry( | ||||
|             visibility, | ||||
|             wgpu::BindingType::Buffer { | ||||
|                 ty: wgpu::BufferBindingType::Uniform, | ||||
|                 has_dynamic_offset: false, | ||||
|                 min_binding_size: None, | ||||
|             }, | ||||
|             None, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_rw_storage_entry(self, visibility: wgpu::ShaderStages) -> Self { | ||||
|         self.with_entry( | ||||
|             visibility, | ||||
|             wgpu::BindingType::Buffer { | ||||
|                 ty: wgpu::BufferBindingType::Storage { read_only: false }, | ||||
|                 has_dynamic_offset: false, | ||||
|                 min_binding_size: None, | ||||
|             }, | ||||
|             None, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_ro_storage_entry(self, visibility: wgpu::ShaderStages) -> Self { | ||||
|         self.with_entry( | ||||
|             visibility, | ||||
|             wgpu::BindingType::Buffer { | ||||
|                 ty: wgpu::BufferBindingType::Storage { read_only: true }, | ||||
|                 has_dynamic_offset: false, | ||||
|                 min_binding_size: None, | ||||
|             }, | ||||
|             None, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn build(self, context: &Context) -> wgpu::BindGroupLayout { | ||||
|         context | ||||
|             .device | ||||
|             .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | ||||
|                 label: self.label, | ||||
|                 entries: &self.entries, | ||||
|             }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Default)] | ||||
| pub struct BindGroupBuilder<'a> { | ||||
|     next_binding: u32, | ||||
|     label: Option<&'a str>, | ||||
|     entries: Vec<wgpu::BindGroupEntry<'a>>, | ||||
|     layout: Option<&'a wgpu::BindGroupLayout>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> BindGroupBuilder<'a> { | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_label(mut self, label: &'a str) -> Self { | ||||
|         self.label = Some(label); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_entry(mut self, resource: wgpu::BindingResource<'a>) -> Self { | ||||
|         self.entries.push(wgpu::BindGroupEntry { | ||||
|             binding: self.next_binding, | ||||
|             resource, | ||||
|         }); | ||||
|         self.next_binding += 1; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_layout(mut self, layout: &'a wgpu::BindGroupLayout) -> Self { | ||||
|         self.layout = Some(layout); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn build(self, context: &Context) -> Result<wgpu::BindGroup> { | ||||
|         Ok(context | ||||
|             .device | ||||
|             .create_bind_group(&wgpu::BindGroupDescriptor { | ||||
|                 label: self.label, | ||||
|                 layout: self.layout.context("BindGroupBuilder has no layout.")?, | ||||
|                 entries: self.entries.as_slice(), | ||||
|             })) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,99 @@ | |||
| use std::ops::RangeBounds; | ||||
| 
 | ||||
| use bytemuck::NoUninit; | ||||
| use wgpu::util::DeviceExt; | ||||
| 
 | ||||
| use super::Context; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct BulkBufferBuilder<'a> { | ||||
|     order: Vec<(bool, usize)>, | ||||
|     init_descriptors: Vec<wgpu::util::BufferInitDescriptor<'a>>, | ||||
|     descriptors: Vec<wgpu::BufferDescriptor<'a>>, | ||||
|     current_usage: wgpu::BufferUsages, | ||||
| } | ||||
| 
 | ||||
| impl<'a> BulkBufferBuilder<'a> { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             order: vec![], | ||||
|             init_descriptors: vec![], | ||||
|             descriptors: vec![], | ||||
|             current_usage: wgpu::BufferUsages::UNIFORM, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_usage(mut self, usage: wgpu::BufferUsages) -> Self { | ||||
|         self.current_usage = usage; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn with_buffer(mut self, label: &'a str, size: u64, mapped: bool) -> Self { | ||||
|         let descriptor = wgpu::BufferDescriptor { | ||||
|             label: Some(label), | ||||
|             size, | ||||
|             usage: self.current_usage, | ||||
|             mapped_at_creation: mapped, | ||||
|         }; | ||||
| 
 | ||||
|         self.order.push((false, self.descriptors.len())); | ||||
|         self.descriptors.push(descriptor); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn with_init_buffer(mut self, label: &'a str, contents: &'a [u8]) -> Self { | ||||
|         let descriptor = wgpu::util::BufferInitDescriptor { | ||||
|             label: Some(label), | ||||
|             contents, | ||||
|             usage: self.current_usage, | ||||
|         }; | ||||
| 
 | ||||
|         self.order.push((true, self.init_descriptors.len())); | ||||
|         self.init_descriptors.push(descriptor); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn with_init_buffer_bm<A: NoUninit>(self, label: &'a str, contents: &'a [A]) -> Self { | ||||
|         self.with_init_buffer(label, bytemuck::cast_slice(contents)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn build(self, context: &Context) -> Vec<wgpu::Buffer> { | ||||
|         let device = &context.device; | ||||
|         let mut buffers = vec![]; | ||||
|         for (init, index) in self.order { | ||||
|             let buffer = if init { | ||||
|                 device.create_buffer_init(&(self.init_descriptors[index])) | ||||
|             } else { | ||||
|                 device.create_buffer(&(self.descriptors[index])) | ||||
|             }; | ||||
| 
 | ||||
|             buffers.push(buffer); | ||||
|         } | ||||
| 
 | ||||
|         buffers | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait BufferExt { | ||||
|     fn get_mapped_range<S: RangeBounds<wgpu::BufferAddress>, T: bytemuck::Pod>( | ||||
|         &self, | ||||
|         context: &Context, | ||||
|         bounds: S, | ||||
|     ) -> Vec<T>; | ||||
| } | ||||
| 
 | ||||
| impl BufferExt for wgpu::Buffer { | ||||
|     fn get_mapped_range<S: RangeBounds<wgpu::BufferAddress>, T: bytemuck::Pod>( | ||||
|         &self, | ||||
|         context: &Context, | ||||
|         bounds: S, | ||||
|     ) -> Vec<T> { | ||||
|         let slice = self.slice(bounds); | ||||
|         slice.map_async(wgpu::MapMode::Read, |_| {}); | ||||
|         context.device.poll(wgpu::Maintain::Wait); | ||||
|         let data: Vec<T> = bytemuck::cast_slice(slice.get_mapped_range().as_ref()).to_vec(); | ||||
|         self.unmap(); | ||||
| 
 | ||||
|         data | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,108 @@ | |||
| use std::sync::Arc; | ||||
| 
 | ||||
| use anyhow::{Context as _, Result}; | ||||
| use winit::{ | ||||
|     dpi::PhysicalSize, event::WindowEvent, event_loop::EventLoopWindowTarget, window::Window, | ||||
| }; | ||||
| 
 | ||||
| pub struct Context<'window> { | ||||
|     pub window: Arc<Window>, | ||||
|     pub instance: wgpu::Instance, | ||||
|     pub size: PhysicalSize<u32>, | ||||
|     pub surface: wgpu::Surface<'window>, | ||||
|     pub surface_config: wgpu::SurfaceConfiguration, | ||||
|     pub adapter: wgpu::Adapter, | ||||
|     pub device: wgpu::Device, | ||||
|     pub queue: wgpu::Queue, | ||||
| } | ||||
| 
 | ||||
| impl<'window> Context<'window> { | ||||
|     pub async fn new(window: Arc<Window>, limits: wgpu::Limits) -> Result<Self> { | ||||
|         log::info!("Initialising WGPU context..."); | ||||
|         let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { | ||||
|             backends: wgpu::Backends::VULKAN, | ||||
|             dx12_shader_compiler: Default::default(), | ||||
|             ..Default::default() | ||||
|         }); | ||||
| 
 | ||||
|         // To be able to start drawing we need a few things:
 | ||||
|         // - A surface
 | ||||
|         // - A GPU device to draw to the surface
 | ||||
|         // - A draw command queue
 | ||||
|         log::info!("Initialising window surface..."); | ||||
|         let surface = instance.create_surface(window.clone())?; | ||||
| 
 | ||||
|         log::info!("Requesting GPU adapter..."); | ||||
|         let adapter = instance | ||||
|             .request_adapter(&wgpu::RequestAdapterOptions { | ||||
|                 power_preference: wgpu::PowerPreference::HighPerformance, | ||||
|                 force_fallback_adapter: false, | ||||
|                 compatible_surface: Some(&surface), | ||||
|             }) | ||||
|             .await | ||||
|             .context("Failed to find suitable GPU adapter")?; | ||||
| 
 | ||||
|         log::info!("Checking GPU adapter meets requirements"); | ||||
|         log::info!("Requesting GPU device..."); | ||||
|         let (device, queue) = adapter | ||||
|             .request_device( | ||||
|                 &wgpu::DeviceDescriptor { | ||||
|                     label: None, | ||||
|                     required_features: wgpu::Features::empty(), | ||||
|                     required_limits: limits, | ||||
|                 }, | ||||
|                 None, | ||||
|             ) | ||||
|             .await?; | ||||
| 
 | ||||
|         log::info!("Configuring window surface..."); | ||||
|         let size = window.inner_size(); | ||||
|         let surface_config = surface | ||||
|             .get_default_config(&adapter, size.width, size.height) | ||||
|             .context("Surface configuration unsupported by adapter")?; | ||||
|         surface.configure(&device, &surface_config); | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             window, | ||||
|             instance, | ||||
|             size, | ||||
|             surface, | ||||
|             surface_config, | ||||
|             adapter, | ||||
|             device, | ||||
|             queue, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn resize_surface(&mut self, new_size: PhysicalSize<u32>) { | ||||
|         if new_size.width > 0 && new_size.height > 0 { | ||||
|             self.size = new_size; | ||||
|             self.surface_config.width = new_size.width; | ||||
|             self.surface_config.height = new_size.height; | ||||
|             self.surface.configure(&self.device, &self.surface_config); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn handle_window_event( | ||||
|         &mut self, | ||||
|         event: &WindowEvent, | ||||
|         elwt: &EventLoopWindowTarget<()>, | ||||
|     ) -> bool { | ||||
|         let mut handled = true; | ||||
|         match event { | ||||
|             WindowEvent::CloseRequested => { | ||||
|                 elwt.exit(); | ||||
|             } | ||||
|             WindowEvent::Resized(physical_size) => { | ||||
|                 self.resize_surface(*physical_size); | ||||
|             } | ||||
|             WindowEvent::ScaleFactorChanged { .. } => { | ||||
|                 self.resize_surface(self.window.inner_size()); | ||||
|             } | ||||
| 
 | ||||
|             _ => handled = false, | ||||
|         } | ||||
| 
 | ||||
|         handled | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/lib.rs
								
								
								
								
							
							
						
						
									
										23
									
								
								src/lib.rs
								
								
								
								
							|  | @ -1,14 +1,11 @@ | |||
| pub fn add(left: usize, right: usize) -> usize { | ||||
|     left + right | ||||
| } | ||||
| mod bind_group; | ||||
| mod buffer; | ||||
| mod context; | ||||
| mod texture; | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn it_works() { | ||||
|         let result = add(2, 2); | ||||
|         assert_eq!(result, 4); | ||||
|     } | ||||
| } | ||||
| pub use self::{ | ||||
|     bind_group::{BindGroupBuilder, BindGroupLayoutBuilder}, | ||||
|     buffer::{BufferExt, BulkBufferBuilder}, | ||||
|     context::Context, | ||||
|     texture::{Texture, TextureBuilder}, | ||||
| }; | ||||
|  |  | |||
|  | @ -0,0 +1,199 @@ | |||
| use anyhow::Result; | ||||
| 
 | ||||
| use super::{BindGroupBuilder, BindGroupLayoutBuilder, Context}; | ||||
| 
 | ||||
| // TODO: Support mip-mapping and multi-sampling
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct TextureAttributes { | ||||
|     pub size: wgpu::Extent3d, | ||||
|     pub dimension: wgpu::TextureDimension, | ||||
|     pub format: wgpu::TextureFormat, | ||||
|     pub usage: wgpu::TextureUsages, | ||||
|     pub address_mode_u: wgpu::AddressMode, | ||||
|     pub address_mode_v: wgpu::AddressMode, | ||||
|     pub address_mode_w: wgpu::AddressMode, | ||||
|     pub mag_filter: wgpu::FilterMode, | ||||
|     pub min_filter: wgpu::FilterMode, | ||||
|     pub mipmap_filter: wgpu::FilterMode, | ||||
|     pub shader_visibility: wgpu::ShaderStages, | ||||
| } | ||||
| 
 | ||||
| impl Default for TextureAttributes { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             size: Default::default(), | ||||
|             dimension: wgpu::TextureDimension::D2, | ||||
|             format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||||
|             usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | ||||
|             address_mode_u: wgpu::AddressMode::default(), | ||||
|             address_mode_v: wgpu::AddressMode::default(), | ||||
|             address_mode_w: wgpu::AddressMode::default(), | ||||
|             mag_filter: wgpu::FilterMode::default(), | ||||
|             min_filter: wgpu::FilterMode::default(), | ||||
|             mipmap_filter: wgpu::FilterMode::default(), | ||||
|             shader_visibility: wgpu::ShaderStages::FRAGMENT, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct TextureBuilder { | ||||
|     pub attributes: TextureAttributes, | ||||
| } | ||||
| 
 | ||||
| impl TextureBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             attributes: Default::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_size(mut self, width: u32, height: u32, depth: u32) -> Self { | ||||
|         self.attributes.size = wgpu::Extent3d { | ||||
|             width, | ||||
|             height, | ||||
|             depth_or_array_layers: depth, | ||||
|         }; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_dimension(mut self, dimension: wgpu::TextureDimension) -> Self { | ||||
|         self.attributes.dimension = dimension; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self { | ||||
|         self.attributes.format = format; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_usage(mut self, usage: wgpu::TextureUsages) -> Self { | ||||
|         self.attributes.usage = usage; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_address_mode(mut self, address_mode: wgpu::AddressMode) -> Self { | ||||
|         self.attributes.address_mode_u = address_mode; | ||||
|         self.attributes.address_mode_v = address_mode; | ||||
|         self.attributes.address_mode_w = address_mode; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_filter_mode(mut self, filter_mode: wgpu::FilterMode) -> Self { | ||||
|         self.attributes.mag_filter = filter_mode; | ||||
|         self.attributes.min_filter = filter_mode; | ||||
|         self.attributes.mipmap_filter = filter_mode; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn with_shader_visibility(mut self, visibility: wgpu::ShaderStages) -> Self { | ||||
|         self.attributes.shader_visibility = visibility; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     pub fn build(self, context: &Context) -> Result<Texture> { | ||||
|         Texture::new(context, self.attributes) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct Texture { | ||||
|     pub attributes: TextureAttributes, | ||||
|     pub texture: wgpu::Texture, | ||||
|     pub view: wgpu::TextureView, | ||||
|     pub sampler: wgpu::Sampler, | ||||
|     pub bind_group_layout: wgpu::BindGroupLayout, | ||||
|     pub bind_group: wgpu::BindGroup, | ||||
| } | ||||
| 
 | ||||
| impl Texture { | ||||
|     pub fn new(context: &Context, attributes: TextureAttributes) -> Result<Self> { | ||||
|         let texture = context.device.create_texture(&wgpu::TextureDescriptor { | ||||
|             label: None, | ||||
|             size: attributes.size, | ||||
|             mip_level_count: 1, | ||||
|             sample_count: 1, | ||||
|             dimension: attributes.dimension, | ||||
|             format: attributes.format, | ||||
|             usage: attributes.usage, | ||||
|             view_formats: &[], | ||||
|         }); | ||||
| 
 | ||||
|         let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); | ||||
|         let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor { | ||||
|             address_mode_u: attributes.address_mode_u, | ||||
|             address_mode_v: attributes.address_mode_v, | ||||
|             address_mode_w: attributes.address_mode_w, | ||||
|             mag_filter: attributes.mag_filter, | ||||
|             min_filter: attributes.min_filter, | ||||
|             mipmap_filter: attributes.mipmap_filter, | ||||
|             ..Default::default() | ||||
|         }); | ||||
| 
 | ||||
|         let view_dimension = match attributes.dimension { | ||||
|             wgpu::TextureDimension::D1 => wgpu::TextureViewDimension::D1, | ||||
|             wgpu::TextureDimension::D2 => wgpu::TextureViewDimension::D2, | ||||
|             wgpu::TextureDimension::D3 => wgpu::TextureViewDimension::D3, | ||||
|         }; | ||||
| 
 | ||||
|         let bind_group_layout = BindGroupLayoutBuilder::new() | ||||
|             .with_entry( | ||||
|                 attributes.shader_visibility, | ||||
|                 wgpu::BindingType::Texture { | ||||
|                     sample_type: wgpu::TextureSampleType::Float { filterable: true }, | ||||
|                     view_dimension, | ||||
|                     multisampled: false, | ||||
|                 }, | ||||
|                 None, | ||||
|             ) | ||||
|             .with_entry( | ||||
|                 attributes.shader_visibility, | ||||
|                 wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), | ||||
|                 None, | ||||
|             ) | ||||
|             .build(context); | ||||
|         let bind_group = BindGroupBuilder::new() | ||||
|             .with_layout(&bind_group_layout) | ||||
|             .with_entry(wgpu::BindingResource::TextureView(&view)) | ||||
|             .with_entry(wgpu::BindingResource::Sampler(&sampler)) | ||||
|             .build(context)?; | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             attributes, | ||||
|             texture, | ||||
|             view, | ||||
|             sampler, | ||||
|             bind_group_layout, | ||||
|             bind_group, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn update(&self, context: &Context, data: &[u8]) { | ||||
|         log::info!("Updating texture contents..."); | ||||
|         let copy_texture = wgpu::ImageCopyTexture { | ||||
|             texture: &self.texture, | ||||
|             mip_level: 0, | ||||
|             origin: wgpu::Origin3d::ZERO, | ||||
|             aspect: wgpu::TextureAspect::All, | ||||
|         }; | ||||
| 
 | ||||
|         let size = self.attributes.size; | ||||
|         let image_layout = wgpu::ImageDataLayout { | ||||
|             offset: 0, | ||||
|             bytes_per_row: Some(4 * size.width), | ||||
|             rows_per_image: Some(size.height), | ||||
|         }; | ||||
| 
 | ||||
|         context | ||||
|             .queue | ||||
|             .write_texture(copy_texture, data, image_layout, size); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue