facett-core 0.1.10

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! **The L1 vello beauty overlay** (feature `l1-vello`) — the OPTIONAL GPU lane
//! that draws the things the L0 SDF base deliberately skips: **gradient fills,
//! antialiased Bézier curves, and gaussian blur / glow effects**, composited on
//! top of an L0 [`Frame`](super::Frame).
//!
//! ## Doctrine (`.nornir/render-engine.md` §"L0 / L1")
//! - **L0** = facett-core SDF instanced base. The speed *and* beauty base; must
//!   look great with **zero vello** (feature off ⇒ this module does not compile in
//!   and L0 is untouched).
//! - **L1** = this overlay. `vello` = the *fat-GPU* member of the velo family
//!   (compute). **Desktop only; never required.** A device / tank / headless-CPU
//!   target never pulls it (the `l1-vello` feature stays off there); the L0 frame
//!   stands alone.
//!
//! ## How it composes
//! vello 0.9 renders a [`vello::Scene`] into a wgpu **storage** texture
//! (`render_to_texture`, `Rgba8Unorm` + `STORAGE_BINDING`). We read that texture
//! back to straight RGBA8 and **alpha-composite (source-over)** it over the L0
//! frame — so the same final frame carries L0 (SDF nodes/edges/markers) *and* L1
//! (the gradient sweep, the AA spline, the soft glow). Both lanes land in **one**
//! picture, the side-by-side the demo's "Velo L1" tab shows is L0 vs this.
//!
//! The overlay is built **headless** (its own throwaway wgpu device), so it is
//! fully testable without a window — the render-proof test injects a scene, runs
//! L0 and L0+L1, and asserts L1 lit pixels the L0 frame lacked, contained in the
//! rect, plus emits the composited PNG as a blob the test reads back.

use super::Frame;

pub use vello::{self, peniko, peniko::kurbo};

/// The recipe for an L1 overlay — the beauty primitives the SDF base can't draw,
/// expressed in **screen pixels** (the same coordinate space as the L0
/// [`prim`](super::prim) instances), so the overlay registers exactly over the L0
/// content. Deliberately small + declarative so a skin (graph / map) builds one
/// without touching vello, and a test injects a known one and asserts the result.
#[derive(Clone, Debug, Default)]
pub struct L1Overlay {
    /// Linear-gradient–filled rounded rects (the SDF base only does flat fills).
    /// `(rect_min, rect_max, corner_radius, [color0, color1])` — the gradient runs
    /// left→right across the rect from `color0` to `color1`.
    pub gradient_rects: Vec<GradientRect>,
    /// Antialiased cubic Bézier strokes (the SDF base only does straight capsules).
    pub curves: Vec<Curve>,
    /// Gaussian-blurred rounded rects — the soft **glow / effects** the L0 row has
    /// hard OFF (`render-engine.md`: "Effects is the only hard OFF on the CPU row").
    pub glows: Vec<Glow>,
}

/// One linear-gradient–filled rounded rect (screen px).
#[derive(Clone, Copy, Debug)]
pub struct GradientRect {
    pub min: [f32; 2],
    pub max: [f32; 2],
    pub corner: f32,
    /// `[r, g, b, a]` in `[0, 1]` at the left and right gradient stops.
    pub color0: [f32; 4],
    pub color1: [f32; 4],
}

/// One antialiased cubic Bézier stroke (screen px): `p0 → (c0, c1) → p1`.
#[derive(Clone, Copy, Debug)]
pub struct Curve {
    pub p0: [f32; 2],
    pub c0: [f32; 2],
    pub c1: [f32; 2],
    pub p1: [f32; 2],
    pub width: f32,
    pub color: [f32; 4],
}

/// One gaussian-blurred rounded rect — a soft glow (screen px).
#[derive(Clone, Copy, Debug)]
pub struct Glow {
    pub min: [f32; 2],
    pub max: [f32; 2],
    pub corner: f32,
    pub color: [f32; 4],
    /// Gaussian standard deviation (px) — the blur radius / glow softness.
    pub std_dev: f32,
}

impl L1Overlay {
    /// Total number of beauty primitives in the overlay (gradients + curves +
    /// glows) — the "what L1 adds over L0" count surfaced in `state_json`.
    pub fn len(&self) -> usize {
        self.gradient_rects.len() + self.curves.len() + self.glows.len()
    }
    /// Whether the overlay draws nothing (then a compose is a no-op pass-through).
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Encode this overlay into a fresh [`vello::Scene`] (screen-pixel coords map
    /// straight to the target texture, identity transform). Pure (no GPU) so the
    /// scene-build is unit-testable and a host can inspect it before rendering.
    pub fn to_scene(&self) -> vello::Scene {
        use kurbo::{Affine, BezPath, Point, Rect, Stroke};
        use peniko::{Color, Fill, Gradient};

        let to_color = |c: [f32; 4]| {
            Color::from_rgba8(
                (c[0] * 255.0).round().clamp(0.0, 255.0) as u8,
                (c[1] * 255.0).round().clamp(0.0, 255.0) as u8,
                (c[2] * 255.0).round().clamp(0.0, 255.0) as u8,
                (c[3] * 255.0).round().clamp(0.0, 255.0) as u8,
            )
        };

        let mut scene = vello::Scene::new();

        // Glows first (under) — the soft halo behind the crisp content.
        for g in &self.glows {
            let rect = Rect::new(g.min[0] as f64, g.min[1] as f64, g.max[0] as f64, g.max[1] as f64);
            scene.draw_blurred_rounded_rect(
                Affine::IDENTITY,
                rect,
                to_color(g.color),
                g.corner as f64,
                g.std_dev as f64,
            );
        }

        // Gradient-filled rounded rects.
        for gr in &self.gradient_rects {
            let rect = Rect::new(
                gr.min[0] as f64,
                gr.min[1] as f64,
                gr.max[0] as f64,
                gr.max[1] as f64,
            );
            let rounded = rect.to_rounded_rect(gr.corner as f64);
            let grad = Gradient::new_linear(
                Point::new(gr.min[0] as f64, gr.min[1] as f64),
                Point::new(gr.max[0] as f64, gr.min[1] as f64),
            )
            .with_stops([(0.0_f32, to_color(gr.color0)), (1.0_f32, to_color(gr.color1))]);
            scene.fill(Fill::NonZero, Affine::IDENTITY, &grad, None, &rounded);
        }

        // Antialiased cubic Bézier strokes (over).
        for cv in &self.curves {
            let mut path = BezPath::new();
            path.move_to(Point::new(cv.p0[0] as f64, cv.p0[1] as f64));
            path.curve_to(
                Point::new(cv.c0[0] as f64, cv.c0[1] as f64),
                Point::new(cv.c1[0] as f64, cv.c1[1] as f64),
                Point::new(cv.p1[0] as f64, cv.p1[1] as f64),
            );
            let stroke = Stroke::new(cv.width as f64);
            scene.stroke(&stroke, Affine::IDENTITY, to_color(cv.color), None, &path);
        }

        scene
    }
}

/// Source-over alpha-composite a straight-RGBA8 `over` frame onto a straight-RGBA8
/// `base` frame of the same size, **in place** on `base`. The L1 overlay (which
/// has transparent background where it drew nothing) lands on top of L0 wherever
/// its alpha is non-zero, so the single output frame carries both lanes.
///
/// Pure CPU + no vello — the *composite* is testable on its own and runs even when
/// the L1 render itself was unavailable (then `over` is simply absent).
pub fn composite_over(base: &mut Frame, over: &Frame) {
    if base.width != over.width || base.height != over.height {
        return;
    }
    for (b, o) in base.rgba.chunks_exact_mut(4).zip(over.rgba.chunks_exact(4)) {
        let sa = o[3] as f32 / 255.0;
        if sa <= 0.0 {
            continue;
        }
        let inv = 1.0 - sa;
        // straight-over: out = src*sa + dst*(1-sa) for colour, alpha = sa + da*(1-sa).
        for k in 0..3 {
            let src = o[k] as f32 / 255.0;
            let dst = b[k] as f32 / 255.0;
            b[k] = ((src * sa + dst * inv) * 255.0).round().clamp(0.0, 255.0) as u8;
        }
        let da = b[3] as f32 / 255.0;
        b[3] = ((sa + da * inv) * 255.0).round().clamp(0.0, 255.0) as u8;
    }
}

/// Render an [`L1Overlay`] **headless** to a straight-RGBA8 [`Frame`] of
/// `width × height` over a fully transparent background, using vello on a throwaway
/// wgpu device. `None` when no usable GPU adapter exists (a CPU-only / sandboxed
/// box) — the caller then keeps the L0 frame alone (L1 is never required).
///
/// The texture is `Rgba8Unorm` + `STORAGE_BINDING` (what `render_to_texture`
/// requires) + `COPY_SRC` (for read-back). Premultiplied vello output is
/// un-premultiplied on read-back so it matches the straight-RGBA8 L0 [`Frame`].
pub fn render_overlay(overlay: &L1Overlay, width: u32, height: u32) -> Option<Frame> {
    if width == 0 || height == 0 {
        return Some(Frame { width, height, rgba: vec![0u8; (width * height * 4) as usize] });
    }
    pollster::block_on(render_overlay_async(overlay, width, height))
}

async fn render_overlay_async(overlay: &L1Overlay, width: u32, height: u32) -> Option<Frame> {
    use vello::{AaConfig, RenderParams, Renderer, RendererOptions};

    let instance = wgpu::Instance::default();
    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::HighPerformance,
            force_fallback_adapter: false,
            compatible_surface: None,
        })
        .await
        .ok()?;
    let (device, queue) = adapter
        .request_device(&wgpu::DeviceDescriptor {
            label: Some("l1-vello-headless"),
            required_features: wgpu::Features::empty(),
            // vello's compute pipeline needs a few limits above the very lowest
            // downlevel floor; the adapter default limits are the safe ask here.
            required_limits: wgpu::Limits::default().using_resolution(adapter.limits()),
            memory_hints: wgpu::MemoryHints::default(),
            experimental_features: wgpu::ExperimentalFeatures::disabled(),
            trace: wgpu::Trace::Off,
        })
        .await
        .ok()?;

    let mut renderer = Renderer::new(
        &device,
        RendererOptions {
            use_cpu: false,
            antialiasing_support: vello::AaSupport::area_only(),
            num_init_threads: None,
            pipeline_cache: None,
        },
    )
    .ok()?;

    let scene = overlay.to_scene();

    let target = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("l1-vello-target"),
        size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: wgpu::TextureFormat::Rgba8Unorm,
        usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
        view_formats: &[],
    });
    let view = target.create_view(&wgpu::TextureViewDescriptor::default());

    // Transparent base so only what vello drew has non-zero alpha → the overlay
    // composites cleanly over L0.
    let params = RenderParams {
        base_color: peniko::Color::TRANSPARENT,
        width,
        height,
        antialiasing_method: AaConfig::Area,
    };
    renderer
        .render_to_texture(&device, &queue, &scene, &view, &params)
        .ok()?;

    // Read back: copy the texture into a row-padded buffer, map, unpad, unpremul.
    let bytes_per_pixel = 4u32;
    let unpadded = width * bytes_per_pixel;
    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
    let padded = unpadded.div_ceil(align) * align;
    let readback = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("l1-vello-readback"),
        size: (padded * height) as u64,
        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
        mapped_at_creation: false,
    });
    let mut encoder =
        device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("l1-vello-enc") });
    encoder.copy_texture_to_buffer(
        wgpu::TexelCopyTextureInfo {
            texture: &target,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        wgpu::TexelCopyBufferInfo {
            buffer: &readback,
            layout: wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(padded),
                rows_per_image: Some(height),
            },
        },
        wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
    );
    queue.submit(Some(encoder.finish()));

    let slice = readback.slice(..);
    let (tx, rx) = std::sync::mpsc::channel();
    slice.map_async(wgpu::MapMode::Read, move |r| {
        let _ = tx.send(r);
    });
    device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
    rx.recv().ok()?.ok()?;

    let data = slice.get_mapped_range();
    let mut rgba = Vec::with_capacity((width * height * 4) as usize);
    for row in 0..height {
        let start = (row * padded) as usize;
        // vello writes PREMULTIPLIED Rgba8Unorm; un-premultiply to straight RGBA8
        // so it matches the L0 frame convention before compositing.
        for px in data[start..start + unpadded as usize].chunks_exact(4) {
            let a = px[3];
            if a == 0 {
                rgba.extend_from_slice(&[0, 0, 0, 0]);
            } else {
                let un = |c: u8| ((c as u32 * 255 + (a as u32) / 2) / a as u32).min(255) as u8;
                rgba.extend_from_slice(&[un(px[0]), un(px[1]), un(px[2]), a]);
            }
        }
    }
    drop(data);
    readback.unmap();

    Some(Frame { width, height, rgba })
}

/// **The L0 + L1 compose** — render the L1 `overlay` headlessly and source-over it
/// onto the given L0 `base` frame, returning the single combined frame. When no
/// GPU is available (`render_overlay` → `None`) the L0 frame is returned unchanged
/// — L1 is never required, so the seam degrades, it does not fail.
///
/// Returns `(frame, l1_drawn)`: `l1_drawn` is whether the L1 lane actually ran
/// (a real GPU produced the overlay) — the observable "did L1 light up" signal the
/// demo surfaces in `state_json` and the proof test asserts.
pub fn compose_l0_l1(mut base: Frame, overlay: &L1Overlay) -> (Frame, bool) {
    let (w, h) = (base.width, base.height);
    match render_overlay(overlay, w, h) {
        Some(over) => {
            composite_over(&mut base, &over);
            (base, true)
        }
        None => (base, false),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::render::Frame;

    /// A representative overlay covering all three L1 beauty kinds, drawn well
    /// inside a `w × h` rect (so a "contained in rect" oracle is meaningful).
    fn sample_overlay(w: u32, h: u32) -> L1Overlay {
        let pad = 0.15;
        let (x0, y0) = (w as f32 * pad, h as f32 * pad);
        let (x1, y1) = (w as f32 * (1.0 - pad), h as f32 * (1.0 - pad));
        L1Overlay {
            gradient_rects: vec![GradientRect {
                min: [x0, y0],
                max: [x1, (y0 + y1) * 0.5],
                corner: 12.0,
                color0: [0.10, 0.45, 0.95, 1.0],
                color1: [0.95, 0.30, 0.55, 1.0],
            }],
            curves: vec![Curve {
                p0: [x0, y1],
                c0: [(x0 + x1) * 0.5, y0],
                c1: [(x0 + x1) * 0.5, y1 + 30.0],
                p1: [x1, (y0 + y1) * 0.5],
                width: 6.0,
                color: [0.95, 0.85, 0.20, 1.0],
            }],
            glows: vec![Glow {
                min: [(x0 + x1) * 0.5 - 30.0, (y0 + y1) * 0.5 - 30.0],
                max: [(x0 + x1) * 0.5 + 30.0, (y0 + y1) * 0.5 + 30.0],
                corner: 16.0,
                color: [0.30, 0.95, 0.70, 1.0],
                std_dev: 8.0,
            }],
        }
    }

    /// INJECT-ASSERT (pure, no GPU): the overlay encodes the right number of vello
    /// scene ops, and `len()`/`is_empty()` report the real primitive count. The
    /// scene's encoding carries non-zero data once primitives are added (proof the
    /// build did real work, not "didn't panic").
    #[test]
    fn overlay_builds_a_nonempty_scene() {
        let empty = L1Overlay::default();
        assert!(empty.is_empty());
        assert_eq!(empty.to_scene().encoding().n_paths, 0, "empty overlay → empty scene");

        let ov = sample_overlay(400, 300);
        assert_eq!(ov.len(), 3, "one gradient + one curve + one glow");
        assert!(!ov.is_empty());
        let scene = ov.to_scene();
        // Gradient fill + curve stroke each encode a path; the blurred-rect glow
        // encodes a path too — so the scene carries real path data.
        assert!(scene.encoding().n_paths >= 2, "the beauty primitives encoded paths, got {}", scene.encoding().n_paths);
    }

    /// INJECT-ASSERT (pure, no GPU): `composite_over` is source-over correct — an
    /// opaque overlay pixel replaces the base, a transparent one leaves it, and a
    /// half-alpha one blends. Return value (the mutated base frame) proves it.
    #[test]
    fn composite_over_is_source_over_correct() {
        let mut base = Frame { width: 3, height: 1, rgba: vec![0, 0, 0, 255, /*p1*/ 10, 20, 30, 255, /*p2*/ 0, 0, 0, 255] };
        let over = Frame {
            width: 3,
            height: 1,
            // p0: opaque red over black → red. p1: transparent → leave base.
            // p2: half-alpha white over black → ~128 grey.
            rgba: vec![255, 0, 0, 255, /*p1*/ 99, 99, 99, 0, /*p2*/ 255, 255, 255, 128],
        };
        composite_over(&mut base, &over);
        assert_eq!(&base.rgba[0..4], &[255, 0, 0, 255], "opaque over → source");
        assert_eq!(&base.rgba[4..8], &[10, 20, 30, 255], "transparent over → base unchanged");
        let g = base.rgba[8];
        assert!((g as i32 - 128).abs() <= 2, "half-alpha white over black ≈ 128, got {g}");
    }

    /// INJECT-ASSERT (the L1 render lane, GPU): render the sample overlay headless
    /// and assert it (a) actually lit pixels and (b) every lit pixel is inside the
    /// rect (the clip oracle). SKIPPED gracefully (asserted as a no-op) when no GPU
    /// adapter is present, so a CPU-only box still passes — L1 is never required.
    #[test]
    fn l1_overlay_renders_and_is_contained_in_rect() {
        let (w, h) = (256u32, 192u32);
        let ov = sample_overlay(w, h);
        match render_overlay(&ov, w, h) {
            None => {
                // No GPU on this box — L1 degrades, not fails (the spec's promise).
                eprintln!("no GPU adapter — L1 render skipped (degraded, not failed)");
            }
            Some(frame) => {
                assert_eq!(frame.rgba.len(), (w * h * 4) as usize);
                let lit = frame.lit_px();
                assert!(lit > 200, "L1 overlay lit real pixels, got {lit}");
                // Containment: no lit pixel outside the [0,w)×[0,h) frame is trivially
                // true; the meaningful oracle is the lit pixels sit inside the padded
                // content band — assert the outer 1-px border ring is fully clear.
                let at = |x: u32, y: u32| frame.rgba[((y * w + x) * 4 + 3) as usize];
                for x in 0..w {
                    assert_eq!(at(x, 0), 0, "top edge clear");
                    assert_eq!(at(x, h - 1), 0, "bottom edge clear");
                }
                for y in 0..h {
                    assert_eq!(at(0, y), 0, "left edge clear");
                    assert_eq!(at(w - 1, y), 0, "right edge clear");
                }
            }
        }
    }
}