[vello_hybrid] Add opacity layers (#1122)
### Context
Add opacity handling in `vello_hybrid` layers!
### Discussion
It's probably best to look at the documentation in the shader code, but
this PR leverages the fact that we have a huge amount of bits that do
nothing. I've packed opacity into bits 0-7.
### Test plan
Tested via the existing opacity tests.
diff --git a/sparse_strips/vello_dev_macros/src/test.rs b/sparse_strips/vello_dev_macros/src/test.rs
index 88f94b8..60a13a4 100644
--- a/sparse_strips/vello_dev_macros/src/test.rs
+++ b/sparse_strips/vello_dev_macros/src/test.rs
@@ -163,7 +163,7 @@
skip_hybrid |= {
input_fn_name_str.contains("compose")
|| input_fn_name_str.contains("gradient")
- || input_fn_name_str.contains("layer")
+ || input_fn_name_str.contains("layer_multiple_properties")
|| input_fn_name_str.contains("mask")
|| input_fn_name_str.contains("mix")
|| input_fn_name_str.contains("blurred_rounded_rect")
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index 38247f2..e99ea52 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -183,9 +183,6 @@
if blend_mode.is_some() {
unimplemented!()
}
- if opacity.is_some() {
- unimplemented!()
- }
if mask.is_some() {
unimplemented!()
}
@@ -194,7 +191,7 @@
clip,
BlendMode::new(Mix::Normal, Compose::SrcOver),
None,
- 1.0,
+ opacity.unwrap_or(1.),
0,
);
}
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index 42928e7..3d5665b 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -181,6 +181,7 @@
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use core::mem;
+use vello_common::peniko::{BlendMode, Compose, Mix};
use vello_common::{
coarse::{Cmd, WideTile},
encode::EncodedPaint,
@@ -244,6 +245,7 @@
struct TileEl {
slot_ix: usize,
round: usize,
+ opacity: f32,
}
#[derive(Debug, Default)]
@@ -377,6 +379,7 @@
state.stack.push(TileEl {
slot_ix: usize::MAX,
round: self.round,
+ opacity: 1.,
});
{
// If the background has a non-zero alpha then we need to render it.
@@ -466,6 +469,7 @@
state.stack.push(TileEl {
slot_ix,
round: self.round,
+ opacity: 1.,
});
}
Cmd::PopBuf => {
@@ -494,7 +498,8 @@
} else {
(clip_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT)
};
- let paint = COLOR_SOURCE_SLOT << 31;
+ // Opacity packed into the first 8 bits – pack full opacity (0xFF).
+ let paint = COLOR_SOURCE_SLOT << 31 | 0xFF;
draw.0.push(GpuStrip {
x,
y,
@@ -516,7 +521,8 @@
} else {
(clip_alpha_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT)
};
- let paint = COLOR_SOURCE_SLOT << 31;
+ // Opacity packed into the first 8 bits – pack full opacity (0xFF).
+ let paint = COLOR_SOURCE_SLOT << 31 | 0xFF;
draw.0.push(GpuStrip {
x,
y,
@@ -529,6 +535,49 @@
paint,
});
}
+ Cmd::Opacity(opacity) => {
+ state.stack.last_mut().unwrap().opacity = *opacity;
+ }
+ Cmd::Blend(mode) => {
+ // This blend mode is implicitly supported. Currently no other blend mode is
+ // supported in `vello_hybrid`.
+ assert!(
+ matches!(
+ mode,
+ BlendMode {
+ mix: Mix::Normal,
+ compose: Compose::SrcOver
+ }
+ ),
+ "Changing blend mode is unsupported"
+ );
+
+ let tos = state.stack.last().unwrap();
+ let nos = &state.stack[state.stack.len() - 2];
+
+ let next_round = clip_depth % 2 == 0 && clip_depth > 2;
+ let round = nos.round.max(tos.round + usize::from(next_round));
+ let draw = self.draw_mut(round, clip_depth - 1);
+ let (x, y) = if clip_depth <= 2 {
+ (wide_tile_x, wide_tile_y)
+ } else {
+ (0, nos.slot_ix as u16 * Tile::HEIGHT)
+ };
+
+ // Opacity packed into the first 8 bits.
+ let opacity_u8 = (tos.opacity * 255.0) as u32;
+ let paint = (COLOR_SOURCE_SLOT << 31) | opacity_u8;
+
+ draw.0.push(GpuStrip {
+ x,
+ y,
+ width: WideTile::WIDTH,
+ dense_width: 0,
+ col_idx: 0,
+ payload: tos.slot_ix as u32,
+ paint,
+ });
+ }
_ => unimplemented!(),
}
}
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index 9410033..1d2bc1b 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -59,8 +59,10 @@
//
// `paint` bit layout:
// - Bit 31: `color_source` 0 = use payload, 1 = use slot texture
-// - Bits 29-30: `paint_type` 0 = solid, 1 = image
-// - Bits 0-28: `paint_texture_id` if paint_type = PAINT_TYPE_IMAGE, index of `EncodedImage`
+// - Bits 29-30: `paint_type` 0 = solid, 1 = image (only used when color_source = 0)
+// - Bits 0-28: Usage depends on color_source:
+// - When color_source = 0 and paint_type = 1: `paint_texture_id` (index of `EncodedImage`)
+// - When color_source = 1: bits 0-7 contain opacity (0-255)
//
// Decision tree for paint/payload interpretation:
//
@@ -69,10 +71,12 @@
// │ └── payload = [r, g, b, a] RGBA (packed as u8s)
// │
// └── paint_type = 1 (PAINT_TYPE_IMAGE) - Image rendering
-// └── payload = [x, y] scene coordinates (packed as u16s)
+// ├── payload = [x, y] scene coordinates (packed as u16s)
+// └── bits 0-28 = paint_texture_id
//
// color_source = 1 (COLOR_SOURCE_SLOT) - Use slot texture
-// └── payload = slot_index (u32)
+// ├── payload = slot_index (u32)
+// └── bits 0-7 = opacity (0-255, where 255 = fully opaque)
struct StripInstance {
// [x, y] packed as u16's
// x, y — coordinates of the strip
@@ -258,7 +262,11 @@
let clip_x = u32(in.position.x) & 0xFFu;
let clip_y = (u32(in.position.y) & 3) + in.payload * config.strip_height;
let clip_in_color = textureLoad(clip_input_texture, vec2(clip_x, clip_y), 0);
- final_color = alpha * clip_in_color;
+
+ // Extract opacity from first 8 bits (quantized from [0, 255])
+ let opacity = f32(in.paint & 0xFFu) * (1.0 / 255.0);
+
+ final_color = alpha * opacity * clip_in_color;
}
return final_color;
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_with_opacity.png b/sparse_strips/vello_sparse_tests/snapshots/image_with_opacity.png
new file mode 100644
index 0000000..e8991cc
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/image_with_opacity.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:001350ccad11b3120dbf2b44350734ac145c5a41e277c281a5da2969e4e13001
+size 152
diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs
index caba16c..2798454 100644
--- a/sparse_strips/vello_sparse_tests/tests/image.rs
+++ b/sparse_strips/vello_sparse_tests/tests/image.rs
@@ -7,6 +7,7 @@
use crate::util::crossed_line_star;
use std::f64::consts::PI;
use std::sync::Arc;
+use vello_common::color::palette::css::REBECCA_PURPLE;
use vello_common::kurbo::{Affine, Point, Rect};
use vello_common::paint::{Image, ImageSource};
use vello_common::peniko::{Extend, ImageQuality};
@@ -252,6 +253,29 @@
ctx.fill_rect(&rect);
}
+#[vello_test]
+fn image_with_opacity(ctx: &mut impl Renderer) {
+ ctx.set_paint(REBECCA_PURPLE);
+ ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
+
+ ctx.push_opacity_layer(0.5);
+
+ let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
+ let image_source = rgb_img_10x10(ctx);
+
+ let image = Image {
+ source: image_source,
+ x_extend: Extend::Repeat,
+ y_extend: Extend::Repeat,
+ quality: ImageQuality::Low,
+ };
+
+ ctx.set_paint(image);
+ ctx.fill_rect(&rect);
+
+ ctx.pop_layer();
+}
+
fn image_format(ctx: &mut impl Renderer, image_source: ImageSource) {
let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
diff --git a/sparse_strips/vello_sparse_tests/tests/renderer.rs b/sparse_strips/vello_sparse_tests/tests/renderer.rs
index b6c8e8c..c7992f8 100644
--- a/sparse_strips/vello_sparse_tests/tests/renderer.rs
+++ b/sparse_strips/vello_sparse_tests/tests/renderer.rs
@@ -274,8 +274,8 @@
unimplemented!()
}
- fn push_opacity_layer(&mut self, _: f32) {
- unimplemented!()
+ fn push_opacity_layer(&mut self, opacity: f32) {
+ self.scene.push_layer(None, None, Some(opacity), None);
}
fn push_mask_layer(&mut self, _: Mask) {
@@ -542,8 +542,8 @@
unimplemented!()
}
- fn push_opacity_layer(&mut self, _: f32) {
- unimplemented!()
+ fn push_opacity_layer(&mut self, opacity: f32) {
+ self.scene.push_layer(None, None, Some(opacity), None);
}
fn push_mask_layer(&mut self, _: Mask) {