Skip to main content

frust_engine/gpu/
config.rs

1//! The `Config` uniform every strip shader binds.
2//!
3//! Named [`GpuConfig`] rather than `Config` because the crate root already
4//! owns a `config` module of process-wide kill switches ([`crate::config`]);
5//! this one is the GPU-side uniform buffer's layout and nothing else.
6//!
7//! Field order and size are part of the shader contract — see the module
8//! header of [`super`].
9//!
10//! ## The width shifts are the source of truth
11//!
12//! The strip program addresses its resource textures by shift and mask alone:
13//! a flat texel index `i` in a texture `1 << bits` wide is the texel
14//! `(i & ((1 << bits) - 1), i >> bits)`. It never asks a texture for its own
15//! width (`textureDimensions` per fragment was measurably not free on a mobile
16//! GPU), so the two shifts here are the only statement of those widths the GPU
17//! sees, and three textures depend on them:
18//!
19//! - the alpha texture, by [`GpuConfig::alphas_tex_width_bits`];
20//! - the encoded-paint texture, by [`GpuConfig::encoded_paints_tex_width_bits`];
21//! - the gradient LUT, by the *same* `encoded_paints_tex_width_bits` — the
22//!   uniform has no field of its own for it, and needs none, because the
23//!   renderer sizes every resource texture `resource_texture_dim` wide.
24//!
25//! [`resource_widths_agree`] is that rule as a check: a `debug_assert!` at the
26//! one site that builds the surface config from the resource textures
27//! (`renderer`'s config upload); the pooled-page configs are built from the
28//! same `dim` in the same frame and are not re-checked, and a release build
29//! checks nothing. The one pass with a narrower stand-in — the
30//! glyph-atlas replay, whose encoded-paint texture is a one-texel placeholder —
31//! draws solid instances only, which read neither the paint texture nor the
32//! LUT, so its config's paint shift addresses nothing.
33
34use bytemuck::{Pod, Zeroable};
35use vello_common::tile::Tile;
36
37/// The uniform block the strip shaders read once per draw.
38///
39/// Laid out to match the reference renderer's `Config` byte for byte: eight
40/// 4-byte scalars in a 16-byte-aligned block.
41#[repr(C, align(16))]
42#[derive(Debug, Clone, Copy, Pod, Zeroable)]
43pub struct GpuConfig {
44    /// Width of the render target in pixels.
45    pub width: u32,
46    /// Height of the render target in pixels.
47    pub height: u32,
48    /// Height of one strip in pixels — always `Tile::HEIGHT`.
49    pub strip_height: u32,
50    /// `log2` of the alpha texture's width in texels.
51    ///
52    /// Pre-computed on the CPU because a downlevel (GLES 3.0 / WebGL2)
53    /// target has no `firstTrailingBit`; the shader shifts by this instead of
54    /// dividing.
55    pub alphas_tex_width_bits: u32,
56    /// `log2` of the encoded-paint texture's width in texels, pre-computed
57    /// for the same reason as [`Self::alphas_tex_width_bits`] — and, because
58    /// the gradient LUT is sized to the same width, `log2` of the LUT's
59    /// width too (see the module doc).
60    pub encoded_paints_tex_width_bits: u32,
61    /// Horizontal offset applied to every strip, in pixels.
62    pub strip_offset_x: i32,
63    /// Vertical offset applied to every strip, in pixels.
64    pub strip_offset_y: i32,
65    /// Whether the shader negates the y component of its NDC position
66    /// (non-zero) or leaves it alone (zero).
67    ///
68    /// Kept for a future GLSL/WebGL backend: transpiling WGSL to GLSL applies
69    /// a y-flip to reconcile WebGPU's y-down clip space with a WebGL
70    /// framebuffer's y-up one. Rendering straight into a caller-provided
71    /// framebuffer wants that flip undone rather than a second framebuffer
72    /// allocated and blitted, and the rest of the strip pipeline (slot
73    /// textures included) assumes y-down throughout. Every backend this crate
74    /// drives today leaves it zero.
75    pub negate_ndc: u32,
76}
77
78const _: () = assert!(
79    size_of::<GpuConfig>() == 32,
80    "`GpuConfig` must stay 32 bytes — the shaders' `Config` uniform layout",
81);
82const _: () = assert!(
83    align_of::<GpuConfig>() == 16,
84    "`GpuConfig` must stay 16-byte aligned — uniform-buffer layout rules",
85);
86
87impl GpuConfig {
88    /// The uniform's size in bytes, for a buffer binding's `min_binding_size`.
89    pub const SIZE: u64 = size_of::<Self>() as u64;
90
91    /// A config for a `width` x `height` target sampling resource textures
92    /// `alphas_tex_width` and `encoded_paints_tex_width` texels wide, with no
93    /// strip offset and no NDC negation.
94    ///
95    /// Both texture widths must be powers of two: the shader reconstructs
96    /// them as `1 << bits` (see [`tex_width_bits`]).
97    #[must_use]
98    pub fn new(
99        width: u32,
100        height: u32,
101        alphas_tex_width: u32,
102        encoded_paints_tex_width: u32,
103    ) -> Self {
104        Self {
105            width,
106            height,
107            strip_height: u32::from(Tile::HEIGHT),
108            alphas_tex_width_bits: tex_width_bits(alphas_tex_width),
109            encoded_paints_tex_width_bits: tex_width_bits(encoded_paints_tex_width),
110            strip_offset_x: 0,
111            strip_offset_y: 0,
112            negate_ndc: 0,
113        }
114    }
115
116    /// The same config with strips offset by `(x, y)` pixels.
117    #[must_use]
118    pub const fn with_strip_offset(mut self, x: i32, y: i32) -> Self {
119        self.strip_offset_x = x;
120        self.strip_offset_y = y;
121        self
122    }
123
124    /// The same config with NDC y negation switched on or off
125    /// ([`Self::negate_ndc`]).
126    #[must_use]
127    pub const fn with_negate_ndc(mut self, negate: bool) -> Self {
128        self.negate_ndc = negate as u32;
129        self
130    }
131}
132
133/// Whether a frame's three strip resource textures are exactly as wide as
134/// the config built for `dim` says — the precondition of the strip program's
135/// shift-and-mask addressing (see the module doc).
136///
137/// `dim` is `TierCaps::resource_texture_dim`; the alpha and encoded-paint
138/// textures must be `dim` wide because their config shifts are taken from it,
139/// and the gradient LUT must be too because the shader addresses it by the
140/// encoded-paint shift.
141#[must_use]
142pub fn resource_widths_agree(dim: u32, alphas: u32, paints: u32, lut: u32) -> bool {
143    dim.is_power_of_two() && alphas == dim && paints == dim && lut == dim
144}
145
146/// `log2` of a resource texture's width in texels.
147///
148/// `width` must be a power of two — every resource texture is sized from
149/// `frust_gpu::TierCaps::resource_texture_dim`, which is. A width that is not
150/// makes the shader's `1 << bits` reconstruction disagree with the real
151/// texture, so the precondition is checked in debug builds; release builds
152/// take the trailing-zero count as-is rather than panicking on a frame path.
153#[must_use]
154pub fn tex_width_bits(width: u32) -> u32 {
155    debug_assert!(
156        width.is_power_of_two(),
157        "resource texture width must be a power of two, got {width}",
158    );
159    width.trailing_zeros()
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn the_lut_and_paint_textures_share_one_width_shift() {
168        let config = GpuConfig::new(800, 600, 2048, 2048);
169        // The LUT is addressed by the paint shift: a ramp texel at flat
170        // index `i` lands at `(i & mask, i >> bits)` of a 2048-wide LUT.
171        let bits = config.encoded_paints_tex_width_bits;
172        let flat = 2048 * 3 + 17;
173        assert_eq!((flat & ((1 << bits) - 1), flat >> bits), (17, 3));
174        assert!(resource_widths_agree(2048, 2048, 2048, 2048));
175    }
176
177    #[test]
178    fn a_resource_texture_of_another_width_breaks_the_guard() {
179        assert!(!resource_widths_agree(2048, 1024, 2048, 2048));
180        assert!(!resource_widths_agree(2048, 2048, 4096, 2048));
181        assert!(!resource_widths_agree(2048, 2048, 2048, 1024));
182        assert!(!resource_widths_agree(1000, 1000, 1000, 1000));
183    }
184
185    /// Shift-and-mask addressing equals `%`/`/` for every power-of-two width —
186    /// the arithmetic the LUT and alpha lookups moved from — over indices past
187    /// several rows.
188    #[test]
189    fn shift_and_mask_addressing_equals_division_for_power_of_two_widths() {
190        for bits in 0..=13_u32 {
191            let width = 1_u32 << bits;
192            for index in (0..width * 5).step_by(((width / 7) as usize).max(1)) {
193                let mask = (1_u32 << bits) - 1;
194                assert_eq!(
195                    (index & mask, index >> bits),
196                    (index % width, index / width)
197                );
198            }
199        }
200    }
201}