Skip to main content

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}