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/// Bit the paint type starts at in [`GpuStrip::paint_and_rect_flag`].
29const PAINT_TYPE_SHIFT: u32 = 26;
30
31/// Mask covering the encoded-paint texel index in
32/// [`GpuStrip::paint_and_rect_flag`] — everything below the paint type.
33pub const PAINT_TEXTURE_INDEX_MASK: u32 = (1 << PAINT_TYPE_SHIFT) - 1;
34
35/// The colour source saying the fragment shader reads
36/// [`GpuStrip::payload`] rather than sampling a rendered layer.
37///
38/// The only source the engine emits: layer compositing is later work, and an
39/// instance that named the layer source would sample the placeholder view
40/// bound in its place.
41const COLOR_SOURCE_PAYLOAD: u32 = 0;
42
43/// How the fragment shader turns an instance's payload into colour.
44///
45/// The discriminants are the shader's own paint-type numbering, not an
46/// arbitrary ordering — they are written into
47/// [`GpuStrip::paint_and_rect_flag`] as-is.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[repr(u32)]
50pub enum PaintType {
51 /// The payload is a premultiplied RGBA8 colour, used directly.
52 Solid = 0,
53 /// The payload is a sample position; the record names an atlas image.
54 Image = 1,
55 /// The payload is a sample position; the record names a linear gradient.
56 LinearGradient = 2,
57 /// The payload is a sample position; the record names a radial gradient.
58 RadialGradient = 3,
59 /// The payload is a sample position; the record names a sweep gradient.
60 SweepGradient = 4,
61 /// The payload is a sample position; the record names a blurred rounded
62 /// rectangle.
63 BlurredRoundedRect = 5,
64}
65
66/// The packed paint descriptor for a `paint_type` instance whose encoded-paint
67/// record starts at `texel_index`.
68///
69/// [`PaintType::Solid`] indexes no record and passes `0`; every other type
70/// carries the texel its record starts at, which is what
71/// `load_encoded_paint_texel` addresses the paint texture by. The index is
72/// masked rather than reported, because it is produced by this crate's own
73/// serialization — a paint table large enough to overflow 26 bits is refused
74/// by the paint texture's own capacity check long before it reaches here.
75#[must_use]
76pub const fn pack_paint_descriptor(paint_type: PaintType, texel_index: u32) -> u32 {
77 (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT)
78 | ((paint_type as u32) << PAINT_TYPE_SHIFT)
79 | (texel_index & PAINT_TEXTURE_INDEX_MASK)
80}
81
82/// The alpha *column* the strip shader addresses coverage by, from the byte
83/// index a [`Strip`] carries.
84///
85/// The two are different units, and the conversion is the shader's own. A
86/// strip's `alpha_idx` counts coverage *bytes* from the start of the frame's
87/// buffer; [`GpuStrip::col_idx_or_rect_frac`] is read as a column ordinal,
88/// which the fragment stage turns into a texel with `col / 4` and a channel
89/// within it with `col % 4` — one channel holding one pixel column's
90/// `Tile::HEIGHT` coverage bytes. So a column is `Tile::HEIGHT` bytes wide,
91/// which is the same unit [`Strip::width_to`] already reports a strip's width
92/// in.
93///
94/// Getting this wrong is invisible on fully-covered geometry and total on
95/// anti-aliased geometry: every alpha-sampled span reads past its own
96/// coverage, lands on unwritten texels, and resolves to zero alpha.
97#[must_use]
98pub const fn alpha_column(alpha_idx: u32) -> u32 {
99 alpha_idx / Tile::HEIGHT as u32
100}
101
102/// One strip instance, matching the shaders' `StripInstance` byte for byte.
103///
104/// Three of the fields are overloaded by [`RECT_STRIP_FLAG`], which is why
105/// they are named for both readings.
106#[repr(C)]
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
108pub struct GpuStrip {
109 /// Left edge in pixels.
110 pub x: u16,
111 /// Top edge in pixels (a multiple of `Tile::HEIGHT`).
112 pub y: u16,
113 /// Width in pixels.
114 pub width: u16,
115 /// Width of the alpha-sampled (dense) part in pixels, or, for a rect
116 /// instance, the rect's height in pixels.
117 pub dense_width_or_rect_height: u16,
118 /// Index of the instance's first alpha column in the alpha texture, or,
119 /// for a rect instance, its packed coverage fraction.
120 pub col_idx_or_rect_frac: u32,
121 /// Paint-dependent payload — a packed color, or the coordinates the paint
122 /// is sampled at.
123 pub payload: u32,
124 /// Packed paint descriptor, with [`RECT_STRIP_FLAG`] in bit 31.
125 pub paint_and_rect_flag: u32,
126 /// Painter's-order index driving early-z rejection: the backmost draw is
127 /// 0 and each draw in front of it increments.
128 pub depth_index: u32,
129}
130
131const _: () = assert!(
132 size_of::<GpuStrip>() == 24,
133 "`GpuStrip` must stay 24 bytes — the shaders' `StripInstance` layout",
134);
135const _: () = assert!(
136 size_of::<GpuStrip>() == size_of::<u16>() * 4 + size_of::<u32>() * 4,
137 "`GpuStrip` must stay padding-free — six `Uint32` vertex attributes read it",
138);
139
140/// The per-draw values every strip instance of one draw shares.
141///
142/// Carried separately from the geometry because a draw resolves them once
143/// (its paint, its painter's-order index) and every strip it emits repeats
144/// them.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct StripDraw {
147 /// [`GpuStrip::payload`].
148 pub payload: u32,
149 /// The packed paint descriptor, without [`RECT_STRIP_FLAG`].
150 pub paint: u32,
151 /// [`GpuStrip::depth_index`].
152 pub depth_index: u32,
153}
154
155impl GpuStrip {
156 /// Vertices in the quad each instance expands to.
157 pub const QUAD_VERTICES: u32 = 4;
158
159 /// The six `Uint32` vertex attributes the 24-byte instance is read
160 /// through, at shader locations 0-5.
161 ///
162 /// The `u16` pairs are read as packed `u32`s and unpacked in the shader,
163 /// so the attribute list stays uniform.
164 #[must_use]
165 pub fn vertex_attributes() -> [wgpu::VertexAttribute; 6] {
166 wgpu::vertex_attr_array![
167 0 => Uint32,
168 1 => Uint32,
169 2 => Uint32,
170 3 => Uint32,
171 4 => Uint32,
172 5 => Uint32,
173 ]
174 }
175
176 /// The instance-stepped vertex buffer layout over [`GpuStrip`].
177 #[must_use]
178 pub fn vertex_layout() -> VertexLayout {
179 VertexLayout {
180 array_stride: size_of::<Self>() as wgpu::BufferAddress,
181 step_mode: wgpu::VertexStepMode::Instance,
182 attributes: Self::vertex_attributes().to_vec(),
183 }
184 }
185
186 /// The vertex range of a strip draw: one quad, built in the shader from
187 /// the vertex index, with no vertex buffer behind it.
188 #[must_use]
189 pub fn vertex_range() -> Range<u32> {
190 0..Self::QUAD_VERTICES
191 }
192
193 /// The instance range of a strip draw covering `count` instances from
194 /// `first_instance` — paired with [`Self::vertex_range`] for one
195 /// `draw` call.
196 #[must_use]
197 pub fn instance_range(first_instance: u32, count: u32) -> Range<u32> {
198 first_instance..first_instance.saturating_add(count)
199 }
200
201 /// The alpha-sampled instance for `strip`, `width` pixels wide.
202 ///
203 /// The strip's alpha index becomes the instance's first alpha *column*,
204 /// and the instance is dense across its whole width: every column samples
205 /// coverage.
206 #[must_use]
207 pub fn from_strip(strip: &Strip, width: u16, draw: StripDraw) -> Self {
208 Self {
209 x: strip.x,
210 y: strip.y,
211 width,
212 dense_width_or_rect_height: width,
213 col_idx_or_rect_frac: alpha_column(strip.alpha_idx()),
214 payload: draw.payload,
215 paint_and_rect_flag: draw.paint,
216 depth_index: draw.depth_index,
217 }
218 }
219
220 /// The alpha-sampled instance for `strip`, taking its width from `next` —
221 /// the strip immediately after it in the same run, which is where a
222 /// strip's extent is recorded.
223 #[must_use]
224 pub fn from_strip_pair(strip: &Strip, next: &Strip, draw: StripDraw) -> Self {
225 Self::from_strip(strip, strip.width_to(next), draw)
226 }
227
228 /// The solid instance filling the gap between `strip` and `next`, or
229 /// `None` when there is no gap to fill.
230 ///
231 /// A gap is filled only when `next` carries the fill-gap flag and sits on
232 /// the same strip row: the flag means "the winding between me and the
233 /// previous strip is non-zero", which says nothing about a strip on
234 /// another row. The instance samples no coverage, so its dense width and
235 /// column index are both zero.
236 #[must_use]
237 pub fn gap_fill(strip: &Strip, next: &Strip, draw: StripDraw) -> Option<Self> {
238 if !next.fill_gap() || next.y != strip.y {
239 return None;
240 }
241 let gap_x = strip.x.saturating_add(strip.width_to(next));
242 let gap_width = next.x.saturating_sub(gap_x);
243 if gap_width == 0 {
244 return None;
245 }
246 Some(Self::solid_fill(gap_x, strip.y, gap_width, draw))
247 }
248
249 /// A solid instance covering one strip row, sampling no coverage.
250 #[must_use]
251 pub fn solid_fill(x: u16, y: u16, width: u16, draw: StripDraw) -> Self {
252 Self {
253 x,
254 y,
255 width,
256 dense_width_or_rect_height: 0,
257 col_idx_or_rect_frac: 0,
258 payload: draw.payload,
259 paint_and_rect_flag: draw.paint,
260 depth_index: draw.depth_index,
261 }
262 }
263
264 /// A rectangle instance of `width` x `height` pixels with packed coverage
265 /// fraction `frac`, flagged with [`RECT_STRIP_FLAG`].
266 ///
267 /// Unlike a strip, a rect instance is not bound to one strip row: its
268 /// height is carried in the instance itself.
269 #[must_use]
270 pub fn from_rect(x: u16, y: u16, width: u16, height: u16, frac: u32, draw: StripDraw) -> Self {
271 Self {
272 x,
273 y,
274 width,
275 dense_width_or_rect_height: height,
276 col_idx_or_rect_frac: frac,
277 payload: draw.payload,
278 paint_and_rect_flag: draw.paint | RECT_STRIP_FLAG,
279 depth_index: draw.depth_index,
280 }
281 }
282
283 /// Whether this instance is a rectangle rather than a strip.
284 #[must_use]
285 pub const fn is_rect(&self) -> bool {
286 self.paint_and_rect_flag & RECT_STRIP_FLAG != 0
287 }
288
289 /// The packed paint descriptor with [`RECT_STRIP_FLAG`] masked off.
290 #[must_use]
291 pub const fn paint(&self) -> u32 {
292 self.paint_and_rect_flag & !RECT_STRIP_FLAG
293 }
294}