Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ clippy.use_self = "warn"

clippy.cargo_common_metadata = "warn"
clippy.negative_feature_names = "warn"
clippy.redundant_feature_names = "warn"
# To support the `full_image_support` feature name.
clippy.redundant_feature_names = "allow"
clippy.wildcard_dependencies = "warn"
# END LINEBENDER LINT SET

Expand Down
4 changes: 2 additions & 2 deletions vello_gpu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ roxmltree = "0.20.0"
[features]
default = ["wgpu", "wgpu_default", "text", "all_shader_features"]
# Enable all optional shader features.
all_shader_features = ["blurred_rounded_rect", "image_bicubic", "gradient_sweep"]
all_shader_features = ["blurred_rounded_rect", "full_image_support", "gradient_sweep"]
blurred_rounded_rect = ["vello_gpu_shaders?/blurred_rounded_rect"]
image_bicubic = ["vello_gpu_shaders?/image_bicubic"]
full_image_support = ["vello_gpu_shaders?/full_image_support"]
gradient_sweep = ["vello_gpu_shaders?/gradient_sweep"]
std = ["vello_common/std", "glifo?/std"]
libm = ["vello_common/libm", "glifo?/libm"]
Expand Down
7 changes: 5 additions & 2 deletions vello_gpu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ preprocessing and GPU rendering:
features.
- `blurred_rounded_rect` (enabled by default): Enables rendering of blurred
rounded rectangles.
- `image_bicubic` (enabled by default): Enables rendering of images with
`ImageQuality::High`
- `full_image_support` (enabled by default): If this feature is disabled, only
external textures using `TextureRegion::Full` with nearest or bilinear
sampling and opacity are supported. Enabling this feature also adds support
for uploaded images, glyph atlas caching, bicubic sampling, cropped external
textures, and colored or alpha-mask tints.
- `gradient_sweep` (enabled by default): Enables rendering of sweep gradients
- `webgl`: Enables the WebGL rendering backend for browser support, using GLSL
shaders for compatibility.
Expand Down
7 changes: 5 additions & 2 deletions vello_gpu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
//! features.
//! - `blurred_rounded_rect` (enabled by default): Enables rendering of blurred
//! rounded rectangles.
//! - `image_bicubic` (enabled by default): Enables rendering of images with
//! `ImageQuality::High`
//! - `full_image_support` (enabled by default): If this feature is disabled, only
//! external textures using `TextureRegion::Full` with nearest or bilinear
//! sampling and opacity are supported. Enabling this feature also adds support
//! for uploaded images, glyph atlas caching, bicubic sampling, cropped external
//! textures, and colored or alpha-mask tints.
//! - `gradient_sweep` (enabled by default): Enables rendering of sweep gradients
//! - `webgl`: Enables the WebGL rendering backend for browser support, using GLSL
//! shaders for compatibility.
Expand Down
20 changes: 19 additions & 1 deletion vello_gpu/src/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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};
use vello_common::paint::{ImageSource, Paint, TextureRegion, Tint, TintMode};
use vello_common::peniko::{Extend, ImageQuality, ImageSampler};

const COLOR_SOURCE_PAYLOAD: u32 = 0;
Expand All @@ -29,6 +29,24 @@ 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 {
Expand Down
18 changes: 2 additions & 16 deletions vello_gpu/src/render/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use core::mem::size_of;
use vello_common::color::{AlphaColor, Srgb};
use vello_common::encode::EncodedImage;
use vello_common::geometry::{RectU16, SizeU16};
use vello_common::paint::{ImageSource, TextureRegion, TintMode};
use vello_common::peniko::ImageQuality;
use vello_common::paint::TintMode;
use vello_common::record::CommandRecorder;

// TODO: If we want to use native bilinear sampling for uploaded images,
Expand Down Expand Up @@ -390,20 +389,7 @@ pub(crate) fn encode_native_image_paint(
texture_width: u16,
texture_height: u16,
) -> Option<GpuEncodedPaint> {
if !matches!(
image.source,
ImageSource::ExternalTexture {
source_region: TextureRegion::Full,
..
}
) {
return None;
}

if image.tint.is_some_and(|tint| {
tint.mode != TintMode::Multiply || tint.color.components[..3] != [1.0; 3]
}) || matches!(image.sampler.quality, ImageQuality::High)
{
if !crate::paint::supports_native_image(&image.source, image.sampler.quality, image.tint) {
return None;
}

Expand Down
3 changes: 3 additions & 0 deletions vello_gpu/src/render/webgl/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub enum WebGlError {
/// Rendering failed for a certain reason.
#[error(transparent)]
Render(#[from] RenderError),
/// An image upload was attempted without the `full_image_support` feature.
#[error("uploaded images require the `full_image_support` feature")]
FullImageSupportDisabled,
/// A WebGL2 context could not be created for the canvas.
#[error("WebGL2 context unavailable")]
ContextUnavailable,
Expand Down
10 changes: 10 additions & 0 deletions vello_gpu/src/render/webgl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,11 @@ impl WebGlRenderer {
///
/// This is the WebGL analogue of the wgpu Renderer's `upload_image` method.
/// It allocates space in the image cache and uploads the image data to the atlas texture.
///
/// # Errors
/// Returns [`WebGlError::FullImageSupportDisabled`] if the `full_image_support` feature
/// is disabled, or an error if allocating or uploading the image fails.
#[track_caller]
pub fn upload_image<T: WebGlAtlasWriter>(

@LaurenzV LaurenzV Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to just compile out this method if the feature is disabled, instead of panicking at runtime.

However, internally we currently have a runtime feature flag, so unless we are willing to go "all-in" on external texture and remove that, we need to keep the method available, even if the feature is disabled.

&mut self,
resources: &mut Resources,
Expand All @@ -781,12 +786,17 @@ impl WebGlRenderer {
self.upload_image_with(&mut resources.image_cache, writer, IMAGE_PADDING)
}

#[track_caller]
pub(crate) fn upload_image_with<T: WebGlAtlasWriter>(
&mut self,
image_cache: &mut ImageCache,
writer: &T,
padding: u16,
) -> Result<ImageId, WebGlError> {
if !cfg!(feature = "full_image_support") {
return Err(WebGlError::FullImageSupportDisabled);
}

let width = writer.width();
let height = writer.height();
let image_id = image_cache
Expand Down
6 changes: 6 additions & 0 deletions vello_gpu/src/render/wgpu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ impl Renderer {
/// 1. Reserves space in the image cache
/// 2. Writes the image data directly to the atlas
/// 3. Returns the `ImageId` for use in rendering
#[track_caller]
pub fn upload_image<T: AtlasWriter>(
&mut self,
resources: &mut Resources,
Expand All @@ -538,6 +539,7 @@ impl Renderer {
)
}

#[track_caller]
pub(crate) fn upload_image_with<T: AtlasWriter>(
&mut self,
image_cache: &mut ImageCache,
Expand All @@ -547,6 +549,10 @@ impl Renderer {
writer: &T,
padding: u16,
) -> vello_common::paint::ImageId {
if !cfg!(feature = "full_image_support") {
panic!("uploaded images require the `full_image_support` feature");
}

let width = writer.width();
let height = writer.height();
let image_id = image_cache.allocate(width, height, padding).unwrap();
Expand Down
30 changes: 19 additions & 11 deletions vello_gpu/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use vello_common::paint::{Paint, PaintType, PremulColor, Tint};
#[cfg(feature = "text")]
use vello_common::peniko::FontData;
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Fill, GradientKind, ImageQuality};
use vello_common::peniko::{BlendMode, Fill, GradientKind};
use vello_common::record::{CommandRecorder, Drawable, LayerClip, LayerProps, PoppedLayer};
use vello_common::render_state::RenderState;
use vello_common::strip::Strip;
Expand Down Expand Up @@ -815,12 +815,16 @@ impl Scene {
#[track_caller]
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
let paint = paint.into();
assert_paint_supported(&paint);
assert_paint_supported(&paint, self.render_state.tint);

self.render_state.paint = paint;
}

/// Set the tint for subsequent image paint operations.
#[track_caller]
pub fn set_tint(&mut self, tint: Option<Tint>) {
assert_paint_supported(&self.render_state.paint, tint);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These checks make otherwise valid state updates depend on setter order:

scene.set_paint(native_image);
scene.fill_rect(&r);
scene.set_tint(Some(red));     // Panics here.
scene.set_paint(BLUE);         // The next draw would be solid.

scene.set_tint(Some(red));
scene.set_paint(native_image); // Panics here.
scene.reset_tint();            // The tint is cleared before drawing.

I can see the argument that every intermediate state should remain renderable, so I don鈥檛 think this necessarily needs to block the PR. Should we allow sequences like these and validate only the final state?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Yeah let me add this as a TODO, not sure what the best thing to do is here.


self.render_state.tint = tint;
}

Expand Down Expand Up @@ -937,7 +941,7 @@ impl Scene {
/// Restore rendering state.
#[track_caller]
pub fn restore_state(&mut self, state: RenderState) {
assert_paint_supported(&state.paint);
assert_paint_supported(&state.paint, state.tint);

self.render_state = state;
}
Expand Down Expand Up @@ -986,37 +990,41 @@ impl vello_common::probe::ProbeRenderer for Scene {
}
}

// TODO: Consider validating paint/tint support at draw time so otherwise valid state updates
// do not depend on setter order. See:
// https://github.com/linebender/vello/pull/1994#discussion_r4278390451
#[track_caller]
fn assert_paint_supported(paint: &PaintType) {
fn assert_paint_supported(paint: &PaintType, tint: Option<Tint>) {
match paint {
PaintType::Solid(_) => {}
PaintType::Gradient(gradient) => assert!(
cfg!(feature = "gradient_sweep") || !matches!(gradient.kind, GradientKind::Sweep(_)),
"sweep gradients require the `gradient_sweep` feature"
),
PaintType::Image(image) => assert!(
cfg!(feature = "image_bicubic") || image.sampler.quality != ImageQuality::High,
"bicubic image sampling requires the `image_bicubic` feature"
cfg!(feature = "full_image_support")
|| crate::paint::supports_native_image(&image.image, image.sampler.quality, tint),
"extended image sampling requires the `full_image_support` feature"
),
}
}

#[cfg(test)]
mod tests {
use super::{RecordedDraw, Scene};
#[cfg(feature = "text")]
#[cfg(all(feature = "text", feature = "full_image_support"))]
use crate::resources::Resources;
#[cfg(feature = "text")]
#[cfg(all(feature = "text", feature = "full_image_support"))]
use alloc::sync::Arc;
#[cfg(feature = "text")]
#[cfg(all(feature = "text", feature = "full_image_support"))]
use glifo::Glyph;
use vello_common::TextureId;
use vello_common::geometry::RectU16;
use vello_common::kurbo::{BezPath, Rect};
use vello_common::paint::{Image, ImageSource, Paint, PremulColor, TextureRegion};
use vello_common::peniko::ImageSampler;
use vello_common::peniko::color::palette::css::BLUE;
#[cfg(feature = "text")]
#[cfg(all(feature = "text", feature = "full_image_support"))]
use vello_common::peniko::{Blob, FontData};
use vello_common::record::Drawable;

Expand Down Expand Up @@ -1101,7 +1109,7 @@ mod tests {
assert!(scene.recorder.nodes.is_empty());
}

#[cfg(feature = "text")]
#[cfg(all(feature = "text", feature = "full_image_support"))]
#[test]
fn glyph_atlas_resources_are_lazy() {
const ROBOTO_FONT: &[u8] = include_bytes!("../../assets/roboto/Roboto-Regular.ttf");
Expand Down
5 changes: 5 additions & 0 deletions vello_gpu/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ impl<'a> GpuGlyphRunBackend<'a> {

impl<'a> GlyphRunBackend<'a> for GpuGlyphRunBackend<'a> {
fn atlas_cache(mut self, enabled: bool) -> Self {
assert!(
!enabled || cfg!(feature = "full_image_support"),
"glyph atlas caching requires the `full_image_support` feature"
);

self.atlas_cache_enabled = enabled;
self
}
Expand Down
4 changes: 2 additions & 2 deletions vello_gpu_shaders/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ naga = { workspace = true, features = ["wgsl-in", "wgsl-out", "glsl-out"] }
wesl = "=0.4.0"

[features]
default = ["blurred_rounded_rect", "image_bicubic", "gradient_sweep"]
default = ["blurred_rounded_rect", "full_image_support", "gradient_sweep"]
blurred_rounded_rect = []
image_bicubic = []
full_image_support = []
gradient_sweep = []
glsl = ["dep:naga"]
unminified = []
Expand Down
2 changes: 1 addition & 1 deletion vello_gpu_shaders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ generated GLSL shader programs, used by the Vello GPU renderer.
[naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga) to minify the resulting
WGSL.
- Optional generation of minified GLSL and reflection metadata for WebGL.
- The `blurred_rounded_rect`, `image_bicubic`, and `gradient_sweep` features are
- The `blurred_rounded_rect`, `full_image_support`, and `gradient_sweep` features are
enabled by default and include their respective shader paths.

## Usage
Expand Down
2 changes: 1 addition & 1 deletion vello_gpu_shaders/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn load_shader_infos(shader_dir: &Path) -> Vec<ShaderInfo> {
"blurred_rounded_rect",
cfg!(feature = "blurred_rounded_rect"),
);
compiler.set_feature("image_bicubic", cfg!(feature = "image_bicubic"));
compiler.set_feature("full_image_support", cfg!(feature = "full_image_support"));
compiler.set_feature("gradient_sweep", cfg!(feature = "gradient_sweep"));

shader_names
Expand Down
12 changes: 9 additions & 3 deletions vello_gpu_shaders/shaders/helpers/external_texture.wesl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

//! Sampling helpers for externally bound image textures.

@if(full_image_support)
import package::helpers::extend::{EXTEND_PAD, extend_mode};
@if(full_image_support)
import package::helpers::image::{
IMAGE_QUALITY_HIGH,
IMAGE_QUALITY_MEDIUM,
Expand All @@ -13,8 +15,9 @@ import package::helpers::image::{
get_image_quality,
get_image_size,
};
@if(image_bicubic)
@if(full_image_support)
import package::helpers::image::cubic_weights;
@if(full_image_support)
import package::helpers::paint::{PAINT_TEXTURE_INDEX_MASK, load_encoded_paint_texel};

fn sample_native_image_paint(
Expand All @@ -27,6 +30,7 @@ fn sample_native_image_paint(
return image_opacity * textureSampleLevel(external_texture, external_sampler, sample_xy, 0.0);
}

@if(full_image_support)
fn sample_image_paint(
encoded_paints_texture: texture_2d<u32>,
tex_width_bits: u32,
Expand Down Expand Up @@ -64,11 +68,13 @@ fn sample_image_paint(
);
}

@if(full_image_support)
fn external_axis_index(coord: f32, extend_mode_id: u32, size: f32) -> i32 {
return i32(min(extend_mode(coord, extend_mode_id, size), size - 1.0));
}

// Bilinear sampling with each tap extended within the source region.
@if(full_image_support)
fn external_bilinear_sample(
external_texture: texture_2d<f32>,
coords: vec2<f32>,
Expand Down Expand Up @@ -110,7 +116,7 @@ fn external_bilinear_sample(
}

// Bicubic sampling with each tap extended within the source region.
@if(image_bicubic)
@if(full_image_support)
fn external_bicubic_sample(
external_texture: texture_2d<f32>,
coords: vec2<f32>,
Expand Down Expand Up @@ -183,6 +189,7 @@ fn external_bicubic_sample(
return vec4<f32>(clamp(result.rgb, vec3(0.0), vec3(a)), a);
}

@if(full_image_support)
fn sample_external_image(
external_texture: texture_2d<f32>,
quality: u32,
Expand All @@ -191,7 +198,6 @@ fn sample_external_image(
image_size: vec2<f32>,
image_extend_modes: vec2<u32>,
) -> vec4<f32> {
@if(image_bicubic)
if quality == IMAGE_QUALITY_HIGH {
return external_bicubic_sample(
external_texture,
Expand Down
Loading
Loading