blob: 4143c349ec2757af8e32dfb185d7246a1fd360a4 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Backend agnostic renderer module.
#![allow(
clippy::cast_possible_truncation,
reason = "GPU paint structures have small, fixed sizes that fit in u32"
)]
use bytemuck::{Pod, Zeroable};
// GPU paint structure sizes in texels (1 texel = 16 bytes for RGBA32Uint texture format).
pub(crate) const GPU_ENCODED_IMAGE_SIZE_TEXELS: u32 = (size_of::<GpuEncodedImage>() / 16) as u32;
pub(crate) const GPU_LINEAR_GRADIENT_SIZE_TEXELS: u32 =
(size_of::<GpuLinearGradient>() / 16) as u32;
pub(crate) const GPU_RADIAL_GRADIENT_SIZE_TEXELS: u32 =
(size_of::<GpuRadialGradient>() / 16) as u32;
pub(crate) const GPU_SWEEP_GRADIENT_SIZE_TEXELS: u32 = (size_of::<GpuSweepGradient>() / 16) as u32;
/// Dimensions of the rendering target.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct RenderSize {
/// Width of the rendering target.
pub width: u32,
/// Height of the rendering target.
pub height: u32,
}
/// Configuration for the GPU renderer.
#[repr(C, align(16))]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
pub struct Config {
/// Width of the rendering target.
pub width: u32,
/// Height of the rendering target.
pub height: u32,
/// Height of a strip in the rendering.
pub strip_height: u32,
/// Number of trailing zeros in `alphas_tex_width` (log2 of width).
/// Pre-calculated on CPU since downlevel targets do not support `firstTrailingBit`.
pub alphas_tex_width_bits: u32,
/// Number of trailing zeros in the encoded paints texture width (log2 of width).
/// Pre-calculated on CPU since downlevel targets do not support `firstTrailingBit`.
pub encoded_paints_tex_width_bits: u32,
/// Padding to satisfy WebGL's 16-byte alignment requirement for uniform buffers.
pub _padding: [u32; 3],
}
/// A GPU strip instance for rendering.
///
/// This struct corresponds to the `StripInstance` struct in the shader.
/// See the `StripInstance` documentation in `render_strips.wgsl` for detailed field descriptions.
#[repr(C)]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
pub struct GpuStrip {
/// See `StripInstance::xy` documentation in `render_strips.wgsl`.
pub x: u16,
/// See `StripInstance::xy` documentation in `render_strips.wgsl`.
pub y: u16,
/// See `StripInstance::dense_width_or_rect_height` documentation in `render_strips.wgsl`.
pub width: u16,
/// See `StripInstance::dense_width_or_rect_height` documentation in `render_strips.wgsl`.
pub dense_width_or_rect_height: u16,
/// See `StripInstance::col_idx_or_rect_frac` documentation in `render_strips.wgsl`.
pub col_idx_or_rect_frac: u32,
/// See `StripInstance::payload` documentation in `render_strips.wgsl`.
pub payload: u32,
/// See `StripInstance::paint_and_rect_flag` documentation in `render_strips.wgsl`.
pub paint_and_rect_flag: u32,
}
/// Different types of GPU encoded paints.
#[derive(Debug)]
pub(crate) enum GpuEncodedPaint {
/// An encoded image.
Image(GpuEncodedImage),
/// An encoded linear gradient.
LinearGradient(GpuLinearGradient),
/// An encoded radial gradient.
RadialGradient(GpuRadialGradient),
/// An encoded sweep gradient.
SweepGradient(GpuSweepGradient),
}
impl GpuEncodedPaint {
/// Returns the byte representation of this paint.
#[inline]
pub(crate) fn as_bytes(&self) -> &[u8] {
match self {
Self::Image(paint) => bytemuck::bytes_of(paint),
Self::LinearGradient(paint) => bytemuck::bytes_of(paint),
Self::RadialGradient(paint) => bytemuck::bytes_of(paint),
Self::SweepGradient(paint) => bytemuck::bytes_of(paint),
}
}
/// Serialize paint enums directly into the provided buffer. Returns the number of bytes written.
pub(crate) fn serialize_to_buffer(paints: &[Self], buffer: &mut [u8]) {
let mut offset = 0;
for paint in paints {
let paint_bytes = paint.as_bytes();
let end_offset = offset + paint_bytes.len();
buffer[offset..end_offset].copy_from_slice(paint_bytes);
offset = end_offset;
}
}
}
/// GPU encoded image data.
/// Align to 16 bytes for `RGBA32Uint` alignment.
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[allow(dead_code, reason = "Clippy fails when --no-default-features")]
pub(crate) struct GpuEncodedImage {
/// Packed rendering quality, extend modes, and atlas index.
/// Bits 6-13: `atlas_index` (8 bits, supports up to 256 atlases)
/// Bits 4-5: `extend_y` (2 bits)
/// Bits 2-3: `extend_x` (2 bits)
/// Bits 0-1: `quality` (2 bits)
pub image_params: u32,
/// Packed image width and height.
pub image_size: u32,
/// The offset of the image in the atlas texture in pixels.
pub image_offset: u32,
/// Transform matrix [a, b, c, d, tx, ty].
pub transform: [f32; 6],
/// Premultiplied tint color packed as RGBA8 unorm (`pack4x8unorm` layout).
/// A value of `0` means no tint is applied.
pub tint: u32,
/// [`TintMode`](vello_common::paint::TintMode) discriminant. Only meaningful when `tint != 0`.
pub tint_mode: u32,
/// Number of transparent padding pixels around the image in the atlas.
pub image_padding: u32,
}
/// GPU encoded linear gradient data.
/// Align to 16 bytes for `RGBA32Uint` alignment.
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[allow(dead_code, reason = "Clippy fails when --no-default-features")]
pub(crate) struct GpuLinearGradient {
/// Packed texture width (bits 0-30) and extend mode (bit 31: 0=Pad, 1=Repeat).
pub texture_width_and_extend_mode: u32,
/// Start coordinate in the flat gradient texture.
pub gradient_start: u32,
/// Transform matrix [a, b, c, d, tx, ty].
pub transform: [f32; 6],
}
/// GPU encoded radial gradient data.
/// Align to 16 bytes for `RGBA32Uint` alignment.
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[allow(dead_code, reason = "Clippy fails when --no-default-features")]
pub(crate) struct GpuRadialGradient {
/// Packed texture width (bits 0-30) and extend mode (bit 31: 0=Pad, 1=Repeat).
pub texture_width_and_extend_mode: u32,
/// Start coordinate in the flat gradient texture for dense packing.
pub gradient_start: u32,
/// Transform matrix [a, b, c, d, tx, ty].
pub transform: [f32; 6],
/// Packed kind (bits 0-1) and `f_is_swapped` (bit 2): 0=Radial, 1=Strip, 2=Focal; bit 2: swapped flag.
pub kind_and_f_is_swapped: u32,
/// Bias value for radial gradient calculation.
pub bias: f32,
/// Scale factor for radial gradient calculation.
pub scale: f32,
/// Focal point 0 parameter for radial gradient.
pub fp0: f32,
/// Focal point 1 parameter for radial gradient.
pub fp1: f32,
/// Focal radius 1 parameter for radial gradient.
pub fr1: f32,
/// Focal X coordinate for radial gradient.
pub f_focal_x: f32,
/// Scaled radius 0 squared parameter for radial gradient strip.
pub scaled_r0_squared: f32,
}
/// GPU encoded sweep gradient data.
/// Align to 16 bytes for `RGBA32Uint` alignment.
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[allow(dead_code, reason = "Clippy fails when --no-default-features")]
pub(crate) struct GpuSweepGradient {
/// Packed texture width (bits 0-30) and extend mode (bit 31: 0=Pad, 1=Repeat).
pub texture_width_and_extend_mode: u32,
/// Start coordinate in the flat gradient texture for dense packing.
pub gradient_start: u32,
/// Transform matrix [a, b, c, d, tx, ty].
pub transform: [f32; 6],
/// Starting angle for sweep gradient.
pub start_angle: f32,
/// Inverse of angle delta for sweep gradient.
pub inv_angle_delta: f32,
/// Padding for 16-byte alignment.
pub _padding: [u32; 2],
}
// Constants for packing extend_mode and texture_width.
const EXTEND_MODE_MASK: u32 = 1 << 30;
const TEXTURE_WIDTH_MASK: u32 = !EXTEND_MODE_MASK;
/// Pack `extend_mode` and `texture_width` into a single u32.
/// `extend_mode`: 0=Pad, 1=Repeat, 2=Reflect (stored in bits 30 & 31)
/// `texture_width`: stored in bits 0-29 (max value: 2^30-1)
#[inline(always)]
pub(crate) fn pack_texture_width_and_extend_mode(texture_width: u32, extend_mode: u32) -> u32 {
debug_assert!(extend_mode <= 2, "extend_mode must be less or equal to 2");
debug_assert!(
texture_width <= TEXTURE_WIDTH_MASK,
"texture_width {texture_width} exceeds maximum value {TEXTURE_WIDTH_MASK}"
);
(extend_mode << 30) | (texture_width & TEXTURE_WIDTH_MASK)
}
/// Pack radial gradient `kind` and `f_is_swapped` into a single u32.
/// `kind`: 0=Radial, 1=Strip, 2=Focal (stored in bits 0-1)
/// `f_is_swapped`: 0=false, 1=true (stored in bit 2)
#[inline(always)]
pub(crate) fn pack_radial_kind_and_swapped(kind: u32, f_is_swapped: u32) -> u32 {
debug_assert!(kind <= 2, "kind must be 0, 1, or 2");
debug_assert!(f_is_swapped <= 1, "f_is_swapped must be 0 or 1");
(f_is_swapped << 2) | (kind & 0x3)
}
/// Pack image `width` and `height` into a single u32.
/// `width`: stored in bits 16-31 (upper 16 bits)
/// `height`: stored in bits 0-15 (lower 16 bits)
#[inline(always)]
pub(crate) fn pack_image_size(width: u16, height: u16) -> u32 {
((width as u32) << 16) | (height as u32)
}
/// Pack image offset coordinates `x` and `y` into a single u32.
/// `x`: stored in bits 16-31 (upper 16 bits)
/// `y`: stored in bits 0-15 (lower 16 bits)
#[inline(always)]
pub(crate) fn pack_image_offset(x: u16, y: u16) -> u32 {
((x as u32) << 16) | (y as u32)
}
/// Pack image `quality`, extend modes, and `atlas_index` into a single u32.
/// `atlas_index`: stored in bits 6-13 (8 bits, supports up to 256 atlases)
/// `extend_y`: stored in bits 4-5 (2 bits)
/// `extend_x`: stored in bits 2-3 (2 bits)
/// `quality`: stored in bits 0-1 (2 bits)
#[inline(always)]
pub(crate) fn pack_image_params(
quality: u32,
extend_x: u32,
extend_y: u32,
atlas_index: u32,
) -> u32 {
debug_assert!(extend_x <= 3, "extend_x must be 0-3 (2 bits)");
debug_assert!(extend_y <= 3, "extend_y must be 0-3 (2 bits)");
debug_assert!(quality <= 3, "quality must be 0-3 (2 bits)");
debug_assert!(atlas_index <= 255, "atlas_index must be 0-255 (8 bits)");
(atlas_index << 6) | (extend_y << 4) | (extend_x << 2) | quality
}
/// Pack an optional [`Tint`](vello_common::paint::Tint) into a (`tint_color_u32`, `tint_mode_u32`) pair for the GPU.
///
/// The tint color is premultiplied before packing into a u32 in the same layout
/// as WGSL `pack4x8unorm`. Returns `(0, 0)` when no tint is specified, which
/// the shader interprets as "no tint".
#[inline(always)]
pub(crate) fn pack_tint(tint: Option<vello_common::paint::Tint>) -> (u32, u32) {
match tint {
Some(t) => {
let color = t.color.premultiply().to_rgba8().to_u32();
(color, t.mode.as_u32())
}
None => (0, 0),
}
}
#[cfg(all(target_arch = "wasm32", feature = "webgl", feature = "wgpu"))]
pub(crate) fn maybe_warn_about_webgl_feature_conflict() {
use core::sync::atomic::{AtomicBool, Ordering};
static HAS_WARNED: AtomicBool = AtomicBool::new(false);
if !HAS_WARNED.swap(true, Ordering::Release)
&& wgpu::Backends::all().contains(wgpu::Backends::GL)
{
log::warn!(
r#"Both WebGL and wgpu with the \"webgl\" feature are enabled.
For optimal performance and binary size on web targets, use only the dedicated WebGL renderer."#
);
}
}
#[cfg(all(
any(all(target_arch = "wasm32", feature = "webgl"), feature = "wgpu"),
not(all(target_arch = "wasm32", feature = "webgl", feature = "wgpu"))
))]
pub(crate) fn maybe_warn_about_webgl_feature_conflict() {}