tessera_ui_basic_components/pipelines/shape/
pipeline.rs

1//! Shape rendering pipeline for UI components.
2//!
3//! This module provides the GPU pipeline and associated data structures for rendering
4//! vector-based shapes in Tessera UI components. Supported shapes include rectangles,
5//! rounded rectangles (with G2 curve support), ellipses, and arbitrary polygons.
6//!
7//! The pipeline supports advanced visual effects such as drop shadows and interactive
8//! ripples, making it suitable for rendering button backgrounds, surfaces, and other
9//! interactive or decorative UI elements.
10//!
11//! Typical usage scenarios include:
12//! - Drawing backgrounds and outlines for buttons, surfaces, and containers
13//! - Rendering custom-shaped UI elements with smooth corners
14//! - Applying shadow and ripple effects for interactive feedback
15//!
16//! This module is intended to be used internally by basic UI components and registered
17//! as part of the rendering pipeline system.
18
19use std::{collections::HashMap, num::NonZeroUsize, sync::Arc};
20
21use encase::{ShaderSize, ShaderType, StorageBuffer};
22use glam::{Vec2, Vec4};
23use lru::LruCache;
24use tessera_ui::{
25    Color, Px, PxPosition, PxSize,
26    renderer::drawer::pipeline::{DrawContext, DrawablePipeline},
27    wgpu::{self, include_wgsl, util::DeviceExt},
28};
29
30use super::command::{RippleProps, ShapeCommand, rect_to_uniforms};
31
32#[allow(dead_code)]
33pub const MAX_CONCURRENT_SHAPES: wgpu::BufferAddress = 1024;
34const SHAPE_CACHE_CAPACITY: usize = 100;
35/// Minimum number of frames a shape must appear before being cached.
36/// This prevents caching transient shapes (e.g., resize animations).
37const CACHE_HEAT_THRESHOLD: u32 = 3;
38/// Number of frames to keep heat tracking data before cleanup.
39const HEAT_TRACKING_WINDOW: u32 = 10;
40
41#[repr(C)]
42#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
43struct Vertex {
44    position: [f32; 2],
45}
46
47/// Uniforms for shape rendering pipeline.
48///
49/// # Fields
50///
51/// - `size_cr_border_width`: Size, corner radius, border width.
52/// - `primary_color`: Main fill color.
53/// - `shadow_color`: Shadow color.
54/// - `render_params`: Additional rendering parameters.
55/// - `ripple_params`: Ripple effect parameters.
56/// - `ripple_color`: Ripple color.
57/// - `g2_k_value`: G2 curve parameter for rounded rectangles.
58#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
59pub struct ShapeUniforms {
60    pub corner_radii: Vec4, // x:tl, y:tr, z:br, w:bl
61    pub primary_color: Vec4,
62    pub border_color: Vec4,
63    pub shadow_color: Vec4,
64    pub render_params: Vec4,
65    pub ripple_params: Vec4,
66    pub ripple_color: Vec4,
67    pub g2_k_value: f32,
68    pub border_width: f32, // separate border_width field
69    pub position: Vec4,    // x, y, width, height
70    pub screen_size: Vec2,
71}
72
73#[derive(ShaderType)]
74struct ShapeInstances {
75    #[shader(size(runtime))]
76    instances: Vec<ShapeUniforms>,
77}
78
79/// Tracks how frequently a shape appears to decide if it should be cached.
80#[derive(Debug, Clone)]
81struct ShapeHeatTracker {
82    /// Number of frames this shape has appeared
83    hit_count: u32,
84    /// Frame number when last seen
85    last_seen_frame: u32,
86}
87
88/// Pipeline for rendering vector shapes in UI components.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// use tessera_ui_basic_components::pipelines::shape::ShapePipeline;
94///
95/// let pipeline = ShapePipeline::new(&device, &config, sample_count);
96/// ```
97pub struct ShapePipeline {
98    pipeline: wgpu::RenderPipeline,
99    bind_group_layout: wgpu::BindGroupLayout,
100    quad_vertex_buffer: wgpu::Buffer,
101    quad_index_buffer: wgpu::Buffer,
102    sample_count: u32,
103    cache_sampler: wgpu::Sampler,
104    cache_texture_bind_group_layout: wgpu::BindGroupLayout,
105    cache_transform_bind_group_layout: wgpu::BindGroupLayout,
106    cached_pipeline: wgpu::RenderPipeline,
107    cache: LruCache<ShapeCacheKey, Arc<ShapeCacheEntry>>,
108    /// Tracks shape usage frequency to avoid caching transient shapes
109    heat_tracker: HashMap<ShapeCacheKey, ShapeHeatTracker>,
110    /// Current frame number for heat tracking
111    current_frame: u32,
112    render_format: wgpu::TextureFormat,
113}
114
115impl ShapePipeline {
116    pub fn new(
117        gpu: &wgpu::Device,
118        config: &wgpu::SurfaceConfiguration,
119        pipeline_cache: Option<&wgpu::PipelineCache>,
120        sample_count: u32,
121    ) -> Self {
122        let shader = gpu.create_shader_module(include_wgsl!("shape.wgsl"));
123
124        let bind_group_layout = gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
125            entries: &[wgpu::BindGroupLayoutEntry {
126                binding: 0,
127                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
128                ty: wgpu::BindingType::Buffer {
129                    ty: wgpu::BufferBindingType::Storage { read_only: true },
130                    has_dynamic_offset: false,
131                    min_binding_size: None,
132                },
133                count: None,
134            }],
135            label: Some("shape_bind_group_layout"),
136        });
137
138        let pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
139            label: Some("Shape Pipeline Layout"),
140            bind_group_layouts: &[&bind_group_layout],
141            push_constant_ranges: &[],
142        });
143
144        let pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
145            label: Some("Shape Pipeline"),
146            layout: Some(&pipeline_layout),
147            vertex: wgpu::VertexState {
148                module: &shader,
149                entry_point: Some("vs_main"),
150                buffers: &[wgpu::VertexBufferLayout {
151                    array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
152                    step_mode: wgpu::VertexStepMode::Vertex,
153                    attributes: &wgpu::vertex_attr_array![0 => Float32x2],
154                }],
155                compilation_options: Default::default(),
156            },
157            primitive: wgpu::PrimitiveState {
158                topology: wgpu::PrimitiveTopology::TriangleList,
159                strip_index_format: None,
160                front_face: wgpu::FrontFace::Ccw,
161                cull_mode: Some(wgpu::Face::Back),
162                unclipped_depth: false,
163                polygon_mode: wgpu::PolygonMode::Fill,
164                conservative: false,
165            },
166            depth_stencil: None,
167            multisample: wgpu::MultisampleState {
168                count: sample_count,
169                mask: !0,
170                alpha_to_coverage_enabled: false,
171            },
172            fragment: Some(wgpu::FragmentState {
173                module: &shader,
174                entry_point: Some("fs_main"),
175                compilation_options: Default::default(),
176                targets: &[Some(wgpu::ColorTargetState {
177                    format: config.format,
178                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
179                    write_mask: wgpu::ColorWrites::ALL,
180                })],
181            }),
182            multiview: None,
183            cache: pipeline_cache,
184        });
185
186        // Create a vertex buffer for a unit quad.
187        let quad_vertices = [
188            Vertex {
189                position: [0.0, 0.0],
190            }, // Top-left
191            Vertex {
192                position: [1.0, 0.0],
193            }, // Top-right
194            Vertex {
195                position: [1.0, 1.0],
196            }, // Bottom-right
197            Vertex {
198                position: [0.0, 1.0],
199            }, // Bottom-left
200        ];
201        let quad_vertex_buffer = gpu.create_buffer_init(&wgpu::util::BufferInitDescriptor {
202            label: Some("Shape Quad Vertex Buffer"),
203            contents: bytemuck::cast_slice(&quad_vertices),
204            usage: wgpu::BufferUsages::VERTEX,
205        });
206
207        // Create an index buffer for a unit quad.
208        let quad_indices: [u16; 6] = [0, 2, 1, 0, 3, 2]; // CCW for backface culling
209        let quad_index_buffer = gpu.create_buffer_init(&wgpu::util::BufferInitDescriptor {
210            label: Some("Shape Quad Index Buffer"),
211            contents: bytemuck::cast_slice(&quad_indices),
212            usage: wgpu::BufferUsages::INDEX,
213        });
214
215        let cache_sampler = gpu.create_sampler(&wgpu::SamplerDescriptor {
216            label: Some("Shape Cache Sampler"),
217            address_mode_u: wgpu::AddressMode::ClampToEdge,
218            address_mode_v: wgpu::AddressMode::ClampToEdge,
219            address_mode_w: wgpu::AddressMode::ClampToEdge,
220            mag_filter: wgpu::FilterMode::Linear,
221            min_filter: wgpu::FilterMode::Linear,
222            mipmap_filter: wgpu::FilterMode::Nearest,
223            ..Default::default()
224        });
225
226        let cache_texture_bind_group_layout =
227            gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
228                label: Some("Shape Cache Texture Layout"),
229                entries: &[
230                    wgpu::BindGroupLayoutEntry {
231                        binding: 0,
232                        visibility: wgpu::ShaderStages::FRAGMENT,
233                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
234                        count: None,
235                    },
236                    wgpu::BindGroupLayoutEntry {
237                        binding: 1,
238                        visibility: wgpu::ShaderStages::FRAGMENT,
239                        ty: wgpu::BindingType::Texture {
240                            multisampled: false,
241                            view_dimension: wgpu::TextureViewDimension::D2,
242                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
243                        },
244                        count: None,
245                    },
246                ],
247            });
248
249        let cache_transform_bind_group_layout =
250            gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
251                label: Some("Shape Cache Transform Layout"),
252                entries: &[wgpu::BindGroupLayoutEntry {
253                    binding: 0,
254                    visibility: wgpu::ShaderStages::VERTEX,
255                    ty: wgpu::BindingType::Buffer {
256                        ty: wgpu::BufferBindingType::Storage { read_only: true },
257                        has_dynamic_offset: false,
258                        min_binding_size: None,
259                    },
260                    count: None,
261                }],
262            });
263
264        let cached_shader = gpu.create_shader_module(include_wgsl!("cached_quad.wgsl"));
265        let cached_pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
266            label: Some("Shape Cached Pipeline Layout"),
267            bind_group_layouts: &[
268                &cache_texture_bind_group_layout,
269                &cache_transform_bind_group_layout,
270            ],
271            push_constant_ranges: &[],
272        });
273
274        let cached_pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
275            label: Some("Shape Cached Pipeline"),
276            layout: Some(&cached_pipeline_layout),
277            vertex: wgpu::VertexState {
278                module: &cached_shader,
279                entry_point: Some("vs_main"),
280                buffers: &[wgpu::VertexBufferLayout {
281                    array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
282                    step_mode: wgpu::VertexStepMode::Vertex,
283                    attributes: &wgpu::vertex_attr_array![0 => Float32x2],
284                }],
285                compilation_options: Default::default(),
286            },
287            primitive: wgpu::PrimitiveState {
288                topology: wgpu::PrimitiveTopology::TriangleList,
289                strip_index_format: None,
290                front_face: wgpu::FrontFace::Ccw,
291                cull_mode: Some(wgpu::Face::Back),
292                unclipped_depth: false,
293                polygon_mode: wgpu::PolygonMode::Fill,
294                conservative: false,
295            },
296            depth_stencil: None,
297            multisample: wgpu::MultisampleState {
298                count: sample_count,
299                mask: !0,
300                alpha_to_coverage_enabled: false,
301            },
302            fragment: Some(wgpu::FragmentState {
303                module: &cached_shader,
304                entry_point: Some("fs_main"),
305                compilation_options: Default::default(),
306                targets: &[Some(wgpu::ColorTargetState {
307                    format: config.format,
308                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
309                    write_mask: wgpu::ColorWrites::ALL,
310                })],
311            }),
312            multiview: None,
313            cache: pipeline_cache,
314        });
315
316        Self {
317            pipeline,
318            bind_group_layout,
319            quad_vertex_buffer,
320            quad_index_buffer,
321            sample_count,
322            cache_sampler,
323            cache_texture_bind_group_layout,
324            cache_transform_bind_group_layout,
325            cached_pipeline,
326            cache: LruCache::new(
327                NonZeroUsize::new(SHAPE_CACHE_CAPACITY).expect("shape cache capacity must be > 0"),
328            ),
329            heat_tracker: HashMap::new(),
330            current_frame: 0,
331            render_format: config.format,
332        }
333    }
334
335    fn get_or_create_cache_entry(
336        &mut self,
337        gpu: &wgpu::Device,
338        gpu_queue: &wgpu::Queue,
339        command: &ShapeCommand,
340        size: PxSize,
341    ) -> Option<Arc<ShapeCacheEntry>> {
342        let key = ShapeCacheKey::from_command(command, size)?;
343
344        // Check if already cached
345        if let Some(entry) = self.cache.get(&key) {
346            return Some(entry.clone());
347        }
348
349        // Update heat tracking
350        let tracker = self
351            .heat_tracker
352            .entry(key.clone())
353            .or_insert(ShapeHeatTracker {
354                hit_count: 0,
355                last_seen_frame: self.current_frame,
356            });
357
358        // Update tracker
359        if tracker.last_seen_frame != self.current_frame {
360            tracker.hit_count += 1;
361            tracker.last_seen_frame = self.current_frame;
362        }
363
364        // Only cache if shape has appeared frequently enough
365        if tracker.hit_count >= CACHE_HEAT_THRESHOLD {
366            let entry = Arc::new(self.build_cache_entry(gpu, gpu_queue, command, size));
367            self.cache.put(key, entry.clone());
368            Some(entry)
369        } else {
370            // Shape is not hot enough yet, don't cache
371            None
372        }
373    }
374
375    fn build_cache_entry(
376        &self,
377        gpu: &wgpu::Device,
378        gpu_queue: &wgpu::Queue,
379        command: &ShapeCommand,
380        size: PxSize,
381    ) -> ShapeCacheEntry {
382        let width = size.width.positive().max(1);
383        let height = size.height.positive().max(1);
384
385        let cache_texture = gpu.create_texture(&wgpu::TextureDescriptor {
386            label: Some("Shape Cache Texture"),
387            size: wgpu::Extent3d {
388                width,
389                height,
390                depth_or_array_layers: 1,
391            },
392            mip_level_count: 1,
393            sample_count: 1,
394            dimension: wgpu::TextureDimension::D2,
395            format: self.render_format,
396            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
397            view_formats: &[],
398        });
399        let cache_view = cache_texture.create_view(&wgpu::TextureViewDescriptor::default());
400
401        let mut uniforms = rect_to_uniforms(
402            command,
403            size,
404            PxPosition {
405                x: Px::new(0),
406                y: Px::new(0),
407            },
408        );
409        uniforms.screen_size = [width as f32, height as f32].into();
410
411        let has_shadow = uniforms.shadow_color[3] > 0.0 && uniforms.render_params[2] > 0.0;
412        let mut instances = Vec::with_capacity(if has_shadow { 2 } else { 1 });
413        if has_shadow {
414            let mut shadow = uniforms;
415            shadow.render_params[3] = 2.0;
416            instances.push(shadow);
417        }
418        instances.push(uniforms);
419
420        let storage_buffer = gpu.create_buffer(&wgpu::BufferDescriptor {
421            label: Some("Shape Cache Storage Buffer"),
422            size: 16 + ShapeUniforms::SHADER_SIZE.get() * instances.len() as u64,
423            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
424            mapped_at_creation: false,
425        });
426
427        let uniforms = ShapeInstances { instances };
428        let mut buffer_content = StorageBuffer::new(Vec::<u8>::new());
429        buffer_content.write(&uniforms).unwrap();
430        gpu_queue.write_buffer(&storage_buffer, 0, buffer_content.as_ref());
431
432        let bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
433            layout: &self.bind_group_layout,
434            entries: &[wgpu::BindGroupEntry {
435                binding: 0,
436                resource: storage_buffer.as_entire_binding(),
437            }],
438            label: Some("shape_cache_bind_group"),
439        });
440
441        let mut encoder = gpu.create_command_encoder(&wgpu::CommandEncoderDescriptor {
442            label: Some("Shape Cache Encoder"),
443        });
444
445        let run_pass = |pass: &mut wgpu::RenderPass<'_>| {
446            pass.set_pipeline(&self.pipeline);
447            pass.set_bind_group(0, &bind_group, &[]);
448            pass.set_vertex_buffer(0, self.quad_vertex_buffer.slice(..));
449            pass.set_index_buffer(self.quad_index_buffer.slice(..), wgpu::IndexFormat::Uint16);
450            pass.draw_indexed(0..6, 0, 0..uniforms.instances.len() as u32);
451        };
452
453        if self.sample_count > 1 {
454            let msaa_texture = gpu.create_texture(&wgpu::TextureDescriptor {
455                label: Some("Shape Cache MSAA Texture"),
456                size: wgpu::Extent3d {
457                    width,
458                    height,
459                    depth_or_array_layers: 1,
460                },
461                mip_level_count: 1,
462                sample_count: self.sample_count,
463                dimension: wgpu::TextureDimension::D2,
464                format: self.render_format,
465                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
466                view_formats: &[],
467            });
468            let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
469
470            {
471                let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
472                    label: Some("Shape Cache Pass"),
473                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
474                        view: &msaa_view,
475                        resolve_target: Some(&cache_view),
476                        ops: wgpu::Operations {
477                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
478                            store: wgpu::StoreOp::Store,
479                        },
480                        depth_slice: None,
481                    })],
482                    ..Default::default()
483                });
484                run_pass(&mut pass);
485            }
486        } else {
487            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
488                label: Some("Shape Cache Pass"),
489                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
490                    view: &cache_view,
491                    resolve_target: None,
492                    ops: wgpu::Operations {
493                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
494                        store: wgpu::StoreOp::Store,
495                    },
496                    depth_slice: None,
497                })],
498                ..Default::default()
499            });
500            run_pass(&mut pass);
501        }
502
503        gpu_queue.submit(Some(encoder.finish()));
504
505        let texture_bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
506            layout: &self.cache_texture_bind_group_layout,
507            entries: &[
508                wgpu::BindGroupEntry {
509                    binding: 0,
510                    resource: wgpu::BindingResource::Sampler(&self.cache_sampler),
511                },
512                wgpu::BindGroupEntry {
513                    binding: 1,
514                    resource: wgpu::BindingResource::TextureView(&cache_view),
515                },
516            ],
517            label: Some("shape_cache_texture_bind_group"),
518        });
519
520        ShapeCacheEntry {
521            _texture: cache_texture,
522            _view: cache_view,
523            texture_bind_group,
524        }
525    }
526
527    fn draw_uncached_batch(
528        &self,
529        gpu: &wgpu::Device,
530        gpu_queue: &wgpu::Queue,
531        config: &wgpu::SurfaceConfiguration,
532        render_pass: &mut wgpu::RenderPass<'_>,
533        commands: &[(&ShapeCommand, PxSize, PxPosition)],
534        indices: &[usize],
535    ) {
536        if indices.is_empty() {
537            return;
538        }
539
540        let subset: Vec<_> = indices.iter().map(|&i| commands[i]).collect();
541        let instances = build_instances(&subset, config);
542        if instances.is_empty() {
543            return;
544        }
545
546        let storage_buffer = gpu.create_buffer(&wgpu::BufferDescriptor {
547            label: Some("Shape Storage Buffer"),
548            size: 16 + ShapeUniforms::SHADER_SIZE.get() * instances.len() as u64,
549            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
550            mapped_at_creation: false,
551        });
552
553        let uniforms = ShapeInstances { instances };
554        let mut buffer_content = StorageBuffer::new(Vec::<u8>::new());
555        buffer_content.write(&uniforms).unwrap();
556        gpu_queue.write_buffer(&storage_buffer, 0, buffer_content.as_ref());
557
558        let bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
559            layout: &self.bind_group_layout,
560            entries: &[wgpu::BindGroupEntry {
561                binding: 0,
562                resource: storage_buffer.as_entire_binding(),
563            }],
564            label: Some("shape_bind_group"),
565        });
566
567        render_pass.set_pipeline(&self.pipeline);
568        render_pass.set_bind_group(0, &bind_group, &[]);
569        render_pass.set_vertex_buffer(0, self.quad_vertex_buffer.slice(..));
570        render_pass.set_index_buffer(self.quad_index_buffer.slice(..), wgpu::IndexFormat::Uint16);
571        render_pass.draw_indexed(0..6, 0, 0..uniforms.instances.len() as u32);
572    }
573
574    fn draw_cached_run(
575        &self,
576        gpu: &wgpu::Device,
577        gpu_queue: &wgpu::Queue,
578        config: &wgpu::SurfaceConfiguration,
579        render_pass: &mut wgpu::RenderPass<'_>,
580        entry: Arc<ShapeCacheEntry>,
581        instances: &[(PxPosition, PxSize)],
582    ) {
583        if instances.is_empty() {
584            return;
585        }
586
587        let rects: Vec<CachedRectUniform> = instances
588            .iter()
589            .map(|(position, size)| CachedRectUniform {
590                position: Vec4::new(
591                    position.x.raw() as f32,
592                    position.y.raw() as f32,
593                    size.width.raw() as f32,
594                    size.height.raw() as f32,
595                ),
596                screen_size: Vec2::new(config.width as f32, config.height as f32),
597                padding: Vec2::ZERO,
598            })
599            .collect();
600
601        let rect_instances = CachedRectInstances { rects };
602        let mut buffer_content = StorageBuffer::new(Vec::<u8>::new());
603        buffer_content.write(&rect_instances).unwrap();
604
605        let instance_buffer = gpu.create_buffer(&wgpu::BufferDescriptor {
606            label: Some("Shape Cache Instance Buffer"),
607            size: buffer_content.as_ref().len() as u64,
608            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
609            mapped_at_creation: false,
610        });
611        gpu_queue.write_buffer(&instance_buffer, 0, buffer_content.as_ref());
612
613        let transform_bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
614            layout: &self.cache_transform_bind_group_layout,
615            entries: &[wgpu::BindGroupEntry {
616                binding: 0,
617                resource: instance_buffer.as_entire_binding(),
618            }],
619            label: Some("shape_cache_transform_bind_group"),
620        });
621
622        render_pass.set_pipeline(&self.cached_pipeline);
623        render_pass.set_vertex_buffer(0, self.quad_vertex_buffer.slice(..));
624        render_pass.set_index_buffer(self.quad_index_buffer.slice(..), wgpu::IndexFormat::Uint16);
625        render_pass.set_bind_group(0, &entry.texture_bind_group, &[]);
626        render_pass.set_bind_group(1, &transform_bind_group, &[]);
627        render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
628    }
629
630    fn flush_cached_run(
631        &mut self,
632        gpu: &wgpu::Device,
633        gpu_queue: &wgpu::Queue,
634        config: &wgpu::SurfaceConfiguration,
635        render_pass: &mut wgpu::RenderPass<'_>,
636        pending: &mut Option<(Arc<ShapeCacheEntry>, Vec<(PxPosition, PxSize)>)>,
637    ) {
638        if let Some((entry, instances)) = pending.take() {
639            self.draw_cached_run(gpu, gpu_queue, config, render_pass, entry, &instances);
640        }
641    }
642}
643
644fn build_instances(
645    commands: &[(&ShapeCommand, PxSize, PxPosition)],
646    config: &wgpu::SurfaceConfiguration,
647) -> Vec<ShapeUniforms> {
648    // Extracted instance-building logic to simplify `draw` and reduce cognitive complexity.
649    commands
650        .iter()
651        .flat_map(|(command, size, start_pos)| {
652            let mut uniforms = rect_to_uniforms(command, *size, *start_pos);
653            uniforms.screen_size = [config.width as f32, config.height as f32].into();
654
655            let has_shadow = uniforms.shadow_color[3] > 0.0 && uniforms.render_params[2] > 0.0;
656
657            if has_shadow {
658                let mut uniforms_for_shadow = uniforms;
659                uniforms_for_shadow.render_params[3] = 2.0;
660                vec![uniforms_for_shadow, uniforms]
661            } else {
662                vec![uniforms]
663            }
664        })
665        .collect()
666}
667
668#[derive(Clone, PartialEq, Eq, Hash)]
669enum ShapeCacheVariant {
670    Rect,
671    OutlinedRect,
672    FilledOutlinedRect,
673    Ellipse,
674    OutlinedEllipse,
675    FilledOutlinedEllipse,
676    RippleRect,
677    RippleOutlinedRect,
678    RippleFilledOutlinedRect,
679}
680
681#[derive(Clone, PartialEq, Eq, Hash)]
682struct ShadowKey {
683    color: [u32; 4],
684    offset: [u32; 2],
685    smoothness: u32,
686}
687
688#[derive(Clone, PartialEq, Eq, Hash)]
689struct RippleKey {
690    center: [u32; 2],
691    radius: u32,
692    alpha: u32,
693    color: [u32; 4],
694}
695
696#[derive(Clone, PartialEq, Eq, Hash)]
697struct ShapeCacheKey {
698    variant: ShapeCacheVariant,
699    primary_color: [u32; 4],
700    border_color: Option<[u32; 4]>,
701    corner_radii: [u32; 4],
702    g2_k_value: u32,
703    border_width: u32,
704    shadow: Option<ShadowKey>,
705    ripple: Option<RippleKey>,
706    width: u32,
707    height: u32,
708}
709
710struct ShapeCacheEntry {
711    _texture: wgpu::Texture,
712    _view: wgpu::TextureView,
713    texture_bind_group: wgpu::BindGroup,
714}
715
716#[repr(C)]
717#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
718struct CachedRectUniform {
719    position: Vec4,
720    screen_size: Vec2,
721    padding: Vec2,
722}
723
724#[derive(ShaderType)]
725struct CachedRectInstances {
726    #[shader(size(runtime))]
727    rects: Vec<CachedRectUniform>,
728}
729
730fn f32_to_bits(value: f32) -> u32 {
731    value.to_bits()
732}
733
734fn color_to_bits(color: Color) -> [u32; 4] {
735    let arr = color.to_array();
736    [
737        f32_to_bits(arr[0]),
738        f32_to_bits(arr[1]),
739        f32_to_bits(arr[2]),
740        f32_to_bits(arr[3]),
741    ]
742}
743
744fn ripple_to_key(ripple: &RippleProps) -> RippleKey {
745    RippleKey {
746        center: [f32_to_bits(ripple.center[0]), f32_to_bits(ripple.center[1])],
747        radius: f32_to_bits(ripple.radius),
748        alpha: f32_to_bits(ripple.alpha),
749        color: color_to_bits(ripple.color),
750    }
751}
752
753impl ShapeCacheKey {
754    fn from_command(command: &ShapeCommand, size: PxSize) -> Option<Self> {
755        let width = size.width.positive();
756        let height = size.height.positive();
757        if width == 0 || height == 0 {
758            return None;
759        }
760
761        match command {
762            ShapeCommand::Rect {
763                color,
764                corner_radii,
765                g2_k_value,
766                shadow,
767            } => Some(Self {
768                variant: ShapeCacheVariant::Rect,
769                primary_color: color_to_bits(*color),
770                border_color: None,
771                corner_radii: corner_radii.map(f32_to_bits),
772                g2_k_value: f32_to_bits(*g2_k_value),
773                border_width: 0,
774                shadow: shadow.as_ref().map(|shadow| ShadowKey {
775                    color: color_to_bits(shadow.color),
776                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
777                    smoothness: f32_to_bits(shadow.smoothness),
778                }),
779                ripple: None,
780                width,
781                height,
782            }),
783            ShapeCommand::OutlinedRect {
784                color,
785                corner_radii,
786                g2_k_value,
787                shadow,
788                border_width,
789            } => Some(Self {
790                variant: ShapeCacheVariant::OutlinedRect,
791                primary_color: color_to_bits(*color),
792                border_color: None,
793                corner_radii: corner_radii.map(f32_to_bits),
794                g2_k_value: f32_to_bits(*g2_k_value),
795                border_width: f32_to_bits(*border_width),
796                shadow: shadow.as_ref().map(|shadow| ShadowKey {
797                    color: color_to_bits(shadow.color),
798                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
799                    smoothness: f32_to_bits(shadow.smoothness),
800                }),
801                ripple: None,
802                width,
803                height,
804            }),
805            ShapeCommand::FilledOutlinedRect {
806                color,
807                border_color,
808                corner_radii,
809                g2_k_value,
810                shadow,
811                border_width,
812            } => Some(Self {
813                variant: ShapeCacheVariant::FilledOutlinedRect,
814                primary_color: color_to_bits(*color),
815                border_color: Some(color_to_bits(*border_color)),
816                corner_radii: corner_radii.map(f32_to_bits),
817                g2_k_value: f32_to_bits(*g2_k_value),
818                border_width: f32_to_bits(*border_width),
819                shadow: shadow.as_ref().map(|shadow| ShadowKey {
820                    color: color_to_bits(shadow.color),
821                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
822                    smoothness: f32_to_bits(shadow.smoothness),
823                }),
824                ripple: None,
825                width,
826                height,
827            }),
828            ShapeCommand::Ellipse { color, shadow } => Some(Self {
829                variant: ShapeCacheVariant::Ellipse,
830                primary_color: color_to_bits(*color),
831                border_color: None,
832                corner_radii: [f32_to_bits(-1.0_f32); 4],
833                g2_k_value: f32_to_bits(0.0),
834                border_width: 0,
835                shadow: shadow.as_ref().map(|shadow| ShadowKey {
836                    color: color_to_bits(shadow.color),
837                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
838                    smoothness: f32_to_bits(shadow.smoothness),
839                }),
840                ripple: None,
841                width,
842                height,
843            }),
844            ShapeCommand::OutlinedEllipse {
845                color,
846                shadow,
847                border_width,
848            } => Some(Self {
849                variant: ShapeCacheVariant::OutlinedEllipse,
850                primary_color: color_to_bits(*color),
851                border_color: None,
852                corner_radii: [f32_to_bits(-1.0_f32); 4],
853                g2_k_value: f32_to_bits(0.0),
854                border_width: f32_to_bits(*border_width),
855                shadow: shadow.as_ref().map(|shadow| ShadowKey {
856                    color: color_to_bits(shadow.color),
857                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
858                    smoothness: f32_to_bits(shadow.smoothness),
859                }),
860                ripple: None,
861                width,
862                height,
863            }),
864            ShapeCommand::FilledOutlinedEllipse {
865                color,
866                border_color,
867                shadow,
868                border_width,
869            } => Some(Self {
870                variant: ShapeCacheVariant::FilledOutlinedEllipse,
871                primary_color: color_to_bits(*color),
872                border_color: Some(color_to_bits(*border_color)),
873                corner_radii: [f32_to_bits(-1.0_f32); 4],
874                g2_k_value: f32_to_bits(0.0),
875                border_width: f32_to_bits(*border_width),
876                shadow: shadow.as_ref().map(|shadow| ShadowKey {
877                    color: color_to_bits(shadow.color),
878                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
879                    smoothness: f32_to_bits(shadow.smoothness),
880                }),
881                ripple: None,
882                width,
883                height,
884            }),
885            ShapeCommand::RippleRect {
886                color,
887                corner_radii,
888                g2_k_value,
889                shadow,
890                ripple,
891            } if ripple.alpha.abs() <= f32::EPSILON => Some(Self {
892                variant: ShapeCacheVariant::RippleRect,
893                primary_color: color_to_bits(*color),
894                border_color: None,
895                corner_radii: corner_radii.map(f32_to_bits),
896                g2_k_value: f32_to_bits(*g2_k_value),
897                border_width: 0,
898                shadow: shadow.as_ref().map(|shadow| ShadowKey {
899                    color: color_to_bits(shadow.color),
900                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
901                    smoothness: f32_to_bits(shadow.smoothness),
902                }),
903                ripple: Some(ripple_to_key(ripple)),
904                width,
905                height,
906            }),
907            ShapeCommand::RippleOutlinedRect {
908                color,
909                corner_radii,
910                g2_k_value,
911                shadow,
912                border_width,
913                ripple,
914            } if ripple.alpha.abs() <= f32::EPSILON => Some(Self {
915                variant: ShapeCacheVariant::RippleOutlinedRect,
916                primary_color: color_to_bits(*color),
917                border_color: None,
918                corner_radii: corner_radii.map(f32_to_bits),
919                g2_k_value: f32_to_bits(*g2_k_value),
920                border_width: f32_to_bits(*border_width),
921                shadow: shadow.as_ref().map(|shadow| ShadowKey {
922                    color: color_to_bits(shadow.color),
923                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
924                    smoothness: f32_to_bits(shadow.smoothness),
925                }),
926                ripple: Some(ripple_to_key(ripple)),
927                width,
928                height,
929            }),
930            ShapeCommand::RippleFilledOutlinedRect {
931                color,
932                border_color,
933                corner_radii,
934                g2_k_value,
935                shadow,
936                border_width,
937                ripple,
938            } if ripple.alpha.abs() <= f32::EPSILON => Some(Self {
939                variant: ShapeCacheVariant::RippleFilledOutlinedRect,
940                primary_color: color_to_bits(*color),
941                border_color: Some(color_to_bits(*border_color)),
942                corner_radii: corner_radii.map(f32_to_bits),
943                g2_k_value: f32_to_bits(*g2_k_value),
944                border_width: f32_to_bits(*border_width),
945                shadow: shadow.as_ref().map(|shadow| ShadowKey {
946                    color: color_to_bits(shadow.color),
947                    offset: [f32_to_bits(shadow.offset[0]), f32_to_bits(shadow.offset[1])],
948                    smoothness: f32_to_bits(shadow.smoothness),
949                }),
950                ripple: Some(ripple_to_key(ripple)),
951                width,
952                height,
953            }),
954            _ => None,
955        }
956    }
957}
958
959impl DrawablePipeline<ShapeCommand> for ShapePipeline {
960    fn draw(&mut self, context: &mut DrawContext<ShapeCommand>) {
961        if context.commands.is_empty() {
962            return;
963        }
964
965        // Advance frame counter and cleanup old heat tracking data
966        self.current_frame = self.current_frame.wrapping_add(1);
967        self.heat_tracker.retain(|_, tracker| {
968            // Remove entries not seen in the last HEAT_TRACKING_WINDOW frames
969            self.current_frame.saturating_sub(tracker.last_seen_frame) < HEAT_TRACKING_WINDOW
970        });
971
972        let mut cache_entries = Vec::with_capacity(context.commands.len());
973        for (command, size, _) in context.commands.iter() {
974            let entry =
975                self.get_or_create_cache_entry(context.device, context.queue, command, *size);
976            cache_entries.push(entry);
977        }
978
979        let mut pending_uncached: Vec<usize> = Vec::new();
980        let mut pending_cached_run: Option<(Arc<ShapeCacheEntry>, Vec<(PxPosition, PxSize)>)> =
981            None;
982
983        for (idx, ((_, size, position), cache_entry)) in context
984            .commands
985            .iter()
986            .zip(cache_entries.iter())
987            .enumerate()
988        {
989            if let Some(entry) = cache_entry {
990                if !pending_uncached.is_empty() {
991                    self.draw_uncached_batch(
992                        context.device,
993                        context.queue,
994                        context.config,
995                        context.render_pass,
996                        context.commands,
997                        &pending_uncached,
998                    );
999                    pending_uncached.clear();
1000                }
1001
1002                if let Some((current_entry, transforms)) = pending_cached_run.as_mut() {
1003                    if Arc::ptr_eq(current_entry, entry) {
1004                        transforms.push((*position, *size));
1005                    } else {
1006                        self.flush_cached_run(
1007                            context.device,
1008                            context.queue,
1009                            context.config,
1010                            context.render_pass,
1011                            &mut pending_cached_run,
1012                        );
1013                        pending_cached_run = Some((entry.clone(), vec![(*position, *size)]));
1014                    }
1015                } else {
1016                    pending_cached_run = Some((entry.clone(), vec![(*position, *size)]));
1017                }
1018            } else {
1019                self.flush_cached_run(
1020                    context.device,
1021                    context.queue,
1022                    context.config,
1023                    context.render_pass,
1024                    &mut pending_cached_run,
1025                );
1026                pending_uncached.push(idx);
1027            }
1028        }
1029
1030        self.flush_cached_run(
1031            context.device,
1032            context.queue,
1033            context.config,
1034            context.render_pass,
1035            &mut pending_cached_run,
1036        );
1037
1038        if !pending_uncached.is_empty() {
1039            self.draw_uncached_batch(
1040                context.device,
1041                context.queue,
1042                context.config,
1043                context.render_pass,
1044                context.commands,
1045                &pending_uncached,
1046            );
1047        }
1048    }
1049}