Skip to main content

guido/renderer/
render.rs

1//! GPU rendering for the render tree using instanced rendering.
2//!
3//! This module uses a single draw call per layer to render all shapes,
4//! significantly reducing CPU-GPU communication overhead.
5
6use std::sync::Arc;
7
8use wgpu::util::DeviceExt;
9use wgpu::{
10    BindGroup, BindGroupLayout, Buffer, BufferUsages, Device, Queue, RenderPipeline, ShaderModule,
11};
12
13use super::backdrop_pass::{BackdropRegion, BackdropRenderer, CoverageMask};
14use super::clip::ClipChains;
15use super::clip_records::ClipRecords;
16use super::commands::{CornerRadii, DrawCommand};
17use super::constants::TEXT_SUPERSAMPLE;
18use super::flatten::{CommandLayer, FlatFrame, FlattenedCommand};
19use super::gpu::{QUAD_INDICES, QUAD_VERTICES, QuadVertex, ShaderUniforms, ShapeInstance};
20use super::gpu_context::RenderTarget;
21use super::image_quad::{ImageQuadRenderer, PreparedImageQuad};
22use super::text::TextRenderState;
23use super::text_mask::{MaskSpec, TextMaskRenderer};
24use super::text_quad::{PreparedTextQuad, TextQuadRenderer};
25use super::types::TextEntry;
26use crate::render_stats::{self, Pipeline};
27use crate::shape::PlacedShape;
28use crate::widgets::{Color, Rect};
29
30/// The renderer using instanced rendering.
31///
32/// This renderer converts [`FlattenedCommand`]s into GPU instance data
33/// and renders all shapes with a single draw call per layer.
34pub struct Renderer {
35    device: Arc<Device>,
36    queue: Arc<Queue>,
37    pipeline: RenderPipeline,
38    #[allow(dead_code)] // Kept alive - bind groups hold reference to layout
39    bind_group_layout: BindGroupLayout,
40
41    // Shared vertex buffer (unit quad)
42    vertex_buffer: Buffer,
43    index_buffer: Buffer,
44
45    // Uniform buffer
46    uniform_buffer: Buffer,
47    uniform_bind_group: BindGroup,
48
49    // Instance buffer (resized as needed)
50    instance_buffer: Buffer,
51    instance_buffer_capacity: usize,
52
53    /// The frame's clip chains, bound at group 1 by every pipeline that cuts
54    /// by a clip.
55    clip_records: ClipRecords,
56
57    // Text rendering via glyphon
58    text_state: TextRenderState,
59
60    // Transformed text rendering (renders text to textures for rotation/scale)
61    text_quad_renderer: TextQuadRenderer,
62
63    /// Glyph coverage for texts that frost their own backdrop.
64    text_mask: TextMaskRenderer,
65
66    // Image rendering
67    image_quad_renderer: ImageQuadRenderer,
68
69    // Reusable per-frame buffers (cleared and reused each frame to avoid allocations)
70    /// Every group's shape and overlay instances, addressed by range.
71    shape_instance_buf: Vec<ShapeInstance>,
72    text_entry_buf: Vec<TextEntry>,
73    image_quads: Vec<PreparedImageQuad>,
74    text_quads: Vec<PreparedTextQuad>,
75    /// What each group's draws address, one entry per group, rebuilt per frame.
76    prepared_layers: Vec<PreparedLayer>,
77    backdrop: BackdropRenderer,
78
79    // Screen dimensions
80    screen_width: f32,
81    screen_height: f32,
82    scale_factor: f32,
83}
84
85impl Renderer {
86    /// Create a new renderer with instanced rendering.
87    pub fn new(device: Arc<Device>, queue: Arc<Queue>, format: wgpu::TextureFormat) -> Self {
88        // Load shader
89        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
90            label: Some("Renderer Shader"),
91            source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
92        });
93
94        // Create bind group layout for uniforms
95        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
96            label: Some("Renderer Bind Group Layout"),
97            entries: &[
98                wgpu::BindGroupLayoutEntry {
99                    binding: 0,
100                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
101                    ty: wgpu::BindingType::Buffer {
102                        ty: wgpu::BufferBindingType::Uniform,
103                        has_dynamic_offset: false,
104                        min_binding_size: None,
105                    },
106                    count: None,
107                },
108                // The instances, read by `@builtin(instance_index)` rather
109                // than fetched as vertex attributes — which is what lifts the
110                // sixteen-attribute ceiling the layout had reached (#398).
111                wgpu::BindGroupLayoutEntry {
112                    binding: 1,
113                    visibility: wgpu::ShaderStages::VERTEX,
114                    ty: wgpu::BindingType::Buffer {
115                        ty: wgpu::BufferBindingType::Storage { read_only: true },
116                        has_dynamic_offset: false,
117                        min_binding_size: None,
118                    },
119                    count: None,
120                },
121            ],
122        });
123
124        let clip_records = ClipRecords::new(&device);
125
126        // Create pipeline
127        let pipeline = Self::create_pipeline(
128            &device,
129            &shader,
130            &bind_group_layout,
131            clip_records.layout(),
132            format,
133        );
134
135        // Create vertex buffer (unit quad)
136        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
137            label: Some("Renderer Vertex Buffer"),
138            contents: bytemuck::cast_slice(QUAD_VERTICES),
139            usage: BufferUsages::VERTEX,
140        });
141
142        // Create index buffer
143        let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
144            label: Some("Renderer Index Buffer"),
145            contents: bytemuck::cast_slice(QUAD_INDICES),
146            usage: BufferUsages::INDEX,
147        });
148
149        // Create uniform buffer
150        let uniforms = ShaderUniforms::new(800.0, 600.0, 1.0);
151        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
152            label: Some("Renderer Uniform Buffer"),
153            contents: bytemuck::cast_slice(&[uniforms]),
154            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
155        });
156
157        // Create initial instance buffer (will be resized as needed)
158        let initial_capacity = 256;
159        let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
160            label: Some("Renderer Instance Buffer"),
161            size: (initial_capacity * std::mem::size_of::<ShapeInstance>()) as u64,
162            usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
163            mapped_at_creation: false,
164        });
165
166        let uniform_bind_group = Self::make_bind_group(
167            &device,
168            &bind_group_layout,
169            &uniform_buffer,
170            &instance_buffer,
171        );
172
173        // Initialize text renderer
174        let text_state = TextRenderState::new(&device, &queue, format);
175
176        // Initialize transformed text renderer
177        let text_quad_renderer = TextQuadRenderer::new(&device, format, clip_records.layout());
178        let text_mask = TextMaskRenderer::new(format);
179
180        // Initialize image renderer
181        let image_quad_renderer = ImageQuadRenderer::new(&device, format, clip_records.layout());
182
183        let backdrop = BackdropRenderer::new(&device, format, clip_records.layout());
184
185        Self {
186            device,
187            queue,
188            pipeline,
189            bind_group_layout,
190            vertex_buffer,
191            index_buffer,
192            uniform_buffer,
193            uniform_bind_group,
194            instance_buffer,
195            instance_buffer_capacity: initial_capacity,
196            clip_records,
197            text_state,
198            text_quad_renderer,
199            text_mask,
200            image_quad_renderer,
201            shape_instance_buf: Vec::new(),
202            text_entry_buf: Vec::new(),
203            image_quads: Vec::new(),
204            text_quads: Vec::new(),
205            prepared_layers: Vec::new(),
206            backdrop,
207            screen_width: 800.0,
208            screen_height: 600.0,
209            scale_factor: 1.0,
210        }
211    }
212
213    /// Create the render pipeline.
214    fn create_pipeline(
215        device: &Device,
216        shader: &ShaderModule,
217        bind_group_layout: &BindGroupLayout,
218        clip_records: &BindGroupLayout,
219        format: wgpu::TextureFormat,
220    ) -> RenderPipeline {
221        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
222            label: Some("Renderer Pipeline Layout"),
223            bind_group_layouts: &[Some(bind_group_layout), Some(clip_records)],
224            immediate_size: 0,
225        });
226
227        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
228            label: Some("Renderer Pipeline"),
229            layout: Some(&pipeline_layout),
230            vertex: wgpu::VertexState {
231                module: shader,
232                entry_point: Some("vs_main"),
233                buffers: &[Some(QuadVertex::desc())],
234                compilation_options: wgpu::PipelineCompilationOptions::default(),
235            },
236            fragment: Some(wgpu::FragmentState {
237                module: shader,
238                entry_point: Some("fs_main"),
239                targets: &[Some(wgpu::ColorTargetState {
240                    format,
241                    blend: Some(wgpu::BlendState {
242                        color: wgpu::BlendComponent {
243                            src_factor: wgpu::BlendFactor::SrcAlpha,
244                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
245                            operation: wgpu::BlendOperation::Add,
246                        },
247                        alpha: wgpu::BlendComponent {
248                            src_factor: wgpu::BlendFactor::One,
249                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
250                            operation: wgpu::BlendOperation::Add,
251                        },
252                    }),
253                    write_mask: wgpu::ColorWrites::ALL,
254                })],
255                compilation_options: wgpu::PipelineCompilationOptions::default(),
256            }),
257            primitive: wgpu::PrimitiveState {
258                topology: wgpu::PrimitiveTopology::TriangleList,
259                strip_index_format: None,
260                front_face: wgpu::FrontFace::Ccw,
261                cull_mode: None, // No culling for 2D
262                polygon_mode: wgpu::PolygonMode::Fill,
263                unclipped_depth: false,
264                conservative: false,
265            },
266            depth_stencil: None,
267            multisample: wgpu::MultisampleState::default(),
268            multiview_mask: None,
269            cache: None,
270        })
271    }
272
273    /// Set the screen size in logical pixels.
274    pub fn set_screen_size(&mut self, width: f32, height: f32) {
275        self.screen_width = width;
276        self.screen_height = height;
277    }
278
279    /// Set the HiDPI scale factor.
280    pub fn set_scale_factor(&mut self, scale: f32) {
281        self.scale_factor = scale;
282    }
283
284    /// The uniforms, and the instances the vertex stage indexes into.
285    ///
286    /// Rebuilt whenever the instance buffer is, because a bind group holds the
287    /// buffer it was made from — which a vertex buffer binding did not, since
288    /// that one was named at draw time.
289    fn make_bind_group(
290        device: &wgpu::Device,
291        layout: &BindGroupLayout,
292        uniforms: &Buffer,
293        instances: &Buffer,
294    ) -> wgpu::BindGroup {
295        device.create_bind_group(&wgpu::BindGroupDescriptor {
296            label: Some("Renderer Bind Group"),
297            layout,
298            entries: &[
299                wgpu::BindGroupEntry {
300                    binding: 0,
301                    resource: uniforms.as_entire_binding(),
302                },
303                wgpu::BindGroupEntry {
304                    binding: 1,
305                    resource: instances.as_entire_binding(),
306                },
307            ],
308        })
309    }
310
311    /// Ensure instance buffer has enough capacity.
312    fn ensure_instance_capacity(&mut self, count: usize) {
313        if count > self.instance_buffer_capacity {
314            // Double capacity or use count, whichever is larger
315            let new_capacity = (self.instance_buffer_capacity * 2).max(count);
316            self.instance_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
317                label: Some("Renderer Instance Buffer"),
318                size: (new_capacity * std::mem::size_of::<ShapeInstance>()) as u64,
319                usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
320                mapped_at_creation: false,
321            });
322            self.instance_buffer_capacity = new_capacity;
323            self.uniform_bind_group = Self::make_bind_group(
324                &self.device,
325                &self.bind_group_layout,
326                &self.uniform_buffer,
327                &self.instance_buffer,
328            );
329        }
330    }
331
332    /// How many image textures are cached.
333    #[cfg(feature = "testing")]
334    pub(crate) fn image_textures(&self) -> usize {
335        self.image_quad_renderer.texture_count()
336    }
337
338    /// How many atlas pages small images are packed on.
339    #[cfg(feature = "testing")]
340    pub(crate) fn image_atlas_pages(&self) -> usize {
341        self.image_quad_renderer.atlas_pages()
342    }
343
344    /// Drop every image texture, as eviction does.
345    #[cfg(feature = "testing")]
346    pub(crate) fn forget_image_textures(&mut self) {
347        self.image_quad_renderer.forget_textures();
348    }
349
350    /// Draw into whatever this surface points at, and hand it over.
351    ///
352    /// `false` means nothing reached the target and the frame should be drawn
353    /// again — a lost or outdated swapchain, which is common right after a
354    /// resize. A texture cannot fail that way: it is already there.
355    pub fn render(
356        &mut self,
357        target: &mut RenderTarget,
358        frame: &FlatFrame<'_>,
359        clear_color: Color,
360    ) -> bool {
361        // Two spellings of one dispatch, and both are the only one clippy
362        // accepts for the build they belong to: the offscreen target exists
363        // only where something reads pixels back, so with it compiled out this
364        // is a one-variant enum and a `match` on it is a `let`. Written out
365        // rather than silenced, because the lint is right in both builds.
366        #[cfg(any(test, feature = "testing"))]
367        let surface = match target {
368            RenderTarget::Offscreen(offscreen) => {
369                let view = offscreen
370                    .texture
371                    .create_view(&wgpu::TextureViewDescriptor::default());
372                let width = offscreen.texture.width();
373                let height = offscreen.texture.height();
374                self.render_to_view(&view, width, height, frame, clear_color);
375                return true;
376            }
377            RenderTarget::Swapchain(surface) => surface,
378        };
379        #[cfg(not(any(test, feature = "testing")))]
380        let RenderTarget::Swapchain(surface) = target;
381        let output = match surface.surface.get_current_texture() {
382            wgpu::CurrentSurfaceTexture::Success(output)
383            | wgpu::CurrentSurfaceTexture::Suboptimal(output) => output,
384            wgpu::CurrentSurfaceTexture::Lost => {
385                surface.resize(surface.width(), surface.height());
386                return false;
387            }
388            other => {
389                log::error!("Surface error: {:?}", other);
390                return false;
391            }
392        };
393
394        let view = output
395            .texture
396            .create_view(&wgpu::TextureViewDescriptor::default());
397
398        self.render_to_view(&view, surface.width(), surface.height(), frame, clear_color);
399
400        self.queue.present(output);
401        true
402    }
403
404    /// Render flattened commands into a texture view the caller owns.
405    ///
406    /// This is everything `render` does once it holds a swapchain texture, with
407    /// the acquire and the present taken off both ends. A caller that has its
408    /// own target — an offscreen texture in a test, with no compositor and no
409    /// surface anywhere — gets the same pixels the screen would have shown.
410    ///
411    /// `width` and `height` are the target's size in physical pixels; the
412    /// backdrop targets are allocated against them.
413    pub fn render_to_view(
414        &mut self,
415        view: &wgpu::TextureView,
416        width: u32,
417        height: u32,
418        frame: &FlatFrame<'_>,
419        clear_color: Color,
420    ) {
421        let FlatFrame {
422            commands,
423            layers,
424            clips,
425        } = *frame;
426        // Update uniform buffer with current screen size (in logical pixels)
427        let uniforms =
428            ShaderUniforms::new(self.screen_width, self.screen_height, self.scale_factor);
429        self.queue
430            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
431
432        self.prepare_layers(commands, layers, clips);
433
434        // A backdrop effect reads pixels already drawn, which a pass cannot do
435        // to its own attachment: the frame goes to an offscreen target and is
436        // blitted over at the end. Frames without one draw straight to the
437        // swapchain and never allocate it.
438        let uses_backdrop = layers.iter().any(|layer| !layer.backdrop.is_empty());
439        if uses_backdrop {
440            self.backdrop
441                .ensure_targets(&self.device, width.max(1), height.max(1));
442        } else {
443            self.backdrop.note_unused();
444        }
445
446        self.clip_records
447            .upload(&self.device, &self.queue, clips, self.scale_factor);
448
449        // Instances for every group live in one buffer; the groups address it
450        // by range, which is what keeps their draws in order.
451        self.ensure_instance_capacity(self.shape_instance_buf.len());
452        if !self.shape_instance_buf.is_empty() {
453            self.queue.write_buffer(
454                &self.instance_buffer,
455                0,
456                bytemuck::cast_slice(&self.shape_instance_buf),
457            );
458        }
459
460        let mut encoder = self
461            .device
462            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
463                label: Some("Renderer Encoder"),
464            });
465
466        let scene_view = uses_backdrop.then(|| self.backdrop.scene_view()).flatten();
467        let target = scene_view.unwrap_or(view);
468
469        let scale = self.scale_factor;
470        {
471            let clear = wgpu::LoadOp::Clear(wgpu::Color {
472                r: clear_color.r as f64,
473                g: clear_color.g as f64,
474                b: clear_color.b as f64,
475                a: clear_color.a as f64,
476            });
477            let mut load = clear;
478            let mut render_pass = begin_pass(&mut encoder, target, load);
479            load = wgpu::LoadOp::Load;
480
481            // Groups are drawn in order; within a group, bucket order. The
482            // shape pipeline is re-bound per draw because the image and text
483            // renderers replace it.
484            for (index, layer) in self.prepared_layers.iter().enumerate() {
485                if !layers[index].backdrop.is_empty() {
486                    // The effect samples the target, so the pass has to end
487                    // and its contents be stored before it can run.
488                    drop(render_pass);
489                    let clip_records = self.clip_records.bind_group();
490                    for command in &commands[layers[index].backdrop.clone()] {
491                        if let Some(region) = command_to_backdrop_region(command, scale) {
492                            self.backdrop
493                                .apply(&self.device, &mut encoder, &region, clip_records);
494                        } else if let Some(frost) = command_to_text_backdrop(command, scale) {
495                            // The mask is rasterized and submitted on its own
496                            // encoder, so it is ready by the time this frame's
497                            // encoder reaches the composite below.
498                            if let Some(view) =
499                                self.text_mask.mask(&self.device, &self.queue, &frost.spec)
500                            {
501                                let mask = CoverageMask {
502                                    view: &view,
503                                    size: frost.spec.size,
504                                };
505                                self.backdrop.apply_masked(
506                                    &self.device,
507                                    &mut encoder,
508                                    &frost.region,
509                                    clip_records,
510                                    mask,
511                                );
512                                // After the blur and before the glyphs: a
513                                // contour on the glass, not under it.
514                                if let Some(outline) = frost.outline {
515                                    self.backdrop.apply_outline(
516                                        &self.device,
517                                        &mut encoder,
518                                        &frost.region,
519                                        clip_records,
520                                        mask,
521                                        outline,
522                                    );
523                                }
524                            }
525                        }
526                    }
527                    render_pass = begin_pass(&mut encoder, target, load);
528                }
529
530                if !layer.shapes.is_empty() {
531                    self.bind_shape_pipeline(&mut render_pass);
532                    render_pass.draw_indexed(0..6, 0, layer.shapes.clone());
533                    render_stats::record_draw_calls(Pipeline::Shapes, 1);
534                }
535
536                if !layer.images.is_empty() {
537                    self.image_quad_renderer.render(
538                        &mut render_pass,
539                        &self.image_quads[layer.images.clone()],
540                        self.clip_records.bind_group(),
541                    );
542                }
543
544                if let Some(slot) = layer.text_slot {
545                    self.text_state.render_slot(slot, &mut render_pass);
546                }
547
548                if !layer.text_quads.is_empty() {
549                    self.text_quad_renderer.render(
550                        &mut render_pass,
551                        &self.text_quads[layer.text_quads.clone()],
552                        self.clip_records.bind_group(),
553                    );
554                }
555
556                if !layer.overlay.is_empty() {
557                    self.bind_shape_pipeline(&mut render_pass);
558                    render_pass.draw_indexed(0..6, 0, layer.overlay.clone());
559                    render_stats::record_draw_calls(Pipeline::Shapes, 1);
560                }
561            }
562        }
563
564        if uses_backdrop {
565            self.backdrop.present(&self.device, &mut encoder, view);
566        }
567
568        self.queue.submit(std::iter::once(encoder.finish()));
569    }
570
571    fn bind_shape_pipeline(&self, pass: &mut wgpu::RenderPass<'_>) {
572        pass.set_pipeline(&self.pipeline);
573        pass.set_bind_group(0, &self.uniform_bind_group, &[]);
574        pass.set_bind_group(1, self.clip_records.bind_group(), &[]);
575        pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
576        pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
577    }
578
579    /// Resolve every group's GPU work before the pass opens, into
580    /// `prepared_layers`.
581    ///
582    /// Uploads cannot happen inside a render pass, so all shaping, atlas
583    /// packing and buffer writes are done here and the pass only issues draws.
584    fn prepare_layers(
585        &mut self,
586        commands: &[FlattenedCommand],
587        layers: &[CommandLayer],
588        clips: &ClipChains,
589    ) {
590        let scale = self.scale_factor;
591
592        self.shape_instance_buf.clear();
593        self.image_quads.clear();
594        self.text_quads.clear();
595        self.prepared_layers.clear();
596        self.image_quad_renderer.begin_frame();
597        self.text_quad_renderer.begin_frame();
598        self.text_mask.begin_frame();
599        self.text_state.begin_frame(
600            &self.queue,
601            (self.screen_width as u32, self.screen_height as u32),
602        );
603
604        // Each group with directly-rendered text needs its own glyphon
605        // renderer, so slots are handed out only to groups that have some.
606        let mut next_text_slot = 0;
607
608        for layer in layers {
609            let shapes_start = self.shape_instance_buf.len() as u32;
610            self.shape_instance_buf.extend(
611                commands[layer.shapes.clone()]
612                    .iter()
613                    .filter_map(|c| faded_instance(c, scale)),
614            );
615            let shapes = shapes_start..self.shape_instance_buf.len() as u32;
616
617            let images_start = self.image_quads.len();
618            if !layer.images.is_empty() {
619                self.image_quad_renderer
620                    .set_screen_size(self.screen_width, self.screen_height);
621                self.image_quad_renderer.prepare(
622                    &self.device,
623                    &self.queue,
624                    &commands[layer.images.clone()],
625                    scale,
626                    &mut self.image_quads,
627                );
628            }
629            let images = images_start..self.image_quads.len();
630
631            self.text_entry_buf.clear();
632            self.text_entry_buf.extend(
633                commands[layer.text.clone()]
634                    .iter()
635                    .filter_map(command_to_text_entry),
636            );
637            let text_quads_start = self.text_quads.len();
638            let mut text_slot = None;
639            if !self.text_entry_buf.is_empty() {
640                let slot = next_text_slot;
641                next_text_slot += 1;
642                let transformed = self.text_state.prepare_layer(
643                    slot,
644                    &self.device,
645                    &self.queue,
646                    &self.text_entry_buf,
647                    clips,
648                    (self.screen_width as u32, self.screen_height as u32),
649                    scale,
650                );
651                // Rotated and scaled text goes through the textured-quad path
652                // instead, to keep the glyphon atlas stable.
653                if transformed.len() < self.text_entry_buf.len() {
654                    text_slot = Some(slot);
655                }
656                if !transformed.is_empty() {
657                    self.text_quad_renderer
658                        .set_screen_size(self.screen_width, self.screen_height);
659                    self.text_quad_renderer.prepare(
660                        &self.device,
661                        &self.queue,
662                        &self.text_entry_buf,
663                        transformed,
664                        scale,
665                        &mut self.text_quads,
666                    );
667                }
668            }
669            let text_quads = text_quads_start..self.text_quads.len();
670
671            let overlay_start = self.shape_instance_buf.len() as u32;
672            self.shape_instance_buf.extend(
673                commands[layer.overlay.clone()]
674                    .iter()
675                    .filter_map(|c| faded_instance(c, scale)),
676            );
677            let overlay = overlay_start..self.shape_instance_buf.len() as u32;
678
679            self.prepared_layers.push(PreparedLayer {
680                shapes,
681                images,
682                text_slot,
683                text_quads,
684                overlay,
685            });
686        }
687
688        self.image_quad_renderer.upload(&self.device, &self.queue);
689        self.text_quad_renderer.upload(&self.device, &self.queue);
690        self.image_quad_renderer.trim(crate::image_cache_budget());
691        self.text_state.end_frame();
692    }
693}
694
695/// One draw group's GPU work, addressed by range into the renderer's
696/// per-frame buffers.
697struct PreparedLayer {
698    shapes: std::ops::Range<u32>,
699    images: std::ops::Range<usize>,
700    /// Glyphon renderer holding this group's directly-rendered text.
701    text_slot: Option<usize>,
702    text_quads: std::ops::Range<usize>,
703    overlay: std::ops::Range<u32>,
704}
705
706/// Open a colour-only render pass over `target`.
707fn begin_pass<'a>(
708    encoder: &'a mut wgpu::CommandEncoder,
709    target: &'a wgpu::TextureView,
710    load: wgpu::LoadOp<wgpu::Color>,
711) -> wgpu::RenderPass<'a> {
712    encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
713        label: Some("Renderer Render Pass"),
714        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
715            view: target,
716            resolve_target: None,
717            ops: wgpu::Operations {
718                load,
719                store: wgpu::StoreOp::Store,
720            },
721            depth_slice: None,
722        })],
723        depth_stencil_attachment: None,
724        timestamp_writes: None,
725        occlusion_query_set: None,
726        multiview_mask: None,
727    })
728}
729
730/// Where a placed shape lands for the backdrop pass.
731///
732/// Four values that all follow from the shape and the scale, so the pass is
733/// given those and works the rest out: the viewport it may touch, the map back
734/// into the shape's own space, and the radius in pixels. They used to be
735/// fields, and the viewport was the world box of the very shape beside it —
736/// one fact written twice, free to drift, with nothing that would have said so.
737fn backdrop_region(
738    shape: PlacedShape,
739    radius: f32,
740    cmd: &FlattenedCommand,
741    scale: f32,
742) -> BackdropRegion {
743    BackdropRegion {
744        shape,
745        scale,
746        radius,
747        // The clip itself, not the box around it: the pass narrows its viewport
748        // by the box and then cuts the difference back off per fragment, the
749        // same two steps it already takes for the shape.
750        clip: cmd.clip(),
751        opacity: cmd.opacity,
752    }
753}
754
755/// Resolve a backdrop command to the physical-pixel region it filters.
756fn command_to_backdrop_region(cmd: &FlattenedCommand, scale: f32) -> Option<BackdropRegion> {
757    let DrawCommand::BackdropBlur {
758        rect,
759        sources,
760        radius,
761        corner_radii,
762        curvature,
763    } = &*cmd.command
764    else {
765        return None;
766    };
767    // The compositor half of the same command is published as a `wl_region`
768    // instead; this pass filters only what the surface has drawn itself.
769    if !sources.contains(crate::backdrop::BackdropSources::SURFACE) {
770        return None;
771    }
772
773    // The corners are the container's own, so the mask is cut where they are
774    // circles and its sides are its sides.
775    Some(backdrop_region(
776        PlacedShape::placed(*rect, *corner_radii, *curvature, cmd.world_transform),
777        *radius,
778        cmd,
779        scale,
780    ))
781}
782
783/// A frosted text resolved to the region it filters and the mask that cuts it.
784struct TextBackdrop<'a> {
785    region: BackdropRegion,
786    spec: MaskSpec<'a>,
787    /// The contour to draw around the coverage, in mask texels — the space it
788    /// is dilated in, so it arrives on the target stretched with the letters.
789    outline: Option<(Color, f32)>,
790}
791
792/// Resolve a text backdrop command to its region and the mask to shape it with.
793///
794/// The mask covers the text's layout box plus slack, in the text's **own**
795/// space, and the region is the box around wherever the transform puts that
796/// frame — the shape a container's blur carries, with a coverage texture where
797/// its corners would be. The composite carries each fragment back into the
798/// frame to read it, so a turned or stretched text keeps a frost that follows
799/// its letters; and where the mask sits is all the transform decides, so an
800/// animating one reuses a rasterization rather than asking for a new one every
801/// frame.
802fn command_to_text_backdrop(cmd: &FlattenedCommand, scale: f32) -> Option<TextBackdrop<'_>> {
803    let DrawCommand::TextBackdropBlur {
804        text,
805        stroke,
806        rect,
807        radius,
808        font_size,
809        font_family,
810        font_weight,
811        line_height,
812        letter_spacing,
813        fit,
814        align,
815    } = &*cmd.command
816    else {
817        return None;
818    };
819
820    // Glyphs overshoot their layout box — descenders, italics, marks — and the
821    // slack here is the one the flattener already uses for a text's bounds. A
822    // contour reaches further still, and what falls outside the frame is not
823    // drawn at all.
824    let slack = font_size * 0.5 + stroke.map(|s| s.width).unwrap_or(0.0);
825    let box_of_ink = rect.outset(slack);
826
827    // Texels per logical pixel. A text the transform only moves is read back at
828    // the density it was made, so the surface scale is the whole of it; one the
829    // transform stretches is read back across more pixels than it has texels,
830    // and takes the supersample the glyph textures take for the same reason.
831    //
832    // A boolean and not the transform's own magnitude, so the mask survives an
833    // animation: a scale sweeping 1.0 -> 1.6 crosses this once and rasterizes
834    // twice, where a density that tracked the stretch would rasterize afresh
835    // every frame.
836    //
837    // Both flat rules were tried and both are worse, in half the cells each.
838    // Pixels of half-covered glyph edge over the six cells of
839    // `frosted_text_follows_its_letters_at_scale_2x` — fewer is tighter:
840    //
841    //                at rest  scale 1.6  stretch  rotate  clipped  wrapped
842    //     this rule      244       2052      989     747      692     1677
843    //     flat 2x        295       2052      989     747      698     1677
844    //     scale only     244       2264     1128     875      692     2102
845    //
846    // The numbers live here and nowhere else; what the reference and the skill
847    // carry is the shape of the result and a pointer back.
848    let density = if cmd.world_transform.is_translation_only() {
849        scale
850    } else {
851        scale * TEXT_SUPERSAMPLE
852    };
853    let width = (box_of_ink.width * density).ceil().max(1.0);
854    let height = (box_of_ink.height * density).ceil().max(1.0);
855
856    // The frame is the texture's extent and not the ink's: a whole number of
857    // texels, so the rect the shader reads the mask over and the rect the mask
858    // was drawn into are the same rect. Sizing it from the ink instead leaves
859    // the mask displayed short by `ceil(w·density)/(w·density)` — a per cent or
860    // so, which is half a pixel of frost beside its letters at the frame's
861    // edges, and the one failure this whole change is about.
862    let frame = Rect::new(
863        box_of_ink.x,
864        box_of_ink.y,
865        width / density,
866        height / density,
867    );
868
869    // Cornerless: the masked composite reads coverage over this frame and never
870    // asks the rounded-rect SDF, so the shape here is entirely the mask's.
871    let region = backdrop_region(
872        PlacedShape::placed(frame, CornerRadii::uniform(0.0), 1.0, cmd.world_transform),
873        *radius,
874        cmd,
875        scale,
876    );
877
878    Some(TextBackdrop {
879        region,
880        spec: MaskSpec {
881            text,
882            font_size: *font_size,
883            font_family: *font_family,
884            font_weight: *font_weight,
885            line_height: *line_height,
886            letter_spacing: *letter_spacing,
887            fit: *fit,
888            align: *align,
889            // The height of whichever path will draw the glyphs over the
890            // frost: the two give an uncut text different heights, and the
891            // frost has to keep the lines the letters keep.
892            buffer_height: if cmd.world_transform.is_translation_only() {
893                super::text::buffer_height(*rect, density)
894            } else {
895                super::text_quad::buffer_height(*rect, density)
896            },
897            box_width: rect.width * density,
898            size: (width as u32, height as u32),
899            offset: (slack * density, slack * density),
900            density,
901        },
902        outline: stroke.map(|s| (s.color, s.width * density)),
903    })
904}
905
906/// A command's shape instance, at the opacity it was flattened under — faded
907/// here, once, so no arm below can forget to.
908fn faded_instance(cmd: &FlattenedCommand, scale: f32) -> Option<ShapeInstance> {
909    command_to_instance(cmd, scale).map(|instance| instance.faded(cmd.opacity))
910}
911
912/// Convert a single flattened command to a shape instance.
913fn command_to_instance(cmd: &FlattenedCommand, scale: f32) -> Option<ShapeInstance> {
914    match &*cmd.command {
915        DrawCommand::RoundedRect {
916            rect,
917            color,
918            radius,
919            curvature,
920            border,
921            shadow,
922            gradient,
923        } => {
924            let mut instance = ShapeInstance::from_rect(
925                [
926                    rect.x * scale,
927                    rect.y * scale,
928                    rect.width * scale,
929                    rect.height * scale,
930                ],
931                [color.r, color.g, color.b, color.a],
932                radius.scaled(scale).to_array(),
933                *curvature,
934            )
935            .with_transform(&cmd.world_transform, scale);
936
937            if let Some(b) = border {
938                instance = instance.with_border(b, scale);
939            }
940            if let Some(s) = shadow {
941                instance = instance.with_shadow(s, scale);
942            }
943            if let Some(g) = gradient {
944                instance = instance.with_gradient(g);
945            }
946            if let Some(clip) = cmd.clip() {
947                instance = instance.with_clip(&clip, scale);
948            }
949
950            Some(instance)
951        }
952        DrawCommand::Circle {
953            center,
954            radius,
955            color,
956        } => {
957            // Convert circle to a rounded rect with radius = half size
958            let rect_x = (center.0 - radius) * scale;
959            let rect_y = (center.1 - radius) * scale;
960            let size = radius * 2.0 * scale;
961
962            let mut instance = ShapeInstance::from_rect(
963                [rect_x, rect_y, size, size],
964                [color.r, color.g, color.b, color.a],
965                [radius * scale; 4], // Full radius = circle
966                1.0,                 // Circular corners
967            )
968            .with_transform(&cmd.world_transform, scale);
969
970            if let Some(clip) = cmd.clip() {
971                instance = instance.with_clip(&clip, scale);
972            }
973
974            Some(instance)
975        }
976        // Text commands are handled separately via command_to_text_entry
977        DrawCommand::Text { .. } => None,
978        // Filters the target rather than adding geometry; handled between
979        // draw groups, not as an instance.
980        DrawCommand::BackdropBlur { .. } | DrawCommand::TextBackdropBlur { .. } => None,
981        // Says where input goes, not what is drawn.
982        DrawCommand::InputRegion { .. } => None,
983        // Image commands are handled separately via ImageQuadRenderer
984        DrawCommand::Image { .. } => None,
985    }
986}
987
988/// Convert a text command to a TextEntry for text rendering.
989fn command_to_text_entry(cmd: &FlattenedCommand) -> Option<TextEntry> {
990    match &*cmd.command {
991        DrawCommand::Text {
992            text,
993            rect,
994            color,
995            font_size,
996            font_family,
997            font_weight,
998            line_height,
999            letter_spacing,
1000            fit,
1001            align,
1002        } => Some(TextEntry {
1003            text: text.clone(),
1004            rect: *rect,
1005            color: *color,
1006            font_size: *font_size,
1007            font_family: *font_family,
1008            font_weight: *font_weight,
1009            line_height: *line_height,
1010            letter_spacing: *letter_spacing,
1011            fit: *fit,
1012            align: *align,
1013            opacity: cmd.opacity,
1014            clip: cmd.clip(),
1015            transform: cmd.world_transform,
1016        }),
1017        _ => None,
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024    use std::rc::Rc;
1025
1026    use crate::renderer::flatten::RenderLayer;
1027    use crate::transform::Transform;
1028    use crate::widgets::FontFamily;
1029
1030    fn frosted(rect: Rect, transform: Transform) -> FlattenedCommand {
1031        FlattenedCommand {
1032            command: Rc::new(DrawCommand::TextBackdropBlur {
1033                text: "09:41".to_owned(),
1034                stroke: None,
1035                rect,
1036                radius: 10.0,
1037                font_size: 20.0,
1038                font_family: FontFamily::default(),
1039                font_weight: Default::default(),
1040                line_height: Default::default(),
1041                letter_spacing: 0.0,
1042                fit: None,
1043                align: Default::default(),
1044            }),
1045            world_transform: transform,
1046            layer: RenderLayer::Backdrop,
1047            opacity: 1.0,
1048            clip: None,
1049        }
1050    }
1051
1052    /// The mask is read over `shape`, with the glyph origin `offset` texels
1053    /// into it. Those two have to meet at the text's own position, or the
1054    /// frost sits beside its letters — and they meet there in the text's own
1055    /// space, which is why no transform can pull them apart.
1056    #[test]
1057    fn the_glyph_origin_in_the_mask_is_where_the_text_is() {
1058        for transform in [
1059            Transform::IDENTITY,
1060            Transform::translate(40.0, 5.0),
1061            Transform::scale(2.5),
1062            Transform::scale_xy(0.5, 3.0),
1063            Transform::rotate(0.4),
1064        ] {
1065            let cmd = frosted(Rect::new(10.3, 20.6, 100.0, 30.0), transform);
1066            let frost = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1067            let frame = frost.region.shape.rect;
1068            let origin = (
1069                frame.x + frost.spec.offset.0 / frost.spec.density,
1070                frame.y + frost.spec.offset.1 / frost.spec.density,
1071            );
1072            assert!(
1073                (origin.0 - 10.3).abs() < 1e-3 && (origin.1 - 20.6).abs() < 1e-3,
1074                "{origin:?} is not the text's own origin under {transform:?}"
1075            );
1076            // Exactly, not to the nearest texel: the shader reads the mask
1077            // over `frame` and divides by its width, so a frame wider than the
1078            // texture it stands for displays the coverage short.
1079            assert_eq!(
1080                (
1081                    frame.width * frost.spec.density,
1082                    frame.height * frost.spec.density
1083                ),
1084                (frost.spec.size.0 as f32, frost.spec.size.1 as f32),
1085                "the frame and the texture are the same extent under {transform:?}"
1086            );
1087        }
1088    }
1089
1090    /// Descenders and italics reach past the layout box, and so must the frost:
1091    /// the same slack the flattener gives a text's bounds.
1092    #[test]
1093    fn the_region_covers_more_than_the_layout_box() {
1094        let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1095        let frost = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1096        assert!(
1097            frost.region.viewport().width >= 110.0,
1098            "{:?}",
1099            frost.region.viewport()
1100        );
1101        assert!(
1102            frost.region.viewport().height >= 40.0,
1103            "{:?}",
1104            frost.region.viewport()
1105        );
1106    }
1107
1108    /// The viewport is the box around the shape it travels with, at every
1109    /// placement — which is a fact about one value now rather than an agreement
1110    /// between two.
1111    ///
1112    /// It used to be a field the caller filled in with the world box of the
1113    /// very shape beside it. Nothing checked they matched, and a turned shape
1114    /// is exactly where they would have stopped matching quietly.
1115    #[test]
1116    fn the_viewport_is_the_box_around_the_shape() {
1117        for transform in [
1118            Transform::IDENTITY,
1119            Transform::translate(40.0, 5.0),
1120            Transform::scale(2.5),
1121            Transform::rotate_degrees(30.0),
1122        ] {
1123            let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), transform);
1124            for scale in [1.0_f32, 2.0] {
1125                let frost = command_to_text_backdrop(&cmd, scale).expect("a frost");
1126                let world = frost.region.shape.world_aabb();
1127                let viewport = frost.region.viewport();
1128                assert!(
1129                    (viewport.x - world.x * scale).abs() < 1e-3
1130                        && (viewport.y - world.y * scale).abs() < 1e-3
1131                        && (viewport.width - world.width * scale).abs() < 1e-3
1132                        && (viewport.height - world.height * scale).abs() < 1e-3,
1133                    "{transform:?} at scale {scale}: {viewport:?} is not \
1134                     {world:?} in physical pixels"
1135                );
1136            }
1137        }
1138    }
1139
1140    /// The surface scale reaches the viewport and the mask's density. It does
1141    /// *not* reach the radius or the shape: those stay logical, and the scale
1142    /// travels beside them so the pass can apply it once, where it also places
1143    /// the shape. Scaling them here as well would apply it twice.
1144    #[test]
1145    fn the_scale_factor_reaches_the_viewport_and_the_density_and_nothing_twice() {
1146        let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1147        let one = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1148        let two = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1149
1150        assert_eq!(two.region.scale, 2.0);
1151        assert_eq!(
1152            two.region.radius, one.region.radius,
1153            "the radius is logical, and the scale is carried rather than baked in"
1154        );
1155        assert_eq!(
1156            two.region.shape.rect, one.region.shape.rect,
1157            "and so is the shape"
1158        );
1159        assert!(two.region.viewport().width >= one.region.viewport().width * 2.0 - 1.0);
1160        assert_eq!(two.spec.density, one.spec.density * 2.0);
1161    }
1162
1163    /// A translation moves the viewport and leaves the mask alone, which is
1164    /// what lets a dragged surface reuse the one it already has.
1165    #[test]
1166    fn a_translated_text_is_frosted_where_it_ends_up() {
1167        let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1168        let at_rest = frosted(rect, Transform::IDENTITY);
1169        let at_rest = command_to_text_backdrop(&at_rest, 1.0).expect("a frost");
1170        let moved = frosted(rect, Transform::translate(40.0, 5.0));
1171        let moved = command_to_text_backdrop(&moved, 1.0).expect("a frost");
1172
1173        assert_eq!(moved.spec.size, at_rest.spec.size);
1174        assert_eq!(moved.spec.offset, at_rest.spec.offset);
1175        assert!((moved.region.viewport().x - at_rest.region.viewport().x - 40.0).abs() < 1e-3);
1176        assert!((moved.region.viewport().y - at_rest.region.viewport().y - 5.0).abs() < 1e-3);
1177    }
1178
1179    /// A frost inside a scroll view must not paint where the text has been
1180    /// scrolled away to: the clip is what the effect is allowed to write.
1181    ///
1182    /// It arrives as the shape it is, corners and placement and all, rather
1183    /// than as the box around it — the pass narrows its viewport by the box and
1184    /// cuts the rest per fragment.
1185    #[test]
1186    fn the_clip_reaches_the_region_as_a_shape() {
1187        let clip = crate::shape::PlacedShape {
1188            rect: Rect::new(0.0, 0.0, 200.0, 200.0),
1189            radii: CornerRadii::uniform(16.0),
1190            curvature: 1.0,
1191            placement: Transform::rotate_degrees(20.0),
1192        };
1193        let mut cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1194        cmd.clip = Some(crate::renderer::clip::ClipRef::placed(clip));
1195
1196        let frost = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1197        let arrived = frost.region.clip.expect("a clip").first;
1198        assert_eq!(arrived.rect, clip.rect, "logical, like the shape beside it");
1199        assert_eq!(
1200            arrived.radii.top_left, 16.0,
1201            "with its corners, which a box would have squared off"
1202        );
1203        assert_eq!(
1204            arrived.placement, clip.placement,
1205            "and its placement, which a box would have flattened"
1206        );
1207    }
1208
1209    /// A turned text is frosted too, and along its letters: the viewport is
1210    /// the box around the turned frame, and the frame carried back through
1211    /// `to_shape` is where the mask is read — so the coverage turns with the
1212    /// glyphs instead of being skipped.
1213    #[test]
1214    fn a_turned_text_is_frosted_along_its_letters() {
1215        let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::rotate(0.4));
1216        let frost = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1217
1218        let frame = frost.region.shape.rect;
1219        let turned = cmd.world_transform.map_rect(frame);
1220        assert!(
1221            (frost.region.viewport().x - turned.x).abs() < 1e-3
1222                && (frost.region.viewport().width - turned.width).abs() < 1e-3,
1223            "the viewport is the box around the turned frame: {:?} vs {turned:?}",
1224            frost.region.viewport()
1225        );
1226        assert!(
1227            turned.width > frame.width,
1228            "a turn widens the box it needs, or this proves nothing"
1229        );
1230
1231        // The frame's own corner, carried from the target back into the text's
1232        // space, lands on the frame — which is what the composite does to find
1233        // its mask texel.
1234        let to_shape = frost
1235            .region
1236            .shape
1237            .to_local(frost.region.scale)
1238            .expect("invertible");
1239        let (x, y) = cmd.world_transform.transform_point(frame.x, frame.y);
1240        let (back_x, back_y) = to_shape.transform_point(x, y);
1241        assert!((back_x - frame.x).abs() < 1e-2 && (back_y - frame.y).abs() < 1e-2);
1242    }
1243
1244    /// The mask is rasterized in the text's own space, as the glyphs of a
1245    /// transformed text already are, and how much the transform stretches it is
1246    /// no part of that. So every frame of a scale animation asks for the same
1247    /// mask while its viewport grows — one rasterization over the length of it,
1248    /// rather than one per size it passes through.
1249    #[test]
1250    fn a_scale_grows_the_viewport_and_leaves_the_mask_alone() {
1251        let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1252        let small = frosted(rect, Transform::scale(1.6));
1253        let small = command_to_text_backdrop(&small, 1.0).expect("a frost");
1254        let large = frosted(rect, Transform::scale(3.0));
1255        let large = command_to_text_backdrop(&large, 1.0).expect("a frost");
1256
1257        assert_eq!(large.spec.size, small.spec.size);
1258        assert_eq!(large.spec.density, small.spec.density);
1259        assert_eq!(large.region.shape.rect, small.region.shape.rect);
1260        assert!(
1261            (large.region.viewport().width - small.region.viewport().width / 1.6 * 3.0).abs()
1262                < 1e-2,
1263            "{:?} did not grow with the scale against {:?}",
1264            large.region.viewport(),
1265            small.region.viewport()
1266        );
1267    }
1268
1269    /// A text the transform only moves is read back at the density it was made,
1270    /// and takes no supersample for it. One the transform stretches is read
1271    /// back across more pixels than it has texels, and does — measured at the
1272    /// glyph edge in `frosted_text_follows_its_letters_at_scale_2x`, it is the
1273    /// wrong way round in both directions otherwise.
1274    #[test]
1275    fn only_a_stretched_text_pays_for_the_supersample() {
1276        let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1277        let moved = frosted(rect, Transform::translate(40.0, 5.0));
1278        let moved = command_to_text_backdrop(&moved, 2.0).expect("a frost");
1279        let turned = frosted(rect, Transform::rotate(0.4));
1280        let turned = command_to_text_backdrop(&turned, 2.0).expect("a frost");
1281
1282        assert_eq!(moved.spec.density, 2.0, "the surface scale, and no more");
1283        assert_eq!(turned.spec.density, 2.0 * TEXT_SUPERSAMPLE);
1284    }
1285
1286    /// The frame's groups are prepared into a buffer the renderer keeps.
1287    ///
1288    /// The one container this change moved out of a local and into the
1289    /// renderer, and so the one a later edit could put back without anything
1290    /// noticing: `prepare_layers` fills it and the draw loop reads it a
1291    /// hundred lines later, which compiles either way. Capacity across two
1292    /// frames is what says it is still the same `Vec` — the mechanism, for the
1293    /// reason the sibling test in `text.rs` gives.
1294    ///
1295    /// Five groups and then one, and neither number is arbitrary. A `Vec` of a
1296    /// 64-byte element starts at capacity 4 however few things are pushed into
1297    /// it, so a first frame of two groups and a rebuilt second frame of one
1298    /// both read 4 and the test would pass against the very regression it is
1299    /// here for. Five crosses that step to 8; one rebuilt would come back at
1300    /// 4.
1301    #[test]
1302    fn the_frame_s_groups_are_prepared_into_a_buffer_the_renderer_keeps() {
1303        /// What `Vec` allocates for the first push of an element this size.
1304        const MIN_CAP: usize = 4;
1305        assert!(std::mem::size_of::<PreparedLayer>() <= 1024);
1306
1307        let Some(gpu) = crate::or_skip(crate::renderer::GpuContext::try_new()) else {
1308            return;
1309        };
1310        let mut renderer = Renderer::new(
1311            gpu.device.clone(),
1312            gpu.queue.clone(),
1313            wgpu::TextureFormat::Rgba8Unorm,
1314        );
1315        let mut target = RenderTarget::offscreen(&gpu, 32, 32);
1316
1317        let box_at = |x: f32| FlattenedCommand {
1318            command: Rc::new(DrawCommand::RoundedRect {
1319                rect: Rect::new(x, 0.0, 10.0, 10.0),
1320                color: crate::widgets::Color::WHITE,
1321                radius: CornerRadii::uniform(0.0),
1322                curvature: 1.0,
1323                border: None,
1324                shadow: None,
1325                gradient: None,
1326            }),
1327            world_transform: Transform::IDENTITY,
1328            layer: RenderLayer::Shapes,
1329            opacity: 1.0,
1330            clip: None,
1331        };
1332        let commands: [FlattenedCommand; 5] = std::array::from_fn(|i| box_at(i as f32 * 12.0));
1333        let group = |shapes: std::ops::Range<usize>| CommandLayer {
1334            backdrop: 0..0,
1335            shapes,
1336            images: 0..0,
1337            text: 0..0,
1338            overlay: 0..0,
1339        };
1340        let layers: [CommandLayer; 5] = std::array::from_fn(|i| group(i..i + 1));
1341
1342        let mut frame = |renderer: &mut Renderer, layers: &[CommandLayer]| {
1343            renderer.render(
1344                &mut target,
1345                &FlatFrame {
1346                    commands: &commands,
1347                    layers,
1348                    clips: &Default::default(),
1349                },
1350                crate::widgets::Color::TRANSPARENT,
1351            );
1352        };
1353
1354        frame(&mut renderer, &layers);
1355        let first = renderer.prepared_layers.capacity();
1356        assert!(
1357            first > MIN_CAP,
1358            "five groups should have grown past a fresh `Vec`'s floor: {first}"
1359        );
1360
1361        // One group this time. A `Vec` built per frame would come back at that
1362        // floor; one that was cleared still has room for five.
1363        frame(&mut renderer, &layers[..1]);
1364        assert_eq!(
1365            renderer.prepared_layers.capacity(),
1366            first,
1367            "the second frame reused the first frame's buffer"
1368        );
1369    }
1370}