From db53ff4b45ad9ecb17c700a883e74d67543902bb Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Wed, 18 Oct 2023 10:07:19 -0400 Subject: [PATCH 1/5] [encoding] Added a f32->f16 conversion utility This will be used to pack the f64 miter_limit field in `kurbo::Stroke` into a 16-bit encoding for GPU consumption. --- crates/encoding/src/math.rs | 97 +++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/encoding/src/math.rs b/crates/encoding/src/math.rs index 5873fba9a..822f43424 100644 --- a/crates/encoding/src/math.rs +++ b/crates/encoding/src/math.rs @@ -76,3 +76,100 @@ impl Mul for Transform { pub fn point_to_f32(point: kurbo::Point) -> [f32; 2] { [point.x as f32, point.y as f32] } + +/// Converts an f32 to IEEE-754 binary16 format represented as the bits of a u16. +/// This implementation was adapted from Fabian Giesen's float_to_half_fast3() +/// function which can be found at https://gist.github.com/rygorous/2156668#file-gistfile1-cpp-L285 +/// +/// TODO: We should consider adopting https://crates.io/crates/half as a dependency since it nicely +/// wraps native ARM and x86 instructions for floating-point conversion. +#[allow(unused)] // for now +pub(crate) fn f32_to_f16(val: f32) -> u16 { + const INF_32: u32 = 255 << 23; + const INF_16: u32 = 31 << 23; + const MAGIC: u32 = 15 << 23; + const SIGN_MASK: u32 = 0x8000_0000u32; + const ROUND_MASK: u32 = !0xFFFu32; + + let u = val.to_bits(); + let sign = u & SIGN_MASK; + let u = u ^ sign; + + // NOTE all the integer compares in this function can be safely + // compiled into signed compares since all operands are below + // 0x80000000. Important if you want fast straight SSE2 code + // (since there's no unsigned PCMPGTD). + + // Inf or NaN (all exponent bits set) + let output: u16 = if u >= INF_32 { + // NaN -> qNaN and Inf->Inf + if u > INF_32 { + 0x7E00 + } else { + 0x7C00 + } + } else { + // (De)normalized number or zero + let mut u = u & ROUND_MASK; + u = (f32::from_bits(u) * f32::from_bits(MAGIC)).to_bits(); + u = u.overflowing_sub(ROUND_MASK).0; + + // Clamp to signed infinity if exponent overflowed + if u > INF_16 { + u = INF_16; + } + (u >> 13) as u16 // Take the bits! + }; + output | (sign >> 16) as u16 +} + +#[cfg(test)] +mod tests { + use super::f32_to_f16; + + #[test] + fn test_f32_to_f16_simple() { + let input: f32 = std::f32::consts::PI; + let output: u16 = f32_to_f16(input); + assert_eq!(0x4248u16, output) // 3.141 + } + + #[test] + fn test_f32_to_f16_nan_overflow() { + // A signaling NaN with unset high bits but a low bit that could get accidentally masked + // should get converted to a quiet NaN and not infinity. + let input: f32 = f32::from_bits(0x7F800001u32); + assert!(input.is_nan()); + let output: u16 = f32_to_f16(input); + assert_eq!(0x7E00, output); + } + + #[test] + fn test_f32_to_16_inf() { + let input: f32 = f32::from_bits(0x7F800000u32); + assert!(input.is_infinite()); + let output: u16 = f32_to_f16(input); + assert_eq!(0x7C00, output); + } + + #[test] + fn test_f32_to_16_exponent_rebias() { + let input: f32 = 0.00003051758; + let output: u16 = f32_to_f16(input); + assert_eq!(0x0200, output); // 0.00003052 + } + + #[test] + fn test_f32_to_16_exponent_overflow() { + let input: f32 = 1.701412e38; + let output: u16 = f32_to_f16(input); + assert_eq!(0x7C00, output); // +inf + } + + #[test] + fn test_f32_to_16_exponent_overflow_neg_inf() { + let input: f32 = -1.701412e38; + let output: u16 = f32_to_f16(input); + assert_eq!(0xFC00, output); // -inf + } +} From 2896cd49b224665db0fe689b19481cdc006c5f16 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Fri, 20 Oct 2023 11:48:12 -0400 Subject: [PATCH 2/5] [encoding] Introduce the "Style" object Style encodes stroke vs fill style and their related parameters in an 8-byte data structure as described in vello#303. This data structure will replace the existing "linewidth" stream. --- crates/encoding/src/path.rs | 219 +++++++++++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/crates/encoding/src/path.rs b/crates/encoding/src/path.rs index b0a52b0b7..9cd10c864 100644 --- a/crates/encoding/src/path.rs +++ b/crates/encoding/src/path.rs @@ -2,10 +2,184 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use bytemuck::{Pod, Zeroable}; -use peniko::kurbo::Shape; +use peniko::{ + kurbo::{Cap, Join, Shape, Stroke}, + Fill, +}; use super::Monoid; +/// Data structure encoding stroke or fill style. +#[derive(Clone, Copy, Debug, Zeroable, Pod, Default, PartialEq)] +#[repr(C)] +pub struct Style { + /// Encodes the stroke and fill style parameters. This field is split into two 16-bit + /// parts: + /// + /// - flags: u16 - encodes fill vs stroke, even-odd vs non-zero fill mode for fills and cap + /// and join style for strokes. See the FLAGS_* constants below for more + /// information. + /// ```text + /// flags: |style|fill|join|start cap|end cap|reserved| + /// bits: 0 1 2-3 4-5 6-7 8-15 + /// ``` + /// + /// - miter_limit: u16 - The miter limit for a stroke, encoded in binary16 (half) floating + /// point representation. This field is on meaningful for the `Join::Miter` + /// join style. It's ignored for other stroke styles and fills. + pub flags_and_miter_limit: u32, + + /// Encodes the stroke width. This field is ignored for fills. + pub line_width: f32, +} + +impl Style { + /// 0 for a fill, 1 for a stroke + pub const FLAGS_STYLE_BIT: u32 = 0x8000_0000; + + /// 0 for non-zero, 1 for even-odd + pub const FLAGS_FILL_BIT: u32 = 0x4000_0000; + + /// Encodings for join style: + /// - 0b00 -> bevel + /// - 0b01 -> miter + /// - 0b10 -> round + pub const FLAGS_JOIN_BITS_BEVEL: u32 = 0; + pub const FLAGS_JOIN_BITS_MITER: u32 = 0x1000_0000; + pub const FLAGS_JOIN_BITS_ROUND: u32 = 0x2000_0000; + + #[cfg(test)] + pub const FLAGS_JOIN_MASK: u32 = 0x3000_0000; + + /// Encodings for cap style: + /// - 0b00 -> butt + /// - 0b01 -> square + /// - 0b10 -> round + pub const FLAGS_START_CAP_BITS_BUTT: u32 = 0; + pub const FLAGS_START_CAP_BITS_SQUARE: u32 = 0x0400_0000; + pub const FLAGS_START_CAP_BITS_ROUND: u32 = 0x0800_0000; + pub const FLAGS_END_CAP_BITS_BUTT: u32 = 0; + pub const FLAGS_END_CAP_BITS_SQUARE: u32 = 0x0100_0000; + pub const FLAGS_END_CAP_BITS_ROUND: u32 = 0x0200_0000; + + pub const FLAGS_START_CAP_MASK: u32 = 0x0C00_0000; + pub const FLAGS_END_CAP_MASK: u32 = 0x0300_0000; + pub const MITER_LIMIT_MASK: u32 = 0xFFFF; + + pub fn from_fill(fill: &Fill) -> Self { + let fill_bit = match fill { + Fill::NonZero => 0, + Fill::EvenOdd => Self::FLAGS_FILL_BIT, + }; + Self { + flags_and_miter_limit: fill_bit, + line_width: 0., + } + } + + pub fn from_stroke(stroke: &Stroke) -> Self { + let style = Self::FLAGS_STYLE_BIT; + let join = match stroke.join { + Join::Bevel => Self::FLAGS_JOIN_BITS_BEVEL, + Join::Miter => Self::FLAGS_JOIN_BITS_MITER, + Join::Round => Self::FLAGS_JOIN_BITS_ROUND, + }; + let start_cap = match stroke.start_cap { + Cap::Butt => Self::FLAGS_START_CAP_BITS_BUTT, + Cap::Square => Self::FLAGS_START_CAP_BITS_SQUARE, + Cap::Round => Self::FLAGS_START_CAP_BITS_ROUND, + }; + let end_cap = match stroke.end_cap { + Cap::Butt => Self::FLAGS_END_CAP_BITS_BUTT, + Cap::Square => Self::FLAGS_END_CAP_BITS_SQUARE, + Cap::Round => Self::FLAGS_END_CAP_BITS_ROUND, + }; + let miter_limit = crate::math::f32_to_f16(stroke.miter_limit as f32) as u32; + Self { + flags_and_miter_limit: style | join | start_cap | end_cap | miter_limit, + line_width: stroke.width as f32, + } + } + + #[cfg(test)] + fn get_fill(&self) -> Option { + if self.is_fill() { + Some( + if (self.flags_and_miter_limit & Self::FLAGS_FILL_BIT) == 0 { + Fill::NonZero + } else { + Fill::EvenOdd + }, + ) + } else { + None + } + } + + #[cfg(test)] + fn get_stroke_width(&self) -> Option { + if self.is_fill() { + return None; + } + Some(self.line_width.into()) + } + + #[cfg(test)] + fn get_stroke_join(&self) -> Option { + if self.is_fill() { + return None; + } + let join = self.flags_and_miter_limit & Self::FLAGS_JOIN_MASK; + Some(match join { + Self::FLAGS_JOIN_BITS_BEVEL => Join::Bevel, + Self::FLAGS_JOIN_BITS_MITER => Join::Miter, + Self::FLAGS_JOIN_BITS_ROUND => Join::Round, + _ => unreachable!("unsupported join encoding"), + }) + } + + #[cfg(test)] + fn get_stroke_start_cap(&self) -> Option { + if self.is_fill() { + return None; + } + let cap = self.flags_and_miter_limit & Self::FLAGS_START_CAP_MASK; + Some(match cap { + Self::FLAGS_START_CAP_BITS_BUTT => Cap::Butt, + Self::FLAGS_START_CAP_BITS_SQUARE => Cap::Square, + Self::FLAGS_START_CAP_BITS_ROUND => Cap::Round, + _ => unreachable!("unsupported start cap encoding"), + }) + } + + #[cfg(test)] + fn get_stroke_end_cap(&self) -> Option { + if self.is_fill() { + return None; + } + let cap = self.flags_and_miter_limit & Self::FLAGS_END_CAP_MASK; + Some(match cap { + Self::FLAGS_END_CAP_BITS_BUTT => Cap::Butt, + Self::FLAGS_END_CAP_BITS_SQUARE => Cap::Square, + Self::FLAGS_END_CAP_BITS_ROUND => Cap::Round, + _ => unreachable!("unsupported end cap encoding"), + }) + } + + #[cfg(test)] + fn get_stroke_miter_limit(&self) -> Option { + if self.is_fill() { + return None; + } + Some((self.flags_and_miter_limit & Self::MITER_LIMIT_MASK) as u16) + } + + #[cfg(test)] + fn is_fill(&self) -> bool { + (self.flags_and_miter_limit & Self::FLAGS_STYLE_BIT) == 0 + } +} + /// Line segment (after flattening, before tiling). #[derive(Clone, Copy, Debug, Zeroable, Pod, Default)] #[repr(C)] @@ -450,3 +624,46 @@ impl fello::scale::Pen for PathEncoder<'_> { self.close() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fill_style() { + assert_eq!( + Some(Fill::NonZero), + Style::from_fill(&Fill::NonZero).get_fill() + ); + assert_eq!( + Some(Fill::EvenOdd), + Style::from_fill(&Fill::EvenOdd).get_fill() + ); + assert_eq!(None, Style::from_stroke(&Stroke::default()).get_fill()); + } + + #[test] + fn test_stroke_style() { + assert_eq!(None, Style::from_fill(&Fill::NonZero).get_stroke_width()); + assert_eq!(None, Style::from_fill(&Fill::EvenOdd).get_stroke_width()); + let caps = [Cap::Butt, Cap::Square, Cap::Round]; + let joins = [Join::Bevel, Join::Miter, Join::Round]; + for start in caps { + for end in caps { + for join in joins { + let stroke = Stroke::new(1.0) + .with_start_cap(start) + .with_end_cap(end) + .with_join(join) + .with_miter_limit(0.); + let encoded = Style::from_stroke(&stroke); + assert_eq!(Some(stroke.width), encoded.get_stroke_width()); + assert_eq!(Some(stroke.join), encoded.get_stroke_join()); + assert_eq!(Some(stroke.start_cap), encoded.get_stroke_start_cap()); + assert_eq!(Some(stroke.end_cap), encoded.get_stroke_end_cap()); + assert_eq!(Some(0), encoded.get_stroke_miter_limit()); + } + } + } + } +} From 216ecce773e7674939af0ab78bc8266ca247bef8 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Sat, 21 Oct 2023 13:18:03 -0400 Subject: [PATCH 3/5] [test_scenes] fill_type scene Added a test scene specifically for fill rules, drawing a star shape and intersecting arcs with opposite winding. --- examples/scenes/src/test_scenes.rs | 51 +++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index fcb4d7656..89606caf4 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -32,10 +32,10 @@ macro_rules! scene { pub fn test_scenes() -> SceneSet { let scenes = vec![ scene!(splash_with_tiger(), "splash_with_tiger", false), - scene!(crate::mmark::MMark::new(80_000), "mmark", false), scene!(funky_paths), scene!(stroke_styles), scene!(tricky_strokes), + scene!(fill_types), scene!(cardioid_and_friends), scene!(animated_text: animated), scene!(gradient_extend), @@ -48,6 +48,7 @@ pub fn test_scenes() -> SceneSet { scene!(clip_test: animated), scene!(longpathdash(Cap::Butt), "longpathdash (butt caps)", false), scene!(longpathdash(Cap::Round), "longpathdash (round caps)", false), + scene!(crate::mmark::MMark::new(80_000), "mmark", false), ]; SceneSet { scenes } @@ -282,6 +283,54 @@ fn tricky_strokes(sb: &mut SceneBuilder, _: &mut SceneParams) { } } +fn fill_types(sb: &mut SceneBuilder, params: &mut SceneParams) { + use PathEl::*; + let rect = Rect::from_origin_size(Point::new(0., 0.), (500., 500.)); + let star = [ + MoveTo((250., 0.).into()), + LineTo((105., 450.).into()), + LineTo((490., 175.).into()), + LineTo((10., 175.).into()), + LineTo((395., 450.).into()), + ClosePath, + ]; + let arcs = [ + MoveTo((0., 480.).into()), + CurveTo((500., 480.).into(), (500., -10.).into(), (0., -10.).into()), + ClosePath, + MoveTo((500., -10.).into()), + CurveTo((0., -10.).into(), (0., 480.).into(), (500., 480.).into()), + ClosePath, + ]; + let scale = Affine::scale(0.6); + let t = Affine::translate((10., 25.)); + let rules = [ + (Fill::NonZero, "Non-Zero", star.as_slice()), + (Fill::EvenOdd, "Even-Odd", &star), + (Fill::NonZero, "Non-Zero", &arcs), + (Fill::EvenOdd, "Even-Odd", &arcs), + ]; + for (i, rule) in rules.iter().enumerate() { + let t = Affine::translate(((i % 2) as f64 * 306., (i / 2) as f64 * 340.)) * t; + params.text.add(sb, None, 24., None, t, rule.1); + let t = Affine::translate((0., 5.)) * t * scale; + sb.fill( + Fill::NonZero, + t, + &Brush::Solid(Color::rgb8(128, 128, 128)), + None, + &rect, + ); + sb.fill( + rule.0, + Affine::translate((0., 10.)) * t, + Color::BLACK, + None, + &rule.2, + ); + } +} + fn cardioid_and_friends(sb: &mut SceneBuilder, _: &mut SceneParams) { render_cardioid(sb); render_clip_test(sb); From fb14961ea71da7b54036afa4dcbb5d5245d34abb Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Sat, 21 Oct 2023 13:36:50 -0400 Subject: [PATCH 4/5] [encoding] Replace linewidth stream with the new style encoding * The linewidth stream is now the "style" stream. This has been wired up all the way up to the `flatten` pipeline which uses the new encoding to extract the fill rule. * Pipelines downstream of `flatten` still use the old linewidth scheme. They will be fixed up in a follow-up change. --- crates/encoding/src/encoding.rs | 33 +++++++++++++---------------- crates/encoding/src/glyph_cache.rs | 2 +- crates/encoding/src/lib.rs | 2 +- crates/encoding/src/path.rs | 13 ++++++------ crates/encoding/src/resolve.rs | 34 +++++++++++++++--------------- shader/flatten.wgsl | 8 ++++++- shader/shared/config.wgsl | 2 +- shader/shared/pathtag.wgsl | 15 +++++++++---- src/cpu_shader/flatten.rs | 18 ++++++++++++---- 9 files changed, 74 insertions(+), 53 deletions(-) diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 997fada65..e56964629 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -1,7 +1,7 @@ // Copyright 2022 The Vello authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform}; +use super::{DrawColor, DrawTag, PathEncoder, PathTag, Style, Transform}; use peniko::{kurbo::Shape, BlendMode, BrushRef, Color, Fill}; @@ -25,8 +25,8 @@ pub struct Encoding { pub draw_data: Vec, /// The transform stream. pub transforms: Vec, - /// The line width stream. - pub linewidths: Vec, + /// The style stream + pub styles: Vec