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 9834e64..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 { @@ -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 { @@ -277,12 +279,17 @@ impl Encoding { gradient: DrawLinearGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); - 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. @@ -292,12 +299,22 @@ impl Encoding { gradient: DrawRadialGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); - 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. @@ -347,7 +364,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, + ) -> RampStops { let offset = self.draw_data.len(); let stops_start = self.resources.color_stops.len(); if alpha != 1.0 { @@ -357,13 +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(), - }); + 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/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index ee95628..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, }; @@ -279,11 +279,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 { .. } => {} @@ -399,11 +401,13 @@ impl Resolver { Patch::Ramp { draw_data_offset, stops, + extend, } => { let ramp_id = self.ramp_cache.add(&resources.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 } => { @@ -532,6 +536,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 { @@ -563,6 +569,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..6696f1c 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -30,6 +30,8 @@ pub fn test_scenes() -> SceneSet { scene!(funky_paths), 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), @@ -249,6 +251,218 @@ 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::rgb8(0, 255, 0), Color::BLUE]; + let width = 300f64; + let height = 300f64; + let gradient: Brush = if is_radial { + 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() + } 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, + None, + &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 * 350.0 + 50.0, y as f64 * 350.0 + 100.0)); + square(sb, is_radial, transform, extend); + } + } + 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 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) { const BLEND_MODES: &[Mix] = &[ Mix::Normal, 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 b149d27..59e9e97 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,44 +138,99 @@ 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 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 c1 = center1 * ra_inv; - let ra = rr * ra_inv; - let roff = rr - 1.0; - 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(roff); + 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 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), + 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(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_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 + ); + } + xform = user_to_scaled; + } + 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]; } @@ -197,3 +245,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 ed47bcb..e6ddb2c 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]); @@ -78,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, 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 { @@ -108,6 +114,26 @@ fn read_end_clip(cmd_ix: u32) -> CmdEndClip { return CmdEndClip(blend, alpha); } +fn extend_mode(t: f32, mode: u32) -> f32 { + let EXTEND_PAD = 0u; + let EXTEND_REPEAT = 1u; + let EXTEND_REFLECT = 2u; + 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 @group(0) @binding(3) @@ -262,7 +288,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; @@ -272,17 +298,46 @@ fn main( // CMD_RAD_GRAD case 7u: { let rad = read_rad_grad(cmd_ix); + let focal_x = rad.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 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); - // 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 - 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 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; + 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); + 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; + } } cmd_ix += 3u; } @@ -352,7 +407,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/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/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/config.wgsl b/shader/shared/config.wgsl index 1063134..4ac718c 100644 --- a/shader/shared/config.wgsl +++ b/shader/shared/config.wgsl @@ -49,3 +49,16 @@ let N_TILE_Y = 16u; 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; +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 5d5e528..d5fc5d4 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,11 +52,13 @@ struct CmdLinGrad { struct CmdRadGrad { index: u32, + extend_mode: u32, matrx: vec4, xlat: vec2, - c1: vec2, - ra: f32, - roff: 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"), ];