use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use super::context::DxContext;
macro_rules! rebuild_if_live {
($cond:expr_2021, $build:expr_2021 $(,)?) => {
if $cond { Some($build?) } else { None }
};
}
const SHADER_EXTENSIONS: &[&str] = &["hlsl", "slang"];
pub(crate) struct WatcherHandle {
#[expect(
dead_code,
reason = "notify keeps its listener thread alive while the handle lives; never read after construction"
)]
watcher: notify::RecommendedWatcher,
}
pub(crate) fn spawn(flag: Arc<AtomicBool>) -> Option<WatcherHandle> {
let dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("directx")
.join("shaders");
if !dir.is_dir() {
tracing::warn!(
"hot-reload: shader source dir {} not found; watcher disabled (debug \
command still works)",
dir.display()
);
return None;
}
let debounce = Duration::from_millis(150);
let last_fire = std::sync::Mutex::new(Instant::now() - debounce);
let flag_for_cb = Arc::clone(&flag);
let mut watcher = match notify::recommended_watcher(move |res: notify::Result<Event>| {
let event = match res {
Ok(e) => e,
Err(e) => {
tracing::debug!("hot-reload watcher error: {e}");
return;
}
};
if !is_relevant(&event) {
return;
}
let mut last = match last_fire.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let now = Instant::now();
if now.duration_since(*last) < debounce {
return;
}
*last = now;
tracing::info!(
"hot-reload: detected change to {:?}, scheduling shader rebuild",
event.paths
);
flag_for_cb.store(true, Ordering::SeqCst);
}) {
Ok(w) => w,
Err(e) => {
tracing::warn!("hot-reload: failed to create notify watcher: {e}");
return None;
}
};
if let Err(e) = watcher.watch(&dir, RecursiveMode::NonRecursive) {
tracing::warn!(
"hot-reload: failed to watch {} ({}); watcher disabled",
dir.display(),
e
);
return None;
}
let slang_dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("shaders");
if slang_dir.is_dir()
&& let Err(e) = watcher.watch(&slang_dir, RecursiveMode::NonRecursive)
{
tracing::warn!(
"hot-reload: failed to watch {} ({e}); .slang edits will not trigger reloads",
slang_dir.display()
);
}
tracing::info!(
"hot-reload: watching {} for {} changes",
dir.display(),
SHADER_EXTENSIONS.join("/"),
);
Some(WatcherHandle { watcher })
}
fn is_relevant(event: &Event) -> bool {
if !matches!(
event.kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
) {
return false;
}
event.paths.iter().any(|p| {
p.extension().and_then(|e| e.to_str()).is_some_and(|e| {
SHADER_EXTENSIONS
.iter()
.any(|&se| se.eq_ignore_ascii_case(e))
})
})
}
impl DxContext {
pub(super) fn shader_reload_requested(&self) -> bool {
self.hot_reload
.reload_pending
.as_ref()
.map(|f| f.load(Ordering::SeqCst))
.unwrap_or(false)
}
pub(super) fn clear_shader_reload_flag(&self) {
if let Some(flag) = &self.hot_reload.reload_pending {
flag.store(false, Ordering::SeqCst);
}
}
pub(super) fn reload_shaders(&mut self) -> Result<(), String> {
if !self.hot_reload.enabled {
return Ok(());
}
let device = &self.device;
let info_queue = self.diagnostics.info_queue.as_ref();
let hr = true;
let (composite_vs, composite_ps) = super::pipeline::compile_composite_shaders(hr)?;
let composite_pso = super::context::dump_on_err(
info_queue,
super::pipeline::create_composite_pso(
device,
&self.composite.root_sig,
&composite_vs,
&composite_ps,
self.swapchain.format,
),
)?;
let text_pso = rebuild_if_live!(self.text.pso.is_some(), {
let (text_vs, text_ps) = super::pipeline::compile_text_shaders(hr)?;
super::context::dump_on_err(
info_queue,
super::pipeline::create_text_pso(
device,
&self.text.root_sig,
&text_vs,
&text_ps,
self.swapchain.format,
1,
),
)
});
let bloom_shaders = super::post::bloom::compile_bloom_shaders(hr)?;
let bloom_prefilter = super::context::dump_on_err(
info_queue,
super::post::bloom::create_bloom_pso(
device,
&self.bloom.root_sig,
&bloom_shaders.vs,
&bloom_shaders.prefilter_ps,
super::texture::HDR_FORMAT,
false,
),
)?;
let bloom_downsample = super::context::dump_on_err(
info_queue,
super::post::bloom::create_bloom_pso(
device,
&self.bloom.root_sig,
&bloom_shaders.vs,
&bloom_shaders.downsample_ps,
super::texture::HDR_FORMAT,
false,
),
)?;
let bloom_upsample = super::context::dump_on_err(
info_queue,
super::post::bloom::create_bloom_pso(
device,
&self.bloom.root_sig,
&bloom_shaders.vs,
&bloom_shaders.upsample_ps,
super::texture::HDR_FORMAT,
true,
),
)?;
let bindless_main_pso = rebuild_if_live!(
self.cull.main_bindless_root_sig.is_some() && self.cull.main_bindless_pso.is_some(),
{
let (vs, ps) = super::init::pipelines::compile_main_bindless_shaders(hr)?;
let engine_pair = super::init::pipelines::BindlessMainShaders { vs, ps };
let pso = self.build_world_main_pso(self.world_shader.as_ref(), &engine_pair)?;
Ok::<_, String>((pso, engine_pair))
}
);
let cull_pso = rebuild_if_live!(
self.cull.cull_root_sig.is_some() && self.cull.cull_pso.is_some(),
{
let cs = super::cull::compile_cull_shader(hr)?;
super::context::dump_on_err(
info_queue,
super::cull::create_cull_pso(
device,
self.cull
.cull_root_sig
.as_ref()
.expect("cull root signature is live alongside its PSO"),
&cs,
),
)
}
);
let cull_pso_phase2 = rebuild_if_live!(
self.cull.cull_root_sig.is_some() && self.cull.cull_pso_phase2.is_some(),
{
let cs2 = super::cull::compile_cull_shader_phase2(hr)?;
super::context::dump_on_err(
info_queue,
super::cull::create_cull_pso(
device,
self.cull
.cull_root_sig
.as_ref()
.expect("cull root signature is live alongside its phase-2 PSO"),
&cs2,
),
)
}
);
let hiz_rebuilt = if let Some(hiz) = self.cull.hiz.as_ref() {
let (init_single_cs, init_msaa_cs, downsample_cs) =
super::hiz::compile_hiz_shaders(hr)?;
let init_single_pso = super::context::dump_on_err(
info_queue,
super::auto_exposure::create_compute_pso(
device,
&hiz.root_sig,
&init_single_cs,
"hiz init_single",
),
)?;
let init_msaa_pso = super::context::dump_on_err(
info_queue,
super::auto_exposure::create_compute_pso(
device,
&hiz.root_sig,
&init_msaa_cs,
"hiz init_msaa",
),
)?;
let downsample_pso = super::context::dump_on_err(
info_queue,
super::auto_exposure::create_compute_pso(
device,
&hiz.root_sig,
&downsample_cs,
"hiz downsample",
),
)?;
Some((init_single_pso, init_msaa_pso, downsample_pso))
} else {
None
};
let (auto_exp_build, auto_exp_average) = if let Some(ae) =
self.auto_exposure.resources.as_ref()
{
let (build_cs, average_cs) = super::auto_exposure::compile_auto_exposure_shaders(hr)?;
let build_pso = super::context::dump_on_err(
info_queue,
super::auto_exposure::create_compute_pso(
device,
ae.build_root_sig(),
&build_cs,
"auto-exposure build",
),
)?;
let average_pso = super::context::dump_on_err(
info_queue,
super::auto_exposure::create_compute_pso(
device,
ae.average_root_sig(),
&average_cs,
"auto-exposure average",
),
)?;
(Some(build_pso), Some(average_pso))
} else {
(None, None)
};
let decal_pso = rebuild_if_live!(
self.decal.state.is_some(),
super::decal::rebuild_decal_pso(
device,
&self
.decal
.state
.as_ref()
.expect("decal state is live")
.root_sig,
self.hdr.msaa_samples,
hr,
info_queue,
)
);
let line_pso = rebuild_if_live!(
self.lines.resources.is_some(),
super::line::rebuild_line_pso(
device,
self.lines
.resources
.as_ref()
.expect("line resources are live"),
self.hdr.msaa_samples,
hr,
info_queue,
)
);
let glass_pso = rebuild_if_live!(
self.transparent.as_ref().is_some_and(|t| t.has_glass()),
super::glass::rebuild_glass_pso(
device,
self.transparent
.as_ref()
.expect("transparent resources are live")
.root_sig(),
self.hdr.msaa_samples,
hr,
info_queue,
)
);
let water_pso = rebuild_if_live!(
self.transparent.as_ref().is_some_and(|t| t.has_water()),
super::water::rebuild_water_pso(
device,
self.transparent
.as_ref()
.expect("transparent resources are live")
.root_sig(),
self.hdr.msaa_samples,
hr,
info_queue,
)
);
let fog_pso = rebuild_if_live!(
self.fog.resources.is_some(),
super::fog::rebuild_fog_pso(
device,
&self
.fog
.resources
.as_ref()
.expect("fog resources are live")
.root_sig,
self.hdr.msaa_samples,
hr,
info_queue,
)
);
let fog_froxel_pso = rebuild_if_live!(
self.fog.resources.is_some(),
super::fog::rebuild_fog_froxel_pso(
device,
&self
.fog
.resources
.as_ref()
.expect("fog resources are live")
.froxel_root_sig,
hr,
info_queue,
)
);
let ssao_rebuilt = rebuild_if_live!(
self.ssao.resources.is_some(),
super::post::ssao::rebuild_ssao_pipelines(
device,
self.ssao
.resources
.as_ref()
.expect("SSAO resources are live"),
hr,
info_queue
)
);
let ssr_rebuilt = rebuild_if_live!(
self.ssr.is_some(),
super::post::ssr::rebuild_ssr_pipelines(
device,
self.ssr.as_ref().expect("SSR resources are live"),
hr,
info_queue
)
);
let ssgi_rebuilt = rebuild_if_live!(
self.ssgi.is_some(),
super::post::ssgi::rebuild_ssgi_pipelines(
device,
self.ssgi.as_ref().expect("SSGI resources are live"),
hr,
info_queue
)
);
let taa_rebuilt = rebuild_if_live!(
self.taa.is_some(),
super::post::taa::rebuild_taa_pipelines(
device,
self.taa.as_ref().expect("TAA resources are live"),
hr,
info_queue
)
);
let rt_rebuilt = rebuild_if_live!(
self.rt_reflections.is_some(),
super::post::rt_reflections::rebuild_rt_reflections_pipelines(
device,
self.rt_reflections
.as_ref()
.expect("RT reflection resources are live"),
hr,
info_queue
)
);
let refl_composite_rebuilt = rebuild_if_live!(
self.reflection_composite.is_some(),
super::post::reflection_composite::rebuild_reflection_composite_pipelines(
device,
self.reflection_composite
.as_ref()
.expect("reflection composite resources are live"),
hr,
info_queue
)
);
self.composite.pso = composite_pso;
if let Some(p) = text_pso {
self.text.pso = Some(p);
}
self.bloom.pso_prefilter = bloom_prefilter;
self.bloom.pso_downsample = bloom_downsample;
self.bloom.pso_upsample = bloom_upsample;
if let Some((p, engine_pair)) = bindless_main_pso {
self.cull.main_bindless_pso = Some(p);
self.bindless_main_shaders = engine_pair;
}
self.invalidate_wireframe_pipelines();
if let Some(p) = cull_pso {
self.cull.cull_pso = Some(p);
}
if let Some(p) = cull_pso_phase2 {
self.cull.cull_pso_phase2 = Some(p);
}
if let (Some((init_s, init_m, ds)), Some(hiz)) = (hiz_rebuilt, self.cull.hiz.as_mut()) {
hiz.swap_pipelines(init_s, init_m, ds);
}
if let (Some(build), Some(average), Some(ae)) = (
auto_exp_build,
auto_exp_average,
self.auto_exposure.resources.as_mut(),
) {
ae.swap_pipelines(build, average);
}
if let (Some(pso), Some(decals)) = (decal_pso, self.decal.state.as_mut()) {
decals.pso = pso;
}
if let (Some(pso), Some(lines)) = (line_pso, self.lines.resources.as_mut()) {
lines.pso = pso;
}
if let Some(transparent) = self.transparent.as_mut() {
transparent.swap_pipelines(glass_pso, water_pso);
}
if let (Some(pso), Some(fog)) = (fog_pso, self.fog.resources.as_mut()) {
fog.pso = pso;
}
if let (Some(pso), Some(fog)) = (fog_froxel_pso, self.fog.resources.as_mut()) {
fog.froxel_pso = pso;
}
if let (Some(rebuilt), Some(ssao)) = (ssao_rebuilt, self.ssao.resources.as_mut()) {
swap_ssao_pipelines(ssao, rebuilt);
}
if let (Some(rebuilt), Some(ssr)) = (ssr_rebuilt, self.ssr.as_mut()) {
swap_ssr_pipelines(ssr, rebuilt);
}
if let (Some(rebuilt), Some(ssgi)) = (ssgi_rebuilt, self.ssgi.as_mut()) {
super::post::ssgi::swap_ssgi_pipelines(ssgi, rebuilt);
}
if let (Some(rebuilt), Some(rt)) = (rt_rebuilt, self.rt_reflections.as_mut()) {
super::post::rt_reflections::swap_rt_reflections_pipelines(rt, rebuilt);
}
if let (Some(rebuilt), Some(rc)) =
(refl_composite_rebuilt, self.reflection_composite.as_mut())
{
super::post::reflection_composite::swap_reflection_composite_pipelines(rc, rebuilt);
}
if let (Some(rebuilt), Some(taa)) = (taa_rebuilt, self.taa.as_mut()) {
swap_taa_pipelines(taa, rebuilt);
}
Ok(())
}
}
fn swap_ssao_pipelines(
ssao: &mut super::post::ssao::SsaoResources,
rebuilt: super::post::ssao::RebuiltSsaoPipelines,
) {
ssao.kernel_pso = rebuilt.kernel_pso;
ssao.blur_pso = rebuilt.blur_pso;
}
fn swap_ssr_pipelines(
ssr: &mut super::post::ssr::SsrResources,
rebuilt: super::post::ssr::RebuiltSsrPipelines,
) {
if let (Some(pso), Some(resolve)) = (rebuilt.resolve_pso, ssr.resolve.as_mut()) {
resolve.resolve_pso = pso;
}
}
fn swap_taa_pipelines(
taa: &mut super::post::taa::TaaResources,
rebuilt: super::post::taa::RebuiltTaaPipelines,
) {
taa.taa_pso = rebuilt.taa_pso;
}