//! RGBA image pipeline: one wgpu texture per image, one draw call per image. //! //! MVP scope: a fixed pool of image slots, each a `wgpu::Texture` and a //! precomputed bind group. `set_image` uploads or replaces a slot's texture; //! `draw_frame` draws all live slots at their placement rectangles. Instance //! batching within a single pipeline is not worth it while we're expecting //! ~1 image on screen at a time (yazi preview pane). use std::collections::HashMap; use bytemuck::{Pod, Zeroable}; use wgpu::util::DeviceExt; /// Placement of an image on the screen, in physical pixels. #[derive(Debug, Clone, Copy)] pub struct ImagePlacement { pub image_id: u32, pub x: f32, pub y: f32, pub w: f32, pub h: f32, } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct ImageUniforms { screen: [f32; 2], _pad: [f32; 2], } const IMAGE_SHADER: &str = r" struct Uniforms { screen: vec2, _pad: vec2, }; @group(0) @binding(0) var u: Uniforms; @group(0) @binding(1) var tex: texture_2d; @group(0) @binding(2) var samp: sampler; struct VOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @vertex fn vs( @location(0) corner: vec2, @location(1) inst_pos: vec2, @location(2) inst_size: vec2, ) -> VOut { let px = inst_pos + corner * inst_size; let ndc = vec2( (px.x / u.screen.x) * 2.0 - 1.0, 1.0 - (px.y / u.screen.y) * 2.0, ); var out: VOut; out.pos = vec4(ndc, 0.0, 1.0); out.uv = corner; return out; } @fragment fn fs(in: VOut) -> @location(0) vec4 { return textureSample(tex, samp, in.uv); } "; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct ImgInstance { pos: [f32; 2], size: [f32; 2], } const QUAD: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; const INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3]; struct ImageSlot { _texture: wgpu::Texture, bind_group: wgpu::BindGroup, } pub struct ImageRenderer { pipeline: wgpu::RenderPipeline, bind_group_layout: wgpu::BindGroupLayout, sampler: wgpu::Sampler, uniforms_buf: wgpu::Buffer, quad_buf: wgpu::Buffer, index_buf: wgpu::Buffer, instance_buf: wgpu::Buffer, images: HashMap, } impl ImageRenderer { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { let uniforms_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.image.uniforms"), contents: bytemuck::bytes_of(&ImageUniforms { screen: [1.0, 1.0], _pad: [0.0, 0.0], }), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let quad_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.image.quad"), contents: bytemuck::cast_slice(&QUAD), usage: wgpu::BufferUsages::VERTEX, }); let index_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.image.index"), contents: bytemuck::cast_slice(&INDICES), usage: wgpu::BufferUsages::INDEX, }); let instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.image.instance"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("shop.image.sampler"), mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("shop.image.bgl"), entries: &[ 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, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("shop.image.shader"), source: wgpu::ShaderSource::Wgsl(IMAGE_SHADER.into()), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("shop.image.pl"), bind_group_layouts: &[Some(&bind_group_layout)], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("shop.image.pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), compilation_options: wgpu::PipelineCompilationOptions::default(), buffers: &[ Some(wgpu::VertexBufferLayout { array_stride: 8, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array![0 => Float32x2], }), Some(wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array![ 1 => Float32x2, 2 => Float32x2, ], }), ], }, fragment: Some(wgpu::FragmentState { module: &module, entry_point: Some("fs"), compilation_options: wgpu::PipelineCompilationOptions::default(), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); Self { pipeline, bind_group_layout, sampler, uniforms_buf, quad_buf, index_buf, instance_buf, images: HashMap::new(), } } pub fn resize(&self, queue: &wgpu::Queue, width: u32, height: u32) { let u = ImageUniforms { screen: [width as f32, height as f32], _pad: [0.0, 0.0], }; queue.write_buffer(&self.uniforms_buf, 0, bytemuck::bytes_of(&u)); } /// Upload an RGBA image and register it under `id`. Replaces any existing /// image at the same id. pub fn set_image( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, id: u32, rgba: &[u8], width: u32, height: u32, ) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("shop.image.tex"), size: wgpu::Extent3d { width, height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, rgba, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(width * 4), rows_per_image: Some(height), }, wgpu::Extent3d { width, height, depth_or_array_layers: 1, }, ); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("shop.image.bg"), layout: &self.bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: self.uniforms_buf.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&view), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler), }, ], }); self.images.insert( id, ImageSlot { _texture: texture, bind_group, }, ); } pub fn drop_image(&mut self, id: u32) { self.images.remove(&id); } pub fn drop_all(&mut self) { self.images.clear(); } /// Draw each placement in order (one texture bind + draw call per image). pub fn draw( &self, queue: &wgpu::Queue, view: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, placements: &[ImagePlacement], ) { if placements.is_empty() { return; } let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("shop.image.pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); pass.set_pipeline(&self.pipeline); pass.set_vertex_buffer(0, self.quad_buf.slice(..)); pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); for placement in placements { let Some(slot) = self.images.get(&placement.image_id) else { continue; }; queue.write_buffer( &self.instance_buf, 0, bytemuck::bytes_of(&ImgInstance { pos: [placement.x, placement.y], size: [placement.w, placement.h], }), ); pass.set_vertex_buffer(1, self.instance_buf.slice(..)); pass.set_bind_group(0, &slot.bind_group, &[]); pass.draw_indexed(0..6, 0, 0..1); } } }