Switch to CPU-side stroke encoding

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).
diff --git a/Cargo.toml b/Cargo.toml
index 1c864f1..dae8d16 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,7 +54,7 @@
 [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" }                                              
diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs
index 0b2a13b..997fada 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, Color};
+use peniko::{kurbo::Shape, BlendMode, BrushRef, Color, Fill};
 
 #[cfg(feature = "full")]
 use {
@@ -163,8 +163,12 @@
         }
     }
 
-    /// 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);
diff --git a/crates/encoding/src/glyph_cache.rs b/crates/encoding/src/glyph_cache.rs
index b1ff693..3469c13 100644
--- a/crates/encoding/src/glyph_cache.rs
+++ b/crates/encoding/src/glyph_cache.rs
@@ -5,9 +5,12 @@
 
 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 {
@@ -36,20 +39,37 @@
         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;
             }
@@ -89,3 +109,35 @@
         }
     }
 }
+
+// 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();
+    }
+}
diff --git a/examples/scenes/src/mmark.rs b/examples/scenes/src/mmark.rs
index c7e439a..11137ee 100644
--- a/examples/scenes/src/mmark.rs
+++ b/examples/scenes/src/mmark.rs
@@ -11,8 +11,7 @@
 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,
 };
 
@@ -92,7 +91,7 @@
                 // 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,
diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs
index d66be10..48d70d3 100644
--- a/examples/scenes/src/test_scenes.rs
+++ b/examples/scenes/src/test_scenes.rs
@@ -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::*;
 
diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs
index a18d0d4..08d8bd4 100644
--- a/examples/with_winit/src/stats.rs
+++ b/examples/with_winit/src/stats.rs
@@ -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;
@@ -174,7 +174,7 @@
                 &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,
diff --git a/integrations/vello_svg/src/lib.rs b/integrations/vello_svg/src/lib.rs
index b19f5d2..2aa3a87 100644
--- a/integrations/vello_svg/src/lib.rs
+++ b/integrations/vello_svg/src/lib.rs
@@ -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;
@@ -133,7 +133,7 @@
                     {
                         // 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),
diff --git a/src/glyph.rs b/src/glyph.rs
index 75fbce5..84e9ef3 100644
--- a/src/glyph.rs
+++ b/src/glyph.rs
@@ -30,6 +30,7 @@
 };
 
 pub use fello;
+use peniko::kurbo::Shape;
 pub use vello_encoding::Glyph;
 
 /// General context for creating scene fragments for glyph outlines.
@@ -102,13 +103,29 @@
     }
 
     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 {
diff --git a/src/scene.rs b/src/scene.rs
index 02e4006..13c61ca 100644
--- a/src/scene.rs
+++ b/src/scene.rs
@@ -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.
@@ -100,7 +100,7 @@
         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.
@@ -126,10 +126,7 @@
     ) {
         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
@@ -152,20 +149,26 @@
         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.