frust_engine/gpu/paint_texture.rs
1//! The encoded-paint records the fragment shader samples, and the
2//! `Rgba32Uint` texture they are serialized into.
3//!
4//! Each record is 16-byte aligned so it starts on a texel boundary; records
5//! are written back to back, and a paint's index into the texture is the
6//! texel it starts at. Sizes are part of the shader contract — a record that
7//! grew or shrank would shift every paint after it — so each carries a
8//! compile-time size assertion.
9
10use bytemuck::{Pod, Zeroable};
11
12use vello_common::encode::{
13 EncodedBlurredRoundedRectangle, EncodedGradient, EncodedKind, EncodedPaint, RadialKind,
14};
15
16use crate::EngineError;
17use crate::cache::CachedRamp;
18
19use super::strips::PaintType;
20use super::{MIN_RESOURCE_TEXTURE_HEIGHT, TEXEL_BYTES_SHIFT, resource_texture_descriptor};
21
22/// An encoded image paint.
23#[repr(C, align(16))]
24#[derive(Debug, Clone, Copy, Pod, Zeroable)]
25pub struct GpuEncodedImage {
26 /// Packed sampling quality, extend modes and atlas index:
27 /// bits 6-13 `atlas_index`, bits 4-5 `extend_y`, bits 2-3 `extend_x`,
28 /// bits 0-1 `quality`.
29 pub image_params: u32,
30 /// Packed image width and height.
31 pub image_size: u32,
32 /// Offset of the image within the atlas texture, in pixels.
33 pub image_offset: u32,
34 /// Transform matrix `[a, b, c, d, tx, ty]`.
35 pub transform: [f32; 6],
36 /// Premultiplied tint color, packed as RGBA8 unorm.
37 pub tint: u32,
38 /// Tint mode.
39 pub tint_mode: u32,
40 /// Transparent padding pixels around the image in the atlas.
41 pub image_padding: u32,
42}
43
44/// An encoded linear gradient paint.
45#[repr(C, align(16))]
46#[derive(Debug, Clone, Copy, Pod, Zeroable)]
47pub struct GpuLinearGradient {
48 /// Packed gradient-texture width and extend mode
49 /// ([`pack_texture_width_and_extend_mode`]).
50 pub texture_width_and_extend_mode: u32,
51 /// Start coordinate in the flat gradient texture.
52 pub gradient_start: u32,
53 /// Transform matrix `[a, b, c, d, tx, ty]`.
54 pub transform: [f32; 6],
55}
56
57/// An encoded radial gradient paint.
58#[repr(C, align(16))]
59#[derive(Debug, Clone, Copy, Pod, Zeroable)]
60pub struct GpuRadialGradient {
61 /// Packed gradient-texture width and extend mode
62 /// ([`pack_texture_width_and_extend_mode`]).
63 pub texture_width_and_extend_mode: u32,
64 /// Start coordinate in the flat gradient texture.
65 pub gradient_start: u32,
66 /// Transform matrix `[a, b, c, d, tx, ty]`.
67 pub transform: [f32; 6],
68 /// Packed gradient kind and focal-swap flag
69 /// ([`pack_kind_and_f_is_swapped`]).
70 pub kind_and_f_is_swapped: u32,
71 /// Bias term.
72 pub bias: f32,
73 /// Scale factor.
74 pub scale: f32,
75 /// First focal-point parameter.
76 pub fp0: f32,
77 /// Second focal-point parameter.
78 pub fp1: f32,
79 /// Focal radius parameter.
80 pub fr1: f32,
81 /// Focal x coordinate.
82 pub f_focal_x: f32,
83 /// Scaled inner radius, squared (strip kind).
84 pub scaled_r0_squared: f32,
85}
86
87/// An encoded sweep gradient paint.
88#[repr(C, align(16))]
89#[derive(Debug, Clone, Copy, Pod, Zeroable)]
90pub struct GpuSweepGradient {
91 /// Packed gradient-texture width and extend mode
92 /// ([`pack_texture_width_and_extend_mode`]).
93 pub texture_width_and_extend_mode: u32,
94 /// Start coordinate in the flat gradient texture.
95 pub gradient_start: u32,
96 /// Transform matrix `[a, b, c, d, tx, ty]`.
97 pub transform: [f32; 6],
98 /// Angle the sweep starts at, in radians.
99 pub start_angle: f32,
100 /// Reciprocal of the sweep's angle delta.
101 pub inv_angle_delta: f32,
102 /// Padding to the 16-byte record alignment.
103 pub _padding: [u32; 2],
104}
105
106/// An encoded blurred rounded rectangle paint.
107#[repr(C, align(16))]
108#[derive(Debug, Clone, Copy, Pod, Zeroable)]
109pub struct GpuBlurredRoundedRect {
110 /// Transform matrix `[a, b, c, d, tx, ty]`.
111 pub transform: [f32; 6],
112 /// Premultiplied color, packed as RGBA8 unorm.
113 pub color: u32,
114 /// Whether to paint the inverse (`1 - alpha`) of the blur coverage.
115 pub invert: u32,
116 /// Blur parameters: exponent, reciprocal exponent, scale, inverse
117 /// standard deviation.
118 pub params0: [f32; 4],
119 /// Blur parameters: minimum edge length, adjusted width, adjusted height,
120 /// outer radius.
121 pub params1: [f32; 4],
122 /// Rectangle size `[width, height]`.
123 pub size: [f32; 2],
124 /// Padding to the 16-byte record alignment.
125 pub _padding1: [u32; 2],
126}
127
128const _: () = assert!(size_of::<GpuEncodedImage>() == 48);
129const _: () = assert!(size_of::<GpuLinearGradient>() == 32);
130const _: () = assert!(size_of::<GpuRadialGradient>() == 64);
131const _: () = assert!(size_of::<GpuSweepGradient>() == 48);
132const _: () = assert!(size_of::<GpuBlurredRoundedRect>() == 80);
133
134/// One encoded paint of any kind.
135///
136/// Records are compared by their bytes ([`GpuEncodedPaint::as_bytes`]) rather
137/// than field-wise: the bytes are the contract the shader reads.
138#[derive(Debug, Clone, Copy)]
139pub enum GpuEncodedPaint {
140 /// An image.
141 Image(GpuEncodedImage),
142 /// A linear gradient.
143 LinearGradient(GpuLinearGradient),
144 /// A radial gradient.
145 RadialGradient(GpuRadialGradient),
146 /// A sweep gradient.
147 SweepGradient(GpuSweepGradient),
148 /// A blurred rounded rectangle.
149 BlurredRoundedRect(GpuBlurredRoundedRect),
150}
151
152macro_rules! paint_record_bytes {
153 ($($ty:ty),+ $(,)?) => {
154 $(
155 impl $ty {
156 /// This record's bytes, exactly as they land in the
157 /// encoded-paint texture.
158 #[must_use]
159 pub fn as_bytes(&self) -> &[u8] {
160 bytemuck::bytes_of(self)
161 }
162 }
163 )+
164 };
165}
166
167paint_record_bytes!(
168 GpuEncodedImage,
169 GpuLinearGradient,
170 GpuRadialGradient,
171 GpuSweepGradient,
172 GpuBlurredRoundedRect,
173);
174
175impl GpuEncodedPaint {
176 /// The paint type a strip instance names this record by.
177 #[must_use]
178 pub const fn paint_type(&self) -> PaintType {
179 match self {
180 Self::Image(_) => PaintType::Image,
181 Self::LinearGradient(_) => PaintType::LinearGradient,
182 Self::RadialGradient(_) => PaintType::RadialGradient,
183 Self::SweepGradient(_) => PaintType::SweepGradient,
184 Self::BlurredRoundedRect(_) => PaintType::BlurredRoundedRect,
185 }
186 }
187
188 /// This paint's bytes, exactly as they land in the encoded-paint texture.
189 #[must_use]
190 pub fn as_bytes(&self) -> &[u8] {
191 match self {
192 Self::Image(paint) => paint.as_bytes(),
193 Self::LinearGradient(paint) => paint.as_bytes(),
194 Self::RadialGradient(paint) => paint.as_bytes(),
195 Self::SweepGradient(paint) => paint.as_bytes(),
196 Self::BlurredRoundedRect(paint) => paint.as_bytes(),
197 }
198 }
199
200 /// This paint's size in bytes — always a multiple of the 16-byte texel.
201 #[must_use]
202 pub fn byte_len(&self) -> usize {
203 self.as_bytes().len()
204 }
205
206 /// The number of texels this paint occupies.
207 #[must_use]
208 pub fn texel_len(&self) -> u32 {
209 (self.byte_len() as u32) >> TEXEL_BYTES_SHIFT
210 }
211
212 /// The total byte length of `paints` serialized back to back.
213 #[must_use]
214 pub fn serialized_len(paints: &[Self]) -> usize {
215 paints.iter().map(Self::byte_len).sum()
216 }
217
218 /// Serializes `paints` back to back into the front of `buffer`, returning
219 /// the number of bytes written.
220 ///
221 /// `buffer` is the full padded upload buffer, so anything past the
222 /// returned length keeps whatever it already held — a paint index only
223 /// ever addresses a record that was written.
224 ///
225 /// # Errors
226 ///
227 /// Returns [`EngineError::AtlasError`] when `buffer` is shorter than the
228 /// serialized paints, rather than writing a partial record.
229 pub fn serialize_to_buffer(paints: &[Self], buffer: &mut [u8]) -> Result<usize, EngineError> {
230 let required = Self::serialized_len(paints);
231 if buffer.len() < required {
232 return Err(EngineError::AtlasError);
233 }
234 let mut offset = 0;
235 for paint in paints {
236 let bytes = paint.as_bytes();
237 buffer[offset..offset + bytes.len()].copy_from_slice(bytes);
238 offset += bytes.len();
239 }
240 Ok(offset)
241 }
242
243 /// The texel each paint in `paints` starts at, in serialization order —
244 /// the index the shader looks a paint up by.
245 #[must_use]
246 pub fn texel_offsets(paints: &[Self]) -> Vec<u32> {
247 let mut offset = 0;
248 paints
249 .iter()
250 .map(|paint| {
251 let start = offset;
252 offset += paint.texel_len();
253 start
254 })
255 .collect()
256 }
257}
258
259/// Lower one `vello_common` encoded paint into the record the strip shader
260/// samples, or `None` when the engine cannot resolve it yet.
261///
262/// `ramp` is where the paint's colour ramp was made resident, which only a
263/// gradient needs and only a caller that already serviced the frame's LUT
264/// requests can supply — a gradient reaching here without one is dropped
265/// rather than pointed at whatever texels the LUT texture happens to hold.
266///
267/// A blurred rounded rectangle carries everything its record needs inside the
268/// encoded entry itself (see [`lower_blurred_rounded_rect`]), so it always
269/// lowers.
270///
271/// An image paint answers `None` here rather than taking a third parameter:
272/// its record needs the atlas rectangle the image's residency was allocated
273/// at, which — unlike a ramp's cache-key lookup — is a stateful, per-frame
274/// resolution keyed by an opaque `vello_common::paint::ImageId` (see
275/// [`crate::renderer`]'s image registry) rather than a value this function's
276/// signature can carry without breaking every other caller of it.
277/// [`crate::gpu::atlas::lower_encoded_image`] is the image counterpart this
278/// dispatcher intentionally does not fold in; the renderer calls it directly
279/// once a frame's residency is known. An external texture answers `None`
280/// unconditionally — the engine does not bind one yet.
281#[must_use]
282pub fn lower_encoded_paint(
283 paint: &EncodedPaint,
284 ramp: Option<CachedRamp>,
285) -> Option<GpuEncodedPaint> {
286 match paint {
287 EncodedPaint::Gradient(gradient) => Some(lower_gradient(gradient, ramp?)),
288 EncodedPaint::BlurredRoundedRect(entry) => Some(lower_blurred_rounded_rect(entry)),
289 EncodedPaint::Image(_) | EncodedPaint::ExternalTexture(_) => None,
290 }
291}
292
293/// Lower a blurred rounded rectangle's encoded entry into the record the
294/// strip shader's `calculate_blurred_rounded_rect` reads.
295///
296/// `entry.transform` is already the full inverse affine (device space into
297/// the rectangle's own local, origin-zeroed space) — the same
298/// "already-inverted, narrow to `f32`" shape [`lower_gradient`] applies to its
299/// own transform — and `entry.x_advance`/`entry.y_advance` are redundant with
300/// that transform's own linear part, so neither is read here.
301fn lower_blurred_rounded_rect(entry: &EncodedBlurredRoundedRectangle) -> GpuEncodedPaint {
302 let transform = entry.transform.as_coeffs().map(|coeff| coeff as f32);
303
304 GpuEncodedPaint::BlurredRoundedRect(GpuBlurredRoundedRect {
305 transform,
306 color: entry.color.as_premul_rgba8().to_u32(),
307 invert: u32::from(entry.invert),
308 params0: [
309 entry.exponent,
310 entry.recip_exponent,
311 entry.scale,
312 entry.std_dev_inv,
313 ],
314 params1: [entry.min_edge, entry.w, entry.h, entry.r1],
315 size: [entry.width, entry.height],
316 _padding1: [0, 0],
317 })
318}
319
320/// Lower a gradient whose ramp is resident at `ramp`.
321///
322/// The transform is the gradient's own encoded transform — already the inverse
323/// mapping from device space into gradient space — narrowed to `f32` because
324/// that is the width the record and the shader both read it at.
325fn lower_gradient(gradient: &EncodedGradient, ramp: CachedRamp) -> GpuEncodedPaint {
326 let transform = gradient.transform.as_coeffs().map(|coeff| coeff as f32);
327 let texture_width_and_extend_mode =
328 pack_texture_width_and_extend_mode(ramp.width, extend_mode(gradient.extend));
329 let gradient_start = ramp.lut_start;
330
331 match &gradient.kind {
332 EncodedKind::Linear(_) => GpuEncodedPaint::LinearGradient(GpuLinearGradient {
333 texture_width_and_extend_mode,
334 gradient_start,
335 transform,
336 }),
337 EncodedKind::Radial(radial) => {
338 let shape = RadialShape::of(radial);
339 GpuEncodedPaint::RadialGradient(GpuRadialGradient {
340 texture_width_and_extend_mode,
341 gradient_start,
342 transform,
343 kind_and_f_is_swapped: pack_kind_and_f_is_swapped(shape.kind, shape.f_is_swapped),
344 bias: shape.bias,
345 scale: shape.scale,
346 fp0: shape.fp0,
347 fp1: shape.fp1,
348 fr1: shape.fr1,
349 f_focal_x: shape.f_focal_x,
350 scaled_r0_squared: shape.scaled_r0_squared,
351 })
352 }
353 EncodedKind::Sweep(sweep) => GpuEncodedPaint::SweepGradient(GpuSweepGradient {
354 texture_width_and_extend_mode,
355 gradient_start,
356 transform,
357 start_angle: sweep.start_angle,
358 inv_angle_delta: sweep.inv_angle_delta,
359 _padding: [0, 0],
360 }),
361 }
362}
363
364/// The shader's extend-mode numbering.
365const fn extend_mode(extend: peniko::Extend) -> u32 {
366 match extend {
367 peniko::Extend::Pad => 0,
368 peniko::Extend::Repeat => 1,
369 peniko::Extend::Reflect => 2,
370 }
371}
372
373/// The radial gradient parameters flattened into the one field set the record
374/// carries.
375///
376/// The three radial shapes populate disjoint subsets of those fields, and the
377/// shader selects between them on `kind`; collecting them here keeps the
378/// record's construction one struct literal rather than a match arm per field.
379struct RadialShape {
380 kind: u32,
381 bias: f32,
382 scale: f32,
383 fp0: f32,
384 fp1: f32,
385 fr1: f32,
386 f_focal_x: f32,
387 f_is_swapped: bool,
388 scaled_r0_squared: f32,
389}
390
391impl RadialShape {
392 /// The unused fields of every shape, which the shader does not read for
393 /// that `kind`.
394 const ZERO: Self = Self {
395 kind: 0,
396 bias: 0.0,
397 scale: 0.0,
398 fp0: 0.0,
399 fp1: 0.0,
400 fr1: 0.0,
401 f_focal_x: 0.0,
402 f_is_swapped: false,
403 scaled_r0_squared: 0.0,
404 };
405
406 fn of(radial: &RadialKind) -> Self {
407 match radial {
408 RadialKind::Radial { bias, scale } => Self {
409 kind: 0,
410 bias: *bias,
411 scale: *scale,
412 ..Self::ZERO
413 },
414 RadialKind::Strip { scaled_r0_squared } => Self {
415 kind: 1,
416 scaled_r0_squared: *scaled_r0_squared,
417 ..Self::ZERO
418 },
419 RadialKind::Focal {
420 focal_data,
421 fp0,
422 fp1,
423 } => Self {
424 kind: 2,
425 // The focal shape reuses `bias`/`scale` as a second copy of the
426 // focal pair, matching the reference encoding the shader reads.
427 bias: *fp0,
428 scale: *fp1,
429 fp0: *fp0,
430 fp1: *fp1,
431 fr1: focal_data.fr1,
432 f_focal_x: focal_data.f_focal_x,
433 f_is_swapped: focal_data.f_is_swapped,
434 scaled_r0_squared: 0.0,
435 },
436 }
437 }
438}
439
440/// The descriptor for an encoded-paint texture of `width` x `height` texels.
441#[must_use]
442pub fn encoded_paints_texture_descriptor(
443 width: u32,
444 height: u32,
445) -> wgpu::TextureDescriptor<'static> {
446 resource_texture_descriptor("frust-engine encoded paints texture", width, height)
447}
448
449/// The encoded-paint texture height needed to hold `texels` records' worth of
450/// texels in a texture `resource_texture_dim` texels wide.
451///
452/// Never drops below [`MIN_RESOURCE_TEXTURE_HEIGHT`], since a texture of
453/// height zero cannot be created.
454///
455/// # Errors
456///
457/// Returns [`EngineError::PaintCapacity`] when the required height exceeds
458/// `resource_texture_dim` — the point at which the reference renderer
459/// asserts. Paint records past that ceiling have nowhere to live, and a frame
460/// path reports it rather than panicking.
461pub fn encoded_paints_texture_height(
462 texels: u32,
463 resource_texture_dim: u32,
464) -> Result<u32, EngineError> {
465 let required = texels.div_ceil(resource_texture_dim);
466 if required > resource_texture_dim {
467 return Err(EngineError::PaintCapacity);
468 }
469 Ok(required.max(MIN_RESOURCE_TEXTURE_HEIGHT))
470}
471
472/// The height an encoded-paint texture currently `current_height` tall must
473/// be recreated at to hold `texels` texels, or `None` when the existing
474/// texture already fits.
475///
476/// The texture grows only, like the alpha texture
477/// ([`super::grow_alpha_texture_height`]).
478///
479/// # Errors
480///
481/// Returns [`EngineError::PaintCapacity`] on the same over-capacity condition
482/// as [`encoded_paints_texture_height`].
483pub fn grow_encoded_paints_texture_height(
484 current_height: u32,
485 texels: u32,
486 resource_texture_dim: u32,
487) -> Result<Option<u32>, EngineError> {
488 let required = encoded_paints_texture_height(texels, resource_texture_dim)?;
489 Ok((required > current_height).then_some(required))
490}
491
492/// The bit of a packed texture-width-and-extend-mode value the extend mode
493/// starts at.
494const EXTEND_MODE_SHIFT: u32 = 30;
495
496/// The mask covering the texture width in a packed
497/// texture-width-and-extend-mode value.
498const TEXTURE_WIDTH_MASK: u32 = (1 << EXTEND_MODE_SHIFT) - 1;
499
500/// Packs a gradient texture's width (bits 0-29) and extend mode (bits 30-31,
501/// `0` pad / `1` repeat / `2` reflect) into one word.
502///
503/// A width past [`TEXTURE_WIDTH_MASK`] or an extend mode past `2` is masked
504/// rather than reported: both are produced by this crate's own encoding, not
505/// by caller input, and the precondition is checked in debug builds.
506#[must_use]
507pub fn pack_texture_width_and_extend_mode(texture_width: u32, extend_mode: u32) -> u32 {
508 debug_assert!(extend_mode <= 2, "extend mode must be 0, 1 or 2");
509 debug_assert!(
510 texture_width <= TEXTURE_WIDTH_MASK,
511 "gradient texture width {texture_width} exceeds {TEXTURE_WIDTH_MASK}",
512 );
513 (extend_mode << EXTEND_MODE_SHIFT) | (texture_width & TEXTURE_WIDTH_MASK)
514}
515
516/// The texture width and extend mode packed by
517/// [`pack_texture_width_and_extend_mode`].
518#[must_use]
519pub const fn unpack_texture_width_and_extend_mode(packed: u32) -> (u32, u32) {
520 (packed & TEXTURE_WIDTH_MASK, packed >> EXTEND_MODE_SHIFT)
521}
522
523/// Packs a radial gradient's kind (bits 0-1, `0` radial / `1` strip / `2`
524/// focal) and focal-swap flag (bit 2) into one word.
525#[must_use]
526pub fn pack_kind_and_f_is_swapped(kind: u32, f_is_swapped: bool) -> u32 {
527 debug_assert!(kind <= 2, "radial gradient kind must be 0, 1 or 2");
528 (kind & 0b11) | (u32::from(f_is_swapped) << 2)
529}