From 15cd306af6b2baa99904b2c4155c328f69df81eb Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Sun, 30 Apr 2023 23:11:57 -0400 Subject: [PATCH 1/9] Extend modes for gradients This patch implements the pad, repeat and reflect extend modes for gradient brushes. Adds a new example demonstrating the functionality. Also fixes a few bugs: * Clamps alpha in blend.wgsl for the `blend_compose` function. The `Plus` mode was generating `alpha > 1.0` leading to incorrect rendering. * Small change to radial gradients in fine.wgsl to reject pixels outside the cone when the circles don't nest. This requires further work to properly extend the cone when one of the radii is not 0. --- crates/encoding/src/encoding.rs | 18 +++++--- crates/encoding/src/resolve.rs | 12 +++++- examples/scenes/src/test_scenes.rs | 67 ++++++++++++++++++++++++++++++ shader/fine.wgsl | 50 +++++++++++++++++----- shader/shared/blend.wgsl | 3 +- shader/shared/ptcl.wgsl | 2 + 6 files changed, 133 insertions(+), 19 deletions(-) diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 65dcbbb..1fc4a1d 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -113,11 +113,13 @@ impl Encoding { Patch::Ramp { draw_data_offset: offset, stops, + extend, } => { let stops = stops.start + stops_base..stops.end + stops_base; Patch::Ramp { draw_data_offset: offset + offsets.draw_data, stops, + extend: *extend, } } Patch::GlyphRun { index } => Patch::GlyphRun { @@ -264,9 +266,9 @@ impl Encoding { gradient: DrawLinearGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); + self.add_ramp(color_stops, alpha, extend); self.draw_tags.push(DrawTag::LINEAR_GRADIENT); self.draw_data .extend_from_slice(bytemuck::bytes_of(&gradient)); @@ -278,9 +280,9 @@ impl Encoding { gradient: DrawRadialGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); + self.add_ramp(color_stops, alpha, extend); self.draw_tags.push(DrawTag::RADIAL_GRADIENT); self.draw_data .extend_from_slice(bytemuck::bytes_of(&gradient)); @@ -331,7 +333,12 @@ impl Encoding { self.path_tags.swap(len - 1, len - 2); } - fn add_ramp(&mut self, color_stops: impl Iterator, alpha: f32) { + fn add_ramp( + &mut self, + color_stops: impl Iterator, + alpha: f32, + extend: Extend, + ) { let offset = self.draw_data.len(); let stops_start = self.color_stops.len(); if alpha != 1.0 { @@ -343,6 +350,7 @@ impl Encoding { self.patches.push(Patch::Ramp { draw_data_offset: offset, stops: stops_start..self.color_stops.len(), + extend, }); } } diff --git a/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index 0f72939..4e6dbc6 100644 --- a/crates/encoding/src/resolve.rs +++ b/crates/encoding/src/resolve.rs @@ -4,7 +4,7 @@ use std::ops::Range; use bytemuck::{Pod, Zeroable}; -use peniko::Image; +use peniko::{Extend, Image}; use super::{ glyph_cache::{CachedRange, GlyphCache, GlyphKey}, @@ -221,11 +221,13 @@ impl Resolver { ResolvedPatch::Ramp { draw_data_offset, ramp_id, + extend, } => { if pos < *draw_data_offset { data.extend_from_slice(&encoding.draw_data[pos..*draw_data_offset]); } - data.extend_from_slice(bytemuck::bytes_of(ramp_id)); + let index_mode = (ramp_id << 2) | *extend as u32; + data.extend_from_slice(bytemuck::bytes_of(&index_mode)); pos = *draw_data_offset + 4; } ResolvedPatch::GlyphRun { .. } => {} @@ -340,11 +342,13 @@ impl Resolver { Patch::Ramp { draw_data_offset, stops, + extend, } => { let ramp_id = self.ramp_cache.add(&encoding.color_stops[stops.clone()]); self.patches.push(ResolvedPatch::Ramp { draw_data_offset: *draw_data_offset + sizes.draw_data, ramp_id, + extend: *extend, }); } Patch::GlyphRun { index } => { @@ -472,6 +476,8 @@ pub enum Patch { draw_data_offset: usize, /// Range of the gradient stops in the resource set. stops: Range, + /// Extend mode for the gradient. + extend: Extend, }, /// Glyph run resource. GlyphRun { @@ -501,6 +507,8 @@ enum ResolvedPatch { draw_data_offset: usize, /// Resolved ramp index. ramp_id: u32, + /// Extend mode for the gradient. + extend: Extend, }, GlyphRun { /// Index of the original glyph run in the encoding. diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 5e4ca8e..e5aff38 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -30,6 +30,7 @@ pub fn test_scenes() -> SceneSet { scene!(funky_paths), scene!(cardioid_and_friends), scene!(animated_text: animated), + scene!(gradient_extend), scene!(brush_transform: animated), scene!(blend_grid), scene!(conflation_artifacts), @@ -249,6 +250,72 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { ); } +fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { + fn square( + sb: &mut SceneBuilder, + is_radial: bool, + transform: Affine, + brush_transform: Option, + extend: Extend, + ) { + let colors = [Color::RED, Color::GREEN, Color::BLUE]; + let width = 400f64; + let height = 400f64; + let gradient: Brush = if is_radial { + Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32) + .with_stops(colors) + .with_extend(extend) + .into() + } else { + Gradient::new_linear((width * 0.35, height * 0.5), (width * 0.65, height * 0.5)) + .with_stops(colors) + .with_extend(extend) + .into() + }; + sb.fill( + Fill::NonZero, + transform, + &gradient, + brush_transform, + &Rect::new(0.0, 0.0, width, height), + ); + } + let extend_modes = [Extend::Pad, Extend::Repeat, Extend::Reflect]; + for x in 0..3 { + let extend = extend_modes[x]; + for y in 0..2 { + let is_radial = y & 1 != 0; + let transform = Affine::translate((x as f64 * 450.0 + 50.0, y as f64 * 450.0 + 100.0)); + square(sb, is_radial, transform, None, extend); + } + } + let white = Color::WHITE.into(); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((50.0, 70.0)), + "Pad", + ); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((500.0, 70.0)), + "Repeat", + ); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((950.0, 70.0)), + "Reflect", + ); +} + fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { const BLEND_MODES: &[Mix] = &[ Mix::Normal, diff --git a/shader/fine.wgsl b/shader/fine.wgsl index ed47bcb..42970e5 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -61,16 +61,20 @@ fn read_color(cmd_ix: u32) -> CmdColor { } fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { - let index = ptcl[cmd_ix + 1u]; + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; let info_offset = ptcl[cmd_ix + 2u]; let line_x = bitcast(info[info_offset]); let line_y = bitcast(info[info_offset + 1u]); let line_c = bitcast(info[info_offset + 2u]); - return CmdLinGrad(index, line_x, line_y, line_c); + return CmdLinGrad(index, extend_mode, line_x, line_y, line_c); } fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { - let index = ptcl[cmd_ix + 1u]; + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; let info_offset = ptcl[cmd_ix + 2u]; let m0 = bitcast(info[info_offset]); let m1 = bitcast(info[info_offset + 1u]); @@ -81,7 +85,7 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { let c1 = vec2(bitcast(info[info_offset + 6u]), bitcast(info[info_offset + 7u])); let ra = bitcast(info[info_offset + 8u]); let roff = bitcast(info[info_offset + 9u]); - return CmdRadGrad(index, matrx, xlat, c1, ra, roff); + return CmdRadGrad(index, extend_mode, matrx, xlat, c1, ra, roff); } fn read_image(cmd_ix: u32) -> CmdImage { @@ -108,6 +112,25 @@ fn read_end_clip(cmd_ix: u32) -> CmdEndClip { return CmdEndClip(blend, alpha); } +fn extend_mode(t: f32, mode: u32) -> f32 { + // This can be replaced with two selects, exchanging the cost + // of a branch for additional ALU + switch mode { + // PAD + case 0u: { + return clamp(t, 0.0, 1.0); + } + // REPEAT + case 1u: { + return fract(t); + } + // REFLECT (2) + default: { + return abs(t - 2.0 * round(0.5 * t)); + } + } +} + #else @group(0) @binding(3) @@ -262,7 +285,7 @@ fn main( let d = lin.line_x * xy.x + lin.line_y * xy.y + lin.line_c; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_d = d + lin.line_x * f32(i); - let x = i32(round(clamp(my_d, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1))); + let x = i32(round(extend_mode(my_d, lin.extend_mode) * f32(GRADIENT_WIDTH - 1))); let fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0); let fg_i = fg_rgba * area[i]; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; @@ -278,11 +301,16 @@ fn main( let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; let ba = dot(xy_xformed, rad.c1); let ca = rad.ra * dot(xy_xformed, xy_xformed); - let t = sqrt(ba * ba + ca) - ba - rad.roff; - let x = i32(round(clamp(t, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + let t0 = sqrt(ba * ba + ca) - ba; + // For radial gradients that generate a cone, reject pixels outside + // the region. + if t0 >= 0.0 { + let t = t0 - rad.roff; + let x = i32(round(extend_mode(t, rad.extend_mode) * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + } } cmd_ix += 3u; } @@ -352,7 +380,7 @@ fn main( let fg = rgba[i]; // Max with a small epsilon to avoid NaNs let a_inv = 1.0 / max(fg.a, 1e-6); - let rgba_sep = vec4(fg.rgb * a_inv, fg.a); + let rgba_sep = vec4(fg.rgb * a_inv, fg.a); textureStore(output, vec2(coords), rgba_sep); } } diff --git a/shader/shared/blend.wgsl b/shader/shared/blend.wgsl index e101c70..e8ec3c7 100644 --- a/shader/shared/blend.wgsl +++ b/shader/shared/blend.wgsl @@ -306,7 +306,8 @@ fn blend_compose( let as_fa = as_ * fa; let ab_fb = ab * fb; let co = as_fa * cs + ab_fb * cb; - return vec4(co, as_fa + ab_fb); + // Modes like COMPOSE_PLUS can generate alpha > 1.0, so clamp. + return vec4(co, min(as_fa + ab_fb, 1.0)); } // Apply color mixing and composition. Both input and output colors are diff --git a/shader/shared/ptcl.wgsl b/shader/shared/ptcl.wgsl index 5d5e528..8137f32 100644 --- a/shader/shared/ptcl.wgsl +++ b/shader/shared/ptcl.wgsl @@ -44,6 +44,7 @@ struct CmdColor { struct CmdLinGrad { index: u32, + extend_mode: u32, line_x: f32, line_y: f32, line_c: f32, @@ -51,6 +52,7 @@ struct CmdLinGrad { struct CmdRadGrad { index: u32, + extend_mode: u32, matrx: vec4, xlat: vec2, c1: vec2, From e9c2ce162358cfdf5b7b0d47f2592bdea1e72a92 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 3 May 2023 14:53:36 -0400 Subject: [PATCH 2/9] resolve merge conflicts --- crates/encoding/src/encoding.rs | 10 +++++++++- crates/encoding/src/resolve.rs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 5232615..b94ac69 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -105,11 +105,13 @@ impl Encoding { Patch::Ramp { draw_data_offset: offset, stops, + extend, } => { let stops = stops.start + stops_base..stops.end + stops_base; Patch::Ramp { draw_data_offset: offset + offsets.draw_data, stops, + extend: *extend, } } Patch::GlyphRun { index } => Patch::GlyphRun { @@ -347,7 +349,12 @@ impl Encoding { } #[cfg(feature = "full")] - fn add_ramp(&mut self, color_stops: impl Iterator, alpha: f32) { + fn add_ramp( + &mut self, + color_stops: impl Iterator, + alpha: f32, + extend: Extend, + ) { let offset = self.draw_data.len(); let stops_start = self.resources.color_stops.len(); if alpha != 1.0 { @@ -360,6 +367,7 @@ impl Encoding { self.resources.patches.push(Patch::Ramp { draw_data_offset: offset, stops: stops_start..self.resources.color_stops.len(), + extend, }); } } diff --git a/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index 37357ef..a1d3eed 100644 --- a/crates/encoding/src/resolve.rs +++ b/crates/encoding/src/resolve.rs @@ -12,7 +12,7 @@ use { image_cache::{ImageCache, Images}, ramp_cache::{RampCache, Ramps}, }, - peniko::Image, + peniko::{Extend, Image}, std::ops::Range, }; From 2db555145eb11acad73b6915bf96a26935c74ea7 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 3 May 2023 15:29:45 -0400 Subject: [PATCH 3/9] clean up test scene code --- examples/scenes/src/test_scenes.rs | 54 ++++++++++-------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index e5aff38..4da8959 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -251,16 +251,10 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { } fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { - fn square( - sb: &mut SceneBuilder, - is_radial: bool, - transform: Affine, - brush_transform: Option, - extend: Extend, - ) { + fn square(sb: &mut SceneBuilder, is_radial: bool, transform: Affine, extend: Extend) { let colors = [Color::RED, Color::GREEN, Color::BLUE]; - let width = 400f64; - let height = 400f64; + let width = 300f64; + let height = 300f64; let gradient: Brush = if is_radial { Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32) .with_stops(colors) @@ -276,7 +270,7 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { Fill::NonZero, transform, &gradient, - brush_transform, + None, &Rect::new(0.0, 0.0, width, height), ); } @@ -285,35 +279,21 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { let extend = extend_modes[x]; for y in 0..2 { let is_radial = y & 1 != 0; - let transform = Affine::translate((x as f64 * 450.0 + 50.0, y as f64 * 450.0 + 100.0)); - square(sb, is_radial, transform, None, extend); + let transform = Affine::translate((x as f64 * 350.0 + 50.0, y as f64 * 350.0 + 100.0)); + square(sb, is_radial, transform, extend); } } - let white = Color::WHITE.into(); - params.text.add( - sb, - None, - 32.0, - Some(&white), - Affine::translate((50.0, 70.0)), - "Pad", - ); - params.text.add( - sb, - None, - 32.0, - Some(&white), - Affine::translate((500.0, 70.0)), - "Repeat", - ); - params.text.add( - sb, - None, - 32.0, - Some(&white), - Affine::translate((950.0, 70.0)), - "Reflect", - ); + for (i, label) in ["Pad", "Repeat", "Reflect"].iter().enumerate() { + let x = i as f64 * 350.0 + 50.0; + params.text.add( + sb, + None, + 32.0, + Some(&Color::WHITE.into()), + Affine::translate((x, 70.0)), + label, + ); + } } fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { From ced6309a3b40798f8ed69c2aa23d2debff8a40ff Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Sat, 6 May 2023 03:27:53 -0400 Subject: [PATCH 4/9] support two point radial with r0 > 0.0 --- examples/scenes/src/test_scenes.rs | 13 +++++++++++++ shader/draw_leaf.wgsl | 14 +++++++++----- shader/fine.wgsl | 11 +++++------ shader/shared/ptcl.wgsl | 2 +- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 4da8959..9c95b3c 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -294,6 +294,19 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { label, ); } + let t = (params.time * 0.5).sin() * 0.5 + 0.5; + let x_delta: f64 = t * 2000.0 - 1000.0; + let gradient = + Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 200.0) + .with_extend(Extend::Reflect) + .with_stops([Color::GREEN, Color::WHITE, Color::RED]); + sb.fill( + Fill::NonZero, + Affine::translate((400.0, 800.0)) * Affine::scale(0.2), + &gradient, + None, + &Rect::new(0.0, 0.0, 2000.0, 2000.0), + ); } fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { diff --git a/shader/draw_leaf.wgsl b/shader/draw_leaf.wgsl index b149d27..beced9c 100644 --- a/shader/draw_leaf.wgsl +++ b/shader/draw_leaf.wgsl @@ -153,13 +153,17 @@ fn main( let r1 = bitcast(scene[dd + 6u]); let inv_det = 1.0 / (matrx.x * matrx.w - matrx.y * matrx.z); let inv_mat = inv_det * vec4(matrx.w, -matrx.y, -matrx.z, matrx.x); + let rr = r1 / (r1 - r0); + var rr1 = rr; + if r0 > 0.0 { + p0 = p0 + (p0 - p1) * rr * 0.5; + rr1 = 1.0; + } let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -translate - p0; let center1 = p1 - p0; - let rr = r1 / (r1 - r0); - let ra_inv = rr / (r1 * r1 - dot(center1, center1)); + let ra_inv = rr1 / (r1 * r1 - dot(center1, center1)); let c1 = center1 * ra_inv; - let ra = rr * ra_inv; - let roff = rr - 1.0; + let ra = rr1 * ra_inv; info[di + 1u] = bitcast(inv_mat.x); info[di + 2u] = bitcast(inv_mat.y); info[di + 3u] = bitcast(inv_mat.z); @@ -169,7 +173,7 @@ fn main( info[di + 7u] = bitcast(c1.x); info[di + 8u] = bitcast(c1.y); info[di + 9u] = bitcast(ra); - info[di + 10u] = bitcast(roff); + info[di + 10u] = bitcast(rr); } // DRAWTAG_FILL_IMAGE case 0x248u: { diff --git a/shader/fine.wgsl b/shader/fine.wgsl index 42970e5..85ed7a4 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -295,18 +295,17 @@ fn main( // CMD_RAD_GRAD case 7u: { let rad = read_rad_grad(cmd_ix); + let rr = rad.rr; + let roff = rr - 1.0; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_xy = vec2(xy.x + f32(i), xy.y); // TODO: can hoist y, but for now stick to the GLSL version let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; let ba = dot(xy_xformed, rad.c1); let ca = rad.ra * dot(xy_xformed, xy_xformed); - let t0 = sqrt(ba * ba + ca) - ba; - // For radial gradients that generate a cone, reject pixels outside - // the region. - if t0 >= 0.0 { - let t = t0 - rad.roff; - let x = i32(round(extend_mode(t, rad.extend_mode) * f32(GRADIENT_WIDTH - 1))); + let t = sqrt(ba * ba + ca) - ba; + if t >= 0.0 { + let x = i32(round(extend_mode(t * rr - roff, rad.extend_mode) * f32(GRADIENT_WIDTH - 1))); let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); let fg_i = fg_rgba * area[i]; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; diff --git a/shader/shared/ptcl.wgsl b/shader/shared/ptcl.wgsl index 8137f32..8e80524 100644 --- a/shader/shared/ptcl.wgsl +++ b/shader/shared/ptcl.wgsl @@ -57,7 +57,7 @@ struct CmdRadGrad { xlat: vec2, c1: vec2, ra: f32, - roff: f32, + rr: f32, } struct CmdImage { From b103a5530178de9857b0e0706d5de936f0fa2b97 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 9 May 2023 18:09:53 -0400 Subject: [PATCH 5/9] rework radial gradients Adds full support for COLRv1 radial gradients based on the two-point conical gradient algorithm at https://skia.org/docs/dev/design/conical/ Also adds robustness to degenerate cases in gradient encoding: * Radial where p0 == p1 && r0 == r1 renders transparent solid * Empty stops render as transparent solid * Single stop renders as solid --- .vscode/settings.json | 3 +- crates/encoding/src/draw.rs | 2 +- crates/encoding/src/encoding.rs | 63 +++++++++--- examples/scenes/src/test_scenes.rs | 44 ++++++++- shader/coarse.wgsl | 2 +- shader/draw_leaf.wgsl | 153 ++++++++++++++++++++--------- shader/fine.wgsl | 56 ++++++++--- shader/pathseg.wgsl | 11 +-- shader/shared/config.wgsl | 9 ++ shader/shared/drawtag.wgsl | 2 +- shader/shared/ptcl.wgsl | 7 +- shader/shared/transform.wgsl | 26 +++++ src/shaders.rs | 1 + 13 files changed, 284 insertions(+), 95 deletions(-) create mode 100644 shader/shared/transform.wgsl diff --git a/.vscode/settings.json b/.vscode/settings.json index 883465a..c7bfa32 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,8 @@ "pathtag": "${workspaceFolder}/shader/shared/pathtag.wgsl", "ptcl": "${workspaceFolder}/shader/shared/ptcl.wgsl", "segment": "${workspaceFolder}/shader/shared/segment.wgsl", - "tile": "${workspaceFolder}/shader/shared/tile.wgsl" + "tile": "${workspaceFolder}/shader/shared/tile.wgsl", + "transform": "${workspaceFolder}/shader/shared/transform.wgsl" }, "wgsl-analyzer.diagnostics.nagaVersion": "main", "wgsl-analyzer.preprocessor.shaderDefs": [ diff --git a/crates/encoding/src/draw.rs b/crates/encoding/src/draw.rs index e26dacd..8e8b1ba 100644 --- a/crates/encoding/src/draw.rs +++ b/crates/encoding/src/draw.rs @@ -22,7 +22,7 @@ impl DrawTag { pub const LINEAR_GRADIENT: Self = Self(0x114); /// Radial gradient fill. - pub const RADIAL_GRADIENT: Self = Self(0x2dc); + pub const RADIAL_GRADIENT: Self = Self(0x29c); /// Image fill. pub const IMAGE: Self = Self(0x248); diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index b94ac69..0b2a13b 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -3,7 +3,7 @@ use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform}; -use peniko::{kurbo::Shape, BlendMode, BrushRef}; +use peniko::{kurbo::Shape, BlendMode, BrushRef, Color}; #[cfg(feature = "full")] use { @@ -281,10 +281,15 @@ impl Encoding { alpha: f32, extend: Extend, ) { - self.add_ramp(color_stops, alpha, extend); - self.draw_tags.push(DrawTag::LINEAR_GRADIENT); - self.draw_data - .extend_from_slice(bytemuck::bytes_of(&gradient)); + match self.add_ramp(color_stops, alpha, extend) { + RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)), + RampStops::One(color) => self.encode_color(DrawColor::new(color)), + _ => { + self.draw_tags.push(DrawTag::LINEAR_GRADIENT); + self.draw_data + .extend_from_slice(bytemuck::bytes_of(&gradient)); + } + } } /// Encodes a radial gradient brush. @@ -296,10 +301,20 @@ impl Encoding { alpha: f32, extend: Extend, ) { - self.add_ramp(color_stops, alpha, extend); - self.draw_tags.push(DrawTag::RADIAL_GRADIENT); - self.draw_data - .extend_from_slice(bytemuck::bytes_of(&gradient)); + // Match Skia's epsilon for radii comparison + const SKIA_EPSILON: f32 = 1.0 / (1 << 12) as f32; + if gradient.p0 == gradient.p1 && (gradient.r0 - gradient.r1).abs() < SKIA_EPSILON { + self.encode_color(DrawColor::new(Color::TRANSPARENT)); + } + match self.add_ramp(color_stops, alpha, extend) { + RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)), + RampStops::One(color) => self.encode_color(DrawColor::new(color)), + _ => { + self.draw_tags.push(DrawTag::RADIAL_GRADIENT); + self.draw_data + .extend_from_slice(bytemuck::bytes_of(&gradient)); + } + } } /// Encodes an image brush. @@ -354,7 +369,7 @@ impl Encoding { color_stops: impl Iterator, alpha: f32, extend: Extend, - ) { + ) -> RampStops { let offset = self.draw_data.len(); let stops_start = self.resources.color_stops.len(); if alpha != 1.0 { @@ -364,14 +379,32 @@ impl Encoding { } else { self.resources.color_stops.extend(color_stops); } - self.resources.patches.push(Patch::Ramp { - draw_data_offset: offset, - stops: stops_start..self.resources.color_stops.len(), - extend, - }); + let stops_end = self.resources.color_stops.len(); + match stops_end - stops_start { + 0 => RampStops::Empty, + 1 => RampStops::One(self.resources.color_stops.pop().unwrap().color), + _ => { + self.resources.patches.push(Patch::Ramp { + draw_data_offset: offset, + stops: stops_start..stops_end, + extend, + }); + RampStops::Many + } + } } } +/// Result for adding a sequence of color stops. +enum RampStops { + /// Color stop sequence was empty. + Empty, + /// Contained a single color stop. + One(Color), + /// More than one color stop. + Many, +} + /// Encoded data for late bound resources. #[cfg(feature = "full")] #[derive(Clone, Default)] diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 9c95b3c..7c48364 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -31,6 +31,7 @@ pub fn test_scenes() -> SceneSet { scene!(cardioid_and_friends), scene!(animated_text: animated), scene!(gradient_extend), + scene!(two_point_radial), scene!(brush_transform: animated), scene!(blend_grid), scene!(conflation_artifacts), @@ -252,11 +253,13 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { fn square(sb: &mut SceneBuilder, is_radial: bool, transform: Affine, extend: Extend) { - let colors = [Color::RED, Color::GREEN, Color::BLUE]; + let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE]; let width = 300f64; let height = 300f64; let gradient: Brush = if is_radial { - Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32) + let center = (width * 0.5, height * 0.5); + let radius = (width * 0.25) as f32; + Gradient::new_two_point_radial(center, radius * 0.25, center, radius) .with_stops(colors) .with_extend(extend) .into() @@ -294,10 +297,45 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { label, ); } +} + +fn two_point_radial(sb: &mut SceneBuilder, params: &mut SceneParams) { + let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE]; + let extend = Extend::Reflect; + let gradient1 = Gradient::new_two_point_radial((150.0, 150.0), 50.0, (200.0, 150.0), 100.0) + .with_extend(extend) + .with_stops(colors); + let gradient2 = Gradient::new_two_point_radial((300.0, 150.0), 100.0, (150.0, 150.0), 50.0) + .with_extend(extend) + .with_stops(colors); + let gradient3 = Gradient::new_two_point_radial((300.0, 150.0), 50.0, (150.0, 150.0), 50.0) + .with_extend(extend) + .with_stops(colors); + sb.fill( + Fill::NonZero, + Affine::scale(1.5) * Affine::translate((50.0, 0.0)), + &gradient1, + None, + &Rect::new(0.0, 0.0, 400.0, 400.0), + ); + sb.fill( + Fill::NonZero, + Affine::scale(1.5) * Affine::translate((50.0, 300.0)), + &gradient2, + None, + &Rect::new(0.0, 0.0, 400.0, 400.0), + ); + sb.fill( + Fill::NonZero, + Affine::scale(1.5) * Affine::translate((50.0, 600.0)), + &gradient3, + None, + &Rect::new(0.0, 0.0, 400.0, 400.0), + ); let t = (params.time * 0.5).sin() * 0.5 + 0.5; let x_delta: f64 = t * 2000.0 - 1000.0; let gradient = - Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 200.0) + Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 0.0) .with_extend(Extend::Reflect) .with_stops([Color::GREEN, Color::WHITE, Color::RED]); sb.fill( diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl index 58fe409..aeb85c2 100644 --- a/shader/coarse.wgsl +++ b/shader/coarse.wgsl @@ -376,7 +376,7 @@ fn main( } } // DRAWTAG_FILL_RAD_GRADIENT - case 0x2dcu: { + case 0x29cu: { let linewidth = bitcast(info_bin_data[di]); if write_path(tile, linewidth) { let index = scene[dd]; diff --git a/shader/draw_leaf.wgsl b/shader/draw_leaf.wgsl index beced9c..ab27619 100644 --- a/shader/draw_leaf.wgsl +++ b/shader/draw_leaf.wgsl @@ -6,6 +6,7 @@ #import clip #import drawtag #import bbox +#import transform @group(0) @binding(0) var config: Config; @@ -30,12 +31,6 @@ var clip_inp: array; let WG_SIZE = 256u; -// Possibly dedup? -struct Transform { - matrx: vec4, - translate: vec2, -} - fn read_transform(transform_base: u32, ix: u32) -> Transform { let base = transform_base + ix * 6u; let c0 = bitcast(scene[base]); @@ -110,18 +105,16 @@ fn main( // let y1 = f32(bbox.y1); // let bbox_f = vec4(x0, y0, x1, y1); let fill_mode = u32(bbox.linewidth >= 0.0); - var matrx: vec4; - var translate: vec2; + var transform = Transform(); var linewidth = bbox.linewidth; if linewidth >= 0.0 || tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE { - let transform = read_transform(config.transform_base, bbox.trans_ix); - matrx = transform.matrx; - translate = transform.translate; + transform = read_transform(config.transform_base, bbox.trans_ix); } if linewidth >= 0.0 { // Note: doesn't deal with anisotropic case + let matrx = transform.matrx; linewidth *= sqrt(abs(matrx.x * matrx.w - matrx.y * matrx.z)); } switch tag_word { @@ -134,8 +127,8 @@ fn main( info[di] = bitcast(linewidth); var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); - p0 = matrx.xy * p0.x + matrx.zw * p0.y + translate; - p1 = matrx.xy * p1.x + matrx.zw * p1.y + translate; + p0 = transform_apply(transform, p0); + p1 = transform_apply(transform, p1); let dxy = p1 - p0; let scale = 1.0 / dot(dxy, dxy); let line_xy = dxy * scale; @@ -145,48 +138,100 @@ fn main( info[di + 3u] = bitcast(line_c); } // DRAWTAG_FILL_RAD_GRADIENT - case 0x2dcu: { + case 0x29cu: { + // Two-point conical gradient implementation based + // on the algorithm at + // This epsilon matches what Skia uses + let GRADIENT_EPSILON = 1.0 / f32(1 << 12u); info[di] = bitcast(linewidth); var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); - let r0 = bitcast(scene[dd + 5u]); - let r1 = bitcast(scene[dd + 6u]); - let inv_det = 1.0 / (matrx.x * matrx.w - matrx.y * matrx.z); - let inv_mat = inv_det * vec4(matrx.w, -matrx.y, -matrx.z, matrx.x); - let rr = r1 / (r1 - r0); - var rr1 = rr; - if r0 > 0.0 { - p0 = p0 + (p0 - p1) * rr * 0.5; - rr1 = 1.0; + var r0 = bitcast(scene[dd + 5u]); + var r1 = bitcast(scene[dd + 6u]); + let user_to_gradient = transform_inverse(transform); + // Output variables + var xform = Transform(); + var focal_x = 0.0; + var radius = 0.0; + var kind = 0u; + var flags = 0u; + if abs(r0 - r1) <= GRADIENT_EPSILON { + // When the radii are the same, emit a strip gradient + kind = RAD_GRAD_KIND_STRIP; + let scaled = r0 / distance(p0, p1); + xform = transform_mul( + two_point_to_unit_line(p0, p1), + user_to_gradient + ); + radius = scaled * scaled; + } else { + // Assume a two point conical gradient unless the centers + // are equal. + kind = RAD_GRAD_KIND_CONE; + if all(p0 == p1) { + kind = RAD_GRAD_KIND_CIRCULAR; + // Nudge p0 a bit to avoid denormals. + p0 += GRADIENT_EPSILON; + } + if r1 == 0.0 { + // If r1 == 0.0, swap the points and radii + flags |= RAD_GRAD_SWAPPED; + let tmp_p = p0; + p0 = p1; + p1 = tmp_p; + let tmp_r = r0; + r0 = r1; + r1 = tmp_r; + } + focal_x = r0 / (r0 - r1); + let one_minus_focal_x = 1.0 - focal_x; + let cf = one_minus_focal_x * p0 + focal_x * p1; + let abs_one_minus_focal_x = abs(one_minus_focal_x); + radius = r1 / (distance(cf, p1)); + let user_to_unit_line = transform_mul( + two_point_to_unit_line(cf, p1), + user_to_gradient + ); + var user_to_scaled = user_to_unit_line; + // When r == 1.0, focal point is on circle + if abs(radius - 1.0) <= GRADIENT_EPSILON { + kind = RAD_GRAD_KIND_FOCAL_ON_CIRCLE; + let scale = 0.5 * abs_one_minus_focal_x; + user_to_scaled = transform_mul( + Transform(vec4(scale, 0.0, 0.0, scale), vec2(0.0)), + user_to_unit_line + ); + } else { + let a = radius * radius - 1.0; + let scale_x = radius / a * abs_one_minus_focal_x; + let scale_y = sqrt(abs(a)) / a * abs_one_minus_focal_x; + user_to_scaled = transform_mul( + Transform(vec4(scale_x, 0.0, 0.0, scale_y), vec2(0.0)), + user_to_unit_line + ); + } + xform = user_to_scaled; } - let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -translate - p0; - let center1 = p1 - p0; - let ra_inv = rr1 / (r1 * r1 - dot(center1, center1)); - let c1 = center1 * ra_inv; - let ra = rr1 * ra_inv; - info[di + 1u] = bitcast(inv_mat.x); - info[di + 2u] = bitcast(inv_mat.y); - info[di + 3u] = bitcast(inv_mat.z); - info[di + 4u] = bitcast(inv_mat.w); - info[di + 5u] = bitcast(inv_tr.x); - info[di + 6u] = bitcast(inv_tr.y); - info[di + 7u] = bitcast(c1.x); - info[di + 8u] = bitcast(c1.y); - info[di + 9u] = bitcast(ra); - info[di + 10u] = bitcast(rr); + info[di + 1u] = bitcast(xform.matrx.x); + info[di + 2u] = bitcast(xform.matrx.y); + info[di + 3u] = bitcast(xform.matrx.z); + info[di + 4u] = bitcast(xform.matrx.w); + info[di + 5u] = bitcast(xform.translate.x); + info[di + 6u] = bitcast(xform.translate.y); + info[di + 7u] = bitcast(focal_x); + info[di + 8u] = bitcast(radius); + info[di + 9u] = bitcast((flags << 3u) | kind); } // DRAWTAG_FILL_IMAGE case 0x248u: { info[di] = bitcast(linewidth); - let inv_det = 1.0 / (matrx.x * matrx.w - matrx.y * matrx.z); - let inv_mat = inv_det * vec4(matrx.w, -matrx.y, -matrx.z, matrx.x); - let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -translate; - info[di + 1u] = bitcast(inv_mat.x); - info[di + 2u] = bitcast(inv_mat.y); - info[di + 3u] = bitcast(inv_mat.z); - info[di + 4u] = bitcast(inv_mat.w); - info[di + 5u] = bitcast(inv_tr.x); - info[di + 6u] = bitcast(inv_tr.y); + let inv = transform_inverse(transform); + info[di + 1u] = bitcast(inv.matrx.x); + info[di + 2u] = bitcast(inv.matrx.y); + info[di + 3u] = bitcast(inv.matrx.z); + info[di + 4u] = bitcast(inv.matrx.w); + info[di + 5u] = bitcast(inv.translate.x); + info[di + 6u] = bitcast(inv.translate.y); info[di + 7u] = scene[dd]; info[di + 8u] = scene[dd + 1u]; } @@ -201,3 +246,17 @@ fn main( clip_inp[m.clip_ix] = ClipInp(ix, i32(path_ix)); } } + +fn two_point_to_unit_line(p0: vec2, p1: vec2) -> Transform { + let tmp1 = from_poly2(p0, p1); + let inv = transform_inverse(tmp1); + let tmp2 = from_poly2(vec2(0.0), vec2(1.0, 0.0)); + return transform_mul(tmp2, inv); +} + +fn from_poly2(p0: vec2, p1: vec2) -> Transform { + return Transform( + vec4(p1.y - p0.y, p0.x - p1.x, p1.x - p0.x, p1.y - p0.y), + vec2(p0.x, p0.y) + ); +} diff --git a/shader/fine.wgsl b/shader/fine.wgsl index 85ed7a4..4047ebd 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -82,10 +82,12 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { let m3 = bitcast(info[info_offset + 3u]); let matrx = vec4(m0, m1, m2, m3); let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); - let c1 = vec2(bitcast(info[info_offset + 6u]), bitcast(info[info_offset + 7u])); - let ra = bitcast(info[info_offset + 8u]); - let roff = bitcast(info[info_offset + 9u]); - return CmdRadGrad(index, extend_mode, matrx, xlat, c1, ra, roff); + let focal_x = bitcast(info[info_offset + 6u]); + let radius = bitcast(info[info_offset + 7u]); + let flags_kind = info[info_offset + 8u]; + let flags = flags_kind >> 3u; + let kind = flags_kind & 0x7u; + return CmdRadGrad(index, extend_mode, matrx, xlat, focal_x, radius, kind, flags); } fn read_image(cmd_ix: u32) -> CmdImage { @@ -295,17 +297,45 @@ fn main( // CMD_RAD_GRAD case 7u: { let rad = read_rad_grad(cmd_ix); - let rr = rad.rr; - let roff = rr - 1.0; + let focal_x = rad.focal_x; + let one_minus_focal_x = 1.0 - focal_x; + let radius = rad.radius; + let is_strip = rad.kind == RAD_GRAD_KIND_STRIP; + let is_circular = rad.kind == RAD_GRAD_KIND_CIRCULAR; + let is_focal_on_circle = rad.kind == RAD_GRAD_KIND_FOCAL_ON_CIRCLE; + let is_swapped = (rad.flags & RAD_GRAD_SWAPPED) != 0u; + let inv_r1 = select(1.0 / radius, 0.0, is_circular); + let root_f = select(1.0, -1.0, is_swapped || one_minus_focal_x < 0.0); + let t_base_scale = select(vec2(0.0, -1.0), vec2(1.0, 1.0), is_swapped); + let t_sign = sign(one_minus_focal_x); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_xy = vec2(xy.x + f32(i), xy.y); - // TODO: can hoist y, but for now stick to the GLSL version - let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; - let ba = dot(xy_xformed, rad.c1); - let ca = rad.ra * dot(xy_xformed, xy_xformed); - let t = sqrt(ba * ba + ca) - ba; - if t >= 0.0 { - let x = i32(round(extend_mode(t * rr - roff, rad.extend_mode) * f32(GRADIENT_WIDTH - 1))); + let local_xy = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; + let x = local_xy.x; + let y = local_xy.y; + let xx = x * x; + let yy = y * y; + let x_inv_r1 = x * inv_r1; + var t = 0.0; + var valid = true; + if is_strip { + let a = radius - yy; + t = sqrt(a) + x; + valid = a >= 0.0; + } else if is_focal_on_circle { + t = (xx + yy) / x; + valid = t >= 0.0; + } else if radius > 1.0 { + t = sqrt(xx + yy) - x_inv_r1; + } else { + let a = xx - yy; + t = root_f * sqrt(a) - x_inv_r1; + valid = a >= 0.0 && t >= 0.0; + } + if valid { + t = extend_mode(focal_x + t_sign * t, rad.extend_mode); + t = (t_base_scale.x - t) * t_base_scale.y; + let x = i32(round(t * f32(GRADIENT_WIDTH - 1))); let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); let fg_i = fg_rgba * area[i]; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; diff --git a/shader/pathseg.wgsl b/shader/pathseg.wgsl index ec059ab..bf02419 100644 --- a/shader/pathseg.wgsl +++ b/shader/pathseg.wgsl @@ -14,6 +14,7 @@ #import config #import pathtag #import cubic +#import transform @group(0) @binding(0) var config: Config; @@ -36,7 +37,6 @@ struct AtomicPathBbox { @group(0) @binding(3) var path_bboxes: array; - @group(0) @binding(4) var cubics: array; @@ -85,11 +85,6 @@ fn read_i16_point(ix: u32) -> vec2 { return vec2(x, y); } -struct Transform { - matrx: vec4, - translate: vec2, -} - fn read_transform(transform_base: u32, ix: u32) -> Transform { let base = transform_base + ix * 6u; let c0 = bitcast(scene[base]); @@ -103,10 +98,6 @@ fn read_transform(transform_base: u32, ix: u32) -> Transform { return Transform(matrx, translate); } -fn transform_apply(transform: Transform, p: vec2) -> vec2 { - return transform.matrx.xy * p.x + transform.matrx.zw * p.y + transform.translate; -} - fn round_down(x: f32) -> i32 { return i32(floor(x)); } diff --git a/shader/shared/config.wgsl b/shader/shared/config.wgsl index 1063134..9b1cd2e 100644 --- a/shader/shared/config.wgsl +++ b/shader/shared/config.wgsl @@ -49,3 +49,12 @@ let N_TILE_Y = 16u; let N_TILE = 256u; let BLEND_STACK_SPLIT = 4u; + +// Radial gradient kinds +let RAD_GRAD_KIND_CIRCULAR = 1u; +let RAD_GRAD_KIND_STRIP = 2u; +let RAD_GRAD_KIND_FOCAL_ON_CIRCLE = 3u; +let RAD_GRAD_KIND_CONE = 4u; + +// Radial gradient flags +let RAD_GRAD_SWAPPED = 1u; diff --git a/shader/shared/drawtag.wgsl b/shader/shared/drawtag.wgsl index 0d532d1..0b8ae41 100644 --- a/shader/shared/drawtag.wgsl +++ b/shader/shared/drawtag.wgsl @@ -18,7 +18,7 @@ struct DrawMonoid { let DRAWTAG_NOP = 0u; let DRAWTAG_FILL_COLOR = 0x44u; let DRAWTAG_FILL_LIN_GRADIENT = 0x114u; -let DRAWTAG_FILL_RAD_GRADIENT = 0x2dcu; +let DRAWTAG_FILL_RAD_GRADIENT = 0x29cu; let DRAWTAG_FILL_IMAGE = 0x248u; let DRAWTAG_BEGIN_CLIP = 0x9u; let DRAWTAG_END_CLIP = 0x21u; diff --git a/shader/shared/ptcl.wgsl b/shader/shared/ptcl.wgsl index 8e80524..d5fc5d4 100644 --- a/shader/shared/ptcl.wgsl +++ b/shader/shared/ptcl.wgsl @@ -55,9 +55,10 @@ struct CmdRadGrad { extend_mode: u32, matrx: vec4, xlat: vec2, - c1: vec2, - ra: f32, - rr: f32, + focal_x: f32, + radius: f32, + kind: u32, + flags: u32, } struct CmdImage { diff --git a/shader/shared/transform.wgsl b/shader/shared/transform.wgsl new file mode 100644 index 0000000..00d7279 --- /dev/null +++ b/shader/shared/transform.wgsl @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Helpers for working with transforms. + +struct Transform { + matrx: vec4, + translate: vec2, +} + +fn transform_apply(transform: Transform, p: vec2) -> vec2 { + return transform.matrx.xy * p.x + transform.matrx.zw * p.y + transform.translate; +} + +fn transform_inverse(transform: Transform) -> Transform { + let inv_det = 1.0 / (transform.matrx.x * transform.matrx.w - transform.matrx.y * transform.matrx.z); + let inv_mat = inv_det * vec4(transform.matrx.w, -transform.matrx.y, -transform.matrx.z, transform.matrx.x); + let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -transform.translate; + return Transform(inv_mat, inv_tr); +} + +fn transform_mul(a: Transform, b: Transform) -> Transform { + return Transform( + a.matrx.xyxy * b.matrx.xxzz + a.matrx.zwzw * b.matrx.yyww, + a.matrx.xy * b.translate.x + a.matrx.zw * b.translate.y + a.translate + ); +} diff --git a/src/shaders.rs b/src/shaders.rs index d1cc0dc..8da1807 100644 --- a/src/shaders.rs +++ b/src/shaders.rs @@ -318,4 +318,5 @@ const SHARED_SHADERS: &[(&str, &str)] = &[ shared_shader!("ptcl"), shared_shader!("segment"), shared_shader!("tile"), + shared_shader!("transform"), ]; From 5e1188f9687173d8039598b8043e0986b1f027d9 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 11 May 2023 12:37:36 -0400 Subject: [PATCH 6/9] replace branches with chained selects This exchanges the per-pixel branching with additional ALU + selects. My expectation is that this will be faster, but that may be hardware/driver dependent and likely requires profiling and examination of generated code. The original code is kept in a comment with notes to explain the more obfuscated select version. --- shader/fine.wgsl | 125 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/shader/fine.wgsl b/shader/fine.wgsl index 4047ebd..bab1b63 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -115,22 +115,33 @@ fn read_end_clip(cmd_ix: u32) -> CmdEndClip { } fn extend_mode(t: f32, mode: u32) -> f32 { - // This can be replaced with two selects, exchanging the cost - // of a branch for additional ALU - switch mode { - // PAD - case 0u: { - return clamp(t, 0.0, 1.0); - } - // REPEAT - case 1u: { - return fract(t); - } - // REFLECT (2) - default: { - return abs(t - 2.0 * round(0.5 * t)); - } - } + let EXTEND_PAD = 0u; + let EXTEND_REPEAT = 1u; + let EXTEND_REFLECT = 2u; + // Branching version of the code below: + // + // switch mode { + // // EXTEND_PAD + // case 0u: { + // return clamp(t, 0.0, 1.0); + // } + // // EXTEND_REPEAT + // case 1u: { + // return fract(t); + // } + // // EXTEND_REFLECT + // default: { + // return abs(t - 2.0 * round(0.5 * t)); + // } + // } + let pad = clamp(t, 0.0, 1.0); + let repeat = fract(t); + let reflect = abs(t - 2.0 * round(0.5 * t)); + return select( + select(pad, repeat, mode == EXTEND_REPEAT), + reflect, + mode == EXTEND_REFLECT + ); } #else @@ -304,9 +315,9 @@ fn main( let is_circular = rad.kind == RAD_GRAD_KIND_CIRCULAR; let is_focal_on_circle = rad.kind == RAD_GRAD_KIND_FOCAL_ON_CIRCLE; let is_swapped = (rad.flags & RAD_GRAD_SWAPPED) != 0u; + let is_greater = radius > 1.0; let inv_r1 = select(1.0 / radius, 0.0, is_circular); - let root_f = select(1.0, -1.0, is_swapped || one_minus_focal_x < 0.0); - let t_base_scale = select(vec2(0.0, -1.0), vec2(1.0, 1.0), is_swapped); + let less_scale = select(1.0, -1.0, is_swapped || one_minus_focal_x < 0.0); let t_sign = sign(one_minus_focal_x); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_xy = vec2(xy.x + f32(i), xy.y); @@ -316,25 +327,67 @@ fn main( let xx = x * x; let yy = y * y; let x_inv_r1 = x * inv_r1; - var t = 0.0; - var valid = true; - if is_strip { - let a = radius - yy; - t = sqrt(a) + x; - valid = a >= 0.0; - } else if is_focal_on_circle { - t = (xx + yy) / x; - valid = t >= 0.0; - } else if radius > 1.0 { - t = sqrt(xx + yy) - x_inv_r1; - } else { - let a = xx - yy; - t = root_f * sqrt(a) - x_inv_r1; - valid = a >= 0.0 && t >= 0.0; - } - if valid { + // This is the branching version of the code implemented + // by the chained selects below: + // + // var t = 0.0; + // var is_valid = true; + // if is_strip { + // let a = radius - yy; + // t = sqrt(a) + x; + // is_valid = a >= 0.0; + // } else if is_focal_on_circle { + // t = (xx + yy) / x; + // is_valid = t >= 0.0; + // } else if radius > 1.0 { + // t = sqrt(xx + yy) - x_inv_r1; + // } else { + // let a = xx - yy; + // t = root_f * sqrt(a) - x_inv_r1; + // is_valid = a >= 0.0 && t >= 0.0; + // } + // + // The pattern is that these can all be computed with + // the expression: a * sqrt(b) + c + // + // The parameters to the expression are computed up front + // and chosen with chained selects based on their + // respective conditions. The same process is done + // for determining the validity of the resulting value. + var strip_params = vec3(1.0, radius - yy, x); + var foc_params = vec3(1.0, 0.0, (xx + yy) / x); + var greater_params = vec3(1.0, xx + yy, -x_inv_r1); + var less_params = vec3(less_scale, xx - yy, -x_inv_r1); + var params = select( + select( + select( + less_params, + greater_params, + is_greater, + ), + foc_params, + is_focal_on_circle, + ), + strip_params, + is_strip, + ); + var t = params.x * sqrt(params.y) + params.z; + let is_valid = select( + select( + select( + params.y >= 0.0 && t >= 0.0, + true, + is_greater + ), + t >= 0.0 && x != 0.0, + is_focal_on_circle, + ), + params.y >= 0.0, + is_strip, + ); + if is_valid { t = extend_mode(focal_x + t_sign * t, rad.extend_mode); - t = (t_base_scale.x - t) * t_base_scale.y; + t = select(t, 1.0 - t, is_swapped); let x = i32(round(t * f32(GRADIENT_WIDTH - 1))); let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); let fg_i = fg_rgba * area[i]; From 58c7df469dba9fbd102706984a2f38c477dfac51 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 15 May 2023 14:45:38 -0400 Subject: [PATCH 7/9] Address review feedback * replace one_minus_focal_x and abs_one_minus_focal_x variables with the actual expressions * replace division by r^2-1 with multiplication by reciprocal * revert chain selects to branchy code for clarity. Branching is dynamically uniform so shouldn't affect performance * add suggested comment describing gradient kind/flags constants --- shader/draw_leaf.wgsl | 11 ++-- shader/fine.wgsl | 121 +++++++++++--------------------------- shader/shared/config.wgsl | 4 ++ 3 files changed, 42 insertions(+), 94 deletions(-) diff --git a/shader/draw_leaf.wgsl b/shader/draw_leaf.wgsl index ab27619..9845fdd 100644 --- a/shader/draw_leaf.wgsl +++ b/shader/draw_leaf.wgsl @@ -184,9 +184,7 @@ fn main( r1 = tmp_r; } focal_x = r0 / (r0 - r1); - let one_minus_focal_x = 1.0 - focal_x; - let cf = one_minus_focal_x * p0 + focal_x * p1; - let abs_one_minus_focal_x = abs(one_minus_focal_x); + let cf = (1.0 - focal_x) * p0 + focal_x * p1; radius = r1 / (distance(cf, p1)); let user_to_unit_line = transform_mul( two_point_to_unit_line(cf, p1), @@ -196,15 +194,16 @@ fn main( // When r == 1.0, focal point is on circle if abs(radius - 1.0) <= GRADIENT_EPSILON { kind = RAD_GRAD_KIND_FOCAL_ON_CIRCLE; - let scale = 0.5 * abs_one_minus_focal_x; + let scale = 0.5 * abs(1.0 - focal_x); user_to_scaled = transform_mul( Transform(vec4(scale, 0.0, 0.0, scale), vec2(0.0)), user_to_unit_line ); } else { let a = radius * radius - 1.0; - let scale_x = radius / a * abs_one_minus_focal_x; - let scale_y = sqrt(abs(a)) / a * abs_one_minus_focal_x; + let a_recip = 1.0 / a; + let scale_x = radius * a_recip * abs(1.0 - focal_x); + let scale_y = sqrt(abs(a)) * a_recip * abs(1.0 - focal_x); user_to_scaled = transform_mul( Transform(vec4(scale_x, 0.0, 0.0, scale_y), vec2(0.0)), user_to_unit_line diff --git a/shader/fine.wgsl b/shader/fine.wgsl index bab1b63..e6ddb2c 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -118,30 +118,20 @@ fn extend_mode(t: f32, mode: u32) -> f32 { let EXTEND_PAD = 0u; let EXTEND_REPEAT = 1u; let EXTEND_REFLECT = 2u; - // Branching version of the code below: - // - // switch mode { - // // EXTEND_PAD - // case 0u: { - // return clamp(t, 0.0, 1.0); - // } - // // EXTEND_REPEAT - // case 1u: { - // return fract(t); - // } - // // EXTEND_REFLECT - // default: { - // return abs(t - 2.0 * round(0.5 * t)); - // } - // } - let pad = clamp(t, 0.0, 1.0); - let repeat = fract(t); - let reflect = abs(t - 2.0 * round(0.5 * t)); - return select( - select(pad, repeat, mode == EXTEND_REPEAT), - reflect, - mode == EXTEND_REFLECT - ); + switch mode { + // EXTEND_PAD + case 0u: { + return clamp(t, 0.0, 1.0); + } + // EXTEND_REPEAT + case 1u: { + return fract(t); + } + // EXTEND_REFLECT + default: { + return abs(t - 2.0 * round(0.5 * t)); + } + } } #else @@ -309,16 +299,14 @@ fn main( case 7u: { let rad = read_rad_grad(cmd_ix); let focal_x = rad.focal_x; - let one_minus_focal_x = 1.0 - focal_x; let radius = rad.radius; let is_strip = rad.kind == RAD_GRAD_KIND_STRIP; let is_circular = rad.kind == RAD_GRAD_KIND_CIRCULAR; let is_focal_on_circle = rad.kind == RAD_GRAD_KIND_FOCAL_ON_CIRCLE; let is_swapped = (rad.flags & RAD_GRAD_SWAPPED) != 0u; - let is_greater = radius > 1.0; - let inv_r1 = select(1.0 / radius, 0.0, is_circular); - let less_scale = select(1.0, -1.0, is_swapped || one_minus_focal_x < 0.0); - let t_sign = sign(one_minus_focal_x); + let r1_recip = select(1.0 / radius, 0.0, is_circular); + let less_scale = select(1.0, -1.0, is_swapped || (1.0 - focal_x) < 0.0); + let t_sign = sign(1.0 - focal_x); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_xy = vec2(xy.x + f32(i), xy.y); let local_xy = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; @@ -326,65 +314,22 @@ fn main( let y = local_xy.y; let xx = x * x; let yy = y * y; - let x_inv_r1 = x * inv_r1; - // This is the branching version of the code implemented - // by the chained selects below: - // - // var t = 0.0; - // var is_valid = true; - // if is_strip { - // let a = radius - yy; - // t = sqrt(a) + x; - // is_valid = a >= 0.0; - // } else if is_focal_on_circle { - // t = (xx + yy) / x; - // is_valid = t >= 0.0; - // } else if radius > 1.0 { - // t = sqrt(xx + yy) - x_inv_r1; - // } else { - // let a = xx - yy; - // t = root_f * sqrt(a) - x_inv_r1; - // is_valid = a >= 0.0 && t >= 0.0; - // } - // - // The pattern is that these can all be computed with - // the expression: a * sqrt(b) + c - // - // The parameters to the expression are computed up front - // and chosen with chained selects based on their - // respective conditions. The same process is done - // for determining the validity of the resulting value. - var strip_params = vec3(1.0, radius - yy, x); - var foc_params = vec3(1.0, 0.0, (xx + yy) / x); - var greater_params = vec3(1.0, xx + yy, -x_inv_r1); - var less_params = vec3(less_scale, xx - yy, -x_inv_r1); - var params = select( - select( - select( - less_params, - greater_params, - is_greater, - ), - foc_params, - is_focal_on_circle, - ), - strip_params, - is_strip, - ); - var t = params.x * sqrt(params.y) + params.z; - let is_valid = select( - select( - select( - params.y >= 0.0 && t >= 0.0, - true, - is_greater - ), - t >= 0.0 && x != 0.0, - is_focal_on_circle, - ), - params.y >= 0.0, - is_strip, - ); + var t = 0.0; + var is_valid = true; + if is_strip { + let a = radius - yy; + t = sqrt(a) + x; + is_valid = a >= 0.0; + } else if is_focal_on_circle { + t = (xx + yy) / x; + is_valid = t >= 0.0 && x != 0.0; + } else if radius > 1.0 { + t = sqrt(xx + yy) - x * r1_recip; + } else { // radius < 1.0 + let a = xx - yy; + t = less_scale * sqrt(a) - x * r1_recip; + is_valid = a >= 0.0 && t >= 0.0; + } if is_valid { t = extend_mode(focal_x + t_sign * t, rad.extend_mode); t = select(t, 1.0 - t, is_swapped); diff --git a/shader/shared/config.wgsl b/shader/shared/config.wgsl index 9b1cd2e..4ac718c 100644 --- a/shader/shared/config.wgsl +++ b/shader/shared/config.wgsl @@ -50,6 +50,10 @@ let N_TILE = 256u; let BLEND_STACK_SPLIT = 4u; +// The following are computed in draw_leaf from the generic gradient parameters +// encoded in the scene, and stored in the gradient's info struct, for +// consumption during fine rasterization. + // Radial gradient kinds let RAD_GRAD_KIND_CIRCULAR = 1u; let RAD_GRAD_KIND_STRIP = 2u; From 7b68630d6a8ce672aa38dbd2c3a3c677d0748458 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 15 May 2023 14:54:44 -0400 Subject: [PATCH 8/9] refactor common scale ratio code --- shader/draw_leaf.wgsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shader/draw_leaf.wgsl b/shader/draw_leaf.wgsl index 9845fdd..59e9e97 100644 --- a/shader/draw_leaf.wgsl +++ b/shader/draw_leaf.wgsl @@ -201,9 +201,9 @@ fn main( ); } else { let a = radius * radius - 1.0; - let a_recip = 1.0 / a; - let scale_x = radius * a_recip * abs(1.0 - focal_x); - let scale_y = sqrt(abs(a)) * a_recip * abs(1.0 - focal_x); + let scale_ratio = abs(1.0 - focal_x) / a; + let scale_x = radius * scale_ratio; + let scale_y = sqrt(abs(a)) * scale_ratio; user_to_scaled = transform_mul( Transform(vec4(scale_x, 0.0, 0.0, scale_y), vec2(0.0)), user_to_unit_line From 780cff09dbc103b6a850a979858d03d6ef17753f Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 16 May 2023 02:35:20 -0400 Subject: [PATCH 9/9] add examples from COLRv1 spec --- examples/scenes/src/test_scenes.rs | 208 ++++++++++++++++++++++------- 1 file changed, 162 insertions(+), 46 deletions(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 7c48364..6696f1c 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -299,52 +299,168 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { } } -fn two_point_radial(sb: &mut SceneBuilder, params: &mut SceneParams) { - let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE]; - let extend = Extend::Reflect; - let gradient1 = Gradient::new_two_point_radial((150.0, 150.0), 50.0, (200.0, 150.0), 100.0) - .with_extend(extend) - .with_stops(colors); - let gradient2 = Gradient::new_two_point_radial((300.0, 150.0), 100.0, (150.0, 150.0), 50.0) - .with_extend(extend) - .with_stops(colors); - let gradient3 = Gradient::new_two_point_radial((300.0, 150.0), 50.0, (150.0, 150.0), 50.0) - .with_extend(extend) - .with_stops(colors); - sb.fill( - Fill::NonZero, - Affine::scale(1.5) * Affine::translate((50.0, 0.0)), - &gradient1, - None, - &Rect::new(0.0, 0.0, 400.0, 400.0), - ); - sb.fill( - Fill::NonZero, - Affine::scale(1.5) * Affine::translate((50.0, 300.0)), - &gradient2, - None, - &Rect::new(0.0, 0.0, 400.0, 400.0), - ); - sb.fill( - Fill::NonZero, - Affine::scale(1.5) * Affine::translate((50.0, 600.0)), - &gradient3, - None, - &Rect::new(0.0, 0.0, 400.0, 400.0), - ); - let t = (params.time * 0.5).sin() * 0.5 + 0.5; - let x_delta: f64 = t * 2000.0 - 1000.0; - let gradient = - Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 0.0) - .with_extend(Extend::Reflect) - .with_stops([Color::GREEN, Color::WHITE, Color::RED]); - sb.fill( - Fill::NonZero, - Affine::translate((400.0, 800.0)) * Affine::scale(0.2), - &gradient, - None, - &Rect::new(0.0, 0.0, 2000.0, 2000.0), - ); +fn two_point_radial(sb: &mut SceneBuilder, _params: &mut SceneParams) { + fn make( + sb: &mut SceneBuilder, + x0: f64, + y0: f64, + r0: f32, + x1: f64, + y1: f64, + r1: f32, + transform: Affine, + extend: Extend, + ) { + let colors = [Color::RED, Color::YELLOW, Color::rgb8(6, 85, 186)]; + let width = 400f64; + let height = 200f64; + let rect = Rect::new(0.0, 0.0, width, height); + sb.fill(Fill::NonZero, transform, Color::WHITE, None, &rect); + sb.fill( + Fill::NonZero, + transform, + &Gradient::new_two_point_radial((x0, y0), r0, (x1, y1), r1) + .with_stops(colors) + .with_extend(extend), + None, + &Rect::new(0.0, 0.0, width, height), + ); + let r0 = r0 as f64 - 1.0; + let r1 = r1 as f64 - 1.0; + let stroke_width = 1.0; + sb.stroke( + &Stroke::new(stroke_width), + transform, + Color::BLACK, + None, + &Ellipse::new((x0, y0), (r0, r0), 0.0), + ); + sb.stroke( + &Stroke::new(stroke_width), + transform, + Color::BLACK, + None, + &Ellipse::new((x1, y1), (r1, r1), 0.0), + ); + } + + // These demonstrate radial gradient patterns similar to the examples shown + // at + + for (i, mode) in [Extend::Pad, Extend::Repeat, Extend::Reflect] + .iter() + .enumerate() + { + let y = 100.0; + let x0 = 140.0; + let x1 = x0 + 140.0; + let r0 = 20.0; + let r1 = 50.0; + make( + sb, + x0, + y, + r0, + x1, + y, + r1, + Affine::translate((i as f64 * 420.0 + 20.0, 20.0)), + *mode, + ); + } + + for (i, mode) in [Extend::Pad, Extend::Repeat, Extend::Reflect] + .iter() + .enumerate() + { + let y = 100.0; + let x0 = 140.0; + let x1 = x0 + 140.0; + let r0 = 20.0; + let r1 = 50.0; + make( + sb, + x1, + y, + r1, + x0, + y, + r0, + Affine::translate((i as f64 * 420.0 + 20.0, 240.0)), + *mode, + ); + } + + for (i, mode) in [Extend::Pad, Extend::Repeat, Extend::Reflect] + .iter() + .enumerate() + { + let y = 100.0; + let x0 = 140.0; + let x1 = x0 + 140.0; + let r0 = 50.0; + let r1 = 50.0; + make( + sb, + x0, + y, + r0, + x1, + y, + r1, + Affine::translate((i as f64 * 420.0 + 20.0, 460.0)), + *mode, + ); + } + + for (i, mode) in [Extend::Pad, Extend::Repeat, Extend::Reflect] + .iter() + .enumerate() + { + let x0 = 140.0; + let y0 = 125.0; + let r0 = 20.0; + let x1 = 190.0; + let y1 = 100.0; + let r1 = 95.0; + make( + sb, + x0, + y0, + r0, + x1, + y1, + r1, + Affine::translate((i as f64 * 420.0 + 20.0, 680.0)), + *mode, + ); + } + + for (i, mode) in [Extend::Pad, Extend::Repeat, Extend::Reflect] + .iter() + .enumerate() + { + let x0 = 140.0; + let y0 = 125.0; + let r0 = 20.0; + let x1 = 190.0; + let y1 = 100.0; + let r1 = 96.0; + // Shift p0 so the outer edges of both circles touch + let p0 = Point::new(x1, y1) + + ((Point::new(x0, y0) - Point::new(x1, y1)).normalize() * (r1 - r0)); + make( + sb, + p0.x, + p0.y, + r0 as f32, + x1, + y1, + r1 as f32, + Affine::translate((i as f64 * 420.0 + 20.0, 900.0)), + *mode, + ); + } } fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) {