CPU: feature flag u8 and f32 pipelines (#1294)

Adds feature flags to `vello_cpu`s `u8` and `f32` pipelines so that you
can save on binary size if you're only planning on using one of them.
Disabling the `f32` pipeline knocks ~60kb off the binary size for me
~250kb -> ~190kb.

---------

Signed-off-by: Nico Burns <nico@nicoburns.com>
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7d7bcf8..70dfc91 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -146,13 +146,19 @@
           save-if: ${{ github.event_name != 'merge_group' }}
 
       - name: cargo clippy (no_std)
-        run: cargo hack clippy ${{ env.RUST_NO_STD_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none -- -D warnings
+        run: cargo hack clippy ${{ env.RUST_NO_STD_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features libm,u8_pipeline,f32_pipeline --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none -- -D warnings
 
       - name: cargo clippy
-        run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std -- -D warnings
+        run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline -- -D warnings
+
+      - name: cargo clippy (vello_cpu - u8 pipeline)
+        run: cargo clippy -p vello_cpu --locked --no-default-features --features std,text,png,u8_pipeline
+
+      - name: cargo clippy (vello_cpu - f32 pipeline)
+        run: cargo clippy -p vello_cpu --locked --no-default-features --features std,text,png,f32_pipeline
 
       - name: cargo clippy (auxiliary)
-        run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std --tests --benches --examples -- -D warnings
+        run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline --tests --benches --examples -- -D warnings
 
   clippy-stable-wasm:
     name: cargo clippy (wasm32)
@@ -178,13 +184,13 @@
           save-if: ${{ github.event_name != 'merge_group' }}
 
       - name: cargo clippy (no_std)
-        run: cargo hack clippy ${{ env.RUST_NO_STD_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} -- -D warnings
+        run: cargo hack clippy ${{ env.RUST_NO_STD_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features libm,u8_pipeline,f32_pipeline --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} -- -D warnings
 
       - name: cargo clippy
-        run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std -- -D warnings
+        run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline -- -D warnings
 
       - name: cargo clippy (auxiliary)
-        run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std --tests --benches --examples -- -D warnings
+        run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline --tests --benches --examples -- -D warnings
 
   prime-lfs-cache:
     name: Prime LFS Cache
@@ -422,10 +428,10 @@
           save-if: ${{ github.event_name != 'merge_group' }}
 
       - name: cargo check (no_std)
-        run: cargo hack check ${{ env.RUST_NO_STD_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none
+        run: cargo hack check ${{ env.RUST_NO_STD_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features libm,u8_pipeline,f32_pipeline --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none
 
       - name: cargo check
-        run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features std
+        run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline
 
   check-msrv-wasm:
     name: cargo check (msrv) (wasm32)
@@ -450,7 +456,7 @@
           save-if: ${{ github.event_name != 'merge_group' }}
 
       - name: cargo check
-        run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std
+        run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std,u8_pipeline,f32_pipeline
 
   doc:
     name: cargo doc
diff --git a/sparse_strips/vello_cpu/Cargo.toml b/sparse_strips/vello_cpu/Cargo.toml
index f31c4f2..1d7842b 100644
--- a/sparse_strips/vello_cpu/Cargo.toml
+++ b/sparse_strips/vello_cpu/Cargo.toml
@@ -26,7 +26,7 @@
 thread_local = { workspace = true, optional = true }
 
 [features]
-default = ["std", "png", "text"]
+default = ["std", "png", "text", "u8_pipeline"]
 # Get floating point functions from the standard library (likely using your target’s libc).
 std = ["vello_common/std"]
 # Use floating point implementations from libm.
@@ -45,6 +45,11 @@
 # Add support for text rendering
 text = ["vello_common/text"]
 
+# Speed focussed rendering using u8 math
+u8_pipeline = []
+# Quality focussed rendering using f32 math
+f32_pipeline = []
+
 [lints]
 workspace = true
 
diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
index 3b4fa80..8a91a7c 100644
--- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
@@ -5,7 +5,7 @@
 use crate::dispatch::Dispatcher;
 use crate::dispatch::multi_threaded::cost::{COST_THRESHOLD, estimate_render_task_cost};
 use crate::dispatch::multi_threaded::worker::Worker;
-use crate::fine::{F32Kernel, Fine, FineKernel, U8Kernel};
+use crate::fine::{Fine, FineKernel};
 use crate::kurbo::{Affine, BezPath, PathEl, Stroke};
 use crate::peniko::{BlendMode, Fill};
 use crate::region::Regions;
@@ -163,6 +163,7 @@
         dispatcher
     }
 
+    #[cfg(feature = "f32_pipeline")]
     fn rasterize_f32(
         &self,
         buffer: &mut [u8],
@@ -170,9 +171,11 @@
         height: u16,
         encoded_paints: &[EncodedPaint],
     ) {
+        use crate::fine::F32Kernel;
         dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints));
     }
 
+    #[cfg(feature = "u8_pipeline")]
     fn rasterize_u8(
         &self,
         buffer: &mut [u8],
@@ -180,6 +183,7 @@
         height: u16,
         encoded_paints: &[EncodedPaint],
     ) {
+        use crate::fine::U8Kernel;
         dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints));
     }
 
@@ -569,6 +573,21 @@
     ) {
         assert!(self.flushed, "attempted to rasterize before flushing");
 
+        // Only u8 pipeline enabled
+        #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
+        {
+            let _ = render_mode;
+            self.rasterize_u8(buffer, width, height, encoded_paints);
+        }
+        // Only f32 pipeline enabled
+        #[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
+        {
+            let _ = render_mode;
+            self.rasterize_f32(buffer, width, height, encoded_paints);
+        }
+
+        // Both pipelines enabled
+        #[cfg(all(feature = "f32_pipeline", feature = "u8_pipeline"))]
         match render_mode {
             RenderMode::OptimizeSpeed => self.rasterize_u8(buffer, width, height, encoded_paints),
             RenderMode::OptimizeQuality => {
diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
index d508979..2f6a50c 100644
--- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
@@ -3,7 +3,7 @@
 
 use crate::RenderMode;
 use crate::dispatch::Dispatcher;
-use crate::fine::{F32Kernel, Fine, FineKernel, U8Kernel};
+use crate::fine::{Fine, FineKernel};
 use crate::kurbo::{Affine, BezPath, Stroke};
 use crate::layer_manager::LayerManager;
 use crate::peniko::{BlendMode, Fill};
@@ -12,7 +12,7 @@
 use vello_common::coarse::{Cmd, LayerKind, MODE_CPU, Wide, WideTilesBbox};
 use vello_common::color::palette::css::TRANSPARENT;
 use vello_common::encode::EncodedPaint;
-use vello_common::fearless_simd::{Level, Simd, dispatch};
+use vello_common::fearless_simd::{Level, Simd};
 use vello_common::filter_effects::Filter;
 use vello_common::mask::Mask;
 use vello_common::paint::{Paint, PremulColor};
@@ -87,6 +87,7 @@
     ///
     /// This dispatches to the appropriate SIMD implementation based on the
     /// configured level, using f32 for intermediate calculations.
+    #[cfg(feature = "f32_pipeline")]
     fn rasterize_f32(
         &self,
         buffer: &mut [u8],
@@ -94,6 +95,8 @@
         height: u16,
         encoded_paints: &[EncodedPaint],
     ) {
+        use crate::fine::F32Kernel;
+        use vello_common::fearless_simd::dispatch;
         dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints));
     }
 
@@ -101,6 +104,7 @@
     ///
     /// This dispatches to the appropriate SIMD implementation based on the
     /// configured level, using u8 for intermediate calculations to maximize speed.
+    #[cfg(feature = "u8_pipeline")]
     fn rasterize_u8(
         &self,
         buffer: &mut [u8],
@@ -108,6 +112,8 @@
         height: u16,
         encoded_paints: &[EncodedPaint],
     ) {
+        use crate::fine::U8Kernel;
+        use vello_common::fearless_simd::dispatch;
         dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints));
     }
 
@@ -526,7 +532,22 @@
         height: u16,
         encoded_paints: &[EncodedPaint],
     ) {
-        // Select precision based on render mode.
+        // If only the u8 pipeline is enabled, then use it
+        #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
+        {
+            let _ = render_mode;
+            self.rasterize_u8(buffer, width, height, encoded_paints);
+        }
+
+        // If only the f32 pipeline is enabled, then use it
+        #[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
+        {
+            let _ = render_mode;
+            self.rasterize_f32(buffer, width, height, encoded_paints);
+        }
+
+        // If both pipelines are enabled, select precision based on render mode parameter.
+        #[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
         match render_mode {
             RenderMode::OptimizeSpeed => {
                 // Use u8 precision for faster rendering.
@@ -537,6 +558,13 @@
                 self.rasterize_f32(buffer, width, height, encoded_paints);
             }
         }
+
+        #[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
+        {
+            // This case never gets hit because there is a compile_error in the root.
+            // But have this code disables some warnings and makes the compile error easier to read
+            let _ = (buffer, render_mode, width, height, encoded_paints);
+        }
     }
 
     fn generate_wide_cmd(&mut self, strip_buf: &[Strip], paint: Paint, blend_mode: BlendMode) {
diff --git a/sparse_strips/vello_cpu/src/lib.rs b/sparse_strips/vello_cpu/src/lib.rs
index 3c13b44..28dbf4b 100644
--- a/sparse_strips/vello_cpu/src/lib.rs
+++ b/sparse_strips/vello_cpu/src/lib.rs
@@ -120,6 +120,9 @@
 #[cfg(feature = "std")]
 extern crate std;
 
+#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
+compile_error!("vello_cpu must have at least one of the u8 or f32 pipelines enabled");
+
 mod render;
 
 mod dispatch;
diff --git a/sparse_strips/vello_sparse_tests/Cargo.toml b/sparse_strips/vello_sparse_tests/Cargo.toml
index 012ad60..aa6dbd5 100644
--- a/sparse_strips/vello_sparse_tests/Cargo.toml
+++ b/sparse_strips/vello_sparse_tests/Cargo.toml
@@ -17,7 +17,7 @@
 [dependencies]
 vello_api = { workspace = true }
 vello_common = { workspace = true, features = ["std"] }
-vello_cpu = { workspace = true, features = ["multithreading", "std"] }
+vello_cpu = { workspace = true, features = ["multithreading", "std", "f32_pipeline"] }
 vello_hybrid = { workspace = true }
 wgpu = { workspace = true, default-features = true }
 pollster = { workspace = true }