Skip to content

Commit

Permalink
Switch to CPU-side stroke encoding
Browse files Browse the repository at this point in the history
Don't use GPU-side signed distance field rendering for strokes, but rather expand them on the CPU (using kurbo) during encoding, then render using fill.

This is a significant performance regression, but is on the critical path path for multisampled path rendering and stroke rework (#303).
  • Loading branch information
raphlinus committed Oct 4, 2023
1 parent 2f5b343 commit 0d8dd7b
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ wgpu-profiler = { workspace = true, optional = true }
[workspace.dependencies]
bytemuck = { version = "1.12.1", features = ["derive"] }
fello = { git = "https://github.com/dfrg/fount", rev = "dadbcf75695f035ca46766bfd60555d05bd421b1" }
peniko = { git = "https://github.com/linebender/peniko", rev = "cafdac9a211a0fb2fec5656bd663d1ac770bcc81" }
peniko = { git = "https://github.com/linebender/peniko", rev = "639f18b25048abcf8130397737c24e02bf49cef2" }

# NOTE: Make sure to keep this in sync with the version badge in README.md
wgpu = { version = "0.17" }
Expand Down
10 changes: 7 additions & 3 deletions crates/encoding/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform};

use peniko::{kurbo::Shape, BlendMode, BrushRef, Color};
use peniko::{kurbo::Shape, BlendMode, BrushRef, Color, Fill};

#[cfg(feature = "full")]
use {
Expand Down Expand Up @@ -163,8 +163,12 @@ impl Encoding {
}
}

/// Encodes a linewidth.
pub fn encode_linewidth(&mut self, linewidth: f32) {
/// Encodes a fill style.
pub fn encode_fill_style(&mut self, fill: Fill) {
let linewidth = match fill {
Fill::NonZero => -1.0,
Fill::EvenOdd => -2.0,
};
if self.linewidths.last() != Some(&linewidth) {
self.path_tags.push(PathTag::LINEWIDTH);
self.linewidths.push(linewidth);
Expand Down
72 changes: 62 additions & 10 deletions crates/encoding/src/glyph_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ use std::collections::HashMap;

use super::{Encoding, StreamOffsets};

use fello::scale::Scaler;
use fello::scale::{Pen, Scaler};
use fello::GlyphId;
use peniko::{Fill, Style};
use peniko::{
kurbo::{BezPath, Shape},
Fill, Style,
};

#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
pub struct GlyphKey {
Expand Down Expand Up @@ -36,20 +39,37 @@ impl GlyphCache {
style: &Style,
scaler: &mut Scaler,
) -> Option<CachedRange> {
let is_fill = matches!(style, Style::Fill(_));
let is_var = !scaler.normalized_coords().is_empty();
let encoding_cache = &mut self.encoding;
let mut encode_glyph = || {
let start = encoding_cache.stream_offsets();
let fill = match style {
Style::Fill(fill) => *fill,
Style::Stroke(_) => Fill::NonZero,
};
encoding_cache.encode_fill_style(fill);
let mut path = encoding_cache.encode_path(true);
match style {
Style::Fill(Fill::NonZero) => encoding_cache.encode_linewidth(-1.0),
Style::Fill(Fill::EvenOdd) => encoding_cache.encode_linewidth(-2.0),
Style::Stroke(stroke) => encoding_cache.encode_linewidth(stroke.width),
Style::Fill(_) => {
scaler
.outline(GlyphId::new(key.glyph_id as u16), &mut path)
.ok()?;
}
Style::Stroke(stroke) => {
const STROKE_TOLERANCE: f64 = 0.01;
let mut pen = BezPathPen::default();
scaler
.outline(GlyphId::new(key.glyph_id as u16), &mut pen)
.ok()?;
let stroked = peniko::kurbo::stroke(
pen.0.path_elements(STROKE_TOLERANCE),
stroke,
&Default::default(),
STROKE_TOLERANCE,
);
path.shape(&stroked);
}
}
let mut path = encoding_cache.encode_path(is_fill);
scaler
.outline(GlyphId::new(key.glyph_id as u16), &mut path)
.ok()?;
if path.finish(false) == 0 {
return None;
}
Expand Down Expand Up @@ -89,3 +109,35 @@ impl CachedRange {
}
}
}

// A wrapper newtype so we can implement the `Pen` trait. Arguably, this could
// be in the fello crate, but will go away when we do GPU-side stroking.
#[derive(Default)]
struct BezPathPen(BezPath);

impl Pen for BezPathPen {
fn move_to(&mut self, x: f32, y: f32) {
self.0.move_to((x as f64, y as f64));
}

fn line_to(&mut self, x: f32, y: f32) {
self.0.line_to((x as f64, y as f64));
}

fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.0
.quad_to((cx0 as f64, cy0 as f64), (x as f64, y as f64));
}

fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.0.curve_to(
(cx0 as f64, cy0 as f64),
(cx1 as f64, cy1 as f64),
(x as f64, y as f64),
);
}

fn close(&mut self) {
self.0.close_path();
}
}
5 changes: 2 additions & 3 deletions examples/scenes/src/mmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use std::cmp::Ordering;
use rand::{seq::SliceRandom, Rng};
use vello::peniko::Color;
use vello::{
kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez},
peniko::Stroke,
kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Stroke},
SceneBuilder,
};

Expand Down Expand Up @@ -92,7 +91,7 @@ impl TestScene for MMark {
// This gets color and width from the last element, original
// gets it from the first, but this should not matter.
sb.stroke(
&Stroke::new(element.width as f32),
&Stroke::new(element.width),
Affine::IDENTITY,
element.color,
None,
Expand Down
2 changes: 1 addition & 1 deletion examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet};
use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect};
use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect, Stroke};
use vello::peniko::*;
use vello::*;

Expand Down
6 changes: 3 additions & 3 deletions examples/with_winit/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
use scenes::SimpleText;
use std::{collections::VecDeque, time::Duration};
use vello::{
kurbo::{Affine, Line, PathEl, Rect},
peniko::{Brush, Color, Fill, Stroke},
kurbo::{Affine, Line, PathEl, Rect, Stroke},
peniko::{Brush, Color, Fill},
BumpAllocators, SceneBuilder,
};
use wgpu_profiler::GpuTimerScopeResult;
Expand Down Expand Up @@ -174,7 +174,7 @@ impl Snapshot {
&format!("{}", t),
);
sb.stroke(
&Stroke::new((graph_max_height * 0.01) as f32),
&Stroke::new(graph_max_height * 0.01),
offset * Affine::translate((left_margin_padding, (1. - y) * graph_max_height)),
Color::WHITE,
None,
Expand Down
6 changes: 3 additions & 3 deletions integrations/vello_svg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
use std::convert::Infallible;

use usvg::NodeExt;
use vello::kurbo::{Affine, BezPath, Rect};
use vello::peniko::{Brush, Color, Fill, Stroke};
use vello::kurbo::{Affine, BezPath, Rect, Stroke};
use vello::peniko::{Brush, Color, Fill};
use vello::SceneBuilder;

pub use usvg;
Expand Down Expand Up @@ -133,7 +133,7 @@ pub fn render_tree_with<F: FnMut(&mut SceneBuilder, &usvg::Node) -> Result<(), E
{
// FIXME: handle stroke options such as linecap, linejoin, etc.
sb.stroke(
&Stroke::new(stroke.width.get() as f32),
&Stroke::new(stroke.width.get()),
transform,
&brush,
Some(brush_transform),
Expand Down
27 changes: 22 additions & 5 deletions src/glyph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use {
};

pub use fello;
use peniko::kurbo::Shape;
pub use vello_encoding::Glyph;

/// General context for creating scene fragments for glyph outlines.
Expand Down Expand Up @@ -102,13 +103,29 @@ impl<'a> GlyphProvider<'a> {
}

pub fn encode_glyph(&mut self, gid: u16, style: &Style, encoding: &mut Encoding) -> Option<()> {
let fill = match style {
Style::Fill(fill) => *fill,
Style::Stroke(_) => Fill::NonZero,
};
encoding.encode_fill_style(fill);
let mut path = encoding.encode_path(true);
match style {
Style::Fill(Fill::NonZero) => encoding.encode_linewidth(-1.0),
Style::Fill(Fill::EvenOdd) => encoding.encode_linewidth(-2.0),
Style::Stroke(stroke) => encoding.encode_linewidth(stroke.width),
Style::Fill(_) => {
self.scaler.outline(GlyphId::new(gid), &mut path).ok()?;
}
Style::Stroke(stroke) => {
const STROKE_TOLERANCE: f64 = 0.01;
let mut pen = BezPathPen::default();
self.scaler.outline(GlyphId::new(gid), &mut pen).ok()?;
let stroked = peniko::kurbo::stroke(
pen.0.path_elements(STROKE_TOLERANCE),
stroke,
&Default::default(),
STROKE_TOLERANCE,
);
path.shape(&stroked);
}
}
let mut path = encoding.encode_path(matches!(style, Style::Fill(_)));
self.scaler.outline(GlyphId::new(gid), &mut path).ok()?;
if path.finish(false) != 0 {
Some(())
} else {
Expand Down
45 changes: 24 additions & 21 deletions src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
// Also licensed under MIT license, at your choice.

use fello::NormalizedCoord;
use peniko::kurbo::{Affine, Rect, Shape};
use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, Stroke, StyleRef};
use peniko::kurbo::{Affine, Rect, Shape, Stroke};
use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, StyleRef};
use vello_encoding::{Encoding, Glyph, GlyphRun, Patch, Transform};

/// Encoded definition of a scene and associated resources.
Expand Down Expand Up @@ -100,7 +100,7 @@ impl<'a> SceneBuilder<'a> {
let blend = blend.into();
self.scene
.encode_transform(Transform::from_kurbo(&transform));
self.scene.encode_linewidth(-1.0);
self.scene.encode_fill_style(Fill::NonZero);
if !self.scene.encode_shape(shape, true) {
// If the layer shape is invalid, encode a valid empty path. This suppresses
// all drawing until the layer is popped.
Expand All @@ -126,10 +126,7 @@ impl<'a> SceneBuilder<'a> {
) {
self.scene
.encode_transform(Transform::from_kurbo(&transform));
self.scene.encode_linewidth(match style {
Fill::NonZero => -1.0,
Fill::EvenOdd => -2.0,
});
self.scene.encode_fill_style(style);
if self.scene.encode_shape(shape, true) {
if let Some(brush_transform) = brush_transform {
if self
Expand All @@ -152,20 +149,26 @@ impl<'a> SceneBuilder<'a> {
brush_transform: Option<Affine>,
shape: &impl Shape,
) {
self.scene
.encode_transform(Transform::from_kurbo(&transform));
self.scene.encode_linewidth(style.width);
if self.scene.encode_shape(shape, false) {
if let Some(brush_transform) = brush_transform {
if self
.scene
.encode_transform(Transform::from_kurbo(&(transform * brush_transform)))
{
self.scene.swap_last_path_tags();
}
}
self.scene.encode_brush(brush, 1.0);
}
// The setting for tolerance are a compromise. For most applications,
// shape tolerance doesn't matter, as the input is likely Bézier paths,
// which is exact. Note that shape tolerance is hard-coded as 0.1 in
// the encoding crate.
//
// Stroke tolerance is a different matter. Generally, the cost scales
// with inverse O(n^6), so there is moderate rendering cost to setting
// too fine a value. On the other hand, error scales with the transform
// applied post-stroking, so may exceed visible threshold. When we do
// GPU-side stroking, the transform will be known. In the meantime,
// this is a compromise.
const SHAPE_TOLERANCE: f64 = 0.01;
const STROKE_TOLERANCE: f64 = SHAPE_TOLERANCE;
let stroked = peniko::kurbo::stroke(
shape.path_elements(SHAPE_TOLERANCE),
style,
&Default::default(),
STROKE_TOLERANCE,
);
self.fill(Fill::NonZero, transform, brush, brush_transform, &stroked);
}

/// Draws an image at its natural size with the given transform.
Expand Down

0 comments on commit 0d8dd7b

Please sign in to comment.