frust_engine/gpu/mod.rs
1//! GPU-side data layouts the strip shaders read.
2//!
3//! Every type in this module is byte-compatible with the sparse-strip
4//! reference renderer's own layouts, so the ported WGSL reads them unchanged.
5//! Layout drift is a silent mis-render rather than a compile error, so each
6//! struct carries a compile-time size assertion beside it and the integration
7//! tests pin the exact bytes.
8//!
9//! Nothing here touches a `wgpu::Device`: sizing, addressing and packing are
10//! pure functions over plain values, host-testable with no GPU in the loop —
11//! the same pure-decision split `frust-gpu` follows. The one live-GPU input
12//! these functions need is `frust_gpu::TierCaps::resource_texture_dim`, which
13//! the caller passes in as a plain `u32`.
14//!
15//! Two resource textures are described here, both `Rgba32Uint`:
16//!
17//! 1. the alpha texture, holding the strip renderer's 1-byte coverage values
18//! 16 to a texel (4 per channel), and
19//! 2. the encoded-paint texture ([`paint_texture`]), holding 16-byte-aligned
20//! paint records back to back.
21//!
22//! Both are sized `resource_texture_dim` texels wide so the shader can turn a
23//! flat index into a texel coordinate with a shift, and both grow in height
24//! only — never shrink — up to the same dimension, past which the frame path
25//! returns an error instead of asserting.
26
27pub mod atlas;
28pub mod bindings;
29pub mod config;
30pub mod depth;
31pub mod paint_texture;
32pub mod pipelines;
33pub mod present;
34pub mod shader_src;
35pub mod strips;
36pub mod targets;
37
38pub use atlas::{ATLAS_FORMAT, ATLAS_USAGES, AtlasArray, lower_encoded_image};
39pub use bindings::{ExternalRuns, ExternalTextures, lower_encoded_external};
40pub use config::{GpuConfig, tex_width_bits};
41pub use depth::{DepthAttachment, DepthTexture};
42pub use paint_texture::{
43 GpuBlurredRoundedRect, GpuEncodedImage, GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient,
44 GpuSweepGradient,
45};
46pub use pipelines::{EnginePipeline, EngineShaderModule, EngineShaders};
47pub use strips::{GpuStrip, StripDraw};
48pub use targets::{IntermediateTargets, IntermediateTexture};
49
50use crate::EngineError;
51
52/// The texture format both resource textures (alphas, encoded paints) use.
53///
54/// `Rgba32Uint` gives 16 bytes per texel with no format conversion applied on
55/// the sampling side, which is what lets the same texel hold either 16 packed
56/// coverage bytes or one quarter of a paint record.
57pub const RESOURCE_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Uint;
58
59/// The usages both resource textures are created with: sampled by the strip
60/// shaders, written by a queue upload, never a render attachment.
61pub const RESOURCE_TEXTURE_USAGES: wgpu::TextureUsages =
62 wgpu::TextureUsages::TEXTURE_BINDING.union(wgpu::TextureUsages::COPY_DST);
63
64/// `log2` of [`TEXEL_BYTES`], so a byte count is a shift away from a texel
65/// count (the shader does the same, since a downlevel target has no
66/// `firstTrailingBit`).
67pub const TEXEL_BYTES_SHIFT: u32 = 4;
68
69/// Bytes in one `Rgba32Uint` texel: four channels of four bytes.
70pub const TEXEL_BYTES: u32 = 1 << TEXEL_BYTES_SHIFT;
71
72/// 1-byte alpha values packed into one texel.
73pub const ALPHAS_PER_TEXEL: u32 = TEXEL_BYTES;
74
75/// 1-byte alpha values packed into one texel channel.
76pub const ALPHAS_PER_CHANNEL: u32 = 4;
77
78/// The height a resource texture starts at, before any content forces it to
79/// grow.
80pub const MIN_RESOURCE_TEXTURE_HEIGHT: u32 = 1;
81
82/// The `bytes_per_row` of a resource texture `width` texels wide.
83#[must_use]
84pub const fn resource_bytes_per_row(width: u32) -> u32 {
85 width << TEXEL_BYTES_SHIFT
86}
87
88/// The total byte footprint of a resource texture of `width` x `height`
89/// texels — the size an upload buffer is padded to.
90#[must_use]
91pub const fn resource_texture_bytes(width: u32, height: u32) -> u64 {
92 (width as u64 * height as u64) << TEXEL_BYTES_SHIFT
93}
94
95/// A resource-texture descriptor: `Rgba32Uint`, single mip, single sample,
96/// sampled-and-uploadable.
97#[must_use]
98pub fn resource_texture_descriptor(
99 label: &str,
100 width: u32,
101 height: u32,
102) -> wgpu::TextureDescriptor<'_> {
103 wgpu::TextureDescriptor {
104 label: Some(label),
105 size: wgpu::Extent3d {
106 width,
107 height,
108 depth_or_array_layers: 1,
109 },
110 mip_level_count: 1,
111 sample_count: 1,
112 dimension: wgpu::TextureDimension::D2,
113 format: RESOURCE_TEXTURE_FORMAT,
114 usage: RESOURCE_TEXTURE_USAGES,
115 view_formats: &[],
116 }
117}
118
119/// The descriptor for an alpha texture of `width` x `height` texels.
120#[must_use]
121pub fn alpha_texture_descriptor(width: u32, height: u32) -> wgpu::TextureDescriptor<'static> {
122 resource_texture_descriptor("frust-engine alpha texture", width, height)
123}
124
125/// The alpha-texture height needed to hold `alphas_len` coverage bytes in a
126/// texture `resource_texture_dim` texels wide.
127///
128/// Each texel holds [`ALPHAS_PER_TEXEL`] alpha values, so one row holds
129/// `resource_texture_dim << 4` of them. The result never drops below
130/// [`MIN_RESOURCE_TEXTURE_HEIGHT`], since a texture of height zero cannot be
131/// created.
132///
133/// # Errors
134///
135/// Returns [`EngineError::AlphaCapacity`] when the required height exceeds
136/// `resource_texture_dim` — the point at which the reference renderer
137/// asserts. Coverage past that ceiling has nowhere to live, and a frame path
138/// reports it rather than panicking.
139pub fn alpha_texture_height(
140 alphas_len: usize,
141 resource_texture_dim: u32,
142) -> Result<u32, EngineError> {
143 let alphas_len = u32::try_from(alphas_len).map_err(|_| EngineError::AlphaCapacity)?;
144 let required = alphas_len.div_ceil(resource_texture_dim << TEXEL_BYTES_SHIFT);
145 if required > resource_texture_dim {
146 return Err(EngineError::AlphaCapacity);
147 }
148 Ok(required.max(MIN_RESOURCE_TEXTURE_HEIGHT))
149}
150
151/// The height an alpha texture currently `current_height` tall must be
152/// recreated at to hold `alphas_len` coverage bytes, or `None` when the
153/// existing texture already fits.
154///
155/// The texture grows only: a frame needing fewer alphas than the last one
156/// keeps the taller texture rather than reallocating.
157///
158/// # Errors
159///
160/// Returns [`EngineError::AlphaCapacity`] on the same over-capacity condition
161/// as [`alpha_texture_height`].
162pub fn grow_alpha_texture_height(
163 current_height: u32,
164 alphas_len: usize,
165 resource_texture_dim: u32,
166) -> Result<Option<u32>, EngineError> {
167 let required = alpha_texture_height(alphas_len, resource_texture_dim)?;
168 Ok((required > current_height).then_some(required))
169}
170
171/// Where one alpha value lives in an alpha texture: its texel, the channel
172/// within that texel, and the byte within that channel.
173///
174/// The shader reconstructs the same address from a strip's `col_idx` with
175/// shifts and masks, using [`GpuConfig::alphas_tex_width_bits`] for the row
176/// stride.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct AlphaAddress {
179 /// Texel column.
180 pub x: u32,
181 /// Texel row.
182 pub y: u32,
183 /// Channel within the texel (0-3).
184 pub channel: u32,
185 /// Byte within the channel (0-3).
186 pub byte: u32,
187}
188
189impl AlphaAddress {
190 /// The offset of this address in an upload buffer of a texture `width`
191 /// texels wide — the inverse of [`alpha_address`], which is what makes
192 /// the packing round-trippable.
193 #[must_use]
194 pub const fn byte_offset(self, width: u32) -> u32 {
195 ((self.y * width + self.x) << TEXEL_BYTES_SHIFT)
196 + self.channel * ALPHAS_PER_CHANNEL
197 + self.byte
198 }
199}
200
201/// The address of alpha value `alpha_idx` in an alpha texture `width` texels
202/// wide.
203///
204/// Alpha values are laid out linearly: 16 per texel, texels left to right
205/// then top to bottom, so a value's byte offset equals its index and a
206/// texel-boundary or row-boundary crossing needs no padding.
207#[must_use]
208pub const fn alpha_address(alpha_idx: u32, width: u32) -> AlphaAddress {
209 let texel = alpha_idx >> TEXEL_BYTES_SHIFT;
210 let within = alpha_idx & (ALPHAS_PER_TEXEL - 1);
211 AlphaAddress {
212 x: texel % width,
213 y: texel / width,
214 channel: within / ALPHAS_PER_CHANNEL,
215 byte: within % ALPHAS_PER_CHANNEL,
216 }
217}
218
219/// Runs `upload` over `alphas` padded with zeroes to the full byte footprint
220/// of a `width` x `height` alpha texture, then truncates it back to its
221/// original length.
222///
223/// A queue write covers the whole texture extent, so the source slice must be
224/// the full footprint even when the frame produced fewer alphas. Padding in
225/// place and truncating afterwards keeps the caller's buffer (and its grown
226/// capacity) reusable across frames instead of allocating a staging copy per
227/// frame.
228pub fn with_padded_alphas<R>(
229 alphas: &mut Vec<u8>,
230 width: u32,
231 height: u32,
232 upload: impl FnOnce(&[u8]) -> R,
233) -> R {
234 let original_len = alphas.len();
235 let padded_len = resource_texture_bytes(width, height);
236 let padded_len = usize::try_from(padded_len)
237 .unwrap_or(usize::MAX)
238 .max(original_len);
239 alphas.resize(padded_len, 0);
240 let result = upload(alphas);
241 alphas.truncate(original_len);
242 result
243}