Repository: linebender/vello (rust)

Issue:
Can we re-export `wgpu` with the feature?
I didn't realize until now that the recent `wgpu` feature given to Vello doesn't re-export the version, e.g. `vello::wgpu::<what I need>`

Could we re-export this with the feature flag on?

Particularly, some of the examples in velato/vello_svg use `wgpu` types, and I'd prefer not to keep separately maintained versions in sync.
<img width="626" alt="image" src="https://github.com/linebender/vello/assets/48108917/e1af9184-4a6b-4996-8153-65b06c9e0474">


Pull request description:
Re-export wgpu
These changes first re-export wgpu under the vello module.
Then, all examples are updated to use vello::wgpu.

Fixes #585

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e3801f00..eedf7f8f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -951,7 +951,6 @@ dependencies = [
  "pollster",
  "scenes",
  "vello",
- "wgpu",
 ]
 
 [[package]]
@@ -1926,7 +1925,6 @@ dependencies = [
  "anyhow",
  "pollster",
  "vello",
- "wgpu",
  "winit",
 ]
 
@@ -2301,7 +2299,6 @@ dependencies = [
  "png",
  "pollster",
  "vello",
- "wgpu",
 ]
 
 [[package]]
@@ -3103,7 +3100,6 @@ dependencies = [
  "wasm-bindgen-futures",
  "web-sys",
  "web-time",
- "wgpu",
  "wgpu-profiler",
  "winit",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 90691de3..dd973b8b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,7 +46,6 @@ static_assertions = "1.1.0"
 thiserror = "1.0.61"
 
 # NOTE: Make sure to keep this in sync with the version badge in README.md
-# as well as examples/simple/Cargo.toml
 wgpu = { version = "0.20.0" }
 log = "0.4.21"
 
diff --git a/examples/headless/Cargo.toml b/examples/headless/Cargo.toml
index c5575dfd..08f861ae 100644
--- a/examples/headless/Cargo.toml
+++ b/examples/headless/Cargo.toml
@@ -15,7 +15,6 @@ scenes = { path = "../scenes" }
 anyhow = { workspace = true }
 clap = { workspace = true, features = ["derive"] }
 
-wgpu = { workspace = true }
 pollster = { workspace = true }
 env_logger = "0.11.3"
 png = "0.17.13"
diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs
index d206b298..b68f4f0c 100644
--- a/examples/headless/src/main.rs
+++ b/examples/headless/src/main.rs
@@ -10,11 +10,11 @@ use clap::Parser;
 use scenes::{ImageCache, SceneParams, SceneSet, SimpleText};
 use vello::kurbo::{Affine, Vec2};
 use vello::util::RenderContext;
-use vello::{block_on_wgpu, RendererOptions, Scene};
-use wgpu::{
-    BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
+use vello::wgpu::{
+    self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
     TextureDescriptor, TextureFormat, TextureUsages,
 };
+use vello::{block_on_wgpu, RendererOptions, Scene};
 
 fn main() -> Result<()> {
     #[cfg(not(target_arch = "wasm32"))]
diff --git a/examples/simple/Cargo.toml b/examples/simple/Cargo.toml
index 927f4855..e3b3fc06 100644
--- a/examples/simple/Cargo.toml
+++ b/examples/simple/Cargo.toml
@@ -15,5 +15,4 @@ workspace = true
 vello = { version = "0.1.0", path = "../../vello" }
 anyhow = "1.0.83"
 pollster = "0.3.0"
-wgpu = "0.20.0"
 winit = "0.30.0"
diff --git a/examples/simple/src/main.rs b/examples/simple/src/main.rs
index 512effae..1b4245b9 100644
--- a/examples/simple/src/main.rs
+++ b/examples/simple/src/main.rs
@@ -13,6 +13,7 @@ use winit::event::*;
 use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
 use winit::window::Window;
 
+use vello::wgpu;
 // Simple struct to hold the state of the renderer
 pub struct ActiveRenderState<'s> {
     // The fields MUST be in this order, so that the surface is dropped before the window
diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml
index a189f62e..939d7c53 100644
--- a/examples/with_winit/Cargo.toml
+++ b/examples/with_winit/Cargo.toml
@@ -33,7 +33,6 @@ clap = { workspace = true, features = ["derive"] }
 pollster = { workspace = true }
 wgpu-profiler = { workspace = true, optional = true }
 
-wgpu = { workspace = true }
 winit = "0.30.0"
 log = { workspace = true }
 
diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs
index 7defd3cb..e649538e 100644
--- a/examples/with_winit/src/lib.rs
+++ b/examples/with_winit/src/lib.rs
@@ -26,6 +26,8 @@ use winit::dpi::LogicalSize;
 use winit::event_loop::EventLoop;
 use winit::window::{Window, WindowAttributes};
 
+use vello::wgpu;
+
 #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
 mod hot_reload;
 mod multi_touch;
@@ -131,7 +133,12 @@ fn run(
                 num_init_threads: NonZeroUsize::new(1),
             },
         )
-        .expect("Could create renderer");
+        .map_err(|e| {
+            // Pretty-print any renderer creation error using Display formatting before unwrapping.
+            eprintln!("{e}");
+            e
+        })
+        .expect("Failed to create renderer");
         #[cfg(feature = "wgpu-profiler")]
         renderer
             .profiler
@@ -571,7 +578,7 @@ fn run(
                     // We know that the only async here (`pop_error_scope`) is actually sync, so blocking is fine
                     match pollster::block_on(result) {
                         Ok(_) => log::info!("Reloading took {:?}", start.elapsed()),
-                        Err(e) => log::warn!("Failed to reload shaders because of {e}"),
+                        Err(e) => log::error!("Failed to reload shaders: {e}"),
                     }
                 }
             },
@@ -624,7 +631,11 @@ fn run(
                                     num_init_threads: NonZeroUsize::new(args.num_init_threads),
                                 },
                             )
-                            .expect("Could create renderer");
+                            .map_err(|e| {
+                                // Pretty-print any renderer creation error using Display formatting before unwrapping.
+                                anyhow::format_err!("{e}")
+                            })
+                            .expect("Failed to create renderer");
                             log::info!("Creating renderer {id} took {:?}", start.elapsed());
                             #[cfg(feature = "wgpu-profiler")]
                             renderer
diff --git a/vello/src/lib.rs b/vello/src/lib.rs
index 43bf435b..689dbc53 100644
--- a/vello/src/lib.rs
+++ b/vello/src/lib.rs
@@ -101,6 +101,9 @@ pub use skrifa;
 
 pub mod glyph;
 
+#[cfg(feature = "wgpu")]
+pub use wgpu;
+
 #[cfg(feature = "wgpu")]
 pub mod util;
 
@@ -209,11 +212,20 @@ pub enum Error {
     #[error("Failed to async map a buffer")]
     BufferAsyncError(#[from] wgpu::BufferAsyncError),
 
+    #[cfg(feature = "wgpu")]
+    #[error("wgpu Error from scope")]
+    WgpuErrorFromScope(#[from] wgpu::Error),
+
     /// Failed to create [`GpuProfiler`].
     /// See [`wgpu_profiler::CreationError`] for more information.
     #[cfg(feature = "wgpu-profiler")]
     #[error("Couldn't create wgpu profiler")]
     ProfilerCreationError(#[from] wgpu_profiler::CreationError),
+
+    /// Failed to compile the shaders.
+    #[cfg(feature = "hot_reload")]
+    #[error("Failed to compile shaders:\n{0}")]
+    ShaderCompilation(#[from] vello_shaders::compile::ErrorVec),
 }
 
 #[allow(dead_code)] // this can be unused when wgpu feature is not used
@@ -293,7 +305,7 @@ impl Renderer {
             #[cfg(not(target_arch = "wasm32"))]
             engine.use_parallel_initialisation();
         }
-        let shaders = shaders::full_shaders(device, &mut engine, &options);
+        let shaders = shaders::full_shaders(device, &mut engine, &options)?;
         #[cfg(not(target_arch = "wasm32"))]
         engine.build_shaders_if_needed(device, options.num_init_threads);
         let blit = options
@@ -435,14 +447,14 @@ impl Renderer {
 
     /// Reload the shaders. This should only be used during `vello` development
     #[cfg(feature = "hot_reload")]
-    pub async fn reload_shaders(&mut self, device: &Device) -> Result<(), wgpu::Error> {
+    pub async fn reload_shaders(&mut self, device: &Device) -> Result<(), Error> {
         device.push_error_scope(wgpu::ErrorFilter::Validation);
         let mut engine = WgpuEngine::new(self.options.use_cpu);
         // We choose not to initialise these shaders in parallel, to ensure the error scope works correctly
-        let shaders = shaders::full_shaders(device, &mut engine, &self.options);
+        let shaders = shaders::full_shaders(device, &mut engine, &self.options)?;
         let error = device.pop_error_scope().await;
         if let Some(error) = error {
-            return Err(error);
+            return Err(error.into());
         }
         self.engine = engine;
         self.shaders = shaders;
diff --git a/vello/src/shaders.rs b/vello/src/shaders.rs
index abba984b..06944f7f 100644
--- a/vello/src/shaders.rs
+++ b/vello/src/shaders.rs
@@ -12,7 +12,7 @@ use crate::ShaderId;
 use crate::{
     recording::{BindType, ImageFormat},
     wgpu_engine::WgpuEngine,
-    RendererOptions,
+    Error, RendererOptions,
 };
 
 // Shaders for the full pipeline
@@ -49,7 +49,7 @@ pub fn full_shaders(
     device: &Device,
     engine: &mut WgpuEngine,
     options: &RendererOptions,
-) -> FullShaders {
+) -> Result<FullShaders, Error> {
     use crate::wgpu_engine::CpuShaderType;
     use BindType::*;
 
@@ -60,7 +60,7 @@ pub fn full_shaders(
     //let force_gpu_from = Some("binning");
 
     #[cfg(feature = "hot_reload")]
-    let mut shaders = vello_shaders::compile::ShaderInfo::from_default();
+    let mut shaders = vello_shaders::compile::ShaderInfo::from_default()?;
     #[cfg(not(feature = "hot_reload"))]
     let shaders = vello_shaders::SHADERS;
 
@@ -247,7 +247,7 @@ pub fn full_shaders(
         None
     };
 
-    FullShaders {
+    Ok(FullShaders {
         pathtag_reduce,
         pathtag_reduce2,
         pathtag_scan,
@@ -271,5 +271,5 @@ pub fn full_shaders(
         fine_msaa8,
         fine_msaa16,
         pathtag_is_cpu: options.use_cpu,
-    }
+    })
 }
diff --git a/vello_shaders/build.rs b/vello_shaders/build.rs
index 4334aef7..6c762b86 100644
--- a/vello_shaders/build.rs
+++ b/vello_shaders/build.rs
@@ -18,7 +18,13 @@ fn main() {
 
     println!("cargo:rerun-if-changed={}", compile::shader_dir().display());
 
-    let mut shaders = compile::ShaderInfo::from_default();
+    let mut shaders = match compile::ShaderInfo::from_default() {
+        Ok(s) => s,
+        Err(err) => {
+            eprintln!("{err}");
+            return;
+        }
+    };
 
     // Drop the HashMap and sort by name so that we get deterministic order.
     let mut shaders = shaders.drain().collect::<Vec<_>>();
diff --git a/vello_shaders/src/compile/mod.rs b/vello_shaders/src/compile/mod.rs
index ab0d9c9e..ab11729b 100644
--- a/vello_shaders/src/compile/mod.rs
+++ b/vello_shaders/src/compile/mod.rs
@@ -5,6 +5,7 @@ use naga::front::wgsl;
 use naga::valid::{Capabilities, ModuleInfo, ValidationError, ValidationFlags};
 use naga::{AddressSpace, ArraySize, ImageClass, Module, StorageAccess, WithSpan};
 use std::collections::{HashMap, HashSet};
+use std::fmt;
 use std::path::{Path, PathBuf};
 use std::sync::OnceLock;
 use thiserror::Error;
@@ -17,18 +18,62 @@ pub mod msl;
 
 use crate::types::{BindType, BindingInfo, WorkgroupBufferInfo};
 
+pub type Result<T> = std::result::Result<T, Error>;
+pub type CoalescedResult<T> = std::result::Result<T, ErrorVec>;
+
+#[derive(Error, Debug)]
+pub struct ErrorVec(Vec<Error>);
+
+#[derive(Error, Debug)]
+#[error("{source} ({name}) {msg}")]
+pub struct Error {
+    name: String,
+    msg: String,
+    source: InnerError,
+}
+
 #[derive(Error, Debug)]
-pub enum Error {
-    #[error("failed to parse shader: {0}")]
+enum InnerError {
+    #[error("failed to parse shader")]
     Parse(#[from] wgsl::ParseError),
 
-    #[error("failed to validate shader: {0}")]
+    #[error("failed to validate shader")]
     Validate(#[from] WithSpan<ValidationError>),
 
     #[error("missing entry point function")]
     EntryPointNotFound,
 }
 
+impl fmt::Display for ErrorVec {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for e in self.0.iter() {
+            write!(f, "{e}")?;
+        }
+        Ok(())
+    }
+}
+
+impl Error {
+    fn new(wgsl: &str, name: &str, error: impl Into<InnerError>) -> Error {
+        let source = error.into();
+        Error {
+            name: name.to_owned(),
+            msg: source.emit_msg(wgsl, &format!("({name} preprocessed)")),
+            source,
+        }
+    }
+}
+
+impl InnerError {
+    fn emit_msg(&self, wgsl: &str, name: &str) -> String {
+        match self {
+            Self::Parse(e) => e.emit_to_string_with_path(wgsl, name),
+            Self::Validate(e) => e.emit_to_string_with_path(wgsl, name),
+            _ => String::default(),
+        }
+    }
+}
+
 #[derive(Debug)]
 pub struct ShaderInfo {
     pub source: String,
@@ -40,16 +85,17 @@ pub struct ShaderInfo {
 }
 
 impl ShaderInfo {
-    pub fn new(source: String, entry_point: &str) -> Result<ShaderInfo, Error> {
-        let module = wgsl::parse_str(&source)?;
+    pub fn new(name: &str, source: String, entry_point: &str) -> Result<ShaderInfo> {
+        let module = wgsl::parse_str(&source).map_err(|error| Error::new(&source, name, error))?;
         let module_info = naga::valid::Validator::new(ValidationFlags::all(), Capabilities::all())
-            .validate(&module)?;
+            .validate(&module)
+            .map_err(|error| Error::new(&source, name, error))?;
         let (entry_index, entry) = module
             .entry_points
             .iter()
             .enumerate()
             .find(|(_, entry)| entry.name.as_str() == entry_point)
-            .ok_or(Error::EntryPointNotFound)?;
+            .ok_or(Error::new(&source, name, InnerError::EntryPointNotFound))?;
         let mut bindings = vec![];
         let mut workgroup_buffers = vec![];
         let mut wg_buffer_idx = 0;
@@ -131,11 +177,11 @@ impl ShaderInfo {
     }
 
     /// Same as [`ShaderInfo::from_dir`] but uses the default shader directory provided by [`shader_dir`].
-    pub fn from_default() -> HashMap<String, Self> {
+    pub fn from_default() -> CoalescedResult<HashMap<String, Self>> {
         ShaderInfo::from_dir(shader_dir())
     }
 
-    pub fn from_dir(shader_dir: impl AsRef<Path>) -> HashMap<String, Self> {
+    pub fn from_dir(shader_dir: impl AsRef<Path>) -> CoalescedResult<HashMap<String, Self>> {
         use std::fs;
         let shader_dir = shader_dir.as_ref();
         let permutation_map = if let Ok(permutations_source) =
@@ -147,6 +193,7 @@ impl ShaderInfo {
         };
         //println!("{permutation_map:?}");
         let imports = preprocess::get_imports(shader_dir);
+        let mut errors = vec![];
         let mut info = HashMap::default();
         let defines: HashSet<_> = if cfg!(feature = "full") {
             vec!["full".to_string()]
@@ -174,18 +221,34 @@ impl ShaderInfo {
                             let mut defines = defines.clone();
                             defines.extend(permutation.defines.iter().cloned());
                             let source = preprocess::preprocess(&contents, &defines, &imports);
-                            let shader_info = Self::new(source.clone(), "main").unwrap();
-                            info.insert(permutation.name.clone(), shader_info);
+                            match Self::new(&permutation.name, source, "main") {
+                                Ok(shader_info) => {
+                                    info.insert(permutation.name.clone(), shader_info);
+                                }
+                                Err(e) => {
+                                    errors.push(e);
+                                }
+                            }
                         }
                     } else {
                         let source = preprocess::preprocess(&contents, &defines, &imports);
-                        let shader_info = Self::new(source.clone(), "main").unwrap();
-                        info.insert(shader_name.to_string(), shader_info);
+                        match Self::new(shader_name, source, "main") {
+                            Ok(shader_info) => {
+                                info.insert(shader_name.to_string(), shader_info);
+                            }
+                            Err(e) => {
+                                errors.push(e);
+                            }
+                        }
                     }
                 }
             }
         }
-        info
+        if !errors.is_empty() {
+            Err(ErrorVec(errors))
+        } else {
+            Ok(info)
+        }
     }
 }
 


Test patch:
diff --git a/vello_tests/Cargo.toml b/vello_tests/Cargo.toml
index 1a56957c..29edfaf0 100644
--- a/vello_tests/Cargo.toml
+++ b/vello_tests/Cargo.toml
@@ -13,7 +13,6 @@ workspace = true
 vello = { workspace = true }
 anyhow = { workspace = true }
 
-wgpu = { workspace = true }
 pollster = { workspace = true }
 png = "0.17.13"
 futures-intrusive = { workspace = true }
diff --git a/vello_tests/src/lib.rs b/vello_tests/src/lib.rs
index efe3d7bf..59246689 100644
--- a/vello_tests/src/lib.rs
+++ b/vello_tests/src/lib.rs
@@ -10,11 +10,11 @@ use std::sync::Arc;
 use anyhow::{anyhow, bail, Result};
 use vello::peniko::{Blob, Color, Format, Image};
 use vello::util::RenderContext;
-use vello::{block_on_wgpu, RendererOptions, Scene};
-use wgpu::{
-    BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
+use vello::wgpu::{
+    self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
     TextureDescriptor, TextureFormat, TextureUsages,
 };
+use vello::{block_on_wgpu, RendererOptions, Scene};
 
 pub struct TestParams {
     pub width: u32,