frust_engine/gpu/strips.rs
1//! The strip instance the vertex shader steps over, and the conversion into
2//! it from a rasterized [`Strip`].
3//!
4//! One [`GpuStrip`] is one instance of a four-vertex quad: there is no
5//! geometry buffer at all, the vertex shader builds the quad's corners from
6//! the instance's own fields (see [`GpuStrip::vertex_range`]).
7
8use bytemuck::{Pod, Zeroable};
9use core::ops::Range;
10use frust_gpu::VertexLayout;
11use vello_common::strip::Strip;
12use vello_common::tile::Tile;
13
14const _: () = assert!(
15 Tile::HEIGHT == 4,
16 "the strip shaders require `Tile::HEIGHT` to be 4",
17);
18
19/// Bit 31 of [`GpuStrip::paint_and_rect_flag`], marking an instance as a
20/// whole rectangle rather than a strip: the shader then reads
21/// [`GpuStrip::dense_width_or_rect_height`] as a height and
22/// [`GpuStrip::col_idx_or_rect_frac`] as a coverage fraction.
23pub const RECT_STRIP_FLAG: u32 = 1 << 31;
24
25/// Bit the colour source starts at in [`GpuStrip::paint_and_rect_flag`].
26const COLOR_SOURCE_SHIFT: u32 = 29;
27
28/// Mask of the two colour-source bits once shifted down.
29const COLOR_SOURCE_MASK: u32 = 0x3;
30
31/// Mask of the three paint-type bits once shifted down.
32const PAINT_TYPE_MASK: u32 = 0x7;
33
34/// The colour source naming a rendered layer page — what a composite instance
35/// carries in bits 29-30.
36const COLOR_SOURCE_LAYER: u32 = 1;
37
38/// The paint type a glyph quad carries in bits 26-28 ([`PaintType::Glyph`]).
39const PAINT_TYPE_GLYPH: u32 = PaintType::Glyph as u32;
40
41/// The shader's `IMAGE_QUALITY_MEDIUM` — bilinear, the highest quality the
42/// [`PaintClass::Image`] program samples at.
43const IMAGE_QUALITY_BILINEAR: u32 = 1;
44
45/// Bit the paint type starts at in [`GpuStrip::paint_and_rect_flag`].
46const PAINT_TYPE_SHIFT: u32 = 26;
47
48/// Mask covering the encoded-paint texel index in
49/// [`GpuStrip::paint_and_rect_flag`] — everything below the paint type.
50pub const PAINT_TEXTURE_INDEX_MASK: u32 = (1 << PAINT_TYPE_SHIFT) - 1;
51
52/// The colour source saying the fragment shader reads
53/// [`GpuStrip::payload`] rather than sampling a rendered layer.
54///
55/// The only source the engine emits: layer compositing is later work, and an
56/// instance that named the layer source would sample the placeholder view
57/// bound in its place.
58const COLOR_SOURCE_PAYLOAD: u32 = 0;
59
60/// How the fragment shader turns an instance's payload into colour.
61///
62/// The discriminants are the shader's own paint-type numbering, not an
63/// arbitrary ordering — they are written into
64/// [`GpuStrip::paint_and_rect_flag`] as-is.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66#[repr(u32)]
67pub enum PaintType {
68 /// The payload is a premultiplied RGBA8 colour, used directly.
69 Solid = 0,
70 /// The payload is a sample position; the record names an atlas image.
71 Image = 1,
72 /// The payload is a sample position; the record names a linear gradient.
73 LinearGradient = 2,
74 /// The payload is a sample position; the record names a radial gradient.
75 RadialGradient = 3,
76 /// The payload is a sample position; the record names a sweep gradient.
77 SweepGradient = 4,
78 /// The payload is a sample position; the record names a blurred rounded
79 /// rectangle.
80 BlurredRoundedRect = 5,
81 /// The payload is the atlas texel under the instance's top-left corner;
82 /// the record names the glyph atlas layer and tint of a whole glyph run.
83 ///
84 /// Only ever carried by a rect instance ([`RECT_STRIP_FLAG`]): the payload
85 /// addresses the atlas from the instance's own origin, which is the glyph's
86 /// corner only when the instance is the glyph's whole rectangle.
87 Glyph = 6,
88}
89
90/// The packed paint descriptor for a `paint_type` instance whose encoded-paint
91/// record starts at `texel_index`.
92///
93/// [`PaintType::Solid`] indexes no record and passes `0`; every other type
94/// carries the texel its record starts at, which is what
95/// `load_encoded_paint_texel` addresses the paint texture by. The index is
96/// masked rather than reported, because it is produced by this crate's own
97/// serialization — a paint table large enough to overflow 26 bits is refused
98/// by the paint texture's own capacity check long before it reaches here.
99#[must_use]
100pub const fn pack_paint_descriptor(paint_type: PaintType, texel_index: u32) -> u32 {
101 (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT)
102 | ((paint_type as u32) << PAINT_TYPE_SHIFT)
103 | (texel_index & PAINT_TEXTURE_INDEX_MASK)
104}
105
106/// The alpha *column* the strip shader addresses coverage by, from the byte
107/// index a [`Strip`] carries.
108///
109/// The two are different units, and the conversion is the shader's own. A
110/// strip's `alpha_idx` counts coverage *bytes* from the start of the frame's
111/// buffer; [`GpuStrip::col_idx_or_rect_frac`] is read as a column ordinal,
112/// which the fragment stage turns into a texel with `col / 4` and a channel
113/// within it with `col % 4` — one channel holding one pixel column's
114/// `Tile::HEIGHT` coverage bytes. So a column is `Tile::HEIGHT` bytes wide,
115/// which is the same unit [`Strip::width_to`] already reports a strip's width
116/// in.
117///
118/// Getting this wrong is invisible on fully-covered geometry and total on
119/// anti-aliased geometry: every alpha-sampled span reads past its own
120/// coverage, lands on unwritten texels, and resolves to zero alpha.
121#[must_use]
122pub const fn alpha_column(alpha_idx: u32) -> u32 {
123 alpha_idx / Tile::HEIGHT as u32
124}
125
126/// One strip instance, matching the shaders' `StripInstance` byte for byte.
127///
128/// Three of the fields are overloaded by [`RECT_STRIP_FLAG`], which is why
129/// they are named for both readings.
130#[repr(C)]
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
132pub struct GpuStrip {
133 /// Left edge in pixels.
134 pub x: u16,
135 /// Top edge in pixels (a multiple of `Tile::HEIGHT`).
136 pub y: u16,
137 /// Width in pixels.
138 pub width: u16,
139 /// Width of the alpha-sampled (dense) part in pixels, or, for a rect
140 /// instance, the rect's height in pixels.
141 pub dense_width_or_rect_height: u16,
142 /// Index of the instance's first alpha column in the alpha texture, or,
143 /// for a rect instance, its packed coverage fraction.
144 pub col_idx_or_rect_frac: u32,
145 /// Paint-dependent payload — a packed color, or the coordinates the paint
146 /// is sampled at.
147 pub payload: u32,
148 /// Packed paint descriptor, with [`RECT_STRIP_FLAG`] in bit 31.
149 pub paint_and_rect_flag: u32,
150 /// Painter's-order index driving early-z rejection: the backmost draw is
151 /// 0 and each draw in front of it increments.
152 pub depth_index: u32,
153}
154
155const _: () = assert!(
156 size_of::<GpuStrip>() == 24,
157 "`GpuStrip` must stay 24 bytes — the shaders' `StripInstance` layout",
158);
159const _: () = assert!(
160 size_of::<GpuStrip>() == size_of::<u16>() * 4 + size_of::<u32>() * 4,
161 "`GpuStrip` must stay padding-free — six `Uint32` vertex attributes read it",
162);
163
164/// The per-draw values every strip instance of one draw shares.
165///
166/// Carried separately from the geometry because a draw resolves them once
167/// (its paint, its painter's-order index) and every strip it emits repeats
168/// them.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct StripDraw {
171 /// [`GpuStrip::payload`].
172 pub payload: u32,
173 /// The packed paint descriptor, without [`RECT_STRIP_FLAG`].
174 pub paint: u32,
175 /// [`GpuStrip::depth_index`].
176 pub depth_index: u32,
177}
178
179impl GpuStrip {
180 /// Vertices in the quad each instance expands to.
181 pub const QUAD_VERTICES: u32 = 4;
182
183 /// The six `Uint32` vertex attributes the 24-byte instance is read
184 /// through, at shader locations 0-5.
185 ///
186 /// The `u16` pairs are read as packed `u32`s and unpacked in the shader,
187 /// so the attribute list stays uniform.
188 #[must_use]
189 pub fn vertex_attributes() -> [wgpu::VertexAttribute; 6] {
190 wgpu::vertex_attr_array![
191 0 => Uint32,
192 1 => Uint32,
193 2 => Uint32,
194 3 => Uint32,
195 4 => Uint32,
196 5 => Uint32,
197 ]
198 }
199
200 /// The instance-stepped vertex buffer layout over [`GpuStrip`].
201 #[must_use]
202 pub fn vertex_layout() -> VertexLayout {
203 VertexLayout {
204 array_stride: size_of::<Self>() as wgpu::BufferAddress,
205 step_mode: wgpu::VertexStepMode::Instance,
206 attributes: Self::vertex_attributes().to_vec(),
207 }
208 }
209
210 /// The vertex range of a strip draw: one quad, built in the shader from
211 /// the vertex index, with no vertex buffer behind it.
212 #[must_use]
213 pub fn vertex_range() -> Range<u32> {
214 0..Self::QUAD_VERTICES
215 }
216
217 /// The instance range of a strip draw covering `count` instances from
218 /// `first_instance` — paired with [`Self::vertex_range`] for one
219 /// `draw` call.
220 #[must_use]
221 pub fn instance_range(first_instance: u32, count: u32) -> Range<u32> {
222 first_instance..first_instance.saturating_add(count)
223 }
224
225 /// The alpha-sampled instance for `strip`, `width` pixels wide.
226 ///
227 /// The strip's alpha index becomes the instance's first alpha *column*,
228 /// and the instance is dense across its whole width: every column samples
229 /// coverage.
230 #[must_use]
231 pub fn from_strip(strip: &Strip, width: u16, draw: StripDraw) -> Self {
232 Self {
233 x: strip.x,
234 y: strip.y,
235 width,
236 dense_width_or_rect_height: width,
237 col_idx_or_rect_frac: alpha_column(strip.alpha_idx()),
238 payload: draw.payload,
239 paint_and_rect_flag: draw.paint,
240 depth_index: draw.depth_index,
241 }
242 }
243
244 /// The alpha-sampled instance for `strip`, taking its width from `next` —
245 /// the strip immediately after it in the same run, which is where a
246 /// strip's extent is recorded.
247 #[must_use]
248 pub fn from_strip_pair(strip: &Strip, next: &Strip, draw: StripDraw) -> Self {
249 Self::from_strip(strip, strip.width_to(next), draw)
250 }
251
252 /// The solid instance filling the gap between `strip` and `next`, or
253 /// `None` when there is no gap to fill.
254 ///
255 /// A gap is filled only when `next` carries the fill-gap flag and sits on
256 /// the same strip row: the flag means "the winding between me and the
257 /// previous strip is non-zero", which says nothing about a strip on
258 /// another row. The instance samples no coverage, so its dense width and
259 /// column index are both zero.
260 #[must_use]
261 pub fn gap_fill(strip: &Strip, next: &Strip, draw: StripDraw) -> Option<Self> {
262 if !next.fill_gap() || next.y != strip.y {
263 return None;
264 }
265 let gap_x = strip.x.saturating_add(strip.width_to(next));
266 let gap_width = next.x.saturating_sub(gap_x);
267 if gap_width == 0 {
268 return None;
269 }
270 Some(Self::solid_fill(gap_x, strip.y, gap_width, draw))
271 }
272
273 /// A solid instance covering one strip row, sampling no coverage.
274 #[must_use]
275 pub fn solid_fill(x: u16, y: u16, width: u16, draw: StripDraw) -> Self {
276 Self {
277 x,
278 y,
279 width,
280 dense_width_or_rect_height: 0,
281 col_idx_or_rect_frac: 0,
282 payload: draw.payload,
283 paint_and_rect_flag: draw.paint,
284 depth_index: draw.depth_index,
285 }
286 }
287
288 /// A rectangle instance of `width` x `height` pixels with packed coverage
289 /// fraction `frac`, flagged with [`RECT_STRIP_FLAG`].
290 ///
291 /// Unlike a strip, a rect instance is not bound to one strip row: its
292 /// height is carried in the instance itself.
293 #[must_use]
294 pub fn from_rect(x: u16, y: u16, width: u16, height: u16, frac: u32, draw: StripDraw) -> Self {
295 Self {
296 x,
297 y,
298 width,
299 dense_width_or_rect_height: height,
300 col_idx_or_rect_frac: frac,
301 payload: draw.payload,
302 paint_and_rect_flag: draw.paint | RECT_STRIP_FLAG,
303 depth_index: draw.depth_index,
304 }
305 }
306
307 /// Whether this instance is a rectangle rather than a strip.
308 #[must_use]
309 pub const fn is_rect(&self) -> bool {
310 self.paint_and_rect_flag & RECT_STRIP_FLAG != 0
311 }
312
313 /// The packed paint descriptor with [`RECT_STRIP_FLAG`] masked off.
314 #[must_use]
315 pub const fn paint(&self) -> u32 {
316 self.paint_and_rect_flag & !RECT_STRIP_FLAG
317 }
318}
319
320/// Which specialised strip program an instance can be drawn with.
321///
322/// The strip fragment stage dispatches on the instance's colour source and
323/// paint type, and a mobile GPU charges every fragment for the branches that
324/// dispatch *could* take, not the one it does (the S1 ablation: a program
325/// keeping only coverage, solid and gradients ran S1's fragments at less than
326/// half the cost of the whole one). So the one `strip.wgsl` source is compiled
327/// once per class, each keeping only its own branches
328/// ([`crate::gpu::shader_src::strip_class_source`]), and a pass's instances are
329/// drawn in runs of one class each ([`split_class_runs`]).
330///
331/// Coverage (strip and rect) and the solid paint are in every class, which is
332/// what lets a short solid run be drawn by its non-solid neighbour's program
333/// (see [`Self::draws`]). [`Self::Uber`] is the whole program — the source
334/// unchanged — and draws everything.
335///
336/// Mostly derived from the instance alone ([`Self::of_flag`]): bits 29-30 of
337/// [`GpuStrip::paint_and_rect_flag`] name the colour source and bits 26-28 the
338/// paint type. The one split the flag cannot make is an image's: whether it
339/// samples nearest/bilinear from the atlas ([`Self::Image`]) or needs the
340/// bicubic or external-texture path ([`Self::ImageFiltered`]) is in its
341/// encoded record, so [`Self::of_flag`] answers the superset and the renderer
342/// refines it per draw from the resolved record ([`Self::of_image`]).
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
344pub enum PaintClass {
345 /// Coverage plus the inline solid colour — and nothing else.
346 SolidCoverage,
347 /// Adds the linear, radial and sweep gradients.
348 Gradient,
349 /// Adds atlas image sampling at nearest and bilinear quality — every
350 /// atlas glyph not drawn as a [glyph quad](Self::Glyph), and every image
351 /// not drawn bicubic.
352 Image,
353 /// Adds every image path: [`Self::Image`]'s, plus bicubic atlas sampling
354 /// and the externally bound texture. Kept apart because the bicubic and
355 /// external code is what an image fragment pays for most, taken or not.
356 ImageFiltered,
357 /// Adds the blurred rounded rectangle.
358 BlurredRect,
359 /// Adds the layer colour source a composite reads its finished page
360 /// through.
361 LayerComposite,
362 /// Adds the glyph quad: one nearest load from the atlas texel the
363 /// instance's payload names, with no paint transform and no extend mode —
364 /// what a cached glyph run draws with. Independent of the image program
365 /// family, since it never samples bilinearly.
366 Glyph,
367 /// The whole program, every branch: what a run too short to be worth its
368 /// own pipeline switch, and every run under `FRUST_ENGINE_UBER_PROGRAM`,
369 /// is drawn with.
370 Uber,
371}
372
373impl PaintClass {
374 /// Every class, in index order.
375 pub const ALL: [Self; 8] = [
376 Self::SolidCoverage,
377 Self::Gradient,
378 Self::Image,
379 Self::ImageFiltered,
380 Self::BlurredRect,
381 Self::LayerComposite,
382 Self::Glyph,
383 Self::Uber,
384 ];
385
386 /// The classes compiled from a specialisation of the source — every class
387 /// but [`Self::Uber`], which is the source itself.
388 pub const SPECIALISED: [Self; 7] = [
389 Self::SolidCoverage,
390 Self::Gradient,
391 Self::Image,
392 Self::ImageFiltered,
393 Self::BlurredRect,
394 Self::LayerComposite,
395 Self::Glyph,
396 ];
397
398 /// The class of an instance whose packed descriptor is
399 /// `paint_and_rect_flag` ([`RECT_STRIP_FLAG`] may be set or not).
400 ///
401 /// A colour source or paint type the shader does not name maps to
402 /// [`Self::Uber`] rather than to a guess: the whole program reads it
403 /// exactly as it always did. An image maps to [`Self::ImageFiltered`],
404 /// the class that draws every image — the flag does not say which path
405 /// the record takes (see [`Self::of_image`]).
406 ///
407 /// A glyph quad is [`Self::Glyph`] only on a rect instance: its payload
408 /// names the atlas texel under the instance's own corner, which is only
409 /// the glyph's corner when the instance is the glyph's whole rectangle. A
410 /// strip carrying the glyph paint type is not an encoding the engine emits,
411 /// and is read as reserved.
412 #[must_use]
413 pub const fn of_flag(paint_and_rect_flag: u32) -> Self {
414 let color_source = (paint_and_rect_flag >> COLOR_SOURCE_SHIFT) & COLOR_SOURCE_MASK;
415 if color_source == COLOR_SOURCE_LAYER {
416 return Self::LayerComposite;
417 }
418 if color_source != COLOR_SOURCE_PAYLOAD {
419 return Self::Uber;
420 }
421 match (paint_and_rect_flag >> PAINT_TYPE_SHIFT) & PAINT_TYPE_MASK {
422 0 => Self::SolidCoverage,
423 1 => Self::ImageFiltered,
424 2..=4 => Self::Gradient,
425 5 => Self::BlurredRect,
426 PAINT_TYPE_GLYPH if paint_and_rect_flag & RECT_STRIP_FLAG != 0 => Self::Glyph,
427 _ => Self::Uber,
428 }
429 }
430
431 /// The class of `strip`, as far as its flag says ([`Self::of_flag`]).
432 #[must_use]
433 pub const fn of_instance(strip: &GpuStrip) -> Self {
434 Self::of_flag(strip.paint_and_rect_flag)
435 }
436
437 /// The class of an image paint whose encoded record samples at `quality`
438 /// (the shader's `IMAGE_QUALITY_*` numbering: 0 nearest, 1 bilinear, 2
439 /// bicubic) from the external texture (`external`) or the atlas.
440 ///
441 /// Only an atlas image at nearest or bilinear quality is [`Self::Image`];
442 /// a bicubic or external one needs [`Self::ImageFiltered`]. The renderer
443 /// asks this once per resolved paint, from the record it lowered.
444 #[must_use]
445 pub const fn of_image(quality: u32, external: bool) -> Self {
446 if external || quality > IMAGE_QUALITY_BILINEAR {
447 Self::ImageFiltered
448 } else {
449 Self::Image
450 }
451 }
452
453 /// This class's position in [`Self::ALL`].
454 #[must_use]
455 pub const fn index(self) -> usize {
456 self as usize
457 }
458
459 /// A short name for logs, pipeline labels and shader-library names.
460 #[must_use]
461 pub const fn label(self) -> &'static str {
462 match self {
463 Self::SolidCoverage => "solid",
464 Self::Gradient => "gradient",
465 Self::Image => "image",
466 Self::ImageFiltered => "image-filtered",
467 Self::BlurredRect => "blurred-rect",
468 Self::LayerComposite => "layer",
469 Self::Glyph => "glyph",
470 Self::Uber => "uber",
471 }
472 }
473
474 /// Whether this class's program draws an instance of class `other`
475 /// exactly as [`Self::Uber`] would.
476 ///
477 /// A class draws itself and the solid paint (coverage and solid are in
478 /// every program); [`Self::ImageFiltered`] also draws [`Self::Image`];
479 /// [`Self::Uber`] draws everything.
480 #[must_use]
481 pub const fn draws(self, other: Self) -> bool {
482 matches!(self, Self::Uber)
483 || matches!(other, Self::SolidCoverage)
484 || matches!((self, other), (Self::ImageFiltered, Self::Image))
485 || self as usize == other as usize
486 }
487
488 /// Whether this class's program draws an instance whose flag is
489 /// `paint_and_rect_flag`, as far as the flag can tell: an image instance
490 /// is admitted by either image class, since which one its record needs is
491 /// not in the flag ([`Self::of_flag`]).
492 #[must_use]
493 pub const fn admits_flag(self, paint_and_rect_flag: u32) -> bool {
494 let class = Self::of_flag(paint_and_rect_flag);
495 self.draws(class) || (matches!(self, Self::Image) && matches!(class, Self::ImageFiltered))
496 }
497}
498
499/// A set of [`PaintClass`]es, as a bitmask — which class programs one pass
500/// needs, so a frame resolves exactly those pipelines and no others.
501#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
502pub struct ClassSet(u8);
503
504const _: () = assert!(
505 PaintClass::ALL.len() <= u8::BITS as usize,
506 "`ClassSet` holds one bit per paint class",
507);
508
509impl ClassSet {
510 /// The empty set.
511 pub const EMPTY: Self = Self(0);
512
513 /// The set holding `class` alone.
514 #[must_use]
515 pub const fn only(class: PaintClass) -> Self {
516 Self(1 << class as u8)
517 }
518
519 /// Adds `class`.
520 pub fn insert(&mut self, class: PaintClass) {
521 self.0 |= 1 << class as u8;
522 }
523
524 /// Whether `class` is in the set.
525 #[must_use]
526 pub const fn contains(self, class: PaintClass) -> bool {
527 self.0 & (1 << class as u8) != 0
528 }
529
530 /// Whether the set is empty.
531 #[must_use]
532 pub const fn is_empty(self) -> bool {
533 self.0 == 0
534 }
535
536 /// The classes in the set, in [`PaintClass::ALL`] order.
537 pub fn iter(self) -> impl Iterator<Item = PaintClass> {
538 PaintClass::ALL
539 .into_iter()
540 .filter(move |class| self.contains(*class))
541 }
542}
543
544/// One run of consecutive instances drawn with one class's program, as an
545/// offset into the slice [`split_class_runs`] was handed.
546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
547pub struct ClassRun {
548 /// Index of the run's first instance within the split slice.
549 pub first: u32,
550 /// How many instances the run holds; never zero.
551 pub count: u32,
552 /// The program the run is drawn with.
553 pub class: PaintClass,
554}
555
556/// The default [`split_class_runs`] threshold: a run of fewer instances than
557/// this is not worth a pipeline switch of its own and is drawn with
558/// [`PaintClass::Uber`] instead.
559///
560/// Measured on a Pixel 4a (Adreno 618) S1 capture, `gpu_main` p50 in ms:
561/// 16 → 13.52 (repeat 13.69), 64 → 13.68, 256 → 13.43 — no GPU difference
562/// within noise, so the default keeps the most runs specialised at about
563/// +13 µs of `record_us`. Overridable at build or launch time with
564/// `FRUST_ENGINE_RUN_SPLIT_MIN` ([`crate::config::run_split_min`]).
565pub const RUN_SPLIT_MIN: u32 = 16;
566
567/// Splits `instances` — one pass segment, in painter order — into consecutive
568/// runs, each drawn with one class's program, appending them to `out`.
569///
570/// Painter order is preserved by construction: the runs are contiguous,
571/// non-overlapping and cover the slice front to back, so drawing them in the
572/// order returned draws exactly the instances the unsplit segment did, in the
573/// same order. Only the program each instance is shaded by changes, and every
574/// program shades the instances it is handed exactly as the whole one would.
575///
576/// Three steps, each linear:
577///
578/// 1. maximal runs of equal [`PaintClass::of_instance`];
579/// 2. a solid run shorter than `min_run` is folded into a neighbouring
580/// non-solid run (the previous one, else the next) — every program draws
581/// solid, so this costs that neighbour nothing and saves a switch; runs of
582/// one class left adjacent by a fold are joined;
583/// 3. a remaining non-solid run shorter than `min_run` is drawn with
584/// [`PaintClass::Uber`], and adjacent Uber runs are joined — the short run
585/// is merged into its neighbours rather than paying for a pipeline switch
586/// of its own.
587///
588/// A solid run is never promoted to Uber: it only survives step 2 short when
589/// it has no non-solid neighbour at all, and then the solid program is the
590/// cheapest one that draws it. `min_run` 0 or 1 disables both merges.
591///
592/// `uber_only` is the kill switch (`FRUST_ENGINE_UBER_PROGRAM`): the whole
593/// slice becomes ONE Uber run, which is the unsplit segment exactly.
594pub fn split_class_runs(
595 instances: &[GpuStrip],
596 min_run: u32,
597 uber_only: bool,
598 out: &mut Vec<ClassRun>,
599) {
600 let total = u32::try_from(instances.len()).unwrap_or(u32::MAX);
601 let mut index = 0_u32;
602 let maximal = std::iter::from_fn(|| {
603 if index >= total {
604 return None;
605 }
606 let class = PaintClass::of_instance(&instances[index as usize]);
607 let mut end = index + 1;
608 while end < total && PaintClass::of_instance(&instances[end as usize]) == class {
609 end += 1;
610 }
611 let run = ClassRun {
612 first: index,
613 count: end - index,
614 class,
615 };
616 index = end;
617 Some(run)
618 });
619 merge_class_runs(maximal, min_run, uber_only, out);
620}
621
622/// [`split_class_runs`] over runs already known to share a class, rather than
623/// over the instances themselves — steps 2 and 3 alone.
624///
625/// `runs` must tile one span front to back (each run's `first` is the end of
626/// the one before it); an empty run is skipped, and adjacent runs sharing a
627/// class are joined before anything else, so the result is exactly what
628/// scanning the instances would give. This is how the renderer splits a segment without
629/// re-reading every instance: every instance of one draw carries the same
630/// paint, so the class changes only at draw boundaries, and it records those
631/// as it emits the draw.
632pub fn merge_class_runs(
633 runs: impl IntoIterator<Item = ClassRun>,
634 min_run: u32,
635 uber_only: bool,
636 out: &mut Vec<ClassRun>,
637) {
638 let start = out.len();
639 if uber_only {
640 let mut runs = runs.into_iter().filter(|run| run.count > 0);
641 if let Some(first) = runs.next() {
642 let count = runs.fold(first.count, |count, run| count.saturating_add(run.count));
643 out.push(ClassRun {
644 first: first.first,
645 count,
646 class: PaintClass::Uber,
647 });
648 }
649 return;
650 }
651
652 // Step 1's maximality first — adjacent runs of one class are joined before
653 // any fold decision looks at a length — then step 2, over a stack of closed
654 // runs: each maximal run is pushed, folding a short solid run into a
655 // non-solid neighbour as soon as both are known.
656 let mut pending: Option<ClassRun> = None;
657 for run in runs {
658 if run.count == 0 {
659 continue;
660 }
661 match pending.as_mut() {
662 Some(open) if open.class == run.class => {
663 open.count = open.count.saturating_add(run.count);
664 }
665 _ => {
666 if let Some(closed) = pending.replace(run) {
667 push_folding(out, start, closed, min_run);
668 }
669 }
670 }
671 }
672 if let Some(closed) = pending {
673 push_folding(out, start, closed, min_run);
674 }
675
676 // Step 3, in place: a short non-solid run is drawn with Uber, and Uber
677 // runs left adjacent are joined.
678 let mut write = start;
679 for read in start..out.len() {
680 let mut run = out[read];
681 if run.class != PaintClass::SolidCoverage && run.count < min_run {
682 run.class = PaintClass::Uber;
683 }
684 if write > start && out[write - 1].class == run.class {
685 out[write - 1].count += run.count;
686 } else {
687 out[write] = run;
688 write += 1;
689 }
690 }
691 out.truncate(write);
692}
693
694/// Pushes `run` onto `out[start..]`, folding a short solid run into a
695/// non-solid neighbour and joining equal neighbours (step 2 of
696/// [`split_class_runs`]).
697fn push_folding(out: &mut Vec<ClassRun>, start: usize, mut run: ClassRun, min_run: u32) {
698 let short_solid =
699 |run: &ClassRun| run.class == PaintClass::SolidCoverage && run.count < min_run;
700 if let Some(top) = out.get_mut(start..).and_then(<[ClassRun]>::last_mut) {
701 if top.class == run.class || (short_solid(&run) && top.class != PaintClass::SolidCoverage) {
702 top.count += run.count;
703 return;
704 }
705 if short_solid(top) && run.class != PaintClass::SolidCoverage {
706 // The solid run before this one had no non-solid run to its left
707 // to fold into; it folds into this one instead.
708 run.first = top.first;
709 run.count += top.count;
710 out.pop();
711 // The fold can leave this run beside one of its own class.
712 if let Some(below) = out.get_mut(start..).and_then(<[ClassRun]>::last_mut)
713 && below.class == run.class
714 {
715 below.count += run.count;
716 return;
717 }
718 }
719 }
720 out.push(run);
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 fn strip(paint: u32) -> GpuStrip {
728 GpuStrip::solid_fill(
729 0,
730 0,
731 1,
732 StripDraw {
733 payload: 0,
734 paint,
735 depth_index: 0,
736 },
737 )
738 }
739
740 fn of(classes: &[(PaintType, u32)]) -> Vec<GpuStrip> {
741 classes
742 .iter()
743 .flat_map(|(paint, count)| {
744 std::iter::repeat_n(strip(pack_paint_descriptor(*paint, 7)), *count as usize)
745 })
746 .collect()
747 }
748
749 fn runs(instances: &[GpuStrip], min_run: u32) -> Vec<(u32, u32, PaintClass)> {
750 let mut out = Vec::new();
751 split_class_runs(instances, min_run, false, &mut out);
752 out.iter().map(|r| (r.first, r.count, r.class)).collect()
753 }
754
755 #[test]
756 fn every_paint_type_and_source_maps_to_its_class() {
757 use PaintClass as C;
758 assert_eq!(
759 C::of_flag(pack_paint_descriptor(PaintType::Solid, 0)),
760 C::SolidCoverage
761 );
762 assert_eq!(
763 C::of_flag(pack_paint_descriptor(PaintType::Image, 3)),
764 C::ImageFiltered,
765 "the flag alone answers the class that draws every image"
766 );
767 for gradient in [
768 PaintType::LinearGradient,
769 PaintType::RadialGradient,
770 PaintType::SweepGradient,
771 ] {
772 assert_eq!(C::of_flag(pack_paint_descriptor(gradient, 9)), C::Gradient);
773 }
774 assert_eq!(
775 C::of_flag(pack_paint_descriptor(PaintType::BlurredRoundedRect, 1)),
776 C::BlurredRect
777 );
778 // A composite: layer source, opacity in the low byte, rect flag set.
779 assert_eq!(
780 C::of_flag((1 << 29) | 0x80 | RECT_STRIP_FLAG),
781 C::LayerComposite
782 );
783 // The rect flag never changes a class.
784 assert_eq!(
785 C::of_flag(pack_paint_descriptor(PaintType::Image, 3) | RECT_STRIP_FLAG),
786 C::ImageFiltered
787 );
788 // The record refines an image: nearest and bilinear atlas images take
789 // the narrow program, bicubic and external ones the filtered one.
790 assert_eq!(C::of_image(0, false), C::Image);
791 assert_eq!(C::of_image(1, false), C::Image);
792 assert_eq!(C::of_image(2, false), C::ImageFiltered);
793 assert_eq!(C::of_image(0, true), C::ImageFiltered);
794 assert_eq!(C::of_image(1, true), C::ImageFiltered);
795 let image = pack_paint_descriptor(PaintType::Image, 3);
796 assert!(C::Image.admits_flag(image));
797 assert!(C::ImageFiltered.admits_flag(image));
798 assert!(!C::Gradient.admits_flag(image));
799 // Reserved sources and paint types fall back to the whole program.
800 assert_eq!(C::of_flag(2 << 29), C::Uber);
801 assert_eq!(C::of_flag(3 << 29), C::Uber);
802 assert_eq!(C::of_flag(6 << 26), C::Uber);
803 assert_eq!(C::of_flag(7 << 26), C::Uber);
804 }
805
806 #[test]
807 fn a_glyph_quad_is_its_own_class_only_on_a_rect_instance() {
808 use PaintClass as C;
809 let glyph = pack_paint_descriptor(PaintType::Glyph, 12);
810 assert_eq!(C::of_flag(glyph | RECT_STRIP_FLAG), C::Glyph);
811 assert_eq!(
812 C::of_flag(glyph),
813 C::Uber,
814 "a strip carrying the glyph paint type is reserved"
815 );
816 assert!(C::Glyph.admits_flag(glyph | RECT_STRIP_FLAG));
817 assert!(C::Glyph.admits_flag(pack_paint_descriptor(PaintType::Solid, 0)));
818 assert!(!C::Glyph.admits_flag(pack_paint_descriptor(PaintType::Image, 3)));
819 assert!(!C::Image.admits_flag(glyph | RECT_STRIP_FLAG));
820 assert!(!C::ImageFiltered.draws(C::Glyph));
821 assert!(C::Uber.admits_flag(glyph | RECT_STRIP_FLAG));
822 assert_eq!(C::Glyph.label(), "glyph");
823 // Every class still fits the set's one bit each.
824 let mut set = ClassSet::EMPTY;
825 for class in C::ALL {
826 set.insert(class);
827 }
828 assert_eq!(set.iter().count(), C::ALL.len());
829 }
830
831 #[test]
832 fn the_class_lattice_is_solid_below_everything_and_uber_above() {
833 for class in PaintClass::ALL {
834 assert!(class.draws(PaintClass::SolidCoverage));
835 assert!(class.draws(class));
836 assert!(PaintClass::Uber.draws(class));
837 }
838 assert!(!PaintClass::Gradient.draws(PaintClass::Image));
839 assert!(PaintClass::ImageFiltered.draws(PaintClass::Image));
840 assert!(!PaintClass::Image.draws(PaintClass::ImageFiltered));
841 assert!(!PaintClass::SolidCoverage.draws(PaintClass::Gradient));
842 assert!(!PaintClass::Image.draws(PaintClass::Uber));
843 for (index, class) in PaintClass::ALL.iter().enumerate() {
844 assert_eq!(class.index(), index);
845 }
846 assert_eq!(PaintClass::SPECIALISED.len() + 1, PaintClass::ALL.len());
847 assert!(!PaintClass::SPECIALISED.contains(&PaintClass::Uber));
848 }
849
850 #[test]
851 fn a_class_set_round_trips() {
852 let mut set = ClassSet::EMPTY;
853 assert!(set.is_empty());
854 set.insert(PaintClass::Image);
855 set.insert(PaintClass::Uber);
856 set.insert(PaintClass::Image);
857 assert!(set.contains(PaintClass::Image));
858 assert!(!set.contains(PaintClass::Gradient));
859 assert_eq!(
860 set.iter().collect::<Vec<_>>(),
861 vec![PaintClass::Image, PaintClass::Uber]
862 );
863 assert_eq!(ClassSet::only(PaintClass::Gradient).iter().count(), 1);
864 }
865
866 #[test]
867 fn long_runs_keep_their_class_and_cover_the_slice_in_order() {
868 use PaintType as P;
869 let instances = of(&[
870 (P::LinearGradient, 20),
871 (P::Image, 30),
872 (P::RadialGradient, 17),
873 ]);
874 assert_eq!(
875 runs(&instances, 16),
876 vec![
877 (0, 20, PaintClass::Gradient),
878 (20, 30, PaintClass::ImageFiltered),
879 (50, 17, PaintClass::Gradient),
880 ]
881 );
882 }
883
884 #[test]
885 fn a_short_solid_run_folds_into_its_non_solid_neighbour() {
886 use PaintType as P;
887 // Behind a gradient: folds backwards.
888 let instances = of(&[
889 (P::LinearGradient, 20),
890 (P::Solid, 3),
891 (P::LinearGradient, 20),
892 ]);
893 assert_eq!(runs(&instances, 16), vec![(0, 43, PaintClass::Gradient)]);
894 // At the front: folds forwards into the first non-solid run.
895 let instances = of(&[(P::Solid, 3), (P::Image, 20)]);
896 assert_eq!(
897 runs(&instances, 16),
898 vec![(0, 23, PaintClass::ImageFiltered)]
899 );
900 // A long solid run stays a run of its own.
901 let instances = of(&[(P::Image, 20), (P::Solid, 40), (P::Image, 20)]);
902 assert_eq!(
903 runs(&instances, 16),
904 vec![
905 (0, 20, PaintClass::ImageFiltered),
906 (20, 40, PaintClass::SolidCoverage),
907 (60, 20, PaintClass::ImageFiltered),
908 ]
909 );
910 // An all-solid segment is one solid run however short.
911 assert_eq!(
912 runs(&of(&[(P::Solid, 2)]), 16),
913 vec![(0, 2, PaintClass::SolidCoverage)]
914 );
915 }
916
917 #[test]
918 fn a_run_below_the_threshold_is_merged_and_drawn_with_uber() {
919 use PaintType as P;
920 // S1's shape: a long gradient run, then a short text (image) run.
921 let instances = of(&[
922 (P::RadialGradient, 40),
923 (P::Image, 5),
924 (P::BlurredRoundedRect, 3),
925 (P::RadialGradient, 40),
926 ]);
927 assert_eq!(
928 runs(&instances, 16),
929 vec![
930 (0, 40, PaintClass::Gradient),
931 (40, 8, PaintClass::Uber),
932 (48, 40, PaintClass::Gradient),
933 ]
934 );
935 // The same slice under a higher threshold: the gradient runs are now
936 // short too, and the whole segment collapses into one Uber run.
937 assert_eq!(runs(&instances, 64), vec![(0, 88, PaintClass::Uber)]);
938 // Threshold 0 disables merging altogether.
939 assert_eq!(runs(&instances, 0).len(), 4);
940 }
941
942 #[test]
943 fn the_kill_switch_yields_one_uber_run() {
944 use PaintType as P;
945 let instances = of(&[(P::RadialGradient, 40), (P::Image, 40), (P::Solid, 40)]);
946 let mut out = Vec::new();
947 split_class_runs(&instances, 16, true, &mut out);
948 assert_eq!(
949 out,
950 vec![ClassRun {
951 first: 0,
952 count: 120,
953 class: PaintClass::Uber
954 }]
955 );
956 out.clear();
957 split_class_runs(&[], 16, false, &mut out);
958 assert!(out.is_empty());
959 }
960
961 #[test]
962 fn appending_to_a_non_empty_vec_never_merges_across_calls() {
963 use PaintType as P;
964 let mut out = Vec::new();
965 split_class_runs(&of(&[(P::Image, 20)]), 16, false, &mut out);
966 split_class_runs(&of(&[(P::Image, 20)]), 16, false, &mut out);
967 assert_eq!(out.len(), 2);
968 assert_eq!(out[1].first, 0);
969 }
970
971 /// The renderer's draw-granular path agrees with the instance scan: the
972 /// same slice, handed over as per-draw runs that may repeat a class, splits
973 /// into exactly the runs the scan produces.
974 #[test]
975 fn merging_draw_runs_equals_scanning_their_instances() {
976 use PaintType as P;
977 let draws = [
978 (P::Solid, 3),
979 (P::RadialGradient, 20),
980 (P::Solid, 10),
981 (P::Solid, 10),
982 (P::LinearGradient, 9),
983 (P::Image, 5),
984 (P::Image, 4),
985 (P::Solid, 2),
986 (P::BlurredRoundedRect, 30),
987 (P::Solid, 40),
988 ];
989 let instances = of(&draws);
990 let mut first = 0;
991 let per_draw: Vec<ClassRun> = draws
992 .iter()
993 .map(|(paint, count)| {
994 let run = ClassRun {
995 first,
996 count: *count,
997 class: PaintClass::of_flag(pack_paint_descriptor(*paint, 7)),
998 };
999 first += count;
1000 run
1001 })
1002 .collect();
1003 for min_run in [0, 1, 4, 16, 64] {
1004 for uber in [false, true] {
1005 let mut scanned = Vec::new();
1006 split_class_runs(&instances, min_run, uber, &mut scanned);
1007 let mut merged = Vec::new();
1008 merge_class_runs(per_draw.iter().copied(), min_run, uber, &mut merged);
1009 assert_eq!(scanned, merged, "threshold {min_run}, uber {uber}");
1010 }
1011 }
1012 }
1013
1014 /// Exhaustive over short sequences: the runs always tile the slice, never
1015 /// hand an instance to a program that cannot draw it, and never leave two
1016 /// adjacent runs of one class.
1017 #[test]
1018 fn the_split_always_tiles_and_every_run_draws_its_instances() {
1019 let kinds = [
1020 PaintType::Solid,
1021 PaintType::Image,
1022 PaintType::LinearGradient,
1023 PaintType::BlurredRoundedRect,
1024 ];
1025 for len in 1..=6_u32 {
1026 for code in 0..(kinds.len() as u32).pow(len) {
1027 let mut instances = Vec::new();
1028 let mut c = code;
1029 for _ in 0..len {
1030 let kind = kinds[(c % kinds.len() as u32) as usize];
1031 c /= kinds.len() as u32;
1032 instances.push(strip(pack_paint_descriptor(kind, 1)));
1033 }
1034 for min_run in [0, 1, 2, 3, 16] {
1035 let mut out = Vec::new();
1036 split_class_runs(&instances, min_run, false, &mut out);
1037 let mut next = 0;
1038 for (i, run) in out.iter().enumerate() {
1039 assert_eq!(run.first, next, "{out:?}");
1040 assert!(run.count > 0);
1041 for instance in
1042 &instances[run.first as usize..(run.first + run.count) as usize]
1043 {
1044 assert!(
1045 run.class.draws(PaintClass::of_instance(instance)),
1046 "{out:?}"
1047 );
1048 }
1049 if i > 0 {
1050 assert_ne!(out[i - 1].class, run.class, "{out:?}");
1051 }
1052 next += run.count;
1053 }
1054 assert_eq!(next, len);
1055 }
1056 }
1057 }
1058 }
1059}