blob: a076b6f9a2d1630b8ec4041d2eb7188ec96b36a7 [file] [edit]
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! GPU paint packing for scheduled strip draws.
use crate::render::common::GpuEncodedPaint;
use crate::util::pack_u16_pair;
use vello_common::TextureId;
use vello_common::encode::{EncodedKind, EncodedPaint};
use vello_common::image_cache::ImageCache;
use vello_common::multi_atlas::AtlasId;
use vello_common::paint::{ImageSource, Paint, TextureRegion, Tint, TintMode};
use vello_common::peniko::{Extend, ImageQuality, ImageSampler};
const COLOR_SOURCE_PAYLOAD: u32 = 0;
pub(crate) const COLOR_SOURCE_LAYER: u32 = 1;
const PAINT_TYPE_SOLID: u32 = 0;
const PAINT_TYPE_IMAGE: u32 = 1;
const PAINT_TYPE_LINEAR_GRADIENT: u32 = 2;
const PAINT_TYPE_RADIAL_GRADIENT: u32 = 3;
const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4;
const PAINT_TYPE_BLURRED_ROUNDED_RECT: u32 = 5;
const PAINT_TYPE_NATIVE_IMAGE: u32 = 6;
#[cfg(all(feature = "wgpu", feature = "wgpu_external_texture"))]
const PAINT_TYPE_WGPU_EXTERNAL_TEXTURE: u32 = 7;
// See the layout information in `render.wesl`.
pub(crate) const COLOR_SOURCE_SHIFT: u32 = 29;
const PAINT_TYPE_SHIFT: u32 = 26;
pub(crate) const EXTERNAL_TEXTURE_SLOT_SHIFT: u32 = 24;
const PAINT_TEXTURE_INDEX_MASK: u32 = (1 << EXTERNAL_TEXTURE_SLOT_SHIFT) - 1;
/// Whether an image can use hardware sampling without the manual image shader.
pub(crate) fn supports_native_image(
source: &ImageSource,
quality: ImageQuality,
tint: Option<Tint>,
) -> bool {
matches!(
source,
ImageSource::ExternalTexture {
source_region: TextureRegion::Full,
..
}
) && quality != ImageQuality::High
&& tint.is_none_or(|tint| {
tint.mode == TintMode::Multiply && tint.color.components[..3] == [1.0; 3]
})
}
/// Texture sampled by an image paint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum TextureSourceId {
/// A renderer-owned image atlas.
Atlas(AtlasId),
/// A texture supplied through the render-time bindings.
External(TextureId, ExternalSampler),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SamplerFilter {
Nearest,
Linear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SamplerAddress {
Pad,
Repeat,
Reflect,
}
impl From<Extend> for SamplerAddress {
fn from(extend: Extend) -> Self {
match extend {
Extend::Pad => Self::Pad,
Extend::Repeat => Self::Repeat,
Extend::Reflect => Self::Reflect,
}
}
}
/// Sampling state bound with one external texture slot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct ExternalSampler {
pub(crate) filter: SamplerFilter,
pub(crate) address_x: SamplerAddress,
pub(crate) address_y: SamplerAddress,
}
impl ExternalSampler {
pub(crate) const DEFAULT: Self = Self {
filter: SamplerFilter::Linear,
address_x: SamplerAddress::Pad,
address_y: SamplerAddress::Pad,
};
fn from_native_image_sampler(sampler: &ImageSampler) -> Self {
let filter = match sampler.quality {
ImageQuality::Low => SamplerFilter::Nearest,
ImageQuality::Medium => SamplerFilter::Linear,
ImageQuality::High => unreachable!("high quality images use manual sampling"),
};
Self {
filter,
address_x: sampler.x_extend.into(),
address_y: sampler.y_extend.into(),
}
}
}
/// Shader-ready paint metadata for a strip.
#[derive(Clone, Copy)]
pub(crate) struct PackedPaint {
/// Value source for the strip's payload field.
payload: PaintPayload,
/// Packed paint kind, source, and data offset.
pub(crate) paint: u32,
/// Texture required by this paint, if any.
pub(crate) texture_source: Option<TextureSourceId>,
/// Whether the paint is fully opaque.
pub(crate) opaque: bool,
}
impl PackedPaint {
pub(crate) fn payload_at(self, x: u16, y: u16) -> u32 {
match self.payload {
PaintPayload::Solid(rgba) => rgba,
PaintPayload::Position => pack_u16_pair(x, y),
}
}
}
/// Source used to populate a strip's paint payload.
#[derive(Clone, Copy)]
enum PaintPayload {
/// Premultiplied RGBA value for a solid paint.
Solid(u32),
/// Scene-space position used to evaluate a non-solid paint.
Position,
}
/// Resolves recorded paints to their encoded GPU offsets.
#[derive(Debug, Clone, Copy)]
pub(crate) struct PaintResolver<'a> {
/// Encoded non-solid paints indexed by [`Paint`].
encoded: &'a [EncodedPaint],
/// GPU data offset corresponding to each encoded paint.
gpu_offsets: &'a [u32],
/// GPU paint selected for each encoded paint.
gpu_paints: &'a [GpuEncodedPaint],
/// Resolves internal images to their atlas textures.
image_cache: Option<&'a ImageCache>,
}
impl<'a> PaintResolver<'a> {
pub(crate) fn new(
encoded: &'a [EncodedPaint],
gpu_offsets: &'a [u32],
gpu_paints: &'a [GpuEncodedPaint],
) -> Self {
debug_assert_eq!(
encoded.len(),
gpu_paints.len(),
"each encoded paint must have a GPU paint"
);
Self {
encoded,
gpu_offsets,
gpu_paints,
image_cache: None,
}
}
/// Add the image cache needed to resolve internal image paints.
pub(crate) fn with_image_cache(mut self, image_cache: &'a ImageCache) -> Self {
self.image_cache = Some(image_cache);
self
}
#[inline]
pub(crate) fn pack(self, paint: &Paint) -> PackedPaint {
match paint {
Paint::Solid(color) => PackedPaint {
payload: PaintPayload::Solid(color.as_premul_rgba8().to_u32()),
paint: (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT)
| (PAINT_TYPE_SOLID << PAINT_TYPE_SHIFT),
texture_source: None,
opaque: color.is_opaque(),
},
Paint::Indexed(indexed_paint) => {
let paint_id = indexed_paint.index();
let gpu_offset = self.gpu_offsets[paint_id];
let encoded_paint = &self.encoded[paint_id];
let (paint_type, texture_source) = match encoded_paint {
EncodedPaint::Image(encoded_image) => match &encoded_image.source {
ImageSource::ExternalTexture { id, .. } => {
let (paint_type, sampler) = match &self.gpu_paints[paint_id] {
GpuEncodedPaint::NativeImage(_) => (
PAINT_TYPE_NATIVE_IMAGE,
ExternalSampler::from_native_image_sampler(
&encoded_image.sampler,
),
),
#[cfg(all(feature = "wgpu", feature = "wgpu_external_texture"))]
GpuEncodedPaint::WgpuExternalTexture(_) => (
PAINT_TYPE_WGPU_EXTERNAL_TEXTURE,
ExternalSampler::from_native_image_sampler(
&encoded_image.sampler,
),
),
GpuEncodedPaint::Image(_) => {
(PAINT_TYPE_IMAGE, ExternalSampler::DEFAULT)
}
_ => unreachable!("external image has an image GPU paint"),
};
(paint_type, Some(TextureSourceId::External(*id, sampler)))
}
ImageSource::OpaqueId { id, .. } => {
let image_resource = self.image_cache.unwrap().get(*id).unwrap();
(
PAINT_TYPE_IMAGE,
Some(TextureSourceId::Atlas(image_resource.atlas_id)),
)
}
ImageSource::Pixmap(_) => unimplemented!("Unsupported image source"),
},
EncodedPaint::Gradient(gradient) => {
let paint_type = match &gradient.kind {
EncodedKind::Linear(_) => PAINT_TYPE_LINEAR_GRADIENT,
EncodedKind::Radial(_) => PAINT_TYPE_RADIAL_GRADIENT,
EncodedKind::Sweep(_) => PAINT_TYPE_SWEEP_GRADIENT,
};
(paint_type, None)
}
EncodedPaint::BlurredRoundedRect(_) => (PAINT_TYPE_BLURRED_ROUNDED_RECT, None),
};
debug_assert!(
gpu_offset <= PAINT_TEXTURE_INDEX_MASK,
"paint offsets fit in 24 bits because resource textures are capped at 4096×4096"
);
PackedPaint {
payload: PaintPayload::Position,
paint: (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT)
| (paint_type << PAINT_TYPE_SHIFT)
| (gpu_offset & PAINT_TEXTURE_INDEX_MASK),
texture_source,
opaque: !encoded_paint.may_have_transparency(),
}
}
}
}
}