1use 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;
35const CACHE_HEAT_THRESHOLD: u32 = 3;
38const 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#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
59pub struct ShapeUniforms {
60 pub corner_radii: Vec4, 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, pub position: Vec4, pub screen_size: Vec2,
71}
72
73#[derive(ShaderType)]
74struct ShapeInstances {
75 #[shader(size(runtime))]
76 instances: Vec<ShapeUniforms>,
77}
78
79#[derive(Debug, Clone)]
81struct ShapeHeatTracker {
82 hit_count: u32,
84 last_seen_frame: u32,
86}
87
88pub 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 heat_tracker: HashMap<ShapeCacheKey, ShapeHeatTracker>,
110 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 let quad_vertices = [
188 Vertex {
189 position: [0.0, 0.0],
190 }, Vertex {
192 position: [1.0, 0.0],
193 }, Vertex {
195 position: [1.0, 1.0],
196 }, Vertex {
198 position: [0.0, 1.0],
199 }, ];
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 let quad_indices: [u16; 6] = [0, 2, 1, 0, 3, 2]; 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 if let Some(entry) = self.cache.get(&key) {
346 return Some(entry.clone());
347 }
348
349 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 if tracker.last_seen_frame != self.current_frame {
360 tracker.hit_count += 1;
361 tracker.last_seen_frame = self.current_frame;
362 }
363
364 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 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 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 self.current_frame = self.current_frame.wrapping_add(1);
967 self.heat_tracker.retain(|_, tracker| {
968 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}