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