From 3c95de89ccd0fafdddae14bea3d39fcac6f5a3af Mon Sep 17 00:00:00 2001 From: Matthew Kim <38759997+friendlymatthew@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:43:01 -0500 Subject: [PATCH] Render via GPU --- Cargo.lock | 48 +++++ Cargo.toml | 20 +- README.md | 6 +- src/grammar.rs | 21 ++- src/lib.rs | 5 + src/main.rs | 17 +- src/renderer.rs | 442 ++++++++++++++++++++++++++++++++++++++++++++ src/shader.vert | 11 ++ src/shader.vert.spv | Bin 0 -> 1096 bytes src/shader.wgsl | 33 ++++ src/texture.rs | 75 ++++++++ 11 files changed, 658 insertions(+), 20 deletions(-) create mode 100644 src/renderer.rs create mode 100644 src/shader.vert create mode 100644 src/shader.vert.spv create mode 100644 src/shader.wgsl create mode 100644 src/texture.rs diff --git a/Cargo.lock b/Cargo.lock index 2fa4862..22a0740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,20 @@ name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] [[package]] name = "bytes" @@ -312,6 +326,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1175,11 +1209,19 @@ name = "png" version = "0.1.0" dependencies = [ "anyhow", + "bytemuck", + "cfg-if", + "console_error_panic_hook", + "console_log", "crc32fast", "env_logger", "flate2", "log", "minifb", + "pollster", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "wgpu", "winit", ] @@ -1199,6 +1241,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "presser" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 57ea1df..5d84fba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,14 @@ name = "png" version = "0.1.0" edition = "2021" default-run = "png" +resolver = "2" [[bin]] name = "profile" path = "./bin/profile.rs" +[lib] +crate-type = ["cdylib", "rlib"] [profile.release] debug = true @@ -19,11 +22,24 @@ flate2 = "1.0.35" anyhow = "1.0.94" # renderer - -winit = { version = "0.29", features = ["rwh_05"] } +cfg-if = "1" +bytemuck = { version = "1.16", features = [ "derive" ] } env_logger = "0.10" log = "0.4" +pollster = "0.3" wgpu = "22.0" +winit = { version = "0.29", features = ["rwh_05"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1" +console_log = "1.0" +wgpu = { version = "22.0", features = ["webgl"]} +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = [ + "Document", + "Window", + "Element", +]} minifb = "0.27.0" \ No newline at end of file diff --git a/README.md b/README.md index 528a829..8ca44a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # png -This project aims to provide a PNG decoder with SIMD-accelerated filters and a feature-rich renderer. +This project aims to provide a PNG decoder with SIMD-accelerated filters and a GPU-based renderer. -## Usage +## Render To render an image, simply run `cargo run `. For example: @@ -10,6 +10,8 @@ To render an image, simply run `cargo run `. For example: cargo run ./potatoe.png ``` +Currently, the renderer supports image resizing. + ## Profile To profile `png`, the `profile.sh` script serves as syntatic sugar for running samply's profiling. diff --git a/src/grammar.rs b/src/grammar.rs index 66168af..365cbfa 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, slice::ChunksExact}; +use std::{borrow::Cow, collections::BTreeMap, slice::ChunksExact}; #[cfg(test)] use std::{ fs::File, @@ -117,7 +117,7 @@ impl Png { } /// The dimensions of the image (width, height). - pub const fn dimension(&self) -> (u32, u32) { + pub const fn dimensions(&self) -> (u32, u32) { (self.width, self.height) } @@ -125,6 +125,23 @@ impl Png { self.color_type } + pub fn to_rgba8(&self) -> Cow<'_, [u8]> { + match self.color_type { + ColorType::RGBA => Cow::from(&self.pixel_buffer), + ColorType::RGB => { + let b = self + .pixel_buffer + .chunks_exact(3) + .map(|b| [b[0], b[1], b[2], 0]) + .flatten() + .collect::>(); + + Cow::from(b) + } + _ => todo!(), + } + } + pub fn pixel_buffer(&self) -> Vec { match self.color_type { ColorType::RGB => self.rgb_buffer(), diff --git a/src/lib.rs b/src/lib.rs index d25d653..9c85ce9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,15 @@ #![warn(clippy::nursery)] +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + pub use decoder::*; pub use grammar::*; +pub mod renderer; pub mod test_file_parser; mod decoder; mod grammar; +mod texture; mod crc32; diff --git a/src/main.rs b/src/main.rs index 03a301d..05b8c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; -use minifb::{Window, WindowOptions}; -use png::Decoder; +use png::{renderer, Decoder}; +use pollster::block_on; fn main() -> Result<()> { let mut args = std::env::args().skip(1); @@ -12,18 +12,7 @@ fn main() -> Result<()> { let mut decoder = Decoder::new(&content); let png = decoder.decode()?; - let (width, height) = png.dimension(); - - let mut window = Window::new( - "PNG renderer", - width as usize, - height as usize, - WindowOptions::default(), - )?; - - while window.is_open() { - window.update_with_buffer(&png.pixel_buffer(), width as usize, height as usize)?; - } + block_on(renderer::run(png)); Ok(()) } diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..b2dc33d --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,442 @@ +use std::iter; + +use wgpu::util::DeviceExt; +use winit::{ + dpi::PhysicalSize, + event::*, + event_loop::EventLoop, + keyboard::{KeyCode, PhysicalKey}, + window::{Window, WindowBuilder}, +}; + +use crate::{texture, Png}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct Vertex { + position: [f32; 3], + tex_coords: [f32; 2], +} + +impl Vertex { + fn desc() -> wgpu::VertexBufferLayout<'static> { + use std::mem; + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + ], + } + } +} + +const VERTICES: &[Vertex] = &[ + Vertex { + position: [-1.0, -1.0, 0.0], + tex_coords: [0.0, 1.0], + }, + Vertex { + position: [-1.0, 1.0, 0.0], + tex_coords: [0.0, 0.0], + }, + Vertex { + position: [1.0, -1.0, 0.0], + tex_coords: [1.0, 1.0], + }, + Vertex { + position: [1.0, 1.0, 0.0], + tex_coords: [1.0, 0.0], + }, +]; + +const INDICES: &[u16] = &[ + 0, 1, 2, // first triangle + 2, 1, 3, // second triangle +]; +struct State<'a> { + surface: wgpu::Surface<'a>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + size: winit::dpi::PhysicalSize, + render_pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + num_indices: u32, + #[allow(dead_code)] + diffuse_texture: texture::Texture, + diffuse_bind_group: wgpu::BindGroup, + window: &'a Window, +} + +impl<'a> State<'a> { + async fn new(window: &'a Window, png: &'a Png) -> State<'a> { + let size = window.inner_size(); + + // The instance is a handle to our GPU + // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + #[cfg(not(target_arch = "wasm32"))] + backends: wgpu::Backends::PRIMARY, + #[cfg(target_arch = "wasm32")] + backends: wgpu::Backends::GL, + ..Default::default() + }); + + let surface = instance.create_surface(window).unwrap(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + // WebGL doesn't support all of wgpu's features, so if + // we're building for the web we'll have to disable some. + required_limits: if cfg!(target_arch = "wasm32") { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }, + memory_hints: Default::default(), + }, + None, // Trace path + ) + .await + .unwrap(); + + let surface_caps = surface.get_capabilities(&adapter); + // Shader code in this tutorial assumes an Srgb surface texture. Using a different + // one will result all the colors comming out darker. If you want to support non + // Srgb surfaces, you'll need to account for that when drawing to the frame. + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(surface_caps.formats[0]); + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + let diffuse_texture = texture::Texture::from_bytes(&device, &queue, png).unwrap(); + + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("texture_bind_group_layout"), + }); + + let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), + }, + ], + label: Some("diffuse_bind_group"), + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[&texture_bind_group_layout], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + // Setting this to anything other than Fill requires Features::POLYGON_MODE_LINE + // or Features::POLYGON_MODE_POINT + polygon_mode: wgpu::PolygonMode::Fill, + // Requires Features::DEPTH_CLIP_CONTROL + unclipped_depth: false, + // Requires Features::CONSERVATIVE_RASTERIZATION + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + // If the pipeline will be used with a multiview render pass, this + // indicates how many array layers the attachments will have. + multiview: None, + // Useful for optimizing shader compilation on Android + cache: None, + }); + + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(VERTICES), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(INDICES), + usage: wgpu::BufferUsages::INDEX, + }); + let num_indices = INDICES.len() as u32; + + Self { + surface, + device, + queue, + config, + size, + render_pipeline, + vertex_buffer, + index_buffer, + num_indices, + diffuse_texture, + diffuse_bind_group, + window, + } + } + + pub fn window(&self) -> &Window { + self.window + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = new_size.width; + self.config.height = new_size.height; + self.surface.configure(&self.device, &self.config); + } + } + + #[allow(unused_variables)] + fn input(&self, event: &WindowEvent) -> bool { + false + } + + fn update(&self) {} + + fn render(&self) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed(0..self.num_indices, 0, 0..1); + } + + self.queue.submit(iter::once(encoder.finish())); + output.present(); + + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] +pub async fn run(png: Png) { + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Warn).expect("Could't initialize logger"); + } else { + env_logger::init(); + } + } + + let event_loop = EventLoop::new().unwrap(); + + let (width, height) = png.dimensions(); + + let window = WindowBuilder::new() + .with_inner_size(PhysicalSize::new(width, height)) + .build(&event_loop) + .unwrap(); + + #[cfg(target_arch = "wasm32")] + { + // Winit prevents sizing with CSS, so we have to set + // the size manually when on web. + use winit::dpi::PhysicalSize; + let _ = window.request_inner_size(PhysicalSize::new(450, 400)); + + use winit::platform::web::WindowExtWebSys; + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| { + let dst = doc.get_element_by_id("wasm-example")?; + let canvas = web_sys::Element::from(window.canvas()?); + dst.append_child(&canvas).ok()?; + Some(()) + }) + .expect("Couldn't append canvas to document body."); + } + + // State::new uses async code, so we're going to wait for it to finish + let mut state = State::new(&window, &png).await; + let mut surface_configured = false; + + event_loop + .run(move |event, control_flow| { + match event { + Event::WindowEvent { + ref event, + window_id, + } if window_id == state.window().id() => { + if !state.input(event) { + match event { + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + physical_key: PhysicalKey::Code(KeyCode::Escape), + .. + }, + .. + } => control_flow.exit(), + WindowEvent::Resized(physical_size) => { + surface_configured = true; + state.resize(*physical_size); + } + WindowEvent::RedrawRequested => { + // This tells winit that we want another frame after this one + state.window().request_redraw(); + + if !surface_configured { + return; + } + + state.update(); + match state.render() { + Ok(_) => {} + // Reconfigure the surface if it's lost or outdated + Err( + wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated, + ) => state.resize(state.size), + // The system is out of memory, we should probably quit + Err(wgpu::SurfaceError::OutOfMemory) => { + log::error!("OutOfMemory"); + control_flow.exit(); + } + + // This happens when the a frame takes too long to present + Err(wgpu::SurfaceError::Timeout) => { + log::warn!("Surface timeout") + } + } + } + _ => {} + } + } + } + _ => {} + } + }) + .unwrap(); +} diff --git a/src/shader.vert b/src/shader.vert new file mode 100644 index 0000000..54ce45c --- /dev/null +++ b/src/shader.vert @@ -0,0 +1,11 @@ +#version 450 + +layout(location=0) in vec3 a_position; +layout(location=1) in vec2 a_tex_coords; + +layout(location=0) out vec2 v_tex_coords; + +void main() { + v_tex_coords = a_tex_coords; + gl_Position = vec4(a_position, 1.0); +} \ No newline at end of file diff --git a/src/shader.vert.spv b/src/shader.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..e8fd20889514ff3c26689a500b50ded4ab8a6601 GIT binary patch literal 1096 zcmZvaL2uJQ5QUc{PC`ptLP$y>6h|D9A`TQGAwUR_dx(^vNL*KuV^u2|CyLz&dg^cM zuj-`|?{mFT4y-kvoi}fH-i%$Zy>1!v!0ed=Gc@E&XV5Qwo`Gmuv5ZfueKvGH=bUgQc5I9lfa!8^h)Qaer7zmm0% z!EfpTXOUaxu5imtXsg8Oxx`&o%-offa^^lqkIH>Z-Rir`i+L6&+Z+=kcbTT!T%fry zBUYe?SrE9OY0n)4#gFCTgPm$eFa8d47qk%lSi1`j+)Oji&%D9(0#61}cU441#_&$!c zhYQYq#LUj0hx=S+%=H)QztWDgXnU`3$h_#01%4|ekpCo<**ualm(R*FKVov!JCRZA yr%q7kMTN<2u|b8Q$MarR7`3OWf)>6t=i!q_^H<%A-cZJjm;*Y9;oqst53+xm##-nA literal 0 HcmV?d00001 diff --git a/src/shader.wgsl b/src/shader.wgsl new file mode 100644 index 0000000..656aac8 --- /dev/null +++ b/src/shader.wgsl @@ -0,0 +1,33 @@ +// Vertex shader + +struct VertexInput { + @location(0) position: vec3, + @location(1) tex_coords: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +} + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.clip_position = vec4(model.position, 1.0); + return out; +} + +// Fragment shader + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(t_diffuse, s_diffuse, in.tex_coords); +} \ No newline at end of file diff --git a/src/texture.rs b/src/texture.rs new file mode 100644 index 0000000..5a3748e --- /dev/null +++ b/src/texture.rs @@ -0,0 +1,75 @@ +use crate::Png; +use anyhow::*; + +pub struct Texture { + #[allow(unused)] + pub texture: wgpu::Texture, + pub view: wgpu::TextureView, + pub sampler: wgpu::Sampler, +} + +impl Texture { + pub fn from_bytes(device: &wgpu::Device, queue: &wgpu::Queue, img: &Png) -> Result { + Self::from_image(device, queue, img, None) + } + + pub fn from_image( + device: &wgpu::Device, + queue: &wgpu::Queue, + img: &Png, + label: Option<&str>, + ) -> Result { + let rgba = img.to_rgba8(); + let dimensions = img.dimensions(); + + let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth_or_array_layers: 1, + }; + let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let texture = device.create_texture(&wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + queue.write_texture( + wgpu::ImageCopyTexture { + aspect: wgpu::TextureAspect::All, + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + }, + &rgba, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(4 * dimensions.0), + rows_per_image: Some(dimensions.1), + }, + size, + ); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Ok(Self { + texture, + view, + sampler, + }) + } +}