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}