1use std::cell::Cell;
12
13use kurbo::{Affine, Point};
14use oxframe_core::{Color, Signal};
15
16use crate::draw::Primitive;
17use crate::graph::{NodeData, NodeKind};
18use crate::image::RasterImage;
19
20#[doc(hidden)]
22#[derive(Clone)]
23pub enum ShaderCode {
24 Wgsl(String),
26 File(std::path::PathBuf),
28}
29
30#[doc(hidden)]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Preset {
34 Custom,
36 Waves,
38 LensDrops,
40}
41
42impl Preset {
43 pub fn name(self) -> &'static str {
45 match self {
46 Self::Custom => "custom",
47 Self::Waves => "waves",
48 Self::LensDrops => "lens_drops",
49 }
50 }
51}
52
53const LENS_PARAMS: [&str; 3] = ["wetness", "seed", "drop_size"];
55
56const LENS_INPUTS: [&str; 1] = ["scene"];
58
59#[doc(hidden)]
61pub struct Uniform {
62 pub name: String,
63 pub value: UniformValue,
64}
65
66#[doc(hidden)]
68#[allow(dead_code)]
69pub enum UniformValue {
70 Num(Signal<f64>),
72 Vec2(Signal<kurbo::Vec2>),
74 Color(Signal<Color>),
76 Bool(Signal<f64>),
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub struct ShaderDiag {
83 pub node: String,
85 pub source: Option<String>,
87 pub rule: &'static str,
89 pub hint: String,
91}
92
93const WAVES: &str = r#"struct Params { amp: f32, speed: f32, deep: vec4<f32>, foam: vec4<f32> }
94@group(1) @binding(0) var<uniform> p: Params;
95
96fn shade(i: ShadeIn) -> vec4<f32> {
97 let h = sin(i.world.x * 0.008 + ox.time * p.speed) * p.amp;
98 let t = smoothstep(h - 0.02, h + 0.02, i.uv.y - 0.5);
99 return mix(p.foam, p.deep, t);
100}
101"#;
102
103pub(crate) fn waves_wgsl() -> &'static str {
105 WAVES
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109const PRELUDE: &str = r#"struct Ox {
110 time: f32,
111 frame: f32,
112 resolution: vec2<f32>,
113 size: vec2<f32>,
114 seed: f32,
115 view: mat4x4<f32>,
116 progress: f32,
117}
118@group(0) @binding(0) var<uniform> ox: Ox;
119
120struct ShadeIn {
121 uv: vec2<f32>,
122 local: vec2<f32>,
123 world: vec2<f32>,
124 screen: vec2<f32>,
125}
126
127"#;
128
129#[cfg(not(target_arch = "wasm32"))]
130const EPILOGUE: &str = r#"
131struct _OxVertexOut {
132 @builtin(position) pos: vec4<f32>,
133 @location(0) uv: vec2<f32>,
134 @location(1) local: vec2<f32>,
135 @location(2) world: vec2<f32>,
136 @location(3) screen: vec2<f32>,
137}
138
139@vertex
140fn _ox_vs(@builtin(vertex_index) index: u32) -> _OxVertexOut {
141 var positions = array<vec2<f32>, 3>(
142 vec2<f32>(-1.0, -1.0),
143 vec2<f32>(3.0, -1.0),
144 vec2<f32>(-1.0, 3.0),
145 );
146 let p = positions[index];
147 var out: _OxVertexOut;
148 out.pos = vec4<f32>(p, 0.0, 1.0);
149 out.uv = vec2<f32>(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
150 out.local = out.uv;
151 out.world = out.uv;
152 out.screen = out.uv;
153 return out;
154}
155
156@fragment
157fn _ox_fs(in: _OxVertexOut) -> @location(0) vec4<f32> {
158 var s: ShadeIn;
159 s.uv = in.uv;
160 s.local = in.local;
161 s.world = in.world;
162 s.screen = in.screen;
163 return shade(s);
164}
165"#;
166
167thread_local! {
168 static SCENE: Cell<((f64, f64), f64)> = const { Cell::new(((1920.0, 1080.0), 60.0)) };
170 static DRAFT: Cell<bool> = const { Cell::new(false) };
171 static BUDGET_MS: Cell<f64> = const { Cell::new(250.0) };
172 static DIAGS: std::cell::RefCell<Vec<ShaderDiag>> = const { std::cell::RefCell::new(Vec::new()) };
173}
174
175pub(crate) fn set_scene_size(size: (f64, f64)) {
178 SCENE.with(|s| s.set((size, s.get().1)));
179}
180
181#[doc(hidden)]
184pub fn set_frame_rate(fps: f64) {
185 SCENE.with(|s| s.set((s.get().0, fps.max(1.0))));
186}
187
188pub(crate) fn clear_diags() {
189 DIAGS.with(|d| d.borrow_mut().clear());
190}
191
192pub(crate) fn push_diag(diag: ShaderDiag) {
193 DIAGS.with(|d| d.borrow_mut().push(diag));
194}
195
196pub(crate) fn take_diags() -> Vec<ShaderDiag> {
197 DIAGS.with(|d| std::mem::take(&mut *d.borrow_mut()))
198}
199
200pub struct ShaderDraft {
202 prev: bool,
203}
204
205impl Drop for ShaderDraft {
206 fn drop(&mut self) {
207 DRAFT.with(|d| d.set(self.prev));
208 }
209}
210
211pub fn draft_shaders() -> ShaderDraft {
216 let prev = DRAFT.with(Cell::get);
217 DRAFT.with(|d| d.set(true));
218 ShaderDraft { prev }
219}
220
221pub fn set_shader_budget_ms(ms: f64) {
223 BUDGET_MS.with(|b| b.set(ms));
224}
225
226fn draft() -> bool {
227 DRAFT.with(Cell::get)
228}
229
230fn budget_ms() -> f64 {
231 BUDGET_MS.with(Cell::get)
232}
233
234pub(crate) fn problems(n: &NodeData) -> Vec<ShaderDiag> {
237 if let NodeKind::Video { data, .. } = &n.kind {
238 return data
239 .problem
240 .iter()
241 .map(|p| ShaderDiag {
242 node: label(n),
243 source: n.source.clone(),
244 rule: "video_unavailable",
245 hint: format!("{}: {p}", data.path.display()),
246 })
247 .collect();
248 }
249 let NodeKind::Shader {
250 code,
251 uniforms,
252 preset,
253 inputs,
254 ..
255 } = &n.kind
256 else {
257 return Vec::new();
258 };
259 let label = label(n);
260 let source = n.source.clone();
261 let diag = |hint: String| {
262 vec![ShaderDiag {
263 node: label.clone(),
264 source: source.clone(),
265 rule: "param_unknown",
266 hint,
267 }]
268 };
269 let known: &[&str] = match preset {
270 Preset::LensDrops => &LENS_INPUTS,
271 Preset::Waves => &[],
272 Preset::Custom => &[],
273 };
274 if *preset == Preset::Custom {
275 for (name, _) in inputs {
276 if !is_ident(name) {
277 return diag(format!(
278 "input `{name}` is read as `sample_{name}(uv)`, so its name must be letters, digits and `_`."
279 ));
280 }
281 }
282 } else {
283 for (name, _) in inputs {
284 if !known.contains(&name.as_str()) {
285 return diag(unknown("input", name, known));
286 }
287 }
288 }
289 if *preset == Preset::LensDrops {
290 for u in uniforms {
291 if !LENS_PARAMS.contains(&u.name.as_str()) {
292 return diag(unknown("parameter", &u.name, &LENS_PARAMS));
293 }
294 if !matches!(u.value, UniformValue::Num(_)) {
295 return diag(format!("`{}` of lens_drops is a number.", u.name));
296 }
297 }
298 return Vec::new();
299 }
300 let text = match load(code) {
301 Ok(text) => text,
302 Err(hint) => {
303 return vec![ShaderDiag {
304 node: label,
305 source,
306 rule: "shader_error",
307 hint,
308 }];
309 }
310 };
311 let names: Vec<&str> = inputs.iter().map(|(n, _)| n.as_str()).collect();
312 match check_wgsl(&text, uniforms, &names) {
313 Ok(()) => Vec::new(),
314 Err(diag) => vec![ShaderDiag {
315 node: label,
316 source,
317 rule: diag.0,
318 hint: diag.1,
319 }],
320 }
321}
322
323pub(crate) fn raster(
325 n: &NodeData,
326 size: (f64, f64),
327 tf: Affine,
328 time: f64,
329 opacity: f64,
330) -> Option<Primitive> {
331 let NodeKind::Shader {
332 preset,
333 uniforms,
334 code,
335 ..
336 } = &n.kind
337 else {
338 return None;
339 };
340 let (w, h) = (size.0.max(1.0), size.1.max(1.0));
341 let scale = if draft() { 0.5 } else { 1.0 };
342 let iw = ((w * scale).round() as u32).clamp(1, 1920);
343 let ih = ((h * scale).round() as u32).clamp(1, 1080);
344 let started = Stopwatch::start();
345 let rgba = match preset {
346 Preset::Waves => {
347 let (amp, speed, deep, foam) = wave_uniforms(uniforms);
348 paint(
349 iw,
350 ih,
351 |uv, local| {
352 let world = tf * Point::new(local.0, local.1);
353 let crest = (world.x * 0.008 + time * speed).sin() * amp;
354 let t = smoothstep(crest - 0.02, crest + 0.02, uv.1 - 0.5);
355 Ok(mix(foam, deep, t, opacity))
356 },
357 (w, h),
358 )
359 }
360 #[cfg(not(target_arch = "wasm32"))]
361 Preset::Custom if !reads_frame_input(n) => {
362 custom(n, code, uniforms, (w, h), tf, time, opacity, (iw, ih))
363 }
364 _ => {
365 let _ = code;
366 return None;
367 }
368 };
369 let rgba = match rgba {
370 Ok(rgba) => rgba,
371 Err(hint) => {
372 push_diag(ShaderDiag {
373 node: label(n),
374 source: n.source.clone(),
375 rule: "shader_error",
376 hint,
377 });
378 return None;
379 }
380 };
381 let ms = started.millis();
382 if ms > budget_ms() {
383 push_diag(ShaderDiag {
384 node: label(n),
385 source: n.source.clone(),
386 rule: "shader_slow",
387 hint: format!(
388 "This shader took {ms:.0} ms in draft. The budget is {:.0} ms.",
389 budget_ms()
390 ),
391 });
392 }
393 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
394 Some(Primitive::Image {
395 image: std::rc::Rc::new(RasterImage {
396 width: iw,
397 height: ih,
398 id: hash_bytes(&rgba),
399 rgba,
400 }),
401 rect,
402 opacity: 1.0,
403 })
404}
405
406#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
412pub(crate) fn read_frame(
413 nodes: &[NodeData],
414 n: &NodeData,
415 size: (f64, f64),
416 tf: Affine,
417 time: f64,
418 opacity: f64,
419) -> Option<crate::post::Shade> {
420 #[cfg(not(target_arch = "wasm32"))]
421 if reads_frame_input(n) {
422 return custom_read_frame(nodes, n, size, tf, time, opacity);
423 }
424 let NodeKind::Shader {
425 preset: Preset::LensDrops,
426 uniforms,
427 ..
428 } = &n.kind
429 else {
430 return None;
431 };
432 let num = |name: &str, default: f64| {
433 uniforms
434 .iter()
435 .find_map(|u| match (&u.name[..], &u.value) {
436 (m, UniformValue::Num(v)) if m == name => Some(v.get()),
437 _ => None,
438 })
439 .unwrap_or(default)
440 };
441 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
442 let area = tf.transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
443 Some(crate::post::Shade {
444 op: crate::post::ShadeOp::LensDrops {
445 wetness: num("wetness", 0.6),
446 seed: num("seed", 0.0).max(0.0) as u64,
447 scale: num("drop_size", 1.0),
448 time,
449 },
450 opacity,
451 area: Some(area),
452 })
453}
454
455#[cfg(not(target_arch = "wasm32"))]
458fn custom_read_frame(
459 nodes: &[NodeData],
460 n: &NodeData,
461 size: (f64, f64),
462 tf: Affine,
463 time: f64,
464 opacity: f64,
465) -> Option<crate::post::Shade> {
466 let NodeKind::Shader {
467 code,
468 uniforms,
469 inputs,
470 ..
471 } = &n.kind
472 else {
473 return None;
474 };
475 let (program, globals) = match prepare(n, code, uniforms, size, time) {
476 Ok(ok) => ok,
477 Err(hint) => {
478 push_diag(ShaderDiag {
479 node: label(n),
480 source: n.source.clone(),
481 rule: "shader_error",
482 hint,
483 });
484 return None;
485 }
486 };
487 let frame = inputs
488 .iter()
489 .filter(|(_, i)| matches!(i, crate::post::Input::Frame | crate::post::Input::Backdrop))
490 .map(|(name, _)| name.clone())
491 .collect();
492 let offscreen = inputs
493 .iter()
494 .filter_map(|(name, i)| match i {
495 crate::post::Input::Node(id) if (id.0 as usize) < nodes.len() => {
496 let (items, bounds) = crate::graph::offscreen_items(nodes, *id, time);
497 Some(NodeInput {
498 name: name.clone(),
499 items,
500 bounds,
501 })
502 }
503 _ => None,
504 })
505 .collect();
506 let ((sw, sh), _) = SCENE.with(Cell::get);
507 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
508 let area = tf.transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
509 Some(crate::post::Shade {
510 op: crate::post::ShadeOp::Custom(Box::new(CustomShade {
511 program,
512 globals,
513 frame,
514 offscreen,
515 tf,
516 size: (w, h),
517 scene: (sw, sh),
518 })),
519 opacity,
520 area: Some(area),
521 })
522}
523
524fn unknown(what: &str, name: &str, known: &[&str]) -> String {
526 let list = if known.is_empty() {
527 "none".to_owned()
528 } else {
529 known.join(", ")
530 };
531 match closest(name, known) {
532 Some(n) => format!("no {what} `{name}`. Did you mean `{n}`? Available: {list}."),
533 None => format!("no {what} `{name}`. Available: {list}."),
534 }
535}
536
537#[cfg(not(target_arch = "wasm32"))]
543struct Stopwatch(std::time::Instant);
544
545#[cfg(target_arch = "wasm32")]
546struct Stopwatch;
547
548impl Stopwatch {
549 fn start() -> Self {
550 #[cfg(not(target_arch = "wasm32"))]
551 {
552 Self(std::time::Instant::now())
553 }
554 #[cfg(target_arch = "wasm32")]
555 {
556 Self
557 }
558 }
559
560 fn millis(&self) -> f64 {
561 #[cfg(not(target_arch = "wasm32"))]
562 {
563 self.0.elapsed().as_secs_f64() * 1000.0
564 }
565 #[cfg(target_arch = "wasm32")]
566 {
567 0.0
568 }
569 }
570}
571
572fn paint(
575 iw: u32,
576 ih: u32,
577 mut pixel: impl FnMut((f64, f64), (f64, f64)) -> Result<Color, String>,
578 size: (f64, f64),
579) -> Result<Vec<u8>, String> {
580 let (w, h) = size;
581 let mut rgba = vec![0u8; iw as usize * ih as usize * 4];
582 for py in 0..ih {
583 for px in 0..iw {
584 let u = (f64::from(px) + 0.5) / f64::from(iw);
585 let v = (f64::from(py) + 0.5) / f64::from(ih);
586 let c = pixel((u, v), (u * w - w / 2.0, v * h - h / 2.0))?;
587 let i = (py as usize * iw as usize + px as usize) * 4;
588 rgba[i] = byte(c.r);
589 rgba[i + 1] = byte(c.g);
590 rgba[i + 2] = byte(c.b);
591 rgba[i + 3] = byte(c.a);
592 }
593 }
594 Ok(rgba)
595}
596
597#[cfg(not(target_arch = "wasm32"))]
599#[allow(clippy::too_many_arguments)]
600fn custom(
601 n: &NodeData,
602 code: &ShaderCode,
603 uniforms: &[Uniform],
604 size: (f64, f64),
605 tf: Affine,
606 time: f64,
607 opacity: f64,
608 raster: (u32, u32),
609) -> Result<Vec<u8>, String> {
610 let (program, globals) = prepare(n, code, uniforms, size, time)?;
611 let ((sw, sh), _) = SCENE.with(Cell::get);
612 let (iw, ih) = raster;
613 let a = opacity.clamp(0.0, 1.0) as f32;
614 let (w, h) = size;
615 let total = iw as usize * ih as usize;
616 let mut rgba = vec![0u8; total * 4];
617 run_tiles(
618 &program,
619 &globals,
620 total,
621 |i| {
622 let (px, py) = ((i % iw as usize) as f64, (i / iw as usize) as f64);
623 let u = (px + 0.5) / f64::from(iw);
624 let v = (py + 0.5) / f64::from(ih);
625 let local = (u * w - w / 2.0, v * h - h / 2.0);
626 let world = tf * Point::new(local.0, local.1);
627 [
628 u,
629 v,
630 local.0,
631 local.1,
632 world.x,
633 world.y,
634 world.x + sw / 2.0,
635 world.y + sh / 2.0,
636 ]
637 },
638 |i, [r, g, b, alpha]| {
639 let o = i * 4;
640 rgba[o] = byte(r);
641 rgba[o + 1] = byte(g);
642 rgba[o + 2] = byte(b);
643 rgba[o + 3] = byte(alpha * a);
644 },
645 )?;
646 Ok(rgba)
647}
648
649#[cfg(not(target_arch = "wasm32"))]
652fn prepare(
653 n: &NodeData,
654 code: &ShaderCode,
655 uniforms: &[Uniform],
656 size: (f64, f64),
657 time: f64,
658) -> Result<
659 (
660 std::rc::Rc<crate::wgsl_cpu::Program>,
661 crate::wgsl_cpu::Globals,
662 ),
663 String,
664> {
665 use crate::wgsl_cpu::{Globals, Value};
666 let inputs = match &n.kind {
667 NodeKind::Shader { inputs, .. } => inputs.as_slice(),
668 _ => &[],
669 };
670 let names: Vec<&str> = inputs.iter().map(|(n, _)| n.as_str()).collect();
671 let text = load(code)?;
672 let program = compiled(&text, &names).map_err(|(_, hint)| hint)?;
673 let ((sw, sh), fps) = SCENE.with(Cell::get);
674 let identity = Value::C(
675 (0..4)
676 .map(|c| {
677 let mut col = [0.0; 4];
678 col[c] = 1.0;
679 Value::fv(&col)
680 })
681 .collect(),
682 );
683 let ox = Value::C(vec![
684 Value::f(time as f32),
685 Value::f((time * fps + 1e-6).floor() as f32),
686 Value::fv(&[sw as f32, sh as f32]),
687 Value::fv(&[size.0 as f32, size.1 as f32]),
688 Value::f(0.0),
689 identity,
690 Value::f(0.0),
691 ]);
692 let params = program
693 .params()
694 .into_iter()
695 .map(|name| {
696 uniforms
697 .iter()
698 .find(|u| u.name == name)
699 .map_or(Value::f(0.0), |u| match &u.value {
700 UniformValue::Num(v) => Value::f(v.get() as f32),
701 UniformValue::Vec2(v) => {
702 let v = v.get();
703 Value::fv(&[v.x as f32, v.y as f32])
704 }
705 UniformValue::Color(v) => {
706 let c = v.get();
707 Value::fv(&[c.r, c.g, c.b, c.a])
708 }
709 UniformValue::Bool(v) => Value::B(1, [v.get() > 0.5, false, false, false]),
710 })
711 })
712 .collect();
713 let globals = Globals {
714 ox,
715 params: Value::C(params),
716 inputs: inputs
717 .iter()
718 .filter_map(|(name, input)| match input {
719 crate::post::Input::Image(img) => Some((name.clone(), img.clone())),
720 _ => None,
721 })
722 .collect(),
723 };
724 Ok((program, globals))
725}
726
727#[cfg(not(target_arch = "wasm32"))]
730fn run_tiles(
731 program: &crate::wgsl_cpu::Program,
732 globals: &crate::wgsl_cpu::Globals,
733 total: usize,
734 at: impl Fn(usize) -> [f64; 8],
735 mut put: impl FnMut(usize, [f32; 4]),
736) -> Result<(), String> {
737 use crate::wgsl_cpu::{LANES, V};
738 let mut runner = program.runner(globals);
739 let mut start = 0;
740 while start < total {
741 let count = (total - start).min(LANES);
742 let mut comps = vec![[0.0f32; LANES]; 8];
743 for k in 0..count {
744 for (c, x) in comps.iter_mut().zip(at(start + k)) {
745 c[k] = x as f32;
746 }
747 }
748 let input = V::C(comps.chunks(2).map(|pair| V::F(pair.to_vec())).collect());
749 let out = runner
750 .run(&input, count)
751 .map_err(|e| format!("{e} (when oxframe runs the shader on the CPU)"))?;
752 for k in 0..count {
753 let rgba = out
754 .lane(k)
755 .floats()
756 .ok_or("`shade` must return vec4<f32>")?;
757 put(start + k, rgba);
758 }
759 start += count;
760 }
761 Ok(())
762}
763
764#[cfg(not(target_arch = "wasm32"))]
768#[derive(Clone)]
769pub struct CustomShade {
770 program: std::rc::Rc<crate::wgsl_cpu::Program>,
771 globals: crate::wgsl_cpu::Globals,
772 frame: Vec<String>,
774 offscreen: Vec<NodeInput>,
776 tf: Affine,
777 size: (f64, f64),
778 scene: (f64, f64),
779}
780
781#[cfg(not(target_arch = "wasm32"))]
784#[derive(Clone)]
785struct NodeInput {
786 name: String,
787 items: Vec<crate::DrawItem>,
788 bounds: kurbo::Rect,
789}
790
791#[cfg(not(target_arch = "wasm32"))]
792impl PartialEq for NodeInput {
793 fn eq(&self, other: &Self) -> bool {
794 self.name == other.name
795 && self.bounds == other.bounds
796 && self.items.len() == other.items.len()
797 && self.items.iter().zip(&other.items).all(|(a, b)| {
798 a.node == b.node
799 && a.transform == b.transform
800 && crate::graph::prim_same(&a.prim, &b.prim)
801 })
802 }
803}
804
805#[cfg(not(target_arch = "wasm32"))]
806impl std::fmt::Debug for CustomShade {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 f.debug_struct("CustomShade")
809 .field("frame", &self.frame)
810 .field("size", &self.size)
811 .finish_non_exhaustive()
812 }
813}
814
815#[cfg(not(target_arch = "wasm32"))]
816impl PartialEq for CustomShade {
817 fn eq(&self, other: &Self) -> bool {
818 std::rc::Rc::ptr_eq(&self.program, &other.program)
819 && self.globals.ox == other.globals.ox
820 && self.globals.params == other.globals.params
821 && self.globals.inputs.len() == other.globals.inputs.len()
822 && self
823 .globals
824 .inputs
825 .iter()
826 .zip(&other.globals.inputs)
827 .all(|(a, b)| a.0 == b.0 && a.1.id == b.1.id)
828 && self.frame == other.frame
829 && self.offscreen == other.offscreen
830 && self.tf == other.tf
831 && self.size == other.size
832 && self.scene == other.scene
833 }
834}
835
836#[cfg(not(target_arch = "wasm32"))]
837impl CustomShade {
838 pub fn offscreen_lists(&self) -> Vec<&[crate::DrawItem]> {
842 self.offscreen.iter().map(|n| n.items.as_slice()).collect()
843 }
844
845 pub(crate) fn apply(
850 &self,
851 img: &mut oxframe_core::Image,
852 to_px: Affine,
853 opacity: f32,
854 drawn: &[oxframe_core::Image],
855 ) {
856 let (w, h) = self.size;
857 if img.width == 0 || img.height == 0 || w <= 0.0 || h <= 0.0 {
858 return;
859 }
860 let snapshot = std::rc::Rc::new(RasterImage {
861 width: img.width,
862 height: img.height,
863 rgba: img.data.clone(),
864 id: 0,
865 });
866 let mut globals = self.globals.clone();
867 for name in &self.frame {
868 globals.inputs.push((name.clone(), snapshot.clone()));
869 }
870 for (input, pixels) in self.offscreen.iter().zip(drawn) {
871 globals.inputs.push((
872 input.name.clone(),
873 std::rc::Rc::new(crop(pixels, to_px, input.bounds)),
874 ));
875 }
876 let node_px = to_px * self.tf;
877 let Some(inv) = (node_px.determinant().abs() > 1e-12).then(|| node_px.inverse()) else {
878 return;
879 };
880 let r = node_px.transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
881 let x0 = r.x0.floor().clamp(0.0, f64::from(img.width)) as usize;
882 let y0 = r.y0.floor().clamp(0.0, f64::from(img.height)) as usize;
883 let x1 = r.x1.ceil().clamp(0.0, f64::from(img.width)) as usize;
884 let y1 = r.y1.ceil().clamp(0.0, f64::from(img.height)) as usize;
885 if x1 <= x0 || y1 <= y0 {
886 return;
887 }
888 let cols = x1 - x0;
889 let total = cols * (y1 - y0);
890 let (sw, sh) = self.scene;
891 let tf = self.tf;
892 let width = img.width as usize;
893 let pixel = |i: usize| (x0 + i % cols, y0 + i / cols);
894 let local = |i: usize| {
895 let (x, y) = pixel(i);
896 inv * Point::new(x as f64 + 0.5, y as f64 + 0.5)
897 };
898 let src = &snapshot.rgba;
899 let data = &mut img.data;
900 let _ = run_tiles(
901 &self.program,
902 &globals,
903 total,
904 |i| {
905 let l = local(i);
906 let world = tf * l;
907 [
908 (l.x + w / 2.0) / w,
909 (l.y + h / 2.0) / h,
910 l.x,
911 l.y,
912 world.x,
913 world.y,
914 world.x + sw / 2.0,
915 world.y + sh / 2.0,
916 ]
917 },
918 |i, [r, g, b, a]| {
919 let l = local(i);
920 if l.x.abs() > w / 2.0 || l.y.abs() > h / 2.0 {
921 return;
922 }
923 let (x, y) = pixel(i);
924 let o = (y * width + x) * 4;
925 let a = (a.clamp(0.0, 1.0) * opacity).clamp(0.0, 1.0);
926 let under = f32::from(src[o + 3]) / 255.0;
927 let out_a = a + under * (1.0 - a);
928 for (c, v) in [r, g, b].into_iter().enumerate() {
929 let below = f32::from(src[o + c]) / 255.0;
930 let mixed = if out_a > 0.0 {
931 (v.clamp(0.0, 1.0) * a + below * under * (1.0 - a)) / out_a
932 } else {
933 0.0
934 };
935 data[o + c] = byte(mixed);
936 }
937 data[o + 3] = byte(out_a);
938 },
939 );
940 }
941}
942
943#[cfg(not(target_arch = "wasm32"))]
945fn crop(img: &oxframe_core::Image, to_px: Affine, bounds: kurbo::Rect) -> RasterImage {
946 let r = to_px.transform_rect_bbox(bounds);
947 let clampx = |v: f64| v.clamp(0.0, f64::from(img.width)) as u32;
948 let clampy = |v: f64| v.clamp(0.0, f64::from(img.height)) as u32;
949 let (x0, y0) = (clampx(r.x0.floor()), clampy(r.y0.floor()));
950 let (x1, y1) = (
951 clampx(r.x1.ceil()).max(x0 + 1),
952 clampy(r.y1.ceil()).max(y0 + 1),
953 );
954 let (w, h) = (x1 - x0, y1 - y0);
955 let mut rgba = vec![0u8; (w * h * 4) as usize];
956 for y in 0..h {
957 for x in 0..w {
958 let (sx, sy) = (x0 + x, y0 + y);
959 if sx < img.width && sy < img.height {
960 let s = ((sy * img.width + sx) * 4) as usize;
961 let d = ((y * w + x) * 4) as usize;
962 rgba[d..d + 4].copy_from_slice(&img.data[s..s + 4]);
963 }
964 }
965 }
966 RasterImage {
967 width: w,
968 height: h,
969 rgba,
970 id: 0,
971 }
972}
973
974#[cfg(target_arch = "wasm32")]
977pub(crate) fn reads_frame_input(_n: &NodeData) -> bool {
978 false
979}
980
981#[cfg(not(target_arch = "wasm32"))]
982pub(crate) fn reads_frame_input(n: &NodeData) -> bool {
983 matches!(&n.kind, NodeKind::Shader { preset: Preset::Custom, inputs, .. }
984 if inputs.iter().any(|(_, i)| matches!(i, crate::post::Input::Frame | crate::post::Input::Backdrop | crate::post::Input::Node(_))))
985}
986
987fn wave_uniforms(uniforms: &[Uniform]) -> (f64, f64, Color, Color) {
988 let mut amp = 0.05;
989 let mut speed = 1.0;
990 let mut deep = Color::BLACK;
991 let mut foam = Color::WHITE;
992 for u in uniforms {
993 match (&u.name[..], &u.value) {
994 ("amp", UniformValue::Num(v)) => amp = v.get(),
995 ("speed", UniformValue::Num(v)) => speed = v.get(),
996 ("deep", UniformValue::Color(v)) => deep = v.get(),
997 ("foam", UniformValue::Color(v)) => foam = v.get(),
998 _ => {}
999 }
1000 }
1001 (amp, speed, deep, foam)
1002}
1003
1004fn load(code: &ShaderCode) -> Result<String, String> {
1005 match code {
1006 ShaderCode::Wgsl(text) => Ok(text.clone()),
1007 ShaderCode::File(path) => read_file(path),
1008 }
1009}
1010
1011#[cfg(not(target_arch = "wasm32"))]
1012fn read_file(path: &std::path::Path) -> Result<String, String> {
1013 std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))
1014}
1015
1016#[cfg(target_arch = "wasm32")]
1017fn read_file(path: &std::path::Path) -> Result<String, String> {
1018 let _ = path;
1019 Err("a shader file is read by the native renderer".into())
1020}
1021
1022fn label(n: &NodeData) -> String {
1023 n.name
1024 .clone()
1025 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0))
1026}
1027
1028#[cfg(target_arch = "wasm32")]
1029fn check_wgsl(
1030 _text: &str,
1031 _uniforms: &[Uniform],
1032 _inputs: &[&str],
1033) -> Result<(), (&'static str, String)> {
1034 Ok(())
1035}
1036
1037#[cfg(not(target_arch = "wasm32"))]
1038type Compiled = Result<std::rc::Rc<crate::wgsl_cpu::Program>, (&'static str, String)>;
1039
1040#[cfg(not(target_arch = "wasm32"))]
1041thread_local! {
1042 static PROGRAMS: std::cell::RefCell<std::collections::HashMap<u64, Compiled>> =
1045 std::cell::RefCell::new(std::collections::HashMap::new());
1046}
1047
1048#[cfg(not(target_arch = "wasm32"))]
1050fn compiled(text: &str, inputs: &[&str]) -> Compiled {
1051 let key = hash_bytes(format!("{text}\0{}", inputs.join(",")).as_bytes());
1052 if let Some(hit) = PROGRAMS.with(|p| p.borrow().get(&key).cloned()) {
1053 return hit;
1054 }
1055 let result = compile(text, inputs);
1056 PROGRAMS.with(|p| {
1057 let mut p = p.borrow_mut();
1058 if p.len() > 64 {
1059 p.clear();
1060 }
1061 p.insert(key, result.clone());
1062 });
1063 result
1064}
1065
1066#[cfg(not(target_arch = "wasm32"))]
1067fn compile(text: &str, inputs: &[&str]) -> Compiled {
1068 let samplers: String = inputs
1070 .iter()
1071 .map(|name| {
1072 format!("\nfn sample_{name}(uv: vec2<f32>) -> vec4<f32> {{ return vec4<f32>(0.0); }}\n")
1073 })
1074 .collect();
1075 let full = format!("{PRELUDE}{text}{samplers}{EPILOGUE}");
1076 let module = naga::front::wgsl::parse_str(&full).map_err(|e| {
1077 let mut hint = shift_lines(&e.emit_to_string(&full), prelude_lines());
1078 if let Some(name) = missing_sampler(&hint) {
1079 hint.push_str(&format!(
1080 "\nRead an image with `.input(\"{name}\", Input::file(\"…png\"))` on the shader."
1081 ));
1082 }
1083 ("shader_error", hint)
1084 })?;
1085 naga::valid::Validator::new(
1086 naga::valid::ValidationFlags::all(),
1087 naga::valid::Capabilities::all(),
1088 )
1089 .validate(&module)
1090 .map_err(|e| ("shader_error", shift_lines(&e.to_string(), prelude_lines())))?;
1091 crate::wgsl_cpu::Program::new(module)
1092 .map(std::rc::Rc::new)
1093 .ok_or(("shader_error", "the shader has no `fn shade`".to_string()))
1094}
1095
1096#[cfg(not(target_arch = "wasm32"))]
1097fn check_wgsl(
1098 text: &str,
1099 uniforms: &[Uniform],
1100 inputs: &[&str],
1101) -> Result<(), (&'static str, String)> {
1102 let program = compiled(text, inputs)?;
1103 let fields = params_of(program.module()).ok_or((
1104 "shader_error",
1105 "the shader has no `struct Params`".to_string(),
1106 ))?;
1107 match params(uniforms, &fields) {
1108 Ok(()) => Ok(()),
1109 Err(hint) => Err(("param_unknown", hint)),
1110 }
1111}
1112
1113#[cfg(not(target_arch = "wasm32"))]
1116fn missing_sampler(message: &str) -> Option<String> {
1117 let at = message.find("sample_")?;
1118 let name: String = message[at + 7..]
1119 .chars()
1120 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
1121 .collect();
1122 (message.contains("no definition in scope") && !name.is_empty()).then_some(name)
1123}
1124
1125fn is_ident(name: &str) -> bool {
1127 let mut chars = name.chars();
1128 chars
1129 .next()
1130 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1131 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
1132}
1133
1134#[cfg(not(target_arch = "wasm32"))]
1135fn prelude_lines() -> usize {
1136 PRELUDE.lines().count()
1137}
1138
1139#[cfg(not(target_arch = "wasm32"))]
1142fn shift_lines(message: &str, by: usize) -> String {
1143 let chars: Vec<char> = message.chars().collect();
1144 let mut out = String::with_capacity(message.len());
1145 let mut i = 0;
1146 while i < chars.len() {
1147 if chars[i] == ':' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
1148 let start = i + 1;
1149 let mut end = start;
1150 while end < chars.len() && chars[end].is_ascii_digit() {
1151 end += 1;
1152 }
1153 let digits: String = chars[start..end].iter().collect();
1154 let line: usize = digits.parse().unwrap_or(0);
1155 out.push(':');
1156 out.push_str(&line.saturating_sub(by).to_string());
1157 i = end;
1158 } else {
1159 out.push(chars[i]);
1160 i += 1;
1161 }
1162 }
1163 out
1164}
1165
1166#[cfg(not(target_arch = "wasm32"))]
1167enum WgslTy {
1168 F32,
1169 Vec2,
1170 Vec4,
1171 Bool,
1172 Other,
1173}
1174
1175#[cfg(not(target_arch = "wasm32"))]
1176fn params_of(module: &naga::Module) -> Option<Vec<(String, WgslTy)>> {
1177 for (_, ty) in module.types.iter() {
1178 if ty.name.as_deref() != Some("Params") {
1179 continue;
1180 }
1181 let naga::TypeInner::Struct { members, .. } = &ty.inner else {
1182 continue;
1183 };
1184 return Some(
1185 members
1186 .iter()
1187 .map(|m| {
1188 let name = m.name.clone().unwrap_or_default();
1189 (name, wgsl_ty(&module.types[m.ty].inner))
1190 })
1191 .collect(),
1192 );
1193 }
1194 None
1195}
1196
1197#[cfg(not(target_arch = "wasm32"))]
1198fn wgsl_ty(inner: &naga::TypeInner) -> WgslTy {
1199 match inner {
1200 naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Float && s.width == 4 => {
1201 WgslTy::F32
1202 }
1203 naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Bool => WgslTy::Bool,
1204 naga::TypeInner::Vector { size, scalar }
1205 if scalar.kind == naga::ScalarKind::Float
1206 && scalar.width == 4
1207 && *size == naga::VectorSize::Bi =>
1208 {
1209 WgslTy::Vec2
1210 }
1211 naga::TypeInner::Vector { size, scalar }
1212 if scalar.kind == naga::ScalarKind::Float
1213 && scalar.width == 4
1214 && *size == naga::VectorSize::Quad =>
1215 {
1216 WgslTy::Vec4
1217 }
1218 _ => WgslTy::Other,
1219 }
1220}
1221
1222#[cfg(not(target_arch = "wasm32"))]
1223fn params(uniforms: &[Uniform], fields: &[(String, WgslTy)]) -> Result<(), String> {
1224 let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
1225 let list = names.join(", ");
1226 for u in uniforms {
1227 let Some((_, ty)) = fields.iter().find(|(n, _)| n == &u.name) else {
1228 let near = closest(&u.name, &names);
1229 let hint = match near {
1230 Some(n) => format!(
1231 "no parameter `{}`. Did you mean `{n}`? Available: {list}.",
1232 u.name
1233 ),
1234 None => format!("no parameter `{}`. Available: {list}.", u.name),
1235 };
1236 return Err(hint);
1237 };
1238 if !type_matches(&u.value, ty) {
1239 return Err(format!(
1240 "`{}` is {} in Params, but the scene sets {}.",
1241 u.name,
1242 ty_name(ty),
1243 value_name(&u.value)
1244 ));
1245 }
1246 }
1247 for (name, _) in fields {
1248 if !uniforms.iter().any(|u| u.name == *name) {
1249 return Err(format!(
1250 "Params.{name} has no value. Set it with `.uniform(\"{name}\", ...)`."
1251 ));
1252 }
1253 }
1254 Ok(())
1255}
1256
1257#[cfg(not(target_arch = "wasm32"))]
1258fn type_matches(value: &UniformValue, ty: &WgslTy) -> bool {
1259 matches!(
1260 (value, ty),
1261 (UniformValue::Num(_), WgslTy::F32)
1262 | (UniformValue::Vec2(_), WgslTy::Vec2)
1263 | (UniformValue::Color(_), WgslTy::Vec4)
1264 | (UniformValue::Bool(_), WgslTy::Bool)
1265 )
1266}
1267
1268#[cfg(not(target_arch = "wasm32"))]
1269fn ty_name(ty: &WgslTy) -> &'static str {
1270 match ty {
1271 WgslTy::F32 => "f32",
1272 WgslTy::Vec2 => "vec2<f32>",
1273 WgslTy::Vec4 => "vec4<f32>",
1274 WgslTy::Bool => "bool",
1275 WgslTy::Other => "another type",
1276 }
1277}
1278
1279#[cfg(not(target_arch = "wasm32"))]
1280fn value_name(value: &UniformValue) -> &'static str {
1281 match value {
1282 UniformValue::Num(_) => "a number",
1283 UniformValue::Vec2(_) => "a point",
1284 UniformValue::Color(_) => "a color",
1285 UniformValue::Bool(_) => "a bool",
1286 }
1287}
1288
1289fn closest<'a>(name: &str, options: &[&'a str]) -> Option<&'a str> {
1290 options
1291 .iter()
1292 .copied()
1293 .filter(|o| edit(name, o) <= 3 && *o != name)
1294 .min_by_key(|o| edit(name, o))
1295}
1296
1297fn edit(a: &str, b: &str) -> usize {
1298 let a: Vec<char> = a.chars().collect();
1299 let b: Vec<char> = b.chars().collect();
1300 let mut prev: Vec<usize> = (0..=b.len()).collect();
1301 let mut cur = vec![0; b.len() + 1];
1302 for (i, ca) in a.iter().enumerate() {
1303 cur[0] = i + 1;
1304 for (j, cb) in b.iter().enumerate() {
1305 let cost = usize::from(ca != cb);
1306 cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
1307 }
1308 std::mem::swap(&mut prev, &mut cur);
1309 }
1310 prev[b.len()]
1311}
1312
1313fn smoothstep(e0: f64, e1: f64, x: f64) -> f64 {
1314 let t = if (e1 - e0).abs() < 1e-9 {
1315 if x < e0 { 0.0 } else { 1.0 }
1316 } else {
1317 ((x - e0) / (e1 - e0)).clamp(0.0, 1.0)
1318 };
1319 t * t * (3.0 - 2.0 * t)
1320}
1321
1322fn mix(foam: Color, deep: Color, t: f64, opacity: f64) -> Color {
1323 let t = t as f32;
1324 let a = opacity.clamp(0.0, 1.0) as f32;
1325 Color {
1326 r: foam.r + (deep.r - foam.r) * t,
1327 g: foam.g + (deep.g - foam.g) * t,
1328 b: foam.b + (deep.b - foam.b) * t,
1329 a: (foam.a + (deep.a - foam.a) * t) * a,
1330 }
1331}
1332
1333fn byte(c: f32) -> u8 {
1334 (c.clamp(0.0, 1.0) * 255.0).round() as u8
1335}
1336
1337fn hash_bytes(bytes: &[u8]) -> u64 {
1338 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1339 for &b in bytes {
1340 h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
1341 }
1342 h
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::builder::NodeBuilder;
1349 use crate::{SceneGraph, View};
1350 use std::rc::Rc;
1351
1352 fn paint(f: impl FnOnce(View)) -> Rc<SceneGraph> {
1353 let graph = SceneGraph::new(320, 180);
1354 graph.enter(|| f(View::current()));
1355 graph
1356 }
1357
1358 fn image_of(graph: &SceneGraph) -> Rc<RasterImage> {
1359 let items = graph.draw_list();
1360 for item in &items {
1361 if let Primitive::Image { image, .. } = &item.prim {
1362 return image.clone();
1363 }
1364 }
1365 panic!("waves did not paint, diags: {:?}", graph.shader_diags());
1366 }
1367
1368 #[test]
1369 fn waves_put_foam_above_deep_water() {
1370 let graph = paint(|view| {
1371 view.add(crate::waves().width(200.0).height(80.0));
1372 });
1373 let image = image_of(&graph);
1374 let px = |x: u32, y: u32| {
1375 let i = (y * image.width + x) as usize * 4;
1376 image.rgba[i + 2]
1377 };
1378 let mid = image.width / 2;
1379 assert!(
1380 px(mid, 2) > px(mid, image.height - 3),
1381 "the crest is the lighter foam"
1382 );
1383 assert!(
1384 graph.shader_diags().is_empty(),
1385 "{:?}",
1386 graph.shader_diags()
1387 );
1388 }
1389
1390 #[test]
1391 fn custom_wgsl_runs_like_the_hand_written_preset() {
1392 let custom = paint(|view| {
1395 view.add(
1396 crate::Shader::source(waves_wgsl())
1397 .uniform("amp", 0.2)
1398 .uniform("speed", 1.0)
1399 .uniform_color("deep", Color::rgba(0.1, 0.3, 0.5, 1.0))
1400 .uniform_color("foam", Color::rgba(0.9, 0.95, 1.0, 1.0))
1401 .width(200.0)
1402 .height(80.0),
1403 );
1404 });
1405 let preset = paint(|view| {
1406 view.add(
1407 crate::waves()
1408 .amp(0.2)
1409 .deep(Color::rgba(0.1, 0.3, 0.5, 1.0))
1410 .foam(Color::rgba(0.9, 0.95, 1.0, 1.0))
1411 .width(200.0)
1412 .height(80.0),
1413 );
1414 });
1415 for t in [0.0, 1.3] {
1416 custom.set_time(t);
1417 preset.set_time(t);
1418 let (a, b) = (image_of(&custom), image_of(&preset));
1419 assert_eq!((a.width, a.height), (b.width, b.height));
1420 let worst = a
1421 .rgba
1422 .iter()
1423 .zip(&b.rgba)
1424 .map(|(x, y)| x.abs_diff(*y))
1425 .max()
1426 .unwrap_or(0);
1427 assert!(worst <= 2, "differs by {worst}/255 at t={t}");
1428 }
1429 assert!(
1430 custom.shader_diags().is_empty(),
1431 "{:?}",
1432 custom.shader_diags()
1433 );
1434 }
1435
1436 fn two_pixels() -> crate::Input {
1437 crate::Input::Image(Rc::new(RasterImage {
1438 width: 2,
1439 height: 1,
1440 rgba: vec![255, 0, 0, 255, 0, 0, 255, 255],
1441 id: 9_001,
1442 }))
1443 }
1444
1445 const SAMPLE_PHOTO: &str = "struct Params { k: f32 }
1446@group(1) @binding(0) var<uniform> p: Params;
1447fn shade(i: ShadeIn) -> vec4<f32> {
1448 return sample_photo(i.uv);
1449}";
1450
1451 #[test]
1452 fn a_custom_shade_reads_an_image_input() {
1453 let graph = paint(|view| {
1454 view.add(
1455 crate::Shader::source(SAMPLE_PHOTO)
1456 .uniform("k", 0.0)
1457 .input("photo", two_pixels())
1458 .width(40.0)
1459 .height(4.0),
1460 );
1461 });
1462 let img = image_of(&graph);
1463 let px = |x: u32| {
1464 let o = ((img.height / 2 * img.width + x) * 4) as usize;
1465 [img.rgba[o], img.rgba[o + 1], img.rgba[o + 2]]
1466 };
1467 assert_eq!(px(0), [255, 0, 0]);
1468 assert_eq!(px(img.width - 1), [0, 0, 255]);
1469 let mid = px(img.width / 2);
1470 assert!(mid[0] > 60 && mid[2] > 60, "bilinear between: {mid:?}");
1471 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
1472 }
1473
1474 #[test]
1475 fn a_custom_shade_with_a_frame_input_reads_what_was_drawn() {
1476 let graph = paint(|view| {
1479 view.add(
1480 crate::Shader::source(
1481 "struct Params { k: f32 }
1482@group(1) @binding(0) var<uniform> p: Params;
1483fn shade(i: ShadeIn) -> vec4<f32> {
1484 let s = i.screen / ox.resolution;
1485 return sample_scene(vec2<f32>(1.0 - s.x, s.y));
1486}",
1487 )
1488 .uniform("k", 0.0)
1489 .input("scene", crate::Input::frame())
1490 .width(160.0)
1491 .height(180.0)
1492 .x(80.0),
1493 );
1494 });
1495 assert!(reads_frame(&graph));
1496 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
1497 let shade = graph
1498 .draw_list()
1499 .iter()
1500 .find_map(|i| match &i.prim {
1501 Primitive::ReadFrame(s) => Some(s.clone()),
1502 _ => None,
1503 })
1504 .expect("a frame pass");
1505 assert_eq!(shade.op.name(), "custom");
1506 let mut img = oxframe_core::Image {
1507 width: 320,
1508 height: 180,
1509 data: (0..320 * 180)
1510 .flat_map(|i| {
1511 if i % 320 < 160 {
1512 [255, 0, 0, 255]
1513 } else {
1514 [0, 0, 255, 255]
1515 }
1516 })
1517 .collect(),
1518 };
1519 let to_px = Affine::translate((160.0, 90.0));
1520 crate::post::apply(&shade, &mut img, to_px);
1521 let px = |x: usize| &img.data[(90 * 320 + x) * 4..(90 * 320 + x) * 4 + 3];
1522 assert_eq!(px(10), [255, 0, 0], "left half is not touched");
1523 assert_eq!(px(300), [255, 0, 0], "right half mirrors the left");
1524 }
1525
1526 #[test]
1527 fn an_offscreen_node_is_not_drawn_and_check_skips_it() {
1528 let graph = paint(|view| {
1529 let card = view.add(crate::Rect::new().size(20.0).name("card").offscreen());
1530 view.add(
1531 crate::Shader::source(SAMPLE_PHOTO.replace("photo", "card"))
1532 .uniform("k", 0.0)
1533 .input("card", crate::Input::node(card))
1534 .width(40.0)
1535 .height(40.0),
1536 );
1537 });
1538 let list = graph.draw_list();
1539 assert!(
1540 list.iter().all(|i| i.prim.is_marker()),
1541 "only the shader's pass is on screen"
1542 );
1543 let shade = list
1544 .iter()
1545 .find_map(|i| match &i.prim {
1546 Primitive::ReadFrame(s) => Some(s),
1547 _ => None,
1548 })
1549 .expect("a node input makes a frame pass");
1550 assert_eq!(shade.offscreen_lists().len(), 1);
1551 assert!(!shade.offscreen_lists()[0].is_empty());
1552 let card = graph
1553 .inspect_nodes()
1554 .into_iter()
1555 .find(|n| n.name == "card")
1556 .expect("card");
1557 assert_eq!(card.opacity, 0.0);
1558 assert!(graph.has_post());
1559 }
1560
1561 #[test]
1562 fn sampling_an_input_that_is_not_declared_says_how_to_add_it() {
1563 let graph = paint(|view| {
1564 view.add(
1565 crate::Shader::source(SAMPLE_PHOTO)
1566 .uniform("k", 0.0)
1567 .width(4.0)
1568 .height(4.0),
1569 );
1570 });
1571 let _ = graph.draw_list();
1572 let diags = graph.shader_diags();
1573 let d = diags
1574 .iter()
1575 .find(|d| d.rule == "shader_error")
1576 .expect("shader_error");
1577 assert!(d.hint.contains(".input(\"photo\""), "{}", d.hint);
1578 }
1579
1580 #[test]
1581 fn an_input_name_must_be_a_wgsl_name() {
1582 let graph = paint(|view| {
1583 view.add(
1584 crate::Shader::source(SAMPLE_PHOTO)
1585 .uniform("k", 0.0)
1586 .input("my photo", two_pixels())
1587 .width(4.0)
1588 .height(4.0),
1589 );
1590 });
1591 assert!(rules(&graph).contains(&"param_unknown"));
1592 }
1593
1594 #[test]
1595 fn a_shader_that_fails_while_running_is_a_shader_error() {
1596 let graph = paint(|view| {
1597 view.add(
1598 crate::Shader::source(
1599 "struct Params { k: f32 }
1600@group(1) @binding(0) var<uniform> p: Params;
1601fn shade(i: ShadeIn) -> vec4<f32> {
1602 var x = 0.0;
1603 loop { x += p.k; }
1604 return vec4<f32>(x);
1605}",
1606 )
1607 .uniform("k", 1.0)
1608 .width(4.0)
1609 .height(4.0),
1610 );
1611 });
1612 let items = graph.draw_list();
1613 assert!(
1614 !items
1615 .iter()
1616 .any(|i| matches!(i.prim, Primitive::Image { .. }))
1617 );
1618 let diags = graph.shader_diags();
1619 assert!(
1620 diags
1621 .iter()
1622 .any(|d| d.rule == "shader_error" && d.hint.contains("loops")),
1623 "{diags:?}"
1624 );
1625 }
1626
1627 #[test]
1628 fn the_crest_moves_along_the_wave() {
1629 let graph = paint(|view| {
1630 view.add(crate::waves().amp(0.2).width(200.0).height(80.0));
1631 });
1632 let a = image_of(&graph).rgba.clone();
1633 graph.set_time(1.0);
1634 let b = image_of(&graph).rgba.clone();
1635 assert_ne!(a, b);
1636 }
1637
1638 #[test]
1639 fn a_misspelled_uniform_names_the_real_one() {
1640 let graph = paint(|view| {
1641 view.add(
1642 crate::Shader::source(waves_wgsl())
1643 .uniform("ampp", 0.1)
1644 .uniform("speed", 1.0)
1645 .uniform_color("deep", Color::BLACK)
1646 .uniform_color("foam", Color::WHITE)
1647 .width(40.0)
1648 .height(20.0)
1649 .name("sea"),
1650 );
1651 });
1652 let _ = graph.draw_list();
1653 let diag = graph
1654 .shader_diags()
1655 .into_iter()
1656 .find(|d| d.rule == "param_unknown")
1657 .expect("param_unknown");
1658 assert_eq!(diag.node, "sea");
1659 assert!(diag.hint.contains("amp"), "{diag:?}");
1660 }
1661
1662 #[test]
1663 fn a_broken_shader_is_an_error() {
1664 let graph = paint(|view| {
1665 view.add(
1666 crate::Shader::source("this is not wgsl")
1667 .width(40.0)
1668 .height(20.0),
1669 );
1670 });
1671 let _ = graph.draw_list();
1672 assert!(
1673 graph
1674 .shader_diags()
1675 .iter()
1676 .any(|d| d.rule == "shader_error"),
1677 "{:?}",
1678 graph.shader_diags()
1679 );
1680 }
1681
1682 #[cfg(not(target_arch = "wasm32"))]
1683 #[test]
1684 fn editing_the_file_is_seen_on_the_next_draw() {
1685 let dir = std::env::temp_dir().join(format!("oxframe-shader-{}", std::process::id()));
1686 std::fs::create_dir_all(&dir).unwrap();
1687 let path = dir.join("sea.wgsl");
1688 std::fs::write(&path, "this is not wgsl").unwrap();
1689 let graph = paint(|view| {
1690 view.add(crate::Shader::file(&path).width(40.0).height(20.0));
1691 });
1692 let _ = graph.draw_list();
1693 assert!(
1694 graph
1695 .shader_diags()
1696 .iter()
1697 .any(|d| d.rule == "shader_error"),
1698 "{:?}",
1699 graph.shader_diags()
1700 );
1701 std::fs::write(&path, waves_wgsl()).unwrap();
1702 let _ = graph.draw_list();
1703 let diags = graph.shader_diags();
1704 assert!(diags.iter().all(|d| d.rule != "shader_error"), "{diags:?}");
1705 let _ = std::fs::remove_dir_all(&dir);
1706 }
1707
1708 fn rules(graph: &SceneGraph) -> Vec<&'static str> {
1709 let _ = graph.draw_list();
1710 graph.shader_diags().iter().map(|d| d.rule).collect()
1711 }
1712
1713 fn reads_frame(graph: &SceneGraph) -> bool {
1714 graph
1715 .draw_list()
1716 .iter()
1717 .any(|i| matches!(i.prim, Primitive::ReadFrame(_)))
1718 }
1719
1720 #[test]
1721 fn lens_drops_read_the_frame() {
1722 let graph = paint(|view| {
1723 view.add(
1724 crate::lens_drops()
1725 .fullscreen()
1726 .input("scene", crate::Input::frame()),
1727 );
1728 });
1729 assert!(graph.has_post());
1730 assert!(reads_frame(&graph));
1731 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
1732 let passes = crate::pass_graph(&graph.draw_list());
1733 assert!(
1734 passes
1735 .iter()
1736 .any(|p| p.kind == crate::PassKind::Effect && p.shader == Some("lens_drops"))
1737 );
1738 }
1739
1740 #[test]
1741 fn a_misspelled_input_or_parameter_names_the_real_one() {
1742 let graph = paint(|view| {
1743 view.add(
1744 crate::lens_drops()
1745 .input("scen", crate::Input::frame())
1746 .name("lens"),
1747 );
1748 });
1749 let _ = graph.draw_list();
1750 let diag = &graph.shader_diags()[0];
1751 assert_eq!((diag.rule, diag.node.as_str()), ("param_unknown", "lens"));
1752 assert!(diag.hint.contains("`scene`"), "{diag:?}");
1753
1754 let graph = paint(|view| {
1755 view.add(crate::lens_drops().uniform("wetnes", 1.0));
1756 });
1757 let _ = graph.draw_list();
1758 let diag = &graph.shader_diags()[0];
1759 assert!(diag.hint.contains("`wetness`"), "{diag:?}");
1760
1761 let graph = paint(|view| {
1762 view.add(crate::waves().input("scene", crate::Input::frame()));
1763 });
1764 assert_eq!(rules(&graph), ["param_unknown"]);
1765 }
1766
1767 #[test]
1768 fn a_lens_inside_a_fading_group_reads_the_layer() {
1769 let graph = paint(|view| {
1770 let group = view.add(crate::Group::new().opacity(0.5));
1771 group.add(crate::Rect::new().size(20.0));
1772 group.add(crate::lens_drops().fullscreen());
1773 });
1774 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
1775 assert!(reads_frame(&graph));
1776 }
1777
1778 #[test]
1779 fn an_effect_inside_an_effect_runs_both() {
1780 let graph = paint(|view| {
1781 let outer = view.add(crate::Group::new());
1782 let inner = outer.add(crate::Rect::new().size(20.0));
1783 outer.effect(crate::ripple());
1784 inner.effect(crate::ripple());
1785 });
1786 assert!(rules(&graph).is_empty(), "{:?}", graph.shader_diags());
1787 let items = graph.draw_list();
1788 let begins = items
1789 .iter()
1790 .filter(|i| matches!(i.prim, Primitive::BeginEffect(_)))
1791 .count();
1792 assert_eq!(begins, 2);
1793 let passes = crate::pass_graph(&items);
1794 assert!(
1795 passes
1796 .iter()
1797 .any(|p| p.kind == crate::PassKind::Effect && p.shader == Some("ripple"))
1798 );
1799 }
1800
1801 #[test]
1802 fn effect_signals_animate_the_marker() {
1803 let graph = SceneGraph::new(320, 180);
1804 let effect = graph.enter(|| {
1805 View::current()
1806 .add(crate::Rect::new().size(40.0))
1807 .effect(crate::ripple().strength(0.0))
1808 });
1809 let strength = |graph: &SceneGraph| {
1810 graph.draw_list().iter().find_map(|i| match &i.prim {
1811 Primitive::BeginEffect(s) => match s.op {
1812 crate::post::ShadeOp::Ripple { strength, .. } => Some(strength),
1813 _ => None,
1814 },
1815 _ => None,
1816 })
1817 };
1818 assert_eq!(strength(&graph), Some(0.0));
1819 graph.enter(|| effect.strength().set(0.7));
1820 assert_eq!(strength(&graph), Some(0.7));
1821 }
1822
1823 #[test]
1824 fn a_slow_raster_is_reported() {
1825 let graph = paint(|view| {
1826 view.add(crate::waves().width(180.0).height(80.0).name("sea"));
1827 });
1828 set_shader_budget_ms(0.0);
1829 let _ = graph.draw_list();
1830 set_shader_budget_ms(250.0);
1831 assert!(
1832 graph.shader_diags().iter().any(|d| d.rule == "shader_slow"),
1833 "{:?}",
1834 graph.shader_diags()
1835 );
1836 }
1837}
1838
1839#[cfg(test)]
1840mod speed {
1841 use super::*;
1842
1843 #[test]
1844 #[ignore = "timing, run by hand in release"]
1845 fn custom_waves_full_frame() {
1846 let graph = crate::SceneGraph::new(1920, 1080);
1847 graph.enter(|| {
1848 crate::View::current().add(
1849 crate::Shader::source(waves_wgsl())
1850 .uniform("amp", 0.2)
1851 .uniform("speed", 1.0)
1852 .uniform_color("deep", Color::BLACK)
1853 .uniform_color("foam", Color::WHITE)
1854 .width(1920.0)
1855 .height(1080.0),
1856 );
1857 });
1858 let t = std::time::Instant::now();
1859 let _ = graph.draw_list();
1860 eprintln!("custom waves 1920x1080: {:?}", t.elapsed());
1861 let graph = crate::SceneGraph::new(1920, 1080);
1862 graph.enter(|| {
1863 crate::View::current().add(crate::waves().width(1920.0).height(1080.0));
1864 });
1865 let t = std::time::Instant::now();
1866 let _ = graph.draw_list();
1867 eprintln!("preset waves 1920x1080: {:?}", t.elapsed());
1868 }
1869}