Skip to content
Open
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
117 changes: 114 additions & 3 deletions sparse_strips/vello_common/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::color::{ColorSpaceTag, HueDirection, Srgb, gradient};
use crate::geometry::RectU16;
use crate::kurbo::{Affine, Point, Vec2};
use crate::math::{FloatExt, compute_erf7};
use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor, Tint};
use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor, Tint, TintMode};
use crate::peniko::{ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageQuality};
use crate::util::f32_to_u8;
use alloc::borrow::Cow;
Expand Down Expand Up @@ -494,9 +494,28 @@ impl EncodeExt for Image {

let mut sampler = self.sampler;

// Fold `sampler.alpha` into the tint instead of adding a separate
// image-opacity path: in both `TintMode`s the rasterized output scales
// linearly with the tint's alpha, so multiplying the tint's alpha by
// `sampler.alpha` (synthesizing a white `Multiply` tint when there is
// none) applies the opacity exactly.
let mut tint = tint;
if sampler.alpha != 1.0 {
// If the sampler alpha is not 1.0, we need to force alpha compositing.
unimplemented!("Applying opacity to image commands");
let a = sampler.alpha;
tint = Some(match tint {
Some(t) => {
let [r, g, b, ta] = t.color.components;
Tint {
color: peniko::Color::new([r, g, b, ta * a]),
mode: t.mode,
}
}
None => Tint {
color: peniko::Color::new([1.0, 1.0, 1.0, a]),
mode: TintMode::Multiply,
},
});
sampler.alpha = 1.0;
}

let c = transform.as_coeffs();
Expand Down Expand Up @@ -1390,4 +1409,96 @@ mod tests {
GREEN.into()
);
}

// Image `sampler.alpha` → tint fold (regression tests for the former
// `unimplemented!("Applying opacity to image commands")` panic).

use crate::paint::{Image, ImageId, ImageSource, Tint, TintMode};
use peniko::{Color, Extend, ImageQuality, ImageSampler};

fn dummy_image(alpha: f32) -> Image {
Image {
image: ImageSource::opaque_id_with_transparency_hint(ImageId::new(1), false),
sampler: ImageSampler {
x_extend: Extend::Pad,
y_extend: Extend::Pad,
quality: ImageQuality::Low,
alpha,
},
}
}

fn expect_image_paint(buf: &[super::EncodedPaint]) -> &super::EncodedImage {
match buf.last().expect("paint pushed") {
super::EncodedPaint::Image(img) => img,
other => panic!("expected EncodedPaint::Image, got {other:?}"),
}
}

#[test]
fn image_sampler_alpha_one_passes_tint_through_unchanged() {
let mut buf = vec![];
let img = dummy_image(1.0);
let tint = Some(Tint {
color: Color::new([0.5, 0.25, 0.75, 0.8]),
mode: TintMode::AlphaMask,
});
img.encode_into(&mut buf, Affine::IDENTITY, tint);

let enc = expect_image_paint(&buf);
assert_eq!(enc.sampler.alpha, 1.0);
let t = enc.tint.expect("tint survives");
assert_eq!(t.color.components, [0.5, 0.25, 0.75, 0.8]);
assert_eq!(t.mode, TintMode::AlphaMask);
}

#[test]
fn image_sampler_alpha_no_tint_synthesises_multiply_tint() {
let mut buf = vec![];
let img = dummy_image(0.4);
img.encode_into(&mut buf, Affine::IDENTITY, None);

let enc = expect_image_paint(&buf);
assert_eq!(enc.sampler.alpha, 1.0);
let t = enc.tint.expect("alpha fold synthesises a tint");
assert_eq!(t.color.components, [1.0, 1.0, 1.0, 0.4]);
assert_eq!(t.mode, TintMode::Multiply);
assert!(enc.may_have_transparency);
}

#[test]
fn image_sampler_alpha_with_existing_tint_scales_alpha_only() {
for mode in [TintMode::AlphaMask, TintMode::Multiply] {
let mut buf = vec![];
let img = dummy_image(0.5);
let tint = Some(Tint {
color: Color::new([0.2, 0.4, 0.6, 0.8]),
mode,
});
img.encode_into(&mut buf, Affine::IDENTITY, tint);

let enc = expect_image_paint(&buf);
assert_eq!(enc.sampler.alpha, 1.0, "α must be folded out");
let t = enc.tint.expect("tint survives");
assert_eq!(
t.color.components,
[0.2, 0.4, 0.6, 0.8 * 0.5],
"mode = {mode:?}"
);
assert_eq!(t.mode, mode, "mode must be preserved");
}
}

#[test]
fn image_sampler_alpha_zero_folds_to_fully_transparent_tint() {
let mut buf = vec![];
let img = dummy_image(0.0);
img.encode_into(&mut buf, Affine::IDENTITY, None);

let enc = expect_image_paint(&buf);
assert_eq!(enc.sampler.alpha, 1.0);
let t = enc.tint.expect("tint synthesised");
assert_eq!(t.color.components, [1.0, 1.0, 1.0, 0.0]);
assert!(enc.may_have_transparency);
}
}
64 changes: 58 additions & 6 deletions sparse_strips/vello_hybrid/src/render/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,44 @@ mod tests {
use super::normalize_atlas_config;
use vello_common::multi_atlas::AtlasConfig;

#[test]
fn pack_tint_signals_presence_by_mode_not_color() {
use super::{
GPU_TINT_MODE_ALPHA_MASK, GPU_TINT_MODE_MULTIPLY, GPU_TINT_MODE_NONE, pack_tint,
};
use vello_common::color::palette::css::RED;
use vello_common::paint::{Tint, TintMode};
use vello_common::peniko::Color;

assert_eq!(pack_tint(None), (0, GPU_TINT_MODE_NONE));

// A fully transparent tint premultiplies to color 0 but must still be
// reported as present (it used to be read as "no tint", rendering the
// image opaque).
let transparent_white = Color::new([1.0, 1.0, 1.0, 0.0]);
assert_eq!(
pack_tint(Some(Tint {
color: transparent_white,
mode: TintMode::Multiply,
})),
(0, GPU_TINT_MODE_MULTIPLY),
);
assert_eq!(
pack_tint(Some(Tint {
color: transparent_white,
mode: TintMode::AlphaMask,
})),
(0, GPU_TINT_MODE_ALPHA_MASK),
);

let (color, mode) = pack_tint(Some(Tint {
color: RED,
mode: TintMode::Multiply,
}));
assert_ne!(color, 0);
assert_eq!(mode, GPU_TINT_MODE_MULTIPLY);
}

#[test]
fn normalize_atlas_config_clamps_to_backend_limits() {
let mut config = AtlasConfig {
Expand Down Expand Up @@ -217,9 +255,9 @@ pub(crate) struct GpuEncodedImage {
/// 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.
/// Only meaningful when `tint_mode != GPU_TINT_MODE_NONE`.
pub tint: u32,
/// [`TintMode`](vello_common::paint::TintMode) discriminant. Only meaningful when `tint != 0`.
/// One of the `GPU_TINT_MODE_*` markers. Carries tint presence, not `tint`.
pub tint_mode: u32,
/// Number of transparent padding pixels around the image in the atlas.
pub image_padding: u32,
Expand Down Expand Up @@ -373,19 +411,33 @@ pub(crate) fn pack_image_params(
(atlas_index << 6) | (extend_y << 4) | (extend_x << 2) | quality
}

/// GPU tint-mode markers written to [`GpuEncodedImage::tint_mode`], kept in sync
/// with `render_strips.wgsl`. Unlike the CPU-side
/// [`TintMode`](vello_common::paint::TintMode), `0` marks the *absence* of a
/// tint: presence must be carried by the mode rather than by the packed color,
/// because a fully transparent tint premultiplies to a packed color of `0`.
pub(crate) const GPU_TINT_MODE_NONE: u32 = 0;
pub(crate) const GPU_TINT_MODE_ALPHA_MASK: u32 = 1;
pub(crate) const GPU_TINT_MODE_MULTIPLY: u32 = 2;

/// 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".
/// as WGSL `pack4x8unorm`. Presence is signalled by the mode, not the color
/// (see [`GPU_TINT_MODE_NONE`]).
#[inline(always)]
pub(crate) fn pack_tint(tint: Option<vello_common::paint::Tint>) -> (u32, u32) {
use vello_common::paint::TintMode;
match tint {
Some(t) => {
let color = t.color.premultiply().to_rgba8().to_u32();
(color, t.mode.as_u32())
let mode = match t.mode {
TintMode::AlphaMask => GPU_TINT_MODE_ALPHA_MASK,
TintMode::Multiply => GPU_TINT_MODE_MULTIPLY,
};
(color, mode)
}
None => (0, 0),
None => (0, GPU_TINT_MODE_NONE),
}
}

Expand Down
21 changes: 12 additions & 9 deletions sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,12 @@ fn fs_main(
let image_source_kind = get_image_source_kind(image_texel0);
let image_padding = get_image_padding(image_texel2);
let packed_tint = image_texel2.y;
let has_tint = packed_tint != 0u;
// When packed_tint is zero (no tint), use identity color vec4(1.0) with
// Multiply mode so the math reduces to sample_color * 1.0 = sample_color.
let tint_mode = image_texel2.z;
let has_tint = tint_mode != TINT_MODE_NONE;
// With no tint, image_tint is identity vec4(1.0) and is_multiply is
// true, so the math reduces to sample_color * 1.0 = sample_color.
let image_tint = select(vec4<f32>(1.0), unpack4x8unorm(packed_tint), has_tint);
let is_multiply = !has_tint || image_texel2.z != TINT_MODE_ALPHA_MASK;
let is_multiply = tint_mode != TINT_MODE_ALPHA_MASK;
let local_xy = sample_xy - image_offset;
// This offset doesn't exist in vello_cpu, and we use it because 45 degree skewing seems to cause
// artifacts on the GPU. We have something similar in place for gradients. It might be worth revisiting
Expand Down Expand Up @@ -895,9 +896,11 @@ fn blend_mix(cb: vec3<f32>, cs: vec3<f32>, mode: u32) -> vec3<f32> {
}


/// Tint mode constants.
const TINT_MODE_ALPHA_MASK: u32 = 0u;
const TINT_MODE_MULTIPLY: u32 = 1u;
/// Tint mode constants. Presence is carried by the mode, not the packed color:
/// a fully transparent tint packs to 0 but must still be applied.
const TINT_MODE_NONE: u32 = 0u;
const TINT_MODE_ALPHA_MASK: u32 = 1u;
const TINT_MODE_MULTIPLY: u32 = 2u;

// Convert a flat texel index to 2D texture coordinates for the encoded paints texture.
fn encoded_paint_coord(flat_idx: u32) -> vec2<u32> {
Expand Down Expand Up @@ -927,8 +930,8 @@ fn load_encoded_paint_texel(paint_tex_idx: u32, texel_offset: u32) -> vec4<u32>
// texel0.z: image_offset, packed as [x:16, y:16]
// texel0.w/texel1.x/texel1.y/texel1.z: transform matrix [a, b, c, d]
// texel1.w/texel2.x: translation [tx, ty]
// texel2.y: premultiplied tint color packed as RGBA8 unorm; 0 means no tint
// texel2.z: tint mode, only meaningful when texel2.y != 0
// texel2.y: premultiplied tint color packed as RGBA8 unorm
// texel2.z: tint mode (TINT_MODE_*); TINT_MODE_NONE = no tint
// texel2.w: transparent padding pixels around the image in the atlas

/// The rendering quality of the image.
Expand Down
Loading