tirbofish/dropbear · diff
feature: hot reloading shaders
Signature present but could not be verified.
Unverified
@@ -96,6 +96,9 @@ naga = { version = "29", features = ["wgsl-in"] } uuid = { version = "1.22", features = ["v4", "serde"] } splines = "5.0" +notify = "9.0.0-rc.2" +arc-swap = "1.9" + [patch.crates-io] egui = { git = "https://github.com/emilk/egui", branch = "main" } emath = { git = "https://github.com/emilk/egui", branch = "main" } @@ -53,6 +53,8 @@ rkyv.workspace = true bevy_mikktspace.workspace = true wesl.workspace = true egui_extras.workspace = true +arc-swap.workspace = true +notify.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -3,7 +3,7 @@ use crate::model::Vertex; use crate::{BindGroupLayouts, texture}; use crate::{State, egui_renderer::EguiRenderer}; use dropbear_future_queue::FutureQueue; -use egui::{Context, TextureId}; +use egui::TextureId; use glam::{DMat4, DQuat, DVec3, Mat3}; use parking_lot::{Mutex, RwLock}; use std::ops::{Deref, DerefMut}; @@ -1,5 +1,5 @@ use crate::asset::{AssetRegistry, Handle}; -use crate::buffer::{MutableDataBuffer, UniformBuffer}; +use crate::buffer::UniformBuffer; use crate::texture::TextureBuilder; use crate::{ graphics::SharedGraphicsContext, @@ -21,7 +21,6 @@ use wgpu::{ BufferAddress, FilterMode, MipmapFilterMode, VertexAttribute, VertexBufferLayout, util::DeviceExt, }; -use dropbear_utils::Dirty; // note to self: do not derive clone otherwise it wil take too much memory // #[derive(Clone)] @@ -1,6 +1,9 @@ +use std::path::PathBuf; use crate::graphics::SharedGraphicsContext; use crate::shader::Shader; use std::sync::Arc; +use arc_swap::ArcSwap; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; pub mod globals; pub mod hdr; @@ -11,6 +14,80 @@ pub mod builder; pub use globals::{Globals, GlobalsUniform}; +/// A render pipeline that hot-swaps itself when shader source files change. +pub struct HotPipeline { + pipeline: Arc<ArcSwap<wgpu::RenderPipeline>>, +} + +impl HotPipeline { + pub fn new<F>( + device: Arc<wgpu::Device>, + watch_dir: PathBuf, + factory: F, + ) -> anyhow::Result<Self> + where + F: Fn(&wgpu::Device) -> anyhow::Result<wgpu::RenderPipeline> + Send + Sync + 'static, + { + let factory = Arc::new(factory); + + let initial = factory(&device)?; + let pipeline = Arc::new(ArcSwap::from_pointee(initial)); + + if !watch_dir.exists() { + log::warn!("HotPipeline: watch directory does not exist: {watch_dir:?}"); + return Ok(Self { pipeline }); + } + + let (tx, rx) = std::sync::mpsc::channel::<notify::Result<notify::Event>>(); + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default())?; + watcher.watch(&watch_dir, RecursiveMode::Recursive)?; + + let pipeline_ref = pipeline.clone(); + std::thread::spawn(move || { + let _watcher = watcher; // keep the watcher alive inside the thread + for event in rx { + match event { + Ok(ev) => { + use notify::EventKind::*; + match ev.kind { + Modify(_) | Create(_) => { + log::info!("Shader change detected: {:?}", ev.paths); + let result = std::panic::catch_unwind( + std::panic::AssertUnwindSafe(|| factory(&device)), + ); + match result { + Ok(Ok(new_pipeline)) => { + pipeline_ref.store(Arc::new(new_pipeline)); + log::info!("Pipeline hot-reloaded successfully"); + } + Ok(Err(e)) => { + log::error!( + "Shader compile error, keeping old pipeline: {e}" + ); + } + Err(_) => { + log::error!( + "Shader factory panicked during hot reload, keeping old pipeline" + ); + } + } + } + _ => {} + } + } + Err(e) => log::error!("Shader watcher error: {e}"), + } + } + }); + + Ok(Self { pipeline }) + } + + pub fn get(&self) -> arc_swap::Guard<Arc<wgpu::RenderPipeline>> { + self.pipeline.load() + } +} + /// A helper in defining a pipelines required information, as well as getters. /// /// This contains the bare minimum for any pipeline. @@ -1,17 +1,15 @@ use crate::graphics::{InstanceRaw, SharedGraphicsContext}; use crate::model; use crate::model::Vertex; -use crate::pipelines::DropbearShaderPipeline; -use crate::shader::Shader; +use crate::pipelines::HotPipeline; use crate::texture::Texture; use std::sync::Arc; +use wesl::ModulePath; use wgpu::{CompareFunction, DepthBiasState, StencilState}; /// As defined in `shaders/shader.wesl` pub struct MainRenderPipeline { - shader: Shader, - pipeline_layout: wgpu::PipelineLayout, - pipeline: wgpu::RenderPipeline, + pipeline: HotPipeline, pub per_frame: Option<wgpu::BindGroup>, pub per_material: Option<wgpu::BindGroup>, @@ -19,92 +17,113 @@ pub struct MainRenderPipeline { pub environment: Option<wgpu::BindGroup>, } -impl DropbearShaderPipeline for MainRenderPipeline { - fn new(graphics: Arc<SharedGraphicsContext>) -> Self { - let source = wesl::Wesl::new("src/shaders") - .add_package(&crate::shader::code::PACKAGE) - .compile(&"dropbear_shaders::shader".parse().unwrap()) - .inspect_err(|e| { - panic!("{e}"); - }) - .unwrap() - .to_string(); - - let shader = Shader::new( - graphics.clone(), - &source, - Some("viewport shaders"), - ); - - let bind_group_layouts = vec![ - Some(&graphics.layouts.per_frame_layout), - Some(&graphics.layouts.material_bind_layout), - Some(&graphics.layouts.animation_layout), - Some(&graphics.layouts.environment_layout), - ]; - - let pipeline_layout = +impl MainRenderPipeline { + pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let pipeline_layout = Arc::new( graphics .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("main render pipeline layout"), - bind_group_layouts: bind_group_layouts.as_slice(), + bind_group_layouts: &[ + Some(&graphics.layouts.per_frame_layout), + Some(&graphics.layouts.material_bind_layout), + Some(&graphics.layouts.animation_layout), + Some(&graphics.layouts.environment_layout), + ], immediate_size: 0, - }); + }), + ); let hdr_format = graphics.hdr.read().format(); let sample_count: u32 = (*graphics.antialiasing.read()).into(); - let pipeline = graphics - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("main render pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader.module, - entry_point: Some("s_fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: hdr_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Cw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: Texture::DEPTH_FORMAT, - depth_write_enabled: Some(true), - depth_compare: Some(CompareFunction::Greater), - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: sample_count, - mask: !0, - alpha_to_coverage_enabled: false, - }, - cache: None, - multiview_mask: None, - }); + let device = graphics.device.clone(); + + let shader_dir = std::path::PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/shaders" + )); + + let pipeline = HotPipeline::new( + device, + shader_dir.clone(), + move |device| { + let source = wesl::Wesl::new(&shader_dir) + .compile(&ModulePath::from_path("/shader.wesl")) + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(); + + // fixes early read + for ep in ["vs_main", "s_fs_main"] { + if !source.contains(&format!("fn {ep}(")) { + return Err(anyhow::anyhow!( + "compiled shader is missing entry point '{ep}' \ + (file may have been read mid-write)" + )); + } + } + + log::debug!("Compiled WGSL: {} bytes", source.len()); + + wgpu::naga::front::wgsl::parse_str(&source) + .map_err(|e| anyhow::anyhow!("WGSL parse error: {}", e.emit_to_string(&source)))?; + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("viewport shaders"), + source: wgpu::ShaderSource::Wgsl(source.into()), + }); - log::debug!("Created main render pipeline"); + let render_pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("main render pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("s_fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: hdr_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Cw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: Texture::DEPTH_FORMAT, + depth_write_enabled: Some(true), + depth_compare: Some(CompareFunction::Greater), + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + cache: None, + multiview_mask: None, + }); + + log::debug!("Created main render pipeline"); + Ok(render_pipeline) + }, + ) + .expect("failed to build initial main render pipeline"); Self { - shader, - pipeline_layout, pipeline, per_frame: None, per_material: None, @@ -113,16 +132,8 @@ impl DropbearShaderPipeline for MainRenderPipeline { } } - fn shader(&self) -> &Shader { - &self.shader - } - - fn pipeline_layout(&self) -> &wgpu::PipelineLayout { - &self.pipeline_layout - } - - fn pipeline(&self) -> &wgpu::RenderPipeline { - &self.pipeline + pub fn pipeline(&self) -> arc_swap::Guard<Arc<wgpu::RenderPipeline>> { + self.pipeline.get() } } @@ -343,7 +343,7 @@ impl RendererCommon { timestamp_writes: None, multiview_mask: None, }); - pass.set_pipeline(pipeline.pipeline()); + pass.set_pipeline(&pipeline.pipeline()); pass.set_vertex_buffer(1, instance_buffer.slice(static_count as usize)); for mesh in &model.meshes { @@ -445,7 +445,7 @@ impl RendererCommon { timestamp_writes: None, multiview_mask: None, }); - pass.set_pipeline(pipeline.pipeline()); + pass.set_pipeline(&pipeline.pipeline()); pass.set_vertex_buffer(1, instance_buffer.slice(1)); for mesh in &model.meshes { @@ -60,6 +60,8 @@ puffin.workspace = true image.workspace = true rkyv.workspace = true wesl.workspace = true +notify.workspace = true +arc-swap.workspace = true [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -66,6 +66,7 @@ use std::cmp::PartialEq; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant}; +use arc_swap::ArcSwap; use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; use wgpu::{Color, Extent3d}; @@ -1623,8 +1624,8 @@ impl Editor { graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, skybox_texture: Option<&Vec<u8>>, ) { - self.main_render_pipeline = Some(MainRenderPipeline::new(graphics.clone())); - self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone())); + let mut main_render_pipeline = MainRenderPipeline::new(graphics.clone()); + let light_cube_pipeline = LightCubePipeline::new(graphics.clone()); self.shader_globals = Some(GlobalsUniform::new( graphics.clone(), Some("editor shader globals"), @@ -1659,18 +1660,13 @@ impl Editor { if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() { self.animation_pipeline = Some(AnimationDefaults::new(graphics.clone())); - if let Some(main_pipeline) = self.main_render_pipeline.as_mut() { - if let (Some(globals), Some(light_pipeline)) = ( - self.shader_globals.as_ref(), - self.light_cube_pipeline.as_ref(), - ) { - let _ = main_pipeline.per_frame_bind_group( - graphics.clone(), - globals.buffer.buffer(), - camera.buffer(), - light_pipeline.light_buffer(), - ); - } + if let Some(globals) = self.shader_globals.as_ref() { + let _ = main_render_pipeline.per_frame_bind_group( + graphics.clone(), + globals.buffer.buffer(), + camera.buffer(), + light_cube_pipeline.light_buffer(), + ); } let sky_texture_result = HdrLoader::from_equirectangular_bytes( @@ -1703,6 +1699,10 @@ impl Editor { if let Some(sky_pipeline) = pending_sky_pipeline { self.sky_pipeline = Some(sky_pipeline); } + + // leave to last + self.main_render_pipeline = Some(main_render_pipeline); + self.light_cube_pipeline = Some(light_cube_pipeline); } /// Initialises another eucalyptus-editor play mode app as a separate process and monitors it in a separate thread. @@ -8,7 +8,6 @@ use dropbear_engine::{ lighting::Light, scene::{Scene, SceneCommand}, }; -use egui::UiBuilder; use eucalyptus_core::billboard::BillboardComponent; use eucalyptus_core::component::KotlinComponentDecl; use eucalyptus_core::properties::CustomProperties; @@ -514,6 +513,7 @@ impl Scene for Editor { log_once::warn_once!("Animation pipeline not ready"); return; }; + let per_frame_bind_group = pipeline.per_frame.as_ref() .expect("Per-frame bind group not initialised") .clone(); @@ -524,7 +524,7 @@ impl Scene for Editor { &graphics, &mut encoder, &hdr, &self.world, &batches, &model_cache, &per_frame_bind_group, environment_bind_group, - pipeline, animation_defaults, + &pipeline, animation_defaults, &self.instance_buffer_cache, &mut self.animated_instance_buffers, &mut self.animated_bind_group_cache, @@ -5,6 +5,7 @@ use glam::{Mat4, Vec2, Vec4}; use kino_ui::camera::Camera2D; use std::sync::Arc; use wgpu::TextureFormat; +use dropbear_engine::pipelines::HotPipeline; pub mod inspector; pub mod viewport; @@ -117,15 +118,11 @@ pub struct UIGridPipeline { camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, - pipeline: wgpu::RenderPipeline, + pipeline: HotPipeline, } impl UIGridPipeline { pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self { - let shader = graphics - .device - .create_shader_module(wgpu::include_wgsl!("shader/grid.wgsl")); - let camera_bind_group_layout = graphics .device @@ -161,56 +158,77 @@ impl UIGridPipeline { }], }); - let layout = graphics - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("ui grid pipeline layout"), - bind_group_layouts: &[Some(&camera_bind_group_layout)], - immediate_size: 0, - }); + let layout = std::sync::Arc::new( + graphics + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("ui grid pipeline layout"), + bind_group_layouts: &[Some(&camera_bind_group_layout)], + immediate_size: 0, + }), + ); let sample_count: u32 = (*graphics.antialiasing.read()).into(); let format = graphics.surface_format.add_srgb_suffix(); - let pipeline = graphics - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("ui grid pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: sample_count, - mask: !0, - alpha_to_coverage_enabled: false, - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - compilation_options: Default::default(), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - cache: None, - multiview_mask: None, - }); + let device = graphics.device.clone(); + let shader_dir = std::path::PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/editor/ui/shader" + )); + let shader_file = shader_dir.join("grid.wgsl"); + + let pipeline = HotPipeline::new( + device, + shader_dir, + move |device| { + let source = std::fs::read_to_string(&shader_file)?; + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ui grid shader"), + source: wgpu::ShaderSource::Wgsl(source.into()), + }); + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("ui grid pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + cache: None, + multiview_mask: None, + }); + Ok(pipeline) + }, + ) + .expect("failed to build initial grid pipeline"); Self { size: wgpu::Extent3d { @@ -377,6 +395,8 @@ impl UIGridPipeline { (resolve_view, None) }; + let pipeline = self.pipeline.get(); + { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("ui viewport grid render pass"), @@ -395,17 +415,13 @@ impl UIGridPipeline { multiview_mask: None, }); - self.render(&mut render_pass); + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, &self.camera_bind_group, &[]); + render_pass.draw(0..3, 0..1); } graphics.queue.submit(std::iter::once(encoder.finish())); } - - pub fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.camera_bind_group, &[]); - render_pass.draw(0..3, 0..1); - } } #[repr(C)] @@ -10,6 +10,7 @@ pub mod signal; pub mod spawn; pub mod stats; pub mod utils; +pub mod shader; // pub mod outline; use editor::docks::asset_viewer::AssetViewerDock; @@ -6,7 +6,7 @@ use dropbear_engine::{ input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}, }; -use egui::{self, FontId, Frame, RichText, Ui, UiBuilder}; +use egui::{self, FontId, Frame, RichText, Ui}; use egui_toast::{ToastOptions, Toasts}; use eucalyptus_core::config::ProjectConfig; use eucalyptus_core::states::PROJECT; @@ -3,7 +3,7 @@ use std::{collections::VecDeque, time::Instant}; use dropbear_engine::WGPU_BACKEND; use dropbear_engine::input::{Controller, Keyboard, Mouse}; use dropbear_engine::scene::Scene; -use egui::{Color32, Context, RichText, Ui}; +use egui::{Color32, RichText, Ui}; use egui_plot::{Legend, Line, Plot, PlotPoints}; use dropbear_engine::gilrs;