1use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ShaderStage {
15 Vertex,
16 Fragment,
17 Compute,
18}
19
20#[derive(Debug, Clone)]
22pub struct EntryPoint {
23 pub name: String,
24 pub stage: ShaderStage,
25}
26
27impl EntryPoint {
28 pub fn new(name: impl Into<String>, stage: ShaderStage) -> Self {
30 Self {
31 name: name.into(),
32 stage,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
41pub struct ShaderSource {
42 pub label: String,
44 pub wgsl_source: String,
46 pub entry_points: Vec<EntryPoint>,
48 pub version: u64,
51 pub last_modified: u64,
54}
55
56impl ShaderSource {
57 fn new(label: impl Into<String>, wgsl_source: impl Into<String>) -> Self {
59 let wgsl = wgsl_source.into();
60 let entry_points = parse_entry_points(&wgsl);
61 Self {
62 label: label.into(),
63 wgsl_source: wgsl,
64 entry_points,
65 version: 1,
66 last_modified: 0,
67 }
68 }
69
70 fn bump(&mut self, new_wgsl: impl Into<String>) {
72 self.wgsl_source = new_wgsl.into();
73 self.entry_points = parse_entry_points(&self.wgsl_source);
74 self.version += 1;
75 self.last_modified = current_unix_secs();
76 }
77}
78
79fn parse_entry_points(wgsl: &str) -> Vec<EntryPoint> {
84 let mut entries = Vec::new();
85 let mut lines = wgsl.lines().peekable();
86
87 while let Some(line) = lines.next() {
88 let trimmed = line.trim();
89
90 let stage_opt = if trimmed.contains("@vertex") {
92 Some(ShaderStage::Vertex)
93 } else if trimmed.contains("@fragment") {
94 Some(ShaderStage::Fragment)
95 } else if trimmed.contains("@compute") {
96 Some(ShaderStage::Compute)
97 } else {
98 None
99 };
100
101 if let Some(stage) = stage_opt {
102 let fn_name = extract_fn_name(trimmed)
104 .or_else(|| lines.peek().and_then(|next| extract_fn_name(next.trim())));
105
106 if let Some(name) = fn_name {
107 entries.push(EntryPoint::new(name, stage));
108 }
109 }
110 }
111
112 entries
113}
114
115fn extract_fn_name(line: &str) -> Option<String> {
117 let idx = line.find("fn ")?;
118 let after = line[idx + 3..].trim();
119 let end = after
121 .find(|c: char| c == '(' || c.is_whitespace())
122 .unwrap_or(after.len());
123 if end == 0 {
124 return None;
125 }
126 Some(after[..end].to_owned())
127}
128
129fn current_unix_secs() -> u64 {
132 use std::time::{SystemTime, UNIX_EPOCH};
133 SystemTime::now()
134 .duration_since(UNIX_EPOCH)
135 .map(|d| d.as_secs())
136 .unwrap_or(0)
137}
138
139#[derive(Debug, Clone)]
144pub struct ShaderChangeEvent {
145 pub label: String,
146 pub old_version: u64,
147 pub new_version: u64,
148}
149
150pub struct ShaderWatcher {
159 pub watch_paths: Vec<String>,
161 pub poll_interval_ms: u64,
163 pub sources: HashMap<String, ShaderSource>,
165 snapshot: HashMap<String, u64>,
167}
168
169impl ShaderWatcher {
170 pub fn new(poll_interval_ms: u64) -> Self {
172 Self {
173 watch_paths: Vec::new(),
174 poll_interval_ms,
175 sources: HashMap::new(),
176 snapshot: HashMap::new(),
177 }
178 }
179
180 pub fn add_path(&mut self, path: impl Into<String>) {
186 self.watch_paths.push(path.into());
187 }
188
189 pub fn add_inline(&mut self, label: impl Into<String>, wgsl: impl Into<String>) {
192 let lbl: String = label.into();
193 let src = ShaderSource::new(lbl.clone(), wgsl);
194 self.snapshot.insert(lbl.clone(), src.version);
195 self.sources.insert(lbl, src);
196 }
197
198 pub fn poll_changes(&mut self) -> Vec<ShaderChangeEvent> {
205 let mut events = Vec::new();
206
207 for (label, src) in &self.sources {
208 let snap_version = self.snapshot.get(label).copied().unwrap_or(0);
209 if src.version != snap_version {
210 events.push(ShaderChangeEvent {
211 label: label.clone(),
212 old_version: snap_version,
213 new_version: src.version,
214 });
215 }
216 }
217
218 for (label, src) in &self.sources {
220 self.snapshot.insert(label.clone(), src.version);
221 }
222
223 events
224 }
225
226 pub fn update_source(&mut self, label: &str, new_wgsl: impl Into<String>) -> bool {
229 if let Some(src) = self.sources.get_mut(label) {
230 src.bump(new_wgsl);
231 true
232 } else {
233 false
234 }
235 }
236
237 #[cfg(feature = "shader-hot-reload")]
253 pub fn poll_filesystem(
254 &mut self,
255 poller: &mut crate::shader_reload_native::FilesystemPoller,
256 ) -> Vec<ShaderChangeEvent> {
257 use crate::shader_reload_native::{PolledChangeKind, read_shader_source};
258
259 let mut events = Vec::new();
260
261 for change in poller.poll() {
262 match change.kind {
264 PolledChangeKind::Modified | PolledChangeKind::Created => {}
265 PolledChangeKind::Deleted => continue,
266 }
267
268 let label = change.path.to_string_lossy().into_owned();
270 let old_version = match self.sources.get(&label) {
271 Some(src) => src.version,
272 None => continue,
274 };
275
276 let new_wgsl = match read_shader_source(&change.path) {
279 Ok(text) => text,
280 Err(_) => continue,
281 };
282
283 if self.update_source(&label, new_wgsl) {
284 if let Some(src) = self.sources.get(&label) {
285 events.push(ShaderChangeEvent {
286 label,
287 old_version,
288 new_version: src.version,
289 });
290 }
291 }
292 }
293
294 events
295 }
296
297 pub fn get_source(&self, label: &str) -> Option<&ShaderSource> {
299 self.sources.get(label)
300 }
301
302 pub fn source_version(&self, label: &str) -> Option<u64> {
305 self.sources.get(label).map(|s| s.version)
306 }
307}
308
309pub struct HotReloadRegistry {
314 pub watcher: ShaderWatcher,
315 pub invalidated_pipelines: HashSet<String>,
317 pub reload_count: u64,
319 pipeline_deps: HashMap<String, HashSet<String>>,
321}
322
323impl Default for HotReloadRegistry {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329impl HotReloadRegistry {
330 pub fn new() -> Self {
332 Self {
333 watcher: ShaderWatcher::new(500),
334 invalidated_pipelines: HashSet::new(),
335 reload_count: 0,
336 pipeline_deps: HashMap::new(),
337 }
338 }
339
340 pub fn register_pipeline(&mut self, pipeline_id: impl Into<String>, shader_label: &str) {
345 self.pipeline_deps
346 .entry(pipeline_id.into())
347 .or_default()
348 .insert(shader_label.to_owned());
349 }
350
351 pub fn process_changes(&mut self) -> Vec<String> {
355 let events = self.watcher.poll_changes();
356 if events.is_empty() {
357 return Vec::new();
358 }
359
360 let changed_labels: HashSet<&str> = events.iter().map(|e| e.label.as_str()).collect();
361
362 let mut newly_invalidated = Vec::new();
363
364 for (pipeline_id, deps) in &self.pipeline_deps {
365 if deps.iter().any(|l| changed_labels.contains(l.as_str()))
366 && !self.invalidated_pipelines.contains(pipeline_id)
367 {
368 newly_invalidated.push(pipeline_id.clone());
369 }
370 }
371
372 for id in &newly_invalidated {
373 self.invalidated_pipelines.insert(id.clone());
374 }
375
376 self.reload_count += events.len() as u64;
377 newly_invalidated
378 }
379
380 pub fn is_invalidated(&self, pipeline_id: &str) -> bool {
382 self.invalidated_pipelines.contains(pipeline_id)
383 }
384
385 pub fn clear_invalidated(&mut self, pipeline_id: &str) {
387 self.invalidated_pipelines.remove(pipeline_id);
388 }
389}
390
391#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
400 fn test_parse_entry_points_compute() {
401 let wgsl = "@compute @workgroup_size(64)\nfn main() {}";
402 let eps = parse_entry_points(wgsl);
403 assert_eq!(eps.len(), 1);
404 assert_eq!(eps[0].name, "main");
405 assert_eq!(eps[0].stage, ShaderStage::Compute);
406 }
407
408 #[test]
409 fn test_parse_entry_points_vertex_fragment() {
410 let wgsl = "@vertex fn vs_main() {}\n@fragment fn fs_main() {}";
411 let eps = parse_entry_points(wgsl);
412 assert_eq!(eps.len(), 2);
413 assert!(eps.iter().any(|e| e.name == "vs_main"));
414 assert!(eps.iter().any(|e| e.name == "fs_main"));
415 }
416
417 #[test]
418 fn test_parse_no_entry_points() {
419 let wgsl = "struct Foo { x: f32 }";
420 assert!(parse_entry_points(wgsl).is_empty());
421 }
422
423 #[test]
426 fn test_add_inline_and_get() {
427 let mut w = ShaderWatcher::new(100);
428 w.add_inline("my_shader", "@compute fn main() {}");
429 let src = w.get_source("my_shader");
430 assert!(src.is_some());
431 let src = src.expect("source should exist");
432 assert_eq!(src.label, "my_shader");
433 assert_eq!(src.version, 1);
434 }
435
436 #[test]
437 fn test_get_unknown_label_returns_none() {
438 let w = ShaderWatcher::new(100);
439 assert!(w.get_source("unknown").is_none());
440 }
441
442 #[test]
443 fn test_source_version_initial() {
444 let mut w = ShaderWatcher::new(100);
445 w.add_inline("s", "@compute fn main() {}");
446 assert_eq!(w.source_version("s"), Some(1));
447 }
448
449 #[test]
450 fn test_source_version_unknown() {
451 let w = ShaderWatcher::new(100);
452 assert_eq!(w.source_version("nope"), None);
453 }
454
455 #[test]
456 fn test_update_source_bumps_version() {
457 let mut w = ShaderWatcher::new(100);
458 w.add_inline("s", "@compute fn main() {}");
459 let ok = w.update_source("s", "@compute fn main_v2() {}");
460 assert!(ok);
461 assert_eq!(w.source_version("s"), Some(2));
462 }
463
464 #[test]
465 fn test_update_source_unknown_returns_false() {
466 let mut w = ShaderWatcher::new(100);
467 assert!(!w.update_source("ghost", "@compute fn x() {}"));
468 }
469
470 #[test]
471 fn test_update_source_multiple_bumps() {
472 let mut w = ShaderWatcher::new(100);
473 w.add_inline("s", "fn main() {}");
474 for expected in 2..=5_u64 {
475 w.update_source("s", format!("fn main_{expected}() {{}}"));
476 assert_eq!(w.source_version("s"), Some(expected));
477 }
478 }
479
480 #[test]
481 fn test_poll_changes_after_update() {
482 let mut w = ShaderWatcher::new(100);
483 w.add_inline("s", "@compute fn main() {}");
484 let first = w.poll_changes();
486 assert!(first.is_empty(), "first poll should be empty");
487
488 w.update_source("s", "@compute fn main_v2() {}");
490 let second = w.poll_changes();
491 assert_eq!(second.len(), 1);
492 assert_eq!(second[0].label, "s");
493 assert_eq!(second[0].old_version, 1);
494 assert_eq!(second[0].new_version, 2);
495 }
496
497 #[test]
498 fn test_poll_changes_clears_on_second_poll() {
499 let mut w = ShaderWatcher::new(100);
500 w.add_inline("s", "fn main() {}");
501 w.update_source("s", "fn main_v2() {}");
502 let _ = w.poll_changes();
503 assert!(w.poll_changes().is_empty());
505 }
506
507 #[test]
508 fn test_add_path_stores_path() {
509 let path = std::env::temp_dir().join("oxigdal_test_shader_bx9f.wgsl");
510 let path_str = path.to_string_lossy().into_owned();
511 let mut w = ShaderWatcher::new(100);
512 w.add_path(path_str.clone());
513 assert_eq!(w.watch_paths, vec![path_str]);
514 }
515
516 #[test]
517 fn test_multiple_inline_sources() {
518 let mut w = ShaderWatcher::new(100);
519 w.add_inline("a", "fn a() {}");
520 w.add_inline("b", "fn b() {}");
521 assert_eq!(w.sources.len(), 2);
522 }
523
524 #[test]
527 fn test_registry_new_not_invalidated() {
528 let reg = HotReloadRegistry::new();
529 assert!(!reg.is_invalidated("pipeline_a"));
530 }
531
532 #[test]
533 fn test_registry_process_changes_invalidates_pipeline() {
534 let mut reg = HotReloadRegistry::new();
535 reg.watcher.add_inline("my_shader", "@compute fn main() {}");
536 reg.register_pipeline("pipeline_a", "my_shader");
537
538 reg.watcher.poll_changes();
540 reg.watcher
541 .update_source("my_shader", "@compute fn main_v2() {}");
542
543 let invalidated = reg.process_changes();
544 assert!(invalidated.contains(&"pipeline_a".to_owned()));
545 assert!(reg.is_invalidated("pipeline_a"));
546 }
547
548 #[test]
549 fn test_registry_process_changes_no_change() {
550 let mut reg = HotReloadRegistry::new();
551 reg.watcher.add_inline("s", "@compute fn main() {}");
552 reg.register_pipeline("p", "s");
553 reg.watcher.poll_changes(); let invalidated = reg.process_changes();
555 assert!(invalidated.is_empty());
556 }
557
558 #[test]
559 fn test_registry_clear_invalidated() {
560 let mut reg = HotReloadRegistry::new();
561 reg.watcher.add_inline("s", "@compute fn main() {}");
562 reg.register_pipeline("p", "s");
563 reg.watcher.poll_changes();
564 reg.watcher.update_source("s", "@compute fn new_main() {}");
565 reg.process_changes();
566 assert!(reg.is_invalidated("p"));
567 reg.clear_invalidated("p");
568 assert!(!reg.is_invalidated("p"));
569 }
570
571 #[test]
572 fn test_registry_reload_count_increments() {
573 let mut reg = HotReloadRegistry::new();
574 reg.watcher.add_inline("s", "fn main() {}");
575 reg.register_pipeline("p", "s");
576 reg.watcher.poll_changes();
577 reg.watcher.update_source("s", "fn main_v2() {}");
578 reg.process_changes();
579 assert_eq!(reg.reload_count, 1);
580 reg.watcher.update_source("s", "fn main_v3() {}");
581 reg.process_changes();
582 assert_eq!(reg.reload_count, 2);
583 }
584
585 #[test]
586 fn test_registry_unrelated_shader_does_not_invalidate() {
587 let mut reg = HotReloadRegistry::new();
588 reg.watcher.add_inline("shader_a", "fn a() {}");
589 reg.watcher.add_inline("shader_b", "fn b() {}");
590 reg.register_pipeline("pipeline_a", "shader_a");
591 reg.watcher.poll_changes();
592
593 reg.watcher.update_source("shader_b", "fn b_v2() {}");
595 let invalidated = reg.process_changes();
596 assert!(!invalidated.contains(&"pipeline_a".to_owned()));
597 assert!(!reg.is_invalidated("pipeline_a"));
598 }
599
600 #[test]
601 fn test_entry_point_new() {
602 let ep = EntryPoint::new("vs_main", ShaderStage::Vertex);
603 assert_eq!(ep.name, "vs_main");
604 assert_eq!(ep.stage, ShaderStage::Vertex);
605 }
606
607 #[test]
608 fn test_shader_source_entry_points_populated() {
609 let mut w = ShaderWatcher::new(100);
610 w.add_inline("s", "@compute\nfn my_compute() {}");
611 let src = w.get_source("s").expect("source should exist");
612 assert_eq!(src.entry_points.len(), 1);
613 assert_eq!(src.entry_points[0].name, "my_compute");
614 }
615
616 #[test]
617 fn test_update_source_refreshes_entry_points() {
618 let mut w = ShaderWatcher::new(100);
619 w.add_inline("s", "@compute fn compute_v1() {}");
620 w.update_source("s", "@vertex fn vs_main() {}");
621 let src = w.get_source("s").expect("source should exist");
622 assert_eq!(src.entry_points[0].stage, ShaderStage::Vertex);
623 assert_eq!(src.entry_points[0].name, "vs_main");
624 }
625}