Skip to main content

oxframe_2d/
shader.rs

1//! A shader node: WGSL `Params` plus `shade`, checked with naga.
2//!
3//! The waves preset is rasterized on the CPU, so a check without a GPU
4//! still paints it. Both backends then draw that image, which keeps the
5//! preset the same on either one. The lens_drops preset reads the frame
6//! drawn before it (see [`crate::post`]). A custom `shade` is checked on
7//! every draw (a changed file is picked up without a rebuild) and run on
8//! the CPU by [`crate::wgsl_cpu`], natively; the browser build checks
9//! nothing and paints nothing for it.
10
11use 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/// Where the WGSL text comes from. Stored on a shader node.
21#[doc(hidden)]
22#[derive(Clone)]
23pub enum ShaderCode {
24    /// Source kept on the node.
25    Wgsl(String),
26    /// Read again on every draw, so an edit shows up without a rebuild.
27    File(std::path::PathBuf),
28}
29
30/// Which built-in a shader node runs. Stored on a shader node.
31#[doc(hidden)]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Preset {
34    /// User WGSL: checked, and run on the CPU per pixel (not in wasm).
35    Custom,
36    /// [`waves`](crate::waves): painted inside the node's box.
37    Waves,
38    /// [`lens_drops`](crate::lens_drops): changes the frame drawn before it.
39    LensDrops,
40}
41
42impl Preset {
43    /// `"custom"`, `"waves"` or `"lens_drops"`.
44    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
53/// Parameters of the lens_drops preset, all numbers.
54const LENS_PARAMS: [&str; 3] = ["wetness", "seed", "drop_size"];
55
56/// Inputs the lens_drops preset reads.
57const LENS_INPUTS: [&str; 1] = ["scene"];
58
59/// One value passed to `Params`. Stored on a shader node.
60#[doc(hidden)]
61pub struct Uniform {
62    pub name: String,
63    pub value: UniformValue,
64}
65
66/// A number, a point, a color, or a bool stored as 0 or 1.
67#[doc(hidden)]
68#[allow(dead_code)]
69pub enum UniformValue {
70    /// An `f32` field.
71    Num(Signal<f64>),
72    /// A `vec2<f32>` field.
73    Vec2(Signal<kurbo::Vec2>),
74    /// A `vec4<f32>` color field.
75    Color(Signal<Color>),
76    /// A `bool` field, stored as 0 or 1 so it can be animated.
77    Bool(Signal<f64>),
78}
79
80/// A WGSL problem found while painting.
81#[derive(Debug, Clone, PartialEq)]
82pub struct ShaderDiag {
83    /// `.name(..)`, or the node kind and id.
84    pub node: String,
85    /// `file:line` of the `add` call, when it was recorded.
86    pub source: Option<String>,
87    /// `shader_error`, `param_unknown`, or `shader_slow`.
88    pub rule: &'static str,
89    /// What to change.
90    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
103/// WGSL of the waves preset.
104pub(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    /// Scene size and frame rate of the graph being drawn, for `ox`.
169    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
175/// Sets the scene size for `ox.resolution` and `ox.screen`; the graph
176/// calls this before it builds a draw list.
177pub(crate) fn set_scene_size(size: (f64, f64)) {
178    SCENE.with(|s| s.set((size, s.get().1)));
179}
180
181/// Frames per second, for `ox.frame` in custom shaders. The engine sets it
182/// before it draws.
183#[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
200/// Restores the previous draft flag when dropped.
201pub 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
211/// Paints shader nodes at half resolution until dropped.
212///
213/// Timings and positions stay the same. `oxframe check` uses this so a
214/// shader layer does not dominate the budget.
215pub fn draft_shaders() -> ShaderDraft {
216    let prev = DRAFT.with(Cell::get);
217    DRAFT.with(|d| d.set(true));
218    ShaderDraft { prev }
219}
220
221/// Milliseconds a shader raster may take before `shader_slow`.
222pub 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
234/// Compile and parameter problems for this node, and a video that could
235/// not be opened. Empty for other nodes.
236pub(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
323/// Raster of a waves node, or nothing when this node is not the preset.
324pub(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/// The frame marker of a lens_drops node, or nothing for other shaders.
407///
408/// `area` is the node's box in scene coordinates. The node reads the frame
409/// drawn before it whatever its input is named: a backdrop outside a layer
410/// is the frame.
411#[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/// The frame-reading pass of a custom shader, or nothing (with a
456/// `shader_error`) when it does not compile.
457#[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
524/// `no input `x`. Did you mean `y`? Available: …` for a preset.
525fn 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/// Wall clock for `shader_slow`.
538///
539/// `Instant::now()` panics on `wasm32-unknown-unknown`, which ended the
540/// playground's waves example in "unreachable executed". A check never runs
541/// in the browser, so there the watch reads 0 and `shader_slow` stays quiet.
542#[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
572/// RGBA bytes of an `iw` × `ih` raster of a `size` box: `pixel` gets the
573/// uv (0..1, from the top left) and the local point (from the center).
574fn 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/// A custom `shade`, run per pixel of its own box.
598#[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/// The compiled shader of `n` and the values it reads: `ox`, the `Params`
650/// fields, and its image inputs.
651#[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/// Runs `shade` for pixels `0..total`, a tile at a time. `at` gives a
728/// pixel's `ShadeIn` (uv, local, world, screen); `put` takes its color.
729#[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/// A custom `shade` with a frame or backdrop input. It runs between the
765/// parts of the draw list, over the pixels drawn before it (see
766/// [`crate::post`]), and is painted over them inside its box.
767#[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    /// Inputs that read the frame.
773    frame: Vec<String>,
774    /// Inputs that read a node drawn alone.
775    offscreen: Vec<NodeInput>,
776    tf: Affine,
777    size: (f64, f64),
778    scene: (f64, f64),
779}
780
781/// A node a custom shader reads: its draw list and its box in scene
782/// coordinates.
783#[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    /// Draw lists of the nodes this shader reads, in order. The engine
839    /// draws each alone on a clear target of the frame's size and passes
840    /// the images to [`crate::post::apply_with`].
841    pub fn offscreen_lists(&self) -> Vec<&[crate::DrawItem]> {
842        self.offscreen.iter().map(|n| n.items.as_slice()).collect()
843    }
844
845    /// Runs the shader over `img` inside the node's box. `to_px` maps
846    /// scene coordinates to the pixels of `img`; `drawn` holds the images
847    /// of [`offscreen_lists`](Self::offscreen_lists). A shader that fails
848    /// while running leaves the pixels as they were; `check` reports it.
849    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/// The part of `img` under `bounds` (scene coordinates), at least a pixel.
944#[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/// True when a custom shader reads the frame or the backdrop. The browser
975/// build does not run custom shaders, so there it is always false.
976#[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    /// Parsed and validated shaders by the hash of their text: a file is
1043    /// read on every draw, but parsed once per change.
1044    static PROGRAMS: std::cell::RefCell<std::collections::HashMap<u64, Compiled>> =
1045        std::cell::RefCell::new(std::collections::HashMap::new());
1046}
1047
1048/// The shader for `text`, parsed and validated once.
1049#[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    // After the user's text, so naga's line numbers still point into it.
1069    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/// The input name in naga's error for a `sample_<name>` that no input
1114/// declares.
1115#[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
1125/// A WGSL identifier: a letter or `_`, then letters, digits and `_`.
1126fn 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/// naga reports lines in the wrapped source. Subtract the prelude so the
1140/// number points at the user's text.
1141#[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        // The waves preset's own WGSL, run by the interpreter, against the
1393        // preset's Rust code: the same picture up to f32 rounding.
1394        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        // A mirror over the right half of a 320x180 frame whose left half
1477        // is red and right half blue: the right half turns red.
1478        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}