1use std::cell::Cell;
11
12use kurbo::{Affine, Point};
13use oxframe_core::{Color, Signal};
14
15use crate::draw::Primitive;
16use crate::graph::{NodeData, NodeKind};
17use crate::image::RasterImage;
18
19#[doc(hidden)]
21#[derive(Clone)]
22pub enum ShaderCode {
23 Wgsl(String),
25 File(std::path::PathBuf),
27}
28
29#[doc(hidden)]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Preset {
33 Custom,
35 Waves,
37 LensDrops,
39}
40
41impl Preset {
42 pub fn name(self) -> &'static str {
44 match self {
45 Self::Custom => "custom",
46 Self::Waves => "waves",
47 Self::LensDrops => "lens_drops",
48 }
49 }
50}
51
52const LENS_PARAMS: [&str; 3] = ["wetness", "seed", "drop_size"];
54
55const LENS_INPUTS: [&str; 1] = ["scene"];
57
58#[doc(hidden)]
60pub struct Uniform {
61 pub name: String,
62 pub value: UniformValue,
63}
64
65#[doc(hidden)]
67#[allow(dead_code)]
68pub enum UniformValue {
69 Num(Signal<f64>),
71 Vec2(Signal<kurbo::Vec2>),
73 Color(Signal<Color>),
75 Bool(Signal<f64>),
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub struct ShaderDiag {
82 pub node: String,
84 pub source: Option<String>,
86 pub rule: &'static str,
88 pub hint: String,
90}
91
92const WAVES: &str = r#"struct Params { amp: f32, speed: f32, deep: vec4<f32>, foam: vec4<f32> }
93@group(1) @binding(0) var<uniform> p: Params;
94
95fn shade(i: ShadeIn) -> vec4<f32> {
96 let h = sin(i.world.x * 0.008 + ox.time * p.speed) * p.amp;
97 let t = smoothstep(h - 0.02, h + 0.02, i.uv.y - 0.5);
98 return mix(p.foam, p.deep, t);
99}
100"#;
101
102pub(crate) fn waves_wgsl() -> &'static str {
104 WAVES
105}
106
107#[cfg(not(target_arch = "wasm32"))]
108const PRELUDE: &str = r#"struct Ox {
109 time: f32,
110 frame: f32,
111 resolution: vec2<f32>,
112 size: vec2<f32>,
113 seed: f32,
114 view: mat4x4<f32>,
115 progress: f32,
116}
117@group(0) @binding(0) var<uniform> ox: Ox;
118
119struct ShadeIn {
120 uv: vec2<f32>,
121 local: vec2<f32>,
122 world: vec2<f32>,
123 screen: vec2<f32>,
124}
125
126"#;
127
128#[cfg(not(target_arch = "wasm32"))]
129const EPILOGUE: &str = r#"
130struct _OxVertexOut {
131 @builtin(position) pos: vec4<f32>,
132 @location(0) uv: vec2<f32>,
133 @location(1) local: vec2<f32>,
134 @location(2) world: vec2<f32>,
135 @location(3) screen: vec2<f32>,
136}
137
138@vertex
139fn _ox_vs(@builtin(vertex_index) index: u32) -> _OxVertexOut {
140 var positions = array<vec2<f32>, 3>(
141 vec2<f32>(-1.0, -1.0),
142 vec2<f32>(3.0, -1.0),
143 vec2<f32>(-1.0, 3.0),
144 );
145 let p = positions[index];
146 var out: _OxVertexOut;
147 out.pos = vec4<f32>(p, 0.0, 1.0);
148 out.uv = vec2<f32>(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
149 out.local = out.uv;
150 out.world = out.uv;
151 out.screen = out.uv;
152 return out;
153}
154
155@fragment
156fn _ox_fs(in: _OxVertexOut) -> @location(0) vec4<f32> {
157 var s: ShadeIn;
158 s.uv = in.uv;
159 s.local = in.local;
160 s.world = in.world;
161 s.screen = in.screen;
162 return shade(s);
163}
164"#;
165
166thread_local! {
167 static DRAFT: Cell<bool> = const { Cell::new(false) };
168 static BUDGET_MS: Cell<f64> = const { Cell::new(250.0) };
169 static DIAGS: std::cell::RefCell<Vec<ShaderDiag>> = const { std::cell::RefCell::new(Vec::new()) };
170}
171
172pub(crate) fn clear_diags() {
173 DIAGS.with(|d| d.borrow_mut().clear());
174}
175
176pub(crate) fn push_diag(diag: ShaderDiag) {
177 DIAGS.with(|d| d.borrow_mut().push(diag));
178}
179
180pub(crate) fn take_diags() -> Vec<ShaderDiag> {
181 DIAGS.with(|d| std::mem::take(&mut *d.borrow_mut()))
182}
183
184pub struct ShaderDraft {
186 prev: bool,
187}
188
189impl Drop for ShaderDraft {
190 fn drop(&mut self) {
191 DRAFT.with(|d| d.set(self.prev));
192 }
193}
194
195pub fn draft_shaders() -> ShaderDraft {
200 let prev = DRAFT.with(Cell::get);
201 DRAFT.with(|d| d.set(true));
202 ShaderDraft { prev }
203}
204
205pub fn set_shader_budget_ms(ms: f64) {
207 BUDGET_MS.with(|b| b.set(ms));
208}
209
210fn draft() -> bool {
211 DRAFT.with(Cell::get)
212}
213
214fn budget_ms() -> f64 {
215 BUDGET_MS.with(Cell::get)
216}
217
218pub(crate) fn problems(n: &NodeData) -> Vec<ShaderDiag> {
221 if let NodeKind::Video { data, .. } = &n.kind {
222 return data
223 .problem
224 .iter()
225 .map(|p| ShaderDiag {
226 node: label(n),
227 source: n.source.clone(),
228 rule: "video_unavailable",
229 hint: format!("{}: {p}", data.path.display()),
230 })
231 .collect();
232 }
233 let NodeKind::Shader {
234 code,
235 uniforms,
236 preset,
237 inputs,
238 ..
239 } = &n.kind
240 else {
241 return Vec::new();
242 };
243 let label = label(n);
244 let source = n.source.clone();
245 let diag = |hint: String| {
246 vec![ShaderDiag {
247 node: label.clone(),
248 source: source.clone(),
249 rule: "param_unknown",
250 hint,
251 }]
252 };
253 let known: &[&str] = match preset {
254 Preset::LensDrops => &LENS_INPUTS,
255 Preset::Waves => &[],
256 Preset::Custom => &[],
258 };
259 if *preset != Preset::Custom {
260 for (name, _) in inputs {
261 if !known.contains(&name.as_str()) {
262 return diag(unknown("input", name, known));
263 }
264 }
265 }
266 if *preset == Preset::LensDrops {
267 for u in uniforms {
268 if !LENS_PARAMS.contains(&u.name.as_str()) {
269 return diag(unknown("parameter", &u.name, &LENS_PARAMS));
270 }
271 if !matches!(u.value, UniformValue::Num(_)) {
272 return diag(format!("`{}` of lens_drops is a number.", u.name));
273 }
274 }
275 return Vec::new();
276 }
277 let text = match load(code) {
278 Ok(text) => text,
279 Err(hint) => {
280 return vec![ShaderDiag {
281 node: label,
282 source,
283 rule: "shader_error",
284 hint,
285 }];
286 }
287 };
288 match check_wgsl(&text, uniforms) {
289 Ok(()) => Vec::new(),
290 Err(diag) => vec![ShaderDiag {
291 node: label,
292 source,
293 rule: diag.0,
294 hint: diag.1,
295 }],
296 }
297}
298
299pub(crate) fn raster(
301 n: &NodeData,
302 size: (f64, f64),
303 tf: Affine,
304 time: f64,
305 opacity: f64,
306) -> Option<Primitive> {
307 let NodeKind::Shader {
308 preset, uniforms, ..
309 } = &n.kind
310 else {
311 return None;
312 };
313 if *preset != Preset::Waves {
314 return None;
315 }
316 let (w, h) = (size.0.max(1.0), size.1.max(1.0));
317 let scale = if draft() { 0.5 } else { 1.0 };
318 let iw = ((w * scale).round() as u32).clamp(1, 1920);
319 let ih = ((h * scale).round() as u32).clamp(1, 1080);
320 let (amp, speed, deep, foam) = wave_uniforms(uniforms);
321 let started = Stopwatch::start();
322 let mut rgba = vec![0u8; iw as usize * ih as usize * 4];
323 for py in 0..ih {
324 for px in 0..iw {
325 let lx = (f64::from(px) + 0.5) / f64::from(iw) * w - w / 2.0;
326 let ly = (f64::from(py) + 0.5) / f64::from(ih) * h - h / 2.0;
327 let world = tf * Point::new(lx, ly);
328 let uv_y = (f64::from(py) + 0.5) / f64::from(ih);
329 let crest = (world.x * 0.008 + time * speed).sin() * amp;
330 let t = smoothstep(crest - 0.02, crest + 0.02, uv_y - 0.5);
331 let c = mix(foam, deep, t, opacity);
332 let i = (py as usize * iw as usize + px as usize) * 4;
333 rgba[i] = byte(c.r);
334 rgba[i + 1] = byte(c.g);
335 rgba[i + 2] = byte(c.b);
336 rgba[i + 3] = byte(c.a);
337 }
338 }
339 let ms = started.millis();
340 if ms > budget_ms() {
341 push_diag(ShaderDiag {
342 node: label(n),
343 source: n.source.clone(),
344 rule: "shader_slow",
345 hint: format!(
346 "This shader took {ms:.0} ms in draft. The budget is {:.0} ms.",
347 budget_ms()
348 ),
349 });
350 }
351 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
352 Some(Primitive::Image {
353 image: std::rc::Rc::new(RasterImage {
354 width: iw,
355 height: ih,
356 id: hash_bytes(&rgba),
357 rgba,
358 }),
359 rect,
360 opacity: 1.0,
361 })
362}
363
364pub(crate) fn read_frame(
370 n: &NodeData,
371 size: (f64, f64),
372 tf: Affine,
373 time: f64,
374 opacity: f64,
375) -> Option<crate::post::Shade> {
376 let NodeKind::Shader {
377 preset: Preset::LensDrops,
378 uniforms,
379 ..
380 } = &n.kind
381 else {
382 return None;
383 };
384 let num = |name: &str, default: f64| {
385 uniforms
386 .iter()
387 .find_map(|u| match (&u.name[..], &u.value) {
388 (m, UniformValue::Num(v)) if m == name => Some(v.get()),
389 _ => None,
390 })
391 .unwrap_or(default)
392 };
393 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
394 let area = tf.transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
395 Some(crate::post::Shade {
396 op: crate::post::ShadeOp::LensDrops {
397 wetness: num("wetness", 0.6),
398 seed: num("seed", 0.0).max(0.0) as u64,
399 scale: num("drop_size", 1.0),
400 time,
401 },
402 opacity,
403 area: Some(area),
404 })
405}
406
407fn unknown(what: &str, name: &str, known: &[&str]) -> String {
409 let list = if known.is_empty() {
410 "none".to_owned()
411 } else {
412 known.join(", ")
413 };
414 match closest(name, known) {
415 Some(n) => format!("no {what} `{name}`. Did you mean `{n}`? Available: {list}."),
416 None => format!("no {what} `{name}`. Available: {list}."),
417 }
418}
419
420#[cfg(not(target_arch = "wasm32"))]
426struct Stopwatch(std::time::Instant);
427
428#[cfg(target_arch = "wasm32")]
429struct Stopwatch;
430
431impl Stopwatch {
432 fn start() -> Self {
433 #[cfg(not(target_arch = "wasm32"))]
434 {
435 Self(std::time::Instant::now())
436 }
437 #[cfg(target_arch = "wasm32")]
438 {
439 Self
440 }
441 }
442
443 fn millis(&self) -> f64 {
444 #[cfg(not(target_arch = "wasm32"))]
445 {
446 self.0.elapsed().as_secs_f64() * 1000.0
447 }
448 #[cfg(target_arch = "wasm32")]
449 {
450 0.0
451 }
452 }
453}
454
455fn wave_uniforms(uniforms: &[Uniform]) -> (f64, f64, Color, Color) {
456 let mut amp = 0.05;
457 let mut speed = 1.0;
458 let mut deep = Color::BLACK;
459 let mut foam = Color::WHITE;
460 for u in uniforms {
461 match (&u.name[..], &u.value) {
462 ("amp", UniformValue::Num(v)) => amp = v.get(),
463 ("speed", UniformValue::Num(v)) => speed = v.get(),
464 ("deep", UniformValue::Color(v)) => deep = v.get(),
465 ("foam", UniformValue::Color(v)) => foam = v.get(),
466 _ => {}
467 }
468 }
469 (amp, speed, deep, foam)
470}
471
472fn load(code: &ShaderCode) -> Result<String, String> {
473 match code {
474 ShaderCode::Wgsl(text) => Ok(text.clone()),
475 ShaderCode::File(path) => read_file(path),
476 }
477}
478
479#[cfg(not(target_arch = "wasm32"))]
480fn read_file(path: &std::path::Path) -> Result<String, String> {
481 std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))
482}
483
484#[cfg(target_arch = "wasm32")]
485fn read_file(path: &std::path::Path) -> Result<String, String> {
486 let _ = path;
487 Err("a shader file is read by the native renderer".into())
488}
489
490fn label(n: &NodeData) -> String {
491 n.name
492 .clone()
493 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0))
494}
495
496#[cfg(target_arch = "wasm32")]
497fn check_wgsl(_text: &str, _uniforms: &[Uniform]) -> Result<(), (&'static str, String)> {
498 Ok(())
499}
500
501#[cfg(not(target_arch = "wasm32"))]
502fn check_wgsl(text: &str, uniforms: &[Uniform]) -> Result<(), (&'static str, String)> {
503 let full = format!("{PRELUDE}{text}{EPILOGUE}");
504 let module = naga::front::wgsl::parse_str(&full).map_err(|e| {
505 (
506 "shader_error",
507 shift_lines(&e.emit_to_string(&full), prelude_lines()),
508 )
509 })?;
510 naga::valid::Validator::new(
511 naga::valid::ValidationFlags::all(),
512 naga::valid::Capabilities::all(),
513 )
514 .validate(&module)
515 .map_err(|e| ("shader_error", shift_lines(&e.to_string(), prelude_lines())))?;
516 let fields = params_of(&module).ok_or((
517 "shader_error",
518 "the shader has no `struct Params`".to_string(),
519 ))?;
520 match params(uniforms, &fields) {
521 Ok(()) => Ok(()),
522 Err(hint) => Err(("param_unknown", hint)),
523 }
524}
525
526#[cfg(not(target_arch = "wasm32"))]
527fn prelude_lines() -> usize {
528 PRELUDE.lines().count()
529}
530
531#[cfg(not(target_arch = "wasm32"))]
534fn shift_lines(message: &str, by: usize) -> String {
535 let chars: Vec<char> = message.chars().collect();
536 let mut out = String::with_capacity(message.len());
537 let mut i = 0;
538 while i < chars.len() {
539 if chars[i] == ':' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
540 let start = i + 1;
541 let mut end = start;
542 while end < chars.len() && chars[end].is_ascii_digit() {
543 end += 1;
544 }
545 let digits: String = chars[start..end].iter().collect();
546 let line: usize = digits.parse().unwrap_or(0);
547 out.push(':');
548 out.push_str(&line.saturating_sub(by).to_string());
549 i = end;
550 } else {
551 out.push(chars[i]);
552 i += 1;
553 }
554 }
555 out
556}
557
558#[cfg(not(target_arch = "wasm32"))]
559enum WgslTy {
560 F32,
561 Vec2,
562 Vec4,
563 Bool,
564 Other,
565}
566
567#[cfg(not(target_arch = "wasm32"))]
568fn params_of(module: &naga::Module) -> Option<Vec<(String, WgslTy)>> {
569 for (_, ty) in module.types.iter() {
570 if ty.name.as_deref() != Some("Params") {
571 continue;
572 }
573 let naga::TypeInner::Struct { members, .. } = &ty.inner else {
574 continue;
575 };
576 return Some(
577 members
578 .iter()
579 .map(|m| {
580 let name = m.name.clone().unwrap_or_default();
581 (name, wgsl_ty(&module.types[m.ty].inner))
582 })
583 .collect(),
584 );
585 }
586 None
587}
588
589#[cfg(not(target_arch = "wasm32"))]
590fn wgsl_ty(inner: &naga::TypeInner) -> WgslTy {
591 match inner {
592 naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Float && s.width == 4 => {
593 WgslTy::F32
594 }
595 naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Bool => WgslTy::Bool,
596 naga::TypeInner::Vector { size, scalar }
597 if scalar.kind == naga::ScalarKind::Float
598 && scalar.width == 4
599 && *size == naga::VectorSize::Bi =>
600 {
601 WgslTy::Vec2
602 }
603 naga::TypeInner::Vector { size, scalar }
604 if scalar.kind == naga::ScalarKind::Float
605 && scalar.width == 4
606 && *size == naga::VectorSize::Quad =>
607 {
608 WgslTy::Vec4
609 }
610 _ => WgslTy::Other,
611 }
612}
613
614#[cfg(not(target_arch = "wasm32"))]
615fn params(uniforms: &[Uniform], fields: &[(String, WgslTy)]) -> Result<(), String> {
616 let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
617 let list = names.join(", ");
618 for u in uniforms {
619 let Some((_, ty)) = fields.iter().find(|(n, _)| n == &u.name) else {
620 let near = closest(&u.name, &names);
621 let hint = match near {
622 Some(n) => format!(
623 "no parameter `{}`. Did you mean `{n}`? Available: {list}.",
624 u.name
625 ),
626 None => format!("no parameter `{}`. Available: {list}.", u.name),
627 };
628 return Err(hint);
629 };
630 if !type_matches(&u.value, ty) {
631 return Err(format!(
632 "`{}` is {} in Params, but the scene sets {}.",
633 u.name,
634 ty_name(ty),
635 value_name(&u.value)
636 ));
637 }
638 }
639 for (name, _) in fields {
640 if !uniforms.iter().any(|u| u.name == *name) {
641 return Err(format!(
642 "Params.{name} has no value. Set it with `.uniform(\"{name}\", ...)`."
643 ));
644 }
645 }
646 Ok(())
647}
648
649#[cfg(not(target_arch = "wasm32"))]
650fn type_matches(value: &UniformValue, ty: &WgslTy) -> bool {
651 matches!(
652 (value, ty),
653 (UniformValue::Num(_), WgslTy::F32)
654 | (UniformValue::Vec2(_), WgslTy::Vec2)
655 | (UniformValue::Color(_), WgslTy::Vec4)
656 | (UniformValue::Bool(_), WgslTy::Bool)
657 )
658}
659
660#[cfg(not(target_arch = "wasm32"))]
661fn ty_name(ty: &WgslTy) -> &'static str {
662 match ty {
663 WgslTy::F32 => "f32",
664 WgslTy::Vec2 => "vec2<f32>",
665 WgslTy::Vec4 => "vec4<f32>",
666 WgslTy::Bool => "bool",
667 WgslTy::Other => "another type",
668 }
669}
670
671#[cfg(not(target_arch = "wasm32"))]
672fn value_name(value: &UniformValue) -> &'static str {
673 match value {
674 UniformValue::Num(_) => "a number",
675 UniformValue::Vec2(_) => "a point",
676 UniformValue::Color(_) => "a color",
677 UniformValue::Bool(_) => "a bool",
678 }
679}
680
681fn closest<'a>(name: &str, options: &[&'a str]) -> Option<&'a str> {
682 options
683 .iter()
684 .copied()
685 .filter(|o| edit(name, o) <= 3 && *o != name)
686 .min_by_key(|o| edit(name, o))
687}
688
689fn edit(a: &str, b: &str) -> usize {
690 let a: Vec<char> = a.chars().collect();
691 let b: Vec<char> = b.chars().collect();
692 let mut prev: Vec<usize> = (0..=b.len()).collect();
693 let mut cur = vec![0; b.len() + 1];
694 for (i, ca) in a.iter().enumerate() {
695 cur[0] = i + 1;
696 for (j, cb) in b.iter().enumerate() {
697 let cost = usize::from(ca != cb);
698 cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
699 }
700 std::mem::swap(&mut prev, &mut cur);
701 }
702 prev[b.len()]
703}
704
705fn smoothstep(e0: f64, e1: f64, x: f64) -> f64 {
706 let t = if (e1 - e0).abs() < 1e-9 {
707 if x < e0 { 0.0 } else { 1.0 }
708 } else {
709 ((x - e0) / (e1 - e0)).clamp(0.0, 1.0)
710 };
711 t * t * (3.0 - 2.0 * t)
712}
713
714fn mix(foam: Color, deep: Color, t: f64, opacity: f64) -> Color {
715 let t = t as f32;
716 let a = opacity.clamp(0.0, 1.0) as f32;
717 Color {
718 r: foam.r + (deep.r - foam.r) * t,
719 g: foam.g + (deep.g - foam.g) * t,
720 b: foam.b + (deep.b - foam.b) * t,
721 a: (foam.a + (deep.a - foam.a) * t) * a,
722 }
723}
724
725fn byte(c: f32) -> u8 {
726 (c.clamp(0.0, 1.0) * 255.0).round() as u8
727}
728
729fn hash_bytes(bytes: &[u8]) -> u64 {
730 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
731 for &b in bytes {
732 h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
733 }
734 h
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::builder::NodeBuilder;
741 use crate::{SceneGraph, View};
742 use std::rc::Rc;
743
744 fn paint(f: impl FnOnce(View)) -> Rc<SceneGraph> {
745 let graph = SceneGraph::new(320, 180);
746 graph.enter(|| f(View::current()));
747 graph
748 }
749
750 fn image_of(graph: &SceneGraph) -> Rc<RasterImage> {
751 let items = graph.draw_list();
752 for item in &items {
753 if let Primitive::Image { image, .. } = &item.prim {
754 return image.clone();
755 }
756 }
757 panic!("waves did not paint, diags: {:?}", graph.shader_diags());
758 }
759
760 #[test]
761 fn waves_put_foam_above_deep_water() {
762 let graph = paint(|view| {
763 view.add(crate::waves().width(200.0).height(80.0));
764 });
765 let image = image_of(&graph);
766 let px = |x: u32, y: u32| {
767 let i = (y * image.width + x) as usize * 4;
768 image.rgba[i + 2]
769 };
770 let mid = image.width / 2;
771 assert!(
772 px(mid, 2) > px(mid, image.height - 3),
773 "the crest is the lighter foam"
774 );
775 assert!(
776 graph.shader_diags().is_empty(),
777 "{:?}",
778 graph.shader_diags()
779 );
780 }
781
782 #[test]
783 fn the_crest_moves_along_the_wave() {
784 let graph = paint(|view| {
785 view.add(crate::waves().amp(0.2).width(200.0).height(80.0));
786 });
787 let a = image_of(&graph).rgba.clone();
788 graph.set_time(1.0);
789 let b = image_of(&graph).rgba.clone();
790 assert_ne!(a, b);
791 }
792
793 #[test]
794 fn a_misspelled_uniform_names_the_real_one() {
795 let graph = paint(|view| {
796 view.add(
797 crate::Shader::source(waves_wgsl())
798 .uniform("ampp", 0.1)
799 .uniform("speed", 1.0)
800 .uniform_color("deep", Color::BLACK)
801 .uniform_color("foam", Color::WHITE)
802 .width(40.0)
803 .height(20.0)
804 .name("sea"),
805 );
806 });
807 let _ = graph.draw_list();
808 let diag = graph
809 .shader_diags()
810 .into_iter()
811 .find(|d| d.rule == "param_unknown")
812 .expect("param_unknown");
813 assert_eq!(diag.node, "sea");
814 assert!(diag.hint.contains("amp"), "{diag:?}");
815 }
816
817 #[test]
818 fn a_broken_shader_is_an_error() {
819 let graph = paint(|view| {
820 view.add(
821 crate::Shader::source("this is not wgsl")
822 .width(40.0)
823 .height(20.0),
824 );
825 });
826 let _ = graph.draw_list();
827 assert!(
828 graph
829 .shader_diags()
830 .iter()
831 .any(|d| d.rule == "shader_error"),
832 "{:?}",
833 graph.shader_diags()
834 );
835 }
836
837 #[cfg(not(target_arch = "wasm32"))]
838 #[test]
839 fn editing_the_file_is_seen_on_the_next_draw() {
840 let dir = std::env::temp_dir().join(format!("oxframe-shader-{}", std::process::id()));
841 std::fs::create_dir_all(&dir).unwrap();
842 let path = dir.join("sea.wgsl");
843 std::fs::write(&path, "this is not wgsl").unwrap();
844 let graph = paint(|view| {
845 view.add(crate::Shader::file(&path).width(40.0).height(20.0));
846 });
847 let _ = graph.draw_list();
848 assert!(
849 graph
850 .shader_diags()
851 .iter()
852 .any(|d| d.rule == "shader_error"),
853 "{:?}",
854 graph.shader_diags()
855 );
856 std::fs::write(&path, waves_wgsl()).unwrap();
857 let _ = graph.draw_list();
858 let diags = graph.shader_diags();
859 assert!(diags.iter().all(|d| d.rule != "shader_error"), "{diags:?}");
860 let _ = std::fs::remove_dir_all(&dir);
861 }
862
863 fn rules(graph: &SceneGraph) -> Vec<&'static str> {
864 let _ = graph.draw_list();
865 graph.shader_diags().iter().map(|d| d.rule).collect()
866 }
867
868 fn reads_frame(graph: &SceneGraph) -> bool {
869 graph
870 .draw_list()
871 .iter()
872 .any(|i| matches!(i.prim, Primitive::ReadFrame(_)))
873 }
874
875 #[test]
876 fn lens_drops_read_the_frame() {
877 let graph = paint(|view| {
878 view.add(
879 crate::lens_drops()
880 .fullscreen()
881 .input("scene", crate::Input::frame()),
882 );
883 });
884 assert!(graph.has_post());
885 assert!(reads_frame(&graph));
886 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
887 let passes = crate::pass_graph(&graph.draw_list());
888 assert!(
889 passes
890 .iter()
891 .any(|p| p.kind == crate::PassKind::Effect && p.shader == Some("lens_drops"))
892 );
893 }
894
895 #[test]
896 fn a_misspelled_input_or_parameter_names_the_real_one() {
897 let graph = paint(|view| {
898 view.add(
899 crate::lens_drops()
900 .input("scen", crate::Input::frame())
901 .name("lens"),
902 );
903 });
904 let _ = graph.draw_list();
905 let diag = &graph.shader_diags()[0];
906 assert_eq!((diag.rule, diag.node.as_str()), ("param_unknown", "lens"));
907 assert!(diag.hint.contains("`scene`"), "{diag:?}");
908
909 let graph = paint(|view| {
910 view.add(crate::lens_drops().uniform("wetnes", 1.0));
911 });
912 let _ = graph.draw_list();
913 let diag = &graph.shader_diags()[0];
914 assert!(diag.hint.contains("`wetness`"), "{diag:?}");
915
916 let graph = paint(|view| {
917 view.add(crate::waves().input("scene", crate::Input::frame()));
918 });
919 assert_eq!(rules(&graph), ["param_unknown"]);
920 }
921
922 #[test]
923 fn a_lens_inside_a_fading_group_is_an_input_cycle() {
924 let graph = paint(|view| {
925 let group = view.add(crate::Group::new().opacity(0.5));
926 group.add(crate::Rect::new().size(20.0));
927 group.add(crate::lens_drops().fullscreen());
928 });
929 assert_eq!(rules(&graph), ["input_cycle"]);
930 assert!(!reads_frame(&graph), "the lens is skipped");
931 }
932
933 #[test]
934 fn an_effect_inside_an_effect_is_an_input_cycle() {
935 let graph = paint(|view| {
936 let outer = view.add(crate::Group::new());
937 let inner = outer.add(crate::Rect::new().size(20.0));
938 outer.effect(crate::ripple());
939 inner.effect(crate::ripple());
940 });
941 assert_eq!(rules(&graph), ["input_cycle"]);
942 let items = graph.draw_list();
943 let begins = items
944 .iter()
945 .filter(|i| matches!(i.prim, Primitive::BeginEffect(_)))
946 .count();
947 assert_eq!(begins, 1, "only the outer effect runs");
948 let passes = crate::pass_graph(&items);
949 assert!(
950 passes
951 .iter()
952 .any(|p| p.kind == crate::PassKind::Effect && p.shader == Some("ripple"))
953 );
954 }
955
956 #[test]
957 fn effect_signals_animate_the_marker() {
958 let graph = SceneGraph::new(320, 180);
959 let effect = graph.enter(|| {
960 View::current()
961 .add(crate::Rect::new().size(40.0))
962 .effect(crate::ripple().strength(0.0))
963 });
964 let strength = |graph: &SceneGraph| {
965 graph.draw_list().iter().find_map(|i| match &i.prim {
966 Primitive::BeginEffect(s) => match s.op {
967 crate::post::ShadeOp::Ripple { strength, .. } => Some(strength),
968 _ => None,
969 },
970 _ => None,
971 })
972 };
973 assert_eq!(strength(&graph), Some(0.0));
974 graph.enter(|| effect.strength().set(0.7));
975 assert_eq!(strength(&graph), Some(0.7));
976 }
977
978 #[test]
979 fn a_slow_raster_is_reported() {
980 let graph = paint(|view| {
981 view.add(crate::waves().width(180.0).height(80.0).name("sea"));
982 });
983 set_shader_budget_ms(0.0);
984 let _ = graph.draw_list();
985 set_shader_budget_ms(250.0);
986 assert!(
987 graph.shader_diags().iter().any(|d| d.rule == "shader_slow"),
988 "{:?}",
989 graph.shader_diags()
990 );
991 }
992}