tessera_ui_basic_components/pipelines/
image.rs

1use std::{
2    collections::HashMap,
3    hash::{Hash, Hasher},
4    sync::Arc,
5};
6
7use encase::{ShaderType, UniformBuffer};
8use glam::Vec4;
9use tessera_ui::{
10    DrawCommand, PxPosition, PxSize, px::PxRect, renderer::drawer::DrawablePipeline, wgpu,
11};
12
13#[derive(Debug, Clone)]
14/// Image pixel data for rendering.
15///
16/// # Fields
17/// - `data`: Raw pixel data (RGBA).
18/// - `width`: Image width in pixels.
19/// - `height`: Image height in pixels.
20///
21/// # Example
22/// ```rust,ignore
23/// use tessera_ui_basic_components::pipelines::image::ImageData;
24/// let img = ImageData { data: Arc::new(vec![255, 0, 0, 255]), width: 1, height: 1 };
25/// ```
26pub struct ImageData {
27    pub data: Arc<Vec<u8>>,
28    pub width: u32,
29    pub height: u32,
30}
31
32impl Hash for ImageData {
33    fn hash<H: Hasher>(&self, state: &mut H) {
34        self.data.as_ref().hash(state);
35        self.width.hash(state);
36        self.height.hash(state);
37    }
38}
39
40impl PartialEq for ImageData {
41    fn eq(&self, other: &Self) -> bool {
42        self.width == other.width
43            && self.height == other.height
44            && self.data.as_ref() == other.data.as_ref()
45    }
46}
47
48impl Eq for ImageData {}
49
50#[derive(Debug, Clone, Hash, PartialEq, Eq)]
51/// Command for rendering an image in a UI component.
52///
53/// # Example
54/// ```rust,ignore
55/// use tessera_ui_basic_components::pipelines::image::{ImageCommand, ImageData};
56/// let cmd = ImageCommand { data: img_data };
57/// ```
58pub struct ImageCommand {
59    pub data: Arc<ImageData>,
60}
61
62impl DrawCommand for ImageCommand {
63    fn barrier(&self) -> Option<tessera_ui::BarrierRequirement> {
64        // This command does not require any specific barriers.
65        None
66    }
67}
68
69#[derive(ShaderType)]
70struct ImageUniforms {
71    rect: Vec4,
72    is_bgra: u32,
73}
74
75struct ImageResources {
76    bind_group: wgpu::BindGroup,
77    uniform_buffer: wgpu::Buffer,
78}
79
80/// Pipeline for rendering images in UI components.
81///
82/// # Example
83/// ```rust,ignore
84/// use tessera_ui_basic_components::pipelines::image::ImagePipeline;
85/// let pipeline = ImagePipeline::new(&device, &config, sample_count);
86/// ```
87pub struct ImagePipeline {
88    pipeline: wgpu::RenderPipeline,
89    bind_group_layout: wgpu::BindGroupLayout,
90    resources: HashMap<ImageData, ImageResources>,
91}
92
93impl ImagePipeline {
94    /// Create a new ImagePipeline.
95    pub fn new(
96        device: &wgpu::Device,
97        config: &wgpu::SurfaceConfiguration,
98        sample_count: u32,
99    ) -> Self {
100        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
101            label: Some("Image Shader"),
102            source: wgpu::ShaderSource::Wgsl(include_str!("image/image.wgsl").into()),
103        });
104
105        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
106            entries: &[
107                wgpu::BindGroupLayoutEntry {
108                    binding: 0,
109                    visibility: wgpu::ShaderStages::FRAGMENT,
110                    ty: wgpu::BindingType::Texture {
111                        multisampled: false,
112                        view_dimension: wgpu::TextureViewDimension::D2,
113                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
114                    },
115                    count: None,
116                },
117                wgpu::BindGroupLayoutEntry {
118                    binding: 1,
119                    visibility: wgpu::ShaderStages::FRAGMENT,
120                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
121                    count: None,
122                },
123                wgpu::BindGroupLayoutEntry {
124                    binding: 2,
125                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
126                    ty: wgpu::BindingType::Buffer {
127                        ty: wgpu::BufferBindingType::Uniform,
128                        has_dynamic_offset: false,
129                        min_binding_size: None,
130                    },
131                    count: None,
132                },
133            ],
134            label: Some("texture_bind_group_layout"),
135        });
136
137        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
138            label: Some("Image Pipeline Layout"),
139            bind_group_layouts: &[&bind_group_layout],
140            push_constant_ranges: &[],
141        });
142
143        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
144            label: Some("Image Render Pipeline"),
145            layout: Some(&pipeline_layout),
146            vertex: wgpu::VertexState {
147                module: &shader,
148                entry_point: Some("vs_main"),
149                buffers: &[],
150                compilation_options: Default::default(),
151            },
152            fragment: Some(wgpu::FragmentState {
153                module: &shader,
154                entry_point: Some("fs_main"),
155                targets: &[Some(wgpu::ColorTargetState {
156                    format: config.format,
157                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
158                    write_mask: wgpu::ColorWrites::ALL,
159                })],
160                compilation_options: Default::default(),
161            }),
162            primitive: wgpu::PrimitiveState::default(),
163            depth_stencil: None,
164            multisample: wgpu::MultisampleState {
165                count: sample_count,
166                mask: !0,
167                alpha_to_coverage_enabled: false,
168            },
169            multiview: None,
170            cache: None,
171        });
172
173        Self {
174            pipeline,
175            bind_group_layout,
176            resources: HashMap::new(),
177        }
178    }
179
180    /// Return existing resources for `data` or create them.
181    fn get_or_create_resources(
182        &mut self,
183        device: &wgpu::Device,
184        queue: &wgpu::Queue,
185        config: &wgpu::SurfaceConfiguration,
186        data: &ImageData,
187    ) -> &ImageResources {
188        self.resources.entry(data.clone()).or_insert_with(|| {
189            Self::create_image_resources(device, queue, config, &self.bind_group_layout, data)
190        })
191    }
192
193    /// Compute the ImageUniforms for a given command size and position.
194    fn compute_uniforms(
195        start_pos: PxPosition,
196        size: PxSize,
197        config: &wgpu::SurfaceConfiguration,
198    ) -> ImageUniforms {
199        // Convert pixel positions/sizes into normalized device coordinates and size ratios.
200        let rect = [
201            (start_pos.x.0 as f32 / config.width as f32) * 2.0 - 1.0
202                + (size.width.0 as f32 / config.width as f32),
203            (start_pos.y.0 as f32 / config.height as f32) * -2.0 + 1.0
204                - (size.height.0 as f32 / config.height as f32),
205            size.width.0 as f32 / config.width as f32,
206            size.height.0 as f32 / config.height as f32,
207        ]
208        .into();
209
210        let is_bgra = matches!(
211            config.format,
212            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
213        );
214
215        ImageUniforms {
216            rect,
217            is_bgra: if is_bgra { 1 } else { 0 },
218        }
219    }
220
221    // Create GPU resources for an image. Kept as a single helper to avoid duplicating
222    // GPU setup logic while keeping `draw` concise.
223    fn create_image_resources(
224        device: &wgpu::Device,
225        queue: &wgpu::Queue,
226        config: &wgpu::SurfaceConfiguration,
227        layout: &wgpu::BindGroupLayout,
228        data: &ImageData,
229    ) -> ImageResources {
230        let texture_size = wgpu::Extent3d {
231            width: data.width,
232            height: data.height,
233            depth_or_array_layers: 1,
234        };
235        let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor {
236            size: texture_size,
237            mip_level_count: 1,
238            sample_count: 1,
239            dimension: wgpu::TextureDimension::D2,
240            format: config.format,
241            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
242            label: Some("diffuse_texture"),
243            view_formats: &[],
244        });
245
246        queue.write_texture(
247            wgpu::TexelCopyTextureInfo {
248                texture: &diffuse_texture,
249                mip_level: 0,
250                origin: wgpu::Origin3d::ZERO,
251                aspect: wgpu::TextureAspect::All,
252            },
253            &data.data,
254            wgpu::TexelCopyBufferLayout {
255                offset: 0,
256                bytes_per_row: Some(4 * data.width),
257                rows_per_image: Some(data.height),
258            },
259            texture_size,
260        );
261
262        let diffuse_texture_view =
263            diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
264        let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
265            address_mode_u: wgpu::AddressMode::ClampToEdge,
266            address_mode_v: wgpu::AddressMode::ClampToEdge,
267            address_mode_w: wgpu::AddressMode::ClampToEdge,
268            mag_filter: wgpu::FilterMode::Linear,
269            min_filter: wgpu::FilterMode::Nearest,
270            mipmap_filter: wgpu::FilterMode::Nearest,
271            ..Default::default()
272        });
273
274        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
275            label: Some("Image Uniform Buffer"),
276            size: ImageUniforms::min_size().get(),
277            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
278            mapped_at_creation: false,
279        });
280
281        let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
282            layout,
283            entries: &[
284                wgpu::BindGroupEntry {
285                    binding: 0,
286                    resource: wgpu::BindingResource::TextureView(&diffuse_texture_view),
287                },
288                wgpu::BindGroupEntry {
289                    binding: 1,
290                    resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
291                },
292                wgpu::BindGroupEntry {
293                    binding: 2,
294                    resource: uniform_buffer.as_entire_binding(),
295                },
296            ],
297            label: Some("diffuse_bind_group"),
298        });
299
300        ImageResources {
301            bind_group: diffuse_bind_group,
302            uniform_buffer,
303        }
304    }
305}
306
307impl DrawablePipeline<ImageCommand> for ImagePipeline {
308    fn draw(
309        &mut self,
310        gpu: &wgpu::Device,
311        gpu_queue: &wgpu::Queue,
312        config: &wgpu::SurfaceConfiguration,
313        render_pass: &mut wgpu::RenderPass<'_>,
314        commands: &[(&ImageCommand, PxSize, PxPosition)],
315        _scene_texture_view: &wgpu::TextureView,
316        _clip_rect: Option<PxRect>,
317    ) {
318        render_pass.set_pipeline(&self.pipeline);
319
320        for (command, size, start_pos) in commands {
321            // Use the extracted helper to obtain or create GPU resources.
322            let resources = self.get_or_create_resources(gpu, gpu_queue, config, &command.data);
323
324            // Use the extracted uniforms computation helper (dereference borrowed tuple elements).
325            let uniforms = Self::compute_uniforms(*start_pos, *size, config);
326
327            let mut buffer = UniformBuffer::new(Vec::new());
328            buffer.write(&uniforms).unwrap();
329            gpu_queue.write_buffer(&resources.uniform_buffer, 0, &buffer.into_inner());
330
331            render_pass.set_bind_group(0, &resources.bind_group, &[]);
332            render_pass.draw(0..6, 0..1);
333        }
334    }
335}