feat(vello_hybrid): Add clipping support for image rendering
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs index 0e7ca3f..038f6a7 100644 --- a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs +++ b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
@@ -5,7 +5,7 @@ use std::f64::consts::PI; use vello_common::color::PremulRgba8; -use vello_common::kurbo::BezPath; +use vello_common::kurbo::{BezPath, Point, Shape, Vec2}; use vello_common::peniko::ImageFormat; use vello_common::pixmap::Pixmap; use vello_common::{ @@ -69,23 +69,21 @@ y_extend: Extend::Pad, quality: ImageQuality::Low, }); - let path = heart_shape(); - scene.fill_path(&path); + scene.fill_path(&heart_shape()); - scene.set_transform( - root_transform - * Affine::translate((700.0, 500.0)) - * Affine::rotate(PI / 4.0) - * Affine::scale(0.5), + scene.set_transform(root_transform * Affine::translate((400.0, 480.0))); + scene.push_clip_layer( + &circular_star(Point::new(300.0, 220.0), 5, 100.0, 150.0).to_path(0.1), ); - scene.set_paint_transform(Affine::scale(0.25)); + scene.set_paint_transform(Affine::IDENTITY); scene.set_paint(Image { - source: ImageSource::OpaqueId(ImageId::new(0)), + source: ImageSource::OpaqueId(splash_flower_id), x_extend: Extend::Repeat, y_extend: Extend::Repeat, quality: ImageQuality::Low, }); scene.fill_rect(&Rect::new(0.0, 0.0, 640.0, 480.0)); + scene.pop_layer(); scene.set_transform(root_transform * Affine::translate((1000.0, 50.0))); scene.set_paint_transform(Affine::scale(0.25)); @@ -126,6 +124,21 @@ quality: ImageQuality::High, }); scene.fill_rect(&Rect::new(0.0, 0.0, 800.0, 160.0)); + + scene.set_transform( + root_transform + * Affine::translate((200.0, 1200.0)) + * Affine::rotate(PI / 4.0) + * Affine::scale(0.5), + ); + scene.set_paint_transform(Affine::scale(0.25)); + scene.set_paint(Image { + source: ImageSource::OpaqueId(ImageId::new(0)), + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + }); + scene.fill_rect(&Rect::new(0.0, 0.0, 640.0, 480.0)); } } @@ -208,3 +221,16 @@ path.close_path(); path } + +fn circular_star(center: Point, n: usize, inner: f64, outer: f64) -> BezPath { + let mut path = BezPath::new(); + let start_angle = -std::f64::consts::FRAC_PI_2; + path.move_to(center + outer * Vec2::from_angle(start_angle)); + for i in 1..n * 2 { + let th = start_angle + i as f64 * std::f64::consts::PI / n as f64; + let r = if i % 2 == 0 { outer } else { inner }; + path.line_to(center + r * Vec2::from_angle(th)); + } + path.close_path(); + path +}
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs index 0feda7b..bfffcf5 100644 --- a/sparse_strips/vello_hybrid/src/render/common.rs +++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -46,7 +46,7 @@ /// There are [`Config::strip_height`] alpha values per column. pub col_idx: u32, /// Color value or slot index when alpha is 0 - pub rgba_or_slot: u32, + pub payload: u32, /// Packed paint type (2 bits) and paint texture id (30 bits) /// Paint type: 0 = solid, 1 = alpha, 2 = image /// Paint texture id locates the encoded image data `EncodedImage`
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs index a5fedde..ddb9097 100644 --- a/sparse_strips/vello_hybrid/src/schedule.rs +++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -389,7 +389,7 @@ width: WideTile::WIDTH, dense_width: 0, col_idx: 0, - rgba_or_slot: bg, + payload: bg, paint: 0, }); } @@ -402,10 +402,12 @@ let el = state.stack.last().unwrap(); let draw = self.draw_mut(el.round, clip_depth); - let (col_idx, rgba_or_slot, paint) = Self::process_paint(&fill.paint, scene, 0); + let (scene_strip_x, scene_strip_y) = (wide_tile_x + fill.x, wide_tile_y); + let (payload, paint) = + Self::process_paint(&fill.paint, scene, (scene_strip_x, scene_strip_y)); let (x, y) = if clip_depth == 1 { - (wide_tile_x + fill.x, wide_tile_y) + (scene_strip_x, scene_strip_y) } else { (fill.x, el.slot_ix as u16 * Tile::HEIGHT) }; @@ -415,8 +417,8 @@ y, width: fill.width, dense_width: 0, - col_idx, - rgba_or_slot, + col_idx: 0, + payload, paint, }); } @@ -424,15 +426,19 @@ let el = state.stack.last().unwrap(); let draw = self.draw_mut(el.round, clip_depth); - let alpha_col = (alpha_fill.alpha_idx / usize::from(Tile::HEIGHT)) + let col_idx = (alpha_fill.alpha_idx / usize::from(Tile::HEIGHT)) .try_into() .expect("Sparse strips are bound to u32 range"); - let (col_idx, rgba_or_slot, paint) = - Self::process_paint(&alpha_fill.paint, scene, alpha_col); + let (scene_strip_x, scene_strip_y) = (wide_tile_x + alpha_fill.x, wide_tile_y); + let (payload, paint) = Self::process_paint( + &alpha_fill.paint, + scene, + (scene_strip_x, scene_strip_y), + ); let (x, y) = if clip_depth == 1 { - (wide_tile_x + alpha_fill.x, wide_tile_y) + (scene_strip_x, scene_strip_y) } else { (alpha_fill.x, el.slot_ix as u16 * Tile::HEIGHT) }; @@ -443,7 +449,7 @@ width: alpha_fill.width, dense_width: alpha_fill.width, col_idx, - rgba_or_slot, + payload, paint, }); } @@ -488,14 +494,15 @@ } else { (clip_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT) }; + let paint = COLOR_SOURCE_SLOT << 31; draw.0.push(GpuStrip { x, y, width: clip_fill.width as u16, dense_width: 0, col_idx: 0, - rgba_or_slot: tos.slot_ix as u32, - paint: 0, + payload: tos.slot_ix as u32, + paint, }); } Cmd::ClipStrip(clip_alpha_fill) => { @@ -509,6 +516,7 @@ } else { (clip_alpha_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT) }; + let paint = COLOR_SOURCE_SLOT << 31; draw.0.push(GpuStrip { x, y, @@ -517,8 +525,8 @@ col_idx: (clip_alpha_fill.alpha_idx / usize::from(Tile::HEIGHT)) .try_into() .expect("Sparse strips are bound to u32 range"), - rgba_or_slot: tos.slot_ix as u32, - paint: 0, + payload: tos.slot_ix as u32, + paint, }); } _ => unimplemented!(), @@ -528,8 +536,12 @@ Ok(()) } - /// Process a paint and return (`col_idx`, `rgba_or_slot`, `paint_type`) - fn process_paint(paint: &Paint, scene: &Scene, alpha_col: u32) -> (u32, u32, u32) { + /// Process a paint and return (`payload`, `paint`) + fn process_paint( + paint: &Paint, + scene: &Scene, + (scene_strip_x, scene_strip_y): (u16, u16), + ) -> (u32, u32) { match paint { Paint::Solid(color) => { let rgba = color.as_premul_rgba8().to_u32(); @@ -537,7 +549,8 @@ has_non_zero_alpha(rgba), "Color fields with 0 alpha are reserved for clipping" ); - (alpha_col, rgba, 0) + let paint_packed = (COLOR_SOURCE_PAYLOAD << 31) | (PAINT_TYPE_SOLID << 29); + (rgba, paint_packed) } Paint::Indexed(indexed_paint) => { let paint_id = indexed_paint.index(); @@ -547,19 +560,28 @@ match scene.encoded_paints.get(paint_id) { Some(EncodedPaint::Image(encoded_image)) => match &encoded_image.source { ImageSource::OpaqueId(_) => { - let paint_type = 2_u32; - let paint_packed = (paint_type << 30) | (paint_tex_id & 0x3FFFFFFF); - (alpha_col, 0, paint_packed) + let paint_packed = (COLOR_SOURCE_PAYLOAD << 31) + | (PAINT_TYPE_IMAGE << 29) + | (paint_tex_id & 0x1FFFFFFF); + let scene_strip_xy = + ((scene_strip_y as u32) << 16) | (scene_strip_x as u32); + (scene_strip_xy, paint_packed) } _ => unimplemented!("unsupported image source"), }, - _ => (0, 0, 0), + _ => (0, 0), } } } } } +const COLOR_SOURCE_PAYLOAD: u32 = 0; +const COLOR_SOURCE_SLOT: u32 = 1; + +const PAINT_TYPE_SOLID: u32 = 0; +const PAINT_TYPE_IMAGE: u32 = 1; + #[inline(always)] fn has_non_zero_alpha(rgba: u32) -> bool { rgba >= 0x1_00_00_00
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl index 7b8bc1b..002d7cd 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl +++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -10,14 +10,19 @@ // The alpha values are stored in a texture and sampled during fragment shading. // This approach optimizes memory usage by only storing alpha data where needed. // -// The `StripInstance`'s `rgba_or_slot` field can either encode a color or a slot index. +// The `StripInstance`'s `payload` field can either encode a color or a slot index. // If the alpha value is non-zero, the fragment shader samples the alpha texture. // Otherwise, the fragment shader samples the source clip texture using the given slot index. -// Paint types to determine how to process a strip +// Color source modes - where the fragment shader gets color data from +// Use payload (color or image coordinates) +const COLOR_SOURCE_PAYLOAD: u32 = 0u; +// Sample from clip texture slot +const COLOR_SOURCE_SLOT: u32 = 1u; + +// Paint types const PAINT_TYPE_SOLID: u32 = 0u; -const PAINT_TYPE_ALPHA: u32 = 1u; -const PAINT_TYPE_IMAGE: u32 = 2u; +const PAINT_TYPE_IMAGE: u32 = 1u; // Image quality const IMAGE_QUALITY_LOW = 0u; @@ -40,8 +45,9 @@ // Strip instance data // -// `rgba_or_slot` field can either encode a color or a slot index. -// If the alpha value is non-zero, the fragment shader samples the alpha texture. +// `payload` field can either encode a color, [x, y] for image sampling or a slot index +// If color source is payload and the paint type is solid, the fragment shader uses the color directly. +// If color source is payload and the paint type is image, the fragment shader samples the image. // Otherwise, the fragment shader samples the source clip texture using the given slot index. struct StripInstance { // [x, y] packed as u16's @@ -50,9 +56,16 @@ @location(1) widths: u32, // Alpha texture column index where this strip's alpha values begin @location(2) col_idx: u32, - // [r, g, b, a] packed as u8's or a slot index when alpha is 0 - @location(3) rgba_or_slot: u32, - // Packed paint type (2 bits) and paint texture id (30 bits) + // Packed data: + // - [r, g, b, a] packed as u8's (when color source is payload and paint type is solid) + // - [x, y] packed as u16's (when color source is payload and paint type is image) + // - a slot index (when color source is slot) + @location(3) payload: u32, + // Packed data: + // - color source (1 bit) + // - paint type (2 bits) + // - paint texture id (29 bits) + // Color source: 0 = use payload, 1 = sample from slot // Paint type: 0 = solid, 1 = alpha, 2 = image // Paint texture id locates the encoded image data `EncodedImage` in the encoded_paints_texture @location(4) paint: u32, @@ -68,7 +81,7 @@ // Ending x-position of the dense (alpha) region @location(3) @interpolate(flat) dense_end: u32, // Color value or slot index when alpha is 0 - @location(4) @interpolate(flat) rgba_or_slot: u32, + @location(4) @interpolate(flat) payload: u32, // Normalized device coordinates (NDC) for the current vertex @builtin(position) position: vec4<f32>, }; @@ -109,28 +122,29 @@ // NDC ranges from -1 to 1, with (0,0) at the center of the viewport let ndc_x = pix_x * 2.0 / f32(config.width) - 1.0; let ndc_y = 1.0 - pix_y * 2.0 / f32(config.height); - let paint_type = instance.paint >> 30u; + let paint_type = (instance.paint >> 29u) & 0x3u; if paint_type == PAINT_TYPE_IMAGE { - let paint_tex_id = instance.paint & 0x3FFFFFFF; + let paint_tex_id = instance.paint & 0x1FFFFFFF; let encoded_image = unpack_encoded_image(paint_tex_id); - // Vertex position within the texture + // Unpack view coordinates for image sampling + let scene_strip_x = instance.payload & 0xffffu; + let scene_strip_y = instance.payload >> 16u; + // Use view coordinates for image sampling (always in global view space) out.sample_xy = encoded_image.translate + encoded_image.image_offset - + encoded_image.transform.xy * f32(x0) - + encoded_image.transform.zw * f32(y0) + + encoded_image.transform.xy * f32(scene_strip_x) + + encoded_image.transform.zw * f32(scene_strip_y) + encoded_image.transform.xy * x * f32(width) + encoded_image.transform.zw * y * f32(config.strip_height); - out.paint = instance.paint; - } else { - out.sample_xy = vec2<f32>(0.0, 0.0); } // Regular texture coordinates for other render types out.tex_coord = vec2<f32>(f32(instance.col_idx) + x * f32(width), y * f32(config.strip_height)); out.position = vec4<f32>(ndc_x, ndc_y, 0.0, 1.0); - out.rgba_or_slot = instance.rgba_or_slot; + out.payload = instance.payload; + out.paint = instance.paint; return out; } @@ -174,57 +188,58 @@ alpha = f32((alphas_u32 >> (y * 8u)) & 0xffu) * (1.0 / 255.0); } // Apply the alpha value to the unpacked RGBA color or slot index - let alpha_byte = in.rgba_or_slot >> 24u; - var final_color = unpack4x8unorm(in.rgba_or_slot); - let paint_type = in.paint >> 30u; + let alpha_byte = in.payload >> 24u; + let color_source = (in.paint >> 31u) & 0x1u; + var final_color = unpack4x8unorm(in.payload); + let paint_type = (in.paint >> 29u) & 0x3u; - if paint_type == PAINT_TYPE_IMAGE { - let paint_tex_id = in.paint & 0x3FFFFFFF; - let encoded_image = unpack_encoded_image(paint_tex_id); - let image_offset = encoded_image.image_offset; - let image_size = encoded_image.image_size; - let local_xy = in.sample_xy - image_offset; - let offset = 0.00001; - let extended_xy = vec2<f32>( - extend_mode(local_xy.x, encoded_image.extend_modes.x, image_size.x - offset), - extend_mode(local_xy.y, encoded_image.extend_modes.y, image_size.y - offset) - ); - - if encoded_image.quality == IMAGE_QUALITY_HIGH { - let final_xy = image_offset + extended_xy; - let sample_color = bicubic_sample( - atlas_texture, - final_xy, - image_offset, - image_size, - encoded_image.extend_modes + if color_source == COLOR_SOURCE_PAYLOAD { + // in.payload encodes a color for PAINT_TYPE_SOLID or sample_xy for PAINT_TYPE_IMAGE + final_color = alpha * final_color; + + if paint_type == PAINT_TYPE_IMAGE { + let paint_tex_id = in.paint & 0x1FFFFFFF; + let encoded_image = unpack_encoded_image(paint_tex_id); + let image_offset = encoded_image.image_offset; + let image_size = encoded_image.image_size; + let local_xy = in.sample_xy - image_offset; + let offset = 0.00001; + let extended_xy = vec2<f32>( + extend_mode(local_xy.x, encoded_image.extend_modes.x, image_size.x - offset), + extend_mode(local_xy.y, encoded_image.extend_modes.y, image_size.y - offset) ); - final_color = alpha * sample_color; - } else if encoded_image.quality == IMAGE_QUALITY_MEDIUM { - let final_xy = image_offset + extended_xy - vec2(0.5); - let sample_color = bilinear_sample( - atlas_texture, - final_xy, - image_offset, - image_size, - encoded_image.extend_modes - ); - final_color = alpha * sample_color; - } else if encoded_image.quality == IMAGE_QUALITY_LOW { - let final_xy = image_offset + extended_xy; - final_color = alpha * textureLoad(atlas_texture, vec2<u32>(final_xy), 0); + + if encoded_image.quality == IMAGE_QUALITY_HIGH { + let final_xy = image_offset + extended_xy; + let sample_color = bicubic_sample( + atlas_texture, + final_xy, + image_offset, + image_size, + encoded_image.extend_modes + ); + final_color = alpha * sample_color; + } else if encoded_image.quality == IMAGE_QUALITY_MEDIUM { + let final_xy = image_offset + extended_xy - vec2(0.5); + let sample_color = bilinear_sample( + atlas_texture, + final_xy, + image_offset, + image_size, + encoded_image.extend_modes + ); + final_color = alpha * sample_color; + } else if encoded_image.quality == IMAGE_QUALITY_LOW { + let final_xy = image_offset + extended_xy; + final_color = alpha * textureLoad(atlas_texture, vec2<u32>(final_xy), 0); + } } } else { - if alpha_byte != 0 { - // in.rgba_or_slot encodes a color - final_color = alpha * final_color; - } else { - // in.rgba_or_slot encodes a slot in the source clip texture - let clip_x = u32(in.position.x) & 0xFFu; - let clip_y = (u32(in.position.y) & 3) + in.rgba_or_slot * config.strip_height; - let clip_in_color = textureLoad(clip_input_texture, vec2(clip_x, clip_y), 0); - final_color = alpha * clip_in_color; - } + // in.payload encodes a slot in the source clip texture + 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; } return final_color;
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png b/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png new file mode 100644 index 0000000..9b6b77c --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png
@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb94d43df371385e172f29059c205df61bae88b68f1fafa817ee53ec74501c98 +size 731
diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs index 5b1608d..caba16c 100644 --- a/sparse_strips/vello_sparse_tests/tests/image.rs +++ b/sparse_strips/vello_sparse_tests/tests/image.rs
@@ -10,6 +10,7 @@ use vello_common::kurbo::{Affine, Point, Rect}; use vello_common::paint::{Image, ImageSource}; use vello_common::peniko::{Extend, ImageQuality}; +use vello_cpu::kurbo::{Shape, Triangle}; use vello_dev_macros::vello_test; fn rgb_img_10x10(ctx: &mut impl Renderer) -> ImageSource { @@ -487,3 +488,28 @@ Extend::Reflect, ); } + +#[vello_test] +fn image_with_multiple_clip_layers(ctx: &mut impl Renderer) { + let image_source = rgb_img_2x2(ctx); + let image_rect = Rect::new(10.0, 10.0, 90.0, 90.0); + let clipped_area1 = Rect::new(20.0, 20.0, 80.0, 80.0); + let clipped_area2 = Triangle::new( + Point::new(90.0, 10.0), + Point::new(32.0, 46.0), + Point::new(54.0, 68.0), + ); + + ctx.push_clip_layer(&clipped_area1.to_path(0.1)); + ctx.push_clip_layer(&clipped_area2.to_path(0.1)); + ctx.set_paint_transform(Affine::IDENTITY); + ctx.set_paint(Image { + source: image_source, + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + }); + ctx.fill_rect(&image_rect); + ctx.pop_layer(); + ctx.pop_layer(); +}