From ed600311855155a70a2f209bae3ab73e6f68acd1 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:24:48 +0000 Subject: [PATCH] Add hot reloading of shaders to the winit example (#252) --- Cargo.lock | 11 ++++++++ Cargo.toml | 1 + examples/with_winit/Cargo.toml | 4 +++ examples/with_winit/src/hot_reload.rs | 28 +++++++++++++++++++ examples/with_winit/src/main.rs | 40 +++++++++++++++++++++++---- src/lib.rs | 15 ++++++++++ src/shaders.rs | 26 +++++++++++++++-- 7 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 examples/with_winit/src/hot_reload.rs diff --git a/Cargo.lock b/Cargo.lock index 6815187..59302d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2452,6 +2452,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "notify-debouncer-mini" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e23e9fa24f094b143c1eb61f90ac6457de87be6987bc70746e0179f7dbc9007b" +dependencies = [ + "crossbeam-channel", + "notify", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4186,6 +4196,7 @@ dependencies = [ "clap", "console_error_panic_hook", "console_log", + "notify-debouncer-mini", "pollster", "roxmltree", "vello", diff --git a/Cargo.toml b/Cargo.toml index c2419ed..3699772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,4 +29,5 @@ moscato = { git = "https://github.com/dfrg/pinot" } peniko = { git = "https://github.com/linebender/peniko" } [features] +hot_reload = [] buffer_labels = [] diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index fd7b9b6..2fa87a9 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -16,6 +16,10 @@ pollster = "0.2.5" roxmltree = "0.13" clap = { version = "4.1.0", features = ["derive"] } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +vello = { path = "../../", features = ["hot_reload"] } +notify-debouncer-mini = "0.2.1" + [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" console_log = "0.2" diff --git a/examples/with_winit/src/hot_reload.rs b/examples/with_winit/src/hot_reload.rs new file mode 100644 index 0000000..4a54e06 --- /dev/null +++ b/examples/with_winit/src/hot_reload.rs @@ -0,0 +1,28 @@ +use std::{path::Path, time::Duration}; + +use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; + +pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> impl Sized { + let mut debouncer = new_debouncer( + Duration::from_millis(500), + None, + move |res: DebounceEventResult| match res { + Ok(_) => f().unwrap(), + Err(errors) => errors.iter().for_each(|e| println!("Error {:?}", e)), + }, + ) + .unwrap(); + + debouncer + .watcher() + .watch( + dbg!(&Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../shader") + .canonicalize() + .unwrap()), + // We currently don't support hot reloading the imports, so don't recurse into there + RecursiveMode::NonRecursive, + ) + .expect("Could watch shaders directory"); + debouncer +} diff --git a/examples/with_winit/src/main.rs b/examples/with_winit/src/main.rs index c17f131..f48e9a1 100644 --- a/examples/with_winit/src/main.rs +++ b/examples/with_winit/src/main.rs @@ -18,7 +18,7 @@ mod pico_svg; mod simple_text; mod test_scene; -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, path::PathBuf, time::Instant}; use clap::Parser; use vello::{ @@ -26,7 +26,13 @@ use vello::{ util::RenderContext, Renderer, Scene, SceneBuilder, }; -use winit::{event_loop::EventLoop, window::Window}; +use winit::{ + event_loop::{EventLoop, EventLoopBuilder}, + window::Window, +}; + +#[cfg(not(target_arch = "wasm32"))] +mod hot_reload; #[derive(Parser, Debug)] #[command(about, long_about = None)] @@ -46,7 +52,7 @@ struct Args { const TIGER: &'static str = include_str!("../../assets/Ghostscript_Tiger.svg"); -async fn run(event_loop: EventLoop<()>, window: Window, args: Args) { +async fn run(event_loop: EventLoop, window: Window, args: Args) { use winit::{event::*, event_loop::ControlFlow}; let mut render_cx = RenderContext::new().unwrap(); let size = window.inner_size(); @@ -194,16 +200,40 @@ async fn run(event_loop: EventLoop<()>, window: Window, args: Args) { surface_texture.present(); device_handle.device.poll(wgpu::Maintain::Wait); } + Event::UserEvent(event) => match event { + #[cfg(not(target_arch = "wasm32"))] + UserEvent::HotReload => { + let device_handle = &render_cx.devices[surface.dev_id]; + eprintln!("==============\nReloading shaders"); + let start = Instant::now(); + let result = renderer.reload_shaders(&device_handle.device); + // We know that the only async here is actually sync, so we just block + match pollster::block_on(result) { + Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()), + Err(e) => eprintln!("Failed to reload shaders because of {e}"), + } + } + }, _ => {} }); } +enum UserEvent { + #[cfg(not(target_arch = "wasm32"))] + HotReload, +} + fn main() { let args = Args::parse(); #[cfg(not(target_arch = "wasm32"))] { use winit::{dpi::LogicalSize, window::WindowBuilder}; - let event_loop = EventLoop::new(); + let event_loop = EventLoopBuilder::::with_user_event().build(); + + let proxy = event_loop.create_proxy(); + let _keep = + hot_reload::hot_reload(move || proxy.send_event(UserEvent::HotReload).ok().map(drop)); + let window = WindowBuilder::new() .with_inner_size(LogicalSize::new(1044, 800)) .with_resizable(true) @@ -214,7 +244,7 @@ fn main() { } #[cfg(target_arch = "wasm32")] { - let event_loop = EventLoop::new(); + let event_loop = EventLoopBuilder::::with_user_event().build(); let window = winit::window::Window::new(&event_loop).unwrap(); std::panic::set_hook(Box::new(console_error_panic_hook::hook)); diff --git a/src/lib.rs b/src/lib.rs index 585ec09..6dc7bb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,6 +149,21 @@ impl Renderer { self.target = Some(target); Ok(()) } + + /// 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<()> { + device.push_error_scope(wgpu::ErrorFilter::Validation); + let mut engine = Engine::new(); + let shaders = shaders::full_shaders(device, &mut engine)?; + let error = device.pop_error_scope().await; + if let Some(error) = error { + return Err(error.into()); + } + self.engine = engine; + self.shaders = shaders; + Ok(()) + } } struct TargetTexture { diff --git a/src/shaders.rs b/src/shaders.rs index 6640938..6e09a97 100644 --- a/src/shaders.rs +++ b/src/shaders.rs @@ -31,9 +31,29 @@ pub const PATH_DRAWOBJ_WG: u32 = 256; pub const CLIP_REDUCE_WG: u32 = 256; macro_rules! shader { - ($name:expr) => { - include_str!(concat!("../shader/", $name, ".wgsl")) - }; + ($name:expr) => {&{ + let shader = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/shader/", + $name, + ".wgsl" + )); + #[cfg(feature = "hot_reload")] + let shader = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/shader/", + $name, + ".wgsl" + )) + .unwrap_or_else(|e| { + eprintln!( + "Failed to read shader {name}, error falling back to version at compilation time. Error: {e:?}", + name = $name + ); + shader.to_string() + }); + shader + }}; } pub struct Shaders {