Skip to main content

viewport_lib/resources/overlay/
overlay_shape.rs

1//! Lazy pipeline creation for SDF overlay shapes.
2
3use crate::renderer::OverlayTextureId;
4
5/// SDF overlay shape pipelines (solid + textured fill) and their shared sampler.
6/// Lazily built on the first frame with non-empty shapes.
7#[derive(Default)]
8pub(crate) struct OverlayShapeResources {
9    /// Render pipeline for screen-space SDF shapes (rounded rects, circles, etc.).
10    pub(crate) pipeline: Option<wgpu::RenderPipeline>,
11    /// Render pipeline for SDF shapes with texture fill.
12    pub(crate) tex_pipeline: Option<wgpu::RenderPipeline>,
13    /// Bind group layout for the texture pipeline (group 0: texture + sampler).
14    pub(crate) tex_bgl: Option<wgpu::BindGroupLayout>,
15    /// Clamp-to-edge linear sampler shared across all texture shape bind groups.
16    pub(crate) tex_sampler: Option<wgpu::Sampler>,
17}
18
19/// Fullscreen separable Gaussian blur pipeline used to produce the blurred
20/// scene texture for `backdrop_blur` overlay shapes.
21#[derive(Default)]
22pub(crate) struct BackdropBlurResources {
23    /// Fullscreen separable Gaussian blur pipeline.
24    pub(crate) pipeline: Option<wgpu::RenderPipeline>,
25    /// Bind group layout (group 0: source texture + sampler + uniforms).
26    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
27    /// Linear clamp sampler shared by blur passes.
28    pub(crate) sampler: Option<wgpu::Sampler>,
29}
30
31impl crate::resources::DeviceResources {
32    /// Lazily create the SDF overlay shape render pipeline.
33    ///
34    /// No-op if already created. Called from `prepare_viewport_internal()` when
35    /// `frame.overlays.shapes` is non-empty.
36    pub(crate) fn ensure_overlay_shape_pipeline(&mut self, device: &wgpu::Device) {
37        if self.overlay_shape.pipeline.is_some() {
38            return;
39        }
40
41        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
42            label: Some("overlay_shape_layout"),
43            bind_group_layouts: &[],
44            push_constant_ranges: &[],
45        });
46
47        let shader = crate::resources::builders::wgsl_module(
48            device,
49            "overlay_shape.wgsl",
50            crate::resources::builders::wgsl_source!("overlay_shape"),
51        );
52
53        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
54            label: Some("overlay_shape_pipeline"),
55            layout: Some(&layout),
56            vertex: wgpu::VertexState {
57                module: &shader,
58                entry_point: Some("vs_main"),
59                buffers: &[OverlayShapeVertex::buffer_layout()],
60                compilation_options: wgpu::PipelineCompilationOptions::default(),
61            },
62            primitive: wgpu::PrimitiveState {
63                topology: wgpu::PrimitiveTopology::TriangleList,
64                ..Default::default()
65            },
66            depth_stencil: Some(wgpu::DepthStencilState {
67                format: wgpu::TextureFormat::Depth24PlusStencil8,
68                depth_write_enabled: false,
69                depth_compare: wgpu::CompareFunction::Always,
70                stencil: wgpu::StencilState::default(),
71                bias: wgpu::DepthBiasState::default(),
72            }),
73            multisample: wgpu::MultisampleState::default(),
74            fragment: Some(wgpu::FragmentState {
75                module: &shader,
76                entry_point: Some("fs_main"),
77                targets: &[Some(wgpu::ColorTargetState {
78                    format: self.target_format,
79                    blend: Some(wgpu::BlendState {
80                        color: wgpu::BlendComponent {
81                            src_factor: wgpu::BlendFactor::SrcAlpha,
82                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
83                            operation: wgpu::BlendOperation::Add,
84                        },
85                        alpha: wgpu::BlendComponent {
86                            src_factor: wgpu::BlendFactor::One,
87                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
88                            operation: wgpu::BlendOperation::Add,
89                        },
90                    }),
91                    write_mask: wgpu::ColorWrites::ALL,
92                })],
93                compilation_options: wgpu::PipelineCompilationOptions::default(),
94            }),
95            multiview: None,
96            cache: None,
97        });
98
99        self.overlay_shape.pipeline = Some(pipeline);
100    }
101
102    /// Lazily create the SDF overlay shape render pipeline with texture fill.
103    ///
104    /// No-op if already created. Called from `prepare_viewport_internal()` when
105    /// any shape in `OverlayFrame.shapes` carries an `OverlayTextureId`.
106    pub(crate) fn ensure_overlay_shape_tex_pipeline(&mut self, device: &wgpu::Device) {
107        if self.overlay_shape.tex_pipeline.is_some() {
108            return;
109        }
110
111        let bgl = crate::resources::builders::texture_sampler_bgl(
112            device,
113            "overlay_shape_tex_bgl",
114            wgpu::ShaderStages::FRAGMENT,
115        );
116
117        let sampler =
118            crate::resources::builders::clamp_linear_sampler(device, "overlay_shape_tex_sampler");
119
120        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
121            label: Some("overlay_shape_tex_layout"),
122            bind_group_layouts: &[&bgl],
123            push_constant_ranges: &[],
124        });
125
126        let shader = crate::resources::builders::wgsl_module(
127            device,
128            "overlay_shape_tex.wgsl",
129            crate::resources::builders::wgsl_source!("overlay_shape_tex"),
130        );
131
132        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
133            label: Some("overlay_shape_tex_pipeline"),
134            layout: Some(&layout),
135            vertex: wgpu::VertexState {
136                module: &shader,
137                entry_point: Some("vs_main"),
138                buffers: &[OverlayShapeTexVertex::buffer_layout()],
139                compilation_options: wgpu::PipelineCompilationOptions::default(),
140            },
141            primitive: wgpu::PrimitiveState {
142                topology: wgpu::PrimitiveTopology::TriangleList,
143                ..Default::default()
144            },
145            depth_stencil: Some(wgpu::DepthStencilState {
146                format: wgpu::TextureFormat::Depth24PlusStencil8,
147                depth_write_enabled: false,
148                depth_compare: wgpu::CompareFunction::Always,
149                stencil: wgpu::StencilState::default(),
150                bias: wgpu::DepthBiasState::default(),
151            }),
152            multisample: wgpu::MultisampleState::default(),
153            fragment: Some(wgpu::FragmentState {
154                module: &shader,
155                entry_point: Some("fs_main"),
156                targets: &[Some(wgpu::ColorTargetState {
157                    format: self.target_format,
158                    blend: Some(wgpu::BlendState {
159                        color: wgpu::BlendComponent {
160                            src_factor: wgpu::BlendFactor::SrcAlpha,
161                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
162                            operation: wgpu::BlendOperation::Add,
163                        },
164                        alpha: wgpu::BlendComponent {
165                            src_factor: wgpu::BlendFactor::One,
166                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
167                            operation: wgpu::BlendOperation::Add,
168                        },
169                    }),
170                    write_mask: wgpu::ColorWrites::ALL,
171                })],
172                compilation_options: wgpu::PipelineCompilationOptions::default(),
173            }),
174            multiview: None,
175            cache: None,
176        });
177
178        self.overlay_shape.tex_bgl = Some(bgl);
179        self.overlay_shape.tex_sampler = Some(sampler);
180        self.overlay_shape.tex_pipeline = Some(pipeline);
181    }
182
183    /// Upload RGBA8 pixel data as a persistent texture for overlay shape fills.
184    ///
185    /// Returns an `OverlayTextureId` that can be stored in
186    /// `OverlayShapeItem::texture`. The texture persists for the lifetime of
187    /// this `DeviceResources`.
188    ///
189    /// `rgba_data` must contain exactly `width * height * 4` bytes in
190    /// row-major, top-to-bottom order. The data is treated as sRGB-encoded
191    /// (standard 8-bit image data).
192    pub fn upload_overlay_texture(
193        &mut self,
194        device: &wgpu::Device,
195        queue: &wgpu::Queue,
196        width: u32,
197        height: u32,
198        rgba_data: &[u8],
199    ) -> OverlayTextureId {
200        assert_eq!(
201            rgba_data.len(),
202            (width * height * 4) as usize,
203            "upload_overlay_texture: rgba_data length does not match width * height * 4"
204        );
205
206        let texture = device.create_texture(&wgpu::TextureDescriptor {
207            label: Some("overlay_shape_tex"),
208            size: wgpu::Extent3d {
209                width,
210                height,
211                depth_or_array_layers: 1,
212            },
213            mip_level_count: 1,
214            sample_count: 1,
215            dimension: wgpu::TextureDimension::D2,
216            format: wgpu::TextureFormat::Rgba8UnormSrgb,
217            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
218            view_formats: &[],
219        });
220
221        queue.write_texture(
222            wgpu::TexelCopyTextureInfo {
223                texture: &texture,
224                mip_level: 0,
225                origin: wgpu::Origin3d::ZERO,
226                aspect: wgpu::TextureAspect::All,
227            },
228            rgba_data,
229            wgpu::TexelCopyBufferLayout {
230                offset: 0,
231                bytes_per_row: Some(width * 4),
232                rows_per_image: Some(height),
233            },
234            wgpu::Extent3d {
235                width,
236                height,
237                depth_or_array_layers: 1,
238            },
239        );
240
241        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
242        let id = self
243            .content
244            .overlay_textures
245            .push(OverlayShapeTextureEntry {
246                _texture: texture,
247                view,
248            });
249        OverlayTextureId(id as u64)
250    }
251
252    /// Start an asynchronous overlay texture upload.
253    ///
254    /// Returns a [`JobId`](crate::resources::JobId) immediately. Texture
255    /// creation and `queue.write_texture` run on a worker thread on cloned
256    /// `Device` and `Queue` handles; the apply step inserts the prepared
257    /// `OverlayShapeTextureEntry` into the store. Take the resulting
258    /// [`OverlayTextureId`] via
259    /// [`upload_result_overlay_texture`](Self::upload_result_overlay_texture).
260    ///
261    /// Ownership of `rgba_data` transfers into the worker.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
266    /// before submission when `rgba_data.len() != width * height * 4`.
267    pub fn begin_upload_overlay_texture(
268        &mut self,
269        device: &wgpu::Device,
270        queue: &wgpu::Queue,
271        width: u32,
272        height: u32,
273        rgba_data: Vec<u8>,
274    ) -> crate::error::ViewportResult<crate::resources::JobId> {
275        let expected = (width * height * 4) as usize;
276        if rgba_data.len() != expected {
277            return Err(crate::error::ViewportError::InvalidTextureData {
278                expected,
279                actual: rgba_data.len(),
280            });
281        }
282
283        let slot = crate::resources::ResultSlot::<OverlayTextureId>::new();
284        let slot_for_apply = slot.clone();
285        let device_for_worker = device.clone();
286        let queue_for_worker = queue.clone();
287
288        let id = {
289            let mut runner = self.jobs.lock().expect("upload job runner poisoned");
290            runner.submit_cpu(move |progress| {
291                progress.set(0.1);
292                let texture = device_for_worker.create_texture(&wgpu::TextureDescriptor {
293                    label: Some("overlay_shape_tex"),
294                    size: wgpu::Extent3d {
295                        width,
296                        height,
297                        depth_or_array_layers: 1,
298                    },
299                    mip_level_count: 1,
300                    sample_count: 1,
301                    dimension: wgpu::TextureDimension::D2,
302                    format: wgpu::TextureFormat::Rgba8UnormSrgb,
303                    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
304                    view_formats: &[],
305                });
306                queue_for_worker.write_texture(
307                    wgpu::TexelCopyTextureInfo {
308                        texture: &texture,
309                        mip_level: 0,
310                        origin: wgpu::Origin3d::ZERO,
311                        aspect: wgpu::TextureAspect::All,
312                    },
313                    &rgba_data,
314                    wgpu::TexelCopyBufferLayout {
315                        offset: 0,
316                        bytes_per_row: Some(width * 4),
317                        rows_per_image: Some(height),
318                    },
319                    wgpu::Extent3d {
320                        width,
321                        height,
322                        depth_or_array_layers: 1,
323                    },
324                );
325                let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
326                progress.set(0.95);
327                Ok(crate::resources::upload_jobs::JobProduct::with_apply(
328                    Box::new(move |resources: &mut crate::resources::DeviceResources| {
329                        let id =
330                            resources
331                                .content
332                                .overlay_textures
333                                .push(OverlayShapeTextureEntry {
334                                    _texture: texture,
335                                    view,
336                                });
337                        slot_for_apply.set(OverlayTextureId(id as u64));
338                    }),
339                ))
340            })
341        };
342
343        self.job_results
344            .overlay_texture
345            .lock()
346            .expect("overlay texture result map poisoned")
347            .insert(id, slot);
348        Ok(id)
349    }
350
351    /// Take the [`OverlayTextureId`] produced by a completed
352    /// [`begin_upload_overlay_texture`](Self::begin_upload_overlay_texture) job.
353    pub fn upload_result_overlay_texture(
354        &mut self,
355        id: crate::resources::JobId,
356    ) -> crate::error::ViewportResult<OverlayTextureId> {
357        let mut map = self
358            .job_results
359            .overlay_texture
360            .lock()
361            .expect("overlay texture result map poisoned");
362        let slot = match map.get(&id) {
363            Some(s) => s.clone(),
364            None => {
365                return Err(crate::error::ViewportError::JobResultMissing {
366                    reason: "unknown id or wrong upload type",
367                });
368            }
369        };
370        match slot.take() {
371            Some(tid) => {
372                map.remove(&id);
373                Ok(tid)
374            }
375            None => Err(crate::error::ViewportError::JobNotReady),
376        }
377    }
378
379    /// Lazily create the separable Gaussian blur pipeline used to blur the
380    /// scene texture for `backdrop_blur` overlay shapes.
381    ///
382    /// No-op if already created.
383    pub(crate) fn ensure_backdrop_blur_pipeline(&mut self, device: &wgpu::Device) {
384        if self.backdrop_blur.pipeline.is_some() {
385            return;
386        }
387
388        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
389            label: Some("backdrop_blur_bgl"),
390            entries: &[
391                // binding 0: source texture
392                wgpu::BindGroupLayoutEntry {
393                    binding: 0,
394                    visibility: wgpu::ShaderStages::FRAGMENT,
395                    ty: wgpu::BindingType::Texture {
396                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
397                        view_dimension: wgpu::TextureViewDimension::D2,
398                        multisampled: false,
399                    },
400                    count: None,
401                },
402                // binding 1: sampler
403                wgpu::BindGroupLayoutEntry {
404                    binding: 1,
405                    visibility: wgpu::ShaderStages::FRAGMENT,
406                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
407                    count: None,
408                },
409                // binding 2: blur parameters uniform
410                wgpu::BindGroupLayoutEntry {
411                    binding: 2,
412                    visibility: wgpu::ShaderStages::FRAGMENT,
413                    ty: wgpu::BindingType::Buffer {
414                        ty: wgpu::BufferBindingType::Uniform,
415                        has_dynamic_offset: false,
416                        min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()),
417                    },
418                    count: None,
419                },
420            ],
421        });
422
423        let sampler =
424            crate::resources::builders::clamp_linear_sampler(device, "backdrop_blur_sampler");
425
426        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
427            label: Some("backdrop_blur_layout"),
428            bind_group_layouts: &[&bgl],
429            push_constant_ranges: &[],
430        });
431
432        let shader = crate::resources::builders::wgsl_module(
433            device,
434            "backdrop_blur.wgsl",
435            crate::resources::builders::wgsl_source!("backdrop_blur"),
436        );
437
438        let pipeline = crate::resources::builders::build_fullscreen_pipeline(
439            device,
440            "backdrop_blur_pipeline",
441            &layout,
442            &shader,
443            self.target_format,
444            None,
445        );
446
447        self.backdrop_blur.bgl = Some(bgl);
448        self.backdrop_blur.sampler = Some(sampler);
449        self.backdrop_blur.pipeline = Some(pipeline);
450    }
451}
452
453/// Per-vertex data for SDF overlay shapes.
454///
455/// Each shape is a bounding quad (6 vertices). The fragment shader uses
456/// `local_pos` and `half_size` to evaluate a signed-distance function,
457/// producing anti-aliased fill and border.
458#[repr(C)]
459#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
460pub(crate) struct OverlayShapeVertex {
461    /// NDC position (xy).
462    pub position: [f32; 2],
463    /// Position relative to shape centre, in logical pixels.
464    pub local_pos: [f32; 2],
465    /// RGBA fill colour (pre-multiplied opacity).
466    pub fill_colour: [f32; 4],
467    /// RGBA border colour (pre-multiplied opacity).
468    pub border_colour: [f32; 4],
469    /// Half-extents of the shape bounding box in logical pixels.
470    pub half_size: [f32; 2],
471    /// Shape-specific radii. For RoundedRect: [top-left, top-right,
472    /// bottom-right, bottom-left]. For Rect: uniform radius in [0].
473    /// Unused components are zero.
474    pub radii: [f32; 4],
475    /// Border thickness in logical pixels.
476    pub border_width: f32,
477    /// Encoded shape type: 0 = Rect/RoundedRect, 1 = Circle, 2 = Ellipse, 3 = Capsule.
478    pub shape_type: f32,
479    /// RGBA end colour for linear gradient (equals fill_colour for solid fill).
480    pub fill_colour2: [f32; 4],
481    /// Gradient parameters: `[type, angle, stop_count, _pad]`.
482    /// `type` selects solid/linear/radial/conical; `stop_count` is the
483    /// number of active gradient stops (0 for solid, 2..4 otherwise).
484    pub gradient_params: [f32; 4],
485    /// RGBA shadow colour (pre-multiplied opacity).
486    pub shadow_colour: [f32; 4],
487    /// Shadow parameters: x = radius (pixels), y = offset_x, z = offset_y.
488    pub shadow_params: [f32; 4],
489    /// Clip rectangle in framebuffer pixels (x0, y0, x1, y1). All-zero means
490    /// no clipping. Fragments outside the rect are discarded.
491    pub clip_rect: [f32; 4],
492    /// Rotation around the shape centre in radians. Applied to `local_pos`
493    /// before SDF evaluation so the shape rotates inside its axis-aligned
494    /// bounding box.
495    pub rotation: f32,
496    /// Third gradient colour stop. Unused for 2-stop and solid fills.
497    pub stop_colour_c: [f32; 4],
498    /// Fourth gradient colour stop. Unused for 2-stop and solid fills.
499    pub stop_colour_d: [f32; 4],
500    /// Stop positions in `[0, 1]` along the gradient axis, sorted ascending.
501    /// For 2-stop fills only `[0]` and `[1]` are valid; remaining entries
502    /// are 1.0 sentinels. The active stop count lives in
503    /// `gradient_params[2]`.
504    pub stop_positions: [f32; 4],
505}
506
507impl OverlayShapeVertex {
508    pub fn buffer_layout() -> wgpu::VertexBufferLayout<'static> {
509        wgpu::VertexBufferLayout {
510            array_stride: std::mem::size_of::<OverlayShapeVertex>() as wgpu::BufferAddress,
511            step_mode: wgpu::VertexStepMode::Vertex,
512            attributes: &[
513                // location 0: position vec2f
514                wgpu::VertexAttribute {
515                    offset: 0,
516                    shader_location: 0,
517                    format: wgpu::VertexFormat::Float32x2,
518                },
519                // location 1: local_pos vec2f
520                wgpu::VertexAttribute {
521                    offset: 8,
522                    shader_location: 1,
523                    format: wgpu::VertexFormat::Float32x2,
524                },
525                // location 2: fill_colour vec4f
526                wgpu::VertexAttribute {
527                    offset: 16,
528                    shader_location: 2,
529                    format: wgpu::VertexFormat::Float32x4,
530                },
531                // location 3: border_colour vec4f
532                wgpu::VertexAttribute {
533                    offset: 32,
534                    shader_location: 3,
535                    format: wgpu::VertexFormat::Float32x4,
536                },
537                // location 4: half_size vec2f
538                wgpu::VertexAttribute {
539                    offset: 48,
540                    shader_location: 4,
541                    format: wgpu::VertexFormat::Float32x2,
542                },
543                // location 5: radii vec4f
544                wgpu::VertexAttribute {
545                    offset: 56,
546                    shader_location: 5,
547                    format: wgpu::VertexFormat::Float32x4,
548                },
549                // location 6: shape_meta vec2f (border_width, shape_type) -- combined
550                wgpu::VertexAttribute {
551                    offset: 72,
552                    shader_location: 6,
553                    format: wgpu::VertexFormat::Float32x2,
554                },
555                // location 7: stop_positions vec4f (gradient stop positions)
556                wgpu::VertexAttribute {
557                    offset: 196,
558                    shader_location: 7,
559                    format: wgpu::VertexFormat::Float32x4,
560                },
561                // location 8: fill_colour2 vec4f (end colour for gradient)
562                wgpu::VertexAttribute {
563                    offset: 80,
564                    shader_location: 8,
565                    format: wgpu::VertexFormat::Float32x4,
566                },
567                // location 9: gradient_params vec4f (type, angle, stop_count, pad)
568                wgpu::VertexAttribute {
569                    offset: 96,
570                    shader_location: 9,
571                    format: wgpu::VertexFormat::Float32x4,
572                },
573                // location 10: shadow_colour vec4f
574                wgpu::VertexAttribute {
575                    offset: 112,
576                    shader_location: 10,
577                    format: wgpu::VertexFormat::Float32x4,
578                },
579                // location 11: shadow_params vec4f (radius, offset_x, offset_y, border_mode)
580                wgpu::VertexAttribute {
581                    offset: 128,
582                    shader_location: 11,
583                    format: wgpu::VertexFormat::Float32x4,
584                },
585                // location 12: clip_rect vec4f (x0, y0, x1, y1 in framebuffer pixels)
586                wgpu::VertexAttribute {
587                    offset: 144,
588                    shader_location: 12,
589                    format: wgpu::VertexFormat::Float32x4,
590                },
591                // location 13: rotation f32 (radians)
592                wgpu::VertexAttribute {
593                    offset: 160,
594                    shader_location: 13,
595                    format: wgpu::VertexFormat::Float32,
596                },
597                // location 14: stop_colour_c vec4f
598                wgpu::VertexAttribute {
599                    offset: 164,
600                    shader_location: 14,
601                    format: wgpu::VertexFormat::Float32x4,
602                },
603                // location 15: stop_colour_d vec4f
604                wgpu::VertexAttribute {
605                    offset: 180,
606                    shader_location: 15,
607                    format: wgpu::VertexFormat::Float32x4,
608                },
609            ],
610        }
611    }
612}
613
614/// Per-vertex data for SDF textured overlay shapes.
615///
616/// Same layout as `OverlayShapeVertex` with an additional UV field at the end.
617/// Used by the texture pipeline; `fill_colour` acts as a tint multiplied with
618/// the sampled texel.
619#[repr(C)]
620#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
621pub(crate) struct OverlayShapeTexVertex {
622    /// NDC position (xy).
623    pub position: [f32; 2],
624    /// Position relative to shape centre, in logical pixels.
625    pub local_pos: [f32; 2],
626    /// RGBA tint colour (pre-multiplied opacity). Multiplied with texture sample.
627    pub fill_colour: [f32; 4],
628    /// RGBA border colour (pre-multiplied opacity).
629    pub border_colour: [f32; 4],
630    /// Half-extents of the shape bounding box in logical pixels.
631    pub half_size: [f32; 2],
632    /// Shape-specific radii (same encoding as `OverlayShapeVertex`).
633    pub radii: [f32; 4],
634    /// Border thickness in logical pixels.
635    pub border_width: f32,
636    /// Encoded shape type (same values as `OverlayShapeVertex`).
637    pub shape_type: f32,
638    /// Texture UV coordinates. (0,0) = top-left of image, (1,1) = bottom-right.
639    /// Slightly outside [0,1] in the border/AA padding region.
640    pub uv: [f32; 2],
641    /// RGBA shadow colour (pre-multiplied opacity).
642    pub shadow_colour: [f32; 4],
643    /// Shadow parameters: x = radius (pixels), y = offset_x, z = offset_y, w = border_mode.
644    pub shadow_params: [f32; 4],
645    /// Per-shape flags.
646    /// - `x` = is_backdrop_blur (0.0 = regular tinted sample; 1.0 = the bound
647    ///   texture is the scene-blur output composited under the tint).
648    /// - `y` = nine-slice centre tile mode (0 = stretch, 1 = tile).
649    /// - `z` = nine-slice edge tile mode (0 = stretch, 1 = tile).
650    /// - `w` = nine-slice enabled flag (0 = disabled, 1 = enabled).
651    pub extras: [f32; 4],
652    /// Nine-slice insets in texture UV space, `[top, right, bottom, left]`.
653    /// Unused when `extras.w == 0`.
654    pub nine_slice_uv: [f32; 4],
655    /// Nine-slice insets as fractions of the shape's bounding box,
656    /// `[top, right, bottom, left]`. Unused when `extras.w == 0`.
657    pub nine_slice_frac: [f32; 4],
658    /// Texture transform part A: `[offset_x, offset_y, scale_x, scale_y]`.
659    /// `scale = 1.0` is 1:1. Applies when 9-slice is disabled.
660    pub texture_transform_a: [f32; 4],
661    /// Texture transform part B:
662    /// `[rotation_radians, tile_mode (0/1/2), flip_x (0/1), flip_y (0/1)]`.
663    pub texture_transform_b: [f32; 4],
664}
665
666impl OverlayShapeTexVertex {
667    pub fn buffer_layout() -> wgpu::VertexBufferLayout<'static> {
668        wgpu::VertexBufferLayout {
669            array_stride: std::mem::size_of::<OverlayShapeTexVertex>() as wgpu::BufferAddress,
670            step_mode: wgpu::VertexStepMode::Vertex,
671            attributes: &[
672                // location 0: position vec2f
673                wgpu::VertexAttribute {
674                    offset: 0,
675                    shader_location: 0,
676                    format: wgpu::VertexFormat::Float32x2,
677                },
678                // location 1: local_pos vec2f
679                wgpu::VertexAttribute {
680                    offset: 8,
681                    shader_location: 1,
682                    format: wgpu::VertexFormat::Float32x2,
683                },
684                // location 2: fill_colour vec4f (tint)
685                wgpu::VertexAttribute {
686                    offset: 16,
687                    shader_location: 2,
688                    format: wgpu::VertexFormat::Float32x4,
689                },
690                // location 3: border_colour vec4f
691                wgpu::VertexAttribute {
692                    offset: 32,
693                    shader_location: 3,
694                    format: wgpu::VertexFormat::Float32x4,
695                },
696                // location 4: half_size vec2f
697                wgpu::VertexAttribute {
698                    offset: 48,
699                    shader_location: 4,
700                    format: wgpu::VertexFormat::Float32x2,
701                },
702                // location 5: radii vec4f
703                wgpu::VertexAttribute {
704                    offset: 56,
705                    shader_location: 5,
706                    format: wgpu::VertexFormat::Float32x4,
707                },
708                // location 6: border_width f32
709                wgpu::VertexAttribute {
710                    offset: 72,
711                    shader_location: 6,
712                    format: wgpu::VertexFormat::Float32,
713                },
714                // location 7: shape_type f32
715                wgpu::VertexAttribute {
716                    offset: 76,
717                    shader_location: 7,
718                    format: wgpu::VertexFormat::Float32,
719                },
720                // location 8: uv vec2f
721                wgpu::VertexAttribute {
722                    offset: 80,
723                    shader_location: 8,
724                    format: wgpu::VertexFormat::Float32x2,
725                },
726                // location 9: shadow_colour vec4f
727                wgpu::VertexAttribute {
728                    offset: 88,
729                    shader_location: 9,
730                    format: wgpu::VertexFormat::Float32x4,
731                },
732                // location 10: shadow_params vec4f (radius, offset_x, offset_y, border_mode)
733                wgpu::VertexAttribute {
734                    offset: 104,
735                    shader_location: 10,
736                    format: wgpu::VertexFormat::Float32x4,
737                },
738                // location 11: extras vec4f (blur, centre_mode, edge_mode, nine_slice_enabled)
739                wgpu::VertexAttribute {
740                    offset: 120,
741                    shader_location: 11,
742                    format: wgpu::VertexFormat::Float32x4,
743                },
744                // location 12: nine_slice_uv vec4f
745                wgpu::VertexAttribute {
746                    offset: 136,
747                    shader_location: 12,
748                    format: wgpu::VertexFormat::Float32x4,
749                },
750                // location 13: nine_slice_frac vec4f
751                wgpu::VertexAttribute {
752                    offset: 152,
753                    shader_location: 13,
754                    format: wgpu::VertexFormat::Float32x4,
755                },
756                // location 14: texture_transform_a vec4f (offset.xy, scale.xy)
757                wgpu::VertexAttribute {
758                    offset: 168,
759                    shader_location: 14,
760                    format: wgpu::VertexFormat::Float32x4,
761                },
762                // location 15: texture_transform_b vec4f (rotation, tile_mode, flip_x, flip_y)
763                wgpu::VertexAttribute {
764                    offset: 184,
765                    shader_location: 15,
766                    format: wgpu::VertexFormat::Float32x4,
767                },
768            ],
769        }
770    }
771}
772
773/// One batch of textured SDF overlay shapes sharing a single texture.
774pub(crate) struct OverlayShapeTexBatch {
775    pub vertex_buf: wgpu::Buffer,
776    pub vertex_count: u32,
777    pub bind_group: wgpu::BindGroup,
778}
779
780/// Persistent texture entry for an overlay shape texture fill.
781///
782/// Stored in `DeviceResources::overlay_textures`.
783pub(crate) struct OverlayShapeTextureEntry {
784    pub _texture: wgpu::Texture,
785    pub view: wgpu::TextureView,
786}
787
788/// Per-frame GPU data for batched SDF overlay shape rendering.
789pub(crate) struct OverlayShapeGpuData {
790    /// Vertex buffer for solid (non-textured) shapes. `None` when all shapes are textured.
791    pub vertex_buf: Option<wgpu::Buffer>,
792    /// Number of solid vertices. Zero when all shapes are textured.
793    pub vertex_count: u32,
794    /// One batch per unique texture, drawn after solid shapes.
795    pub tex_batches: Vec<OverlayShapeTexBatch>,
796    /// Vertex buffer for backdrop-blur shapes (frosted glass). Uses the same
797    /// vertex layout as `OverlayShapeTexVertex` with screen-space UVs.
798    /// The bind group is created at render time once the blurred scene texture
799    /// is available.
800    pub blur_vertex_buf: Option<wgpu::Buffer>,
801    /// Number of blur backdrop vertices.
802    pub blur_vertex_count: u32,
803    /// Maximum `backdrop_blur` value across all blur shapes this frame.
804    /// Used as the spread parameter for the Gaussian blur passes.
805    pub max_blur_radius: f32,
806}