Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-pipeline/tests/flowsim_gpu_failsafe.rs

//! **THE FAIL-SAFE THAT KEEPS THE OVERLAY FROM SILENTLY VANISHING.**
//! (`--features flowsim-gpu`)
//!
//! `FlowSim::render_gpu` returning `true` makes `paint_dag` **skip the CPU particle
//! painter**, because the GPU lane is supposed to draw them instead. If a host opts in
//! with `set_gpu_active(true)` but never calls `install_flow_renderer`, the paint
//! callback finds no `FlowRenderer` in `callback_resources` and draws nothing — while
//! the CPU painter has already stood down. The overlay is then **silently missing**, and
//! neither the pixels nor `state_json` would blame the missing install.
//!
//! `facett-core`'s `graphcloud` documents exactly this shape shipping once (the "3D
//! graphs are missing" symptom). So `draw_flow` refuses to claim a frame until a renderer
//! really exists, and this file proves the refusal.
//!
//! # Why this is its own test binary
//!
//! The install flag is process-global and monotonic, while `callback_resources` is
//! per-`RenderState` — so once anything in a process installs, "not installed" cannot be
//! reproduced. `graphcloud`'s equivalent guard handles that by skipping itself when the
//! flag is already set, which makes it a test that can quietly stop testing. Each
//! `tests/*.rs` is its own process, so keeping this in a separate binary that installs
//! nothing removes the ordering hazard **by construction**: there is no path by which
//! this file's premise can be lost, and therefore no need for a self-skip.
//!
//! Run: `cargo test -p facett-pipeline --features flowsim-gpu --test flowsim_gpu_failsafe`
#![cfg(feature = "flowsim-gpu")]

use facett_core::Facet;
use facett_flowsim::gpu::host::{flow_frames_drawn, flow_renderer_installed};
use facett_flowsim::{FlowMode, FlowMsg};
use facett_pipeline::{PipelineNode, PipelineView, Status};

const EDGES: [(usize, usize); 3] = [(1, 0), (2, 1), (3, 2)];

fn dag() -> Vec<PipelineNode> {
    vec![
        PipelineNode::new("raw", Status::Done).with_rows(120_000).with_throughput_rows_s(500.0),
        PipelineNode::new("clean", Status::Running).with_rows(48_000).with_throughput_rows_s(9_000.0),
        PipelineNode::new("agg", Status::Running).with_rows(12_000).with_throughput_rows_s(2_000.0),
        PipelineNode::new("out", Status::Running).with_rows(4_000).with_throughput_rows_s(300.0),
    ]
}

fn view(mode: FlowMode, gpu: bool) -> PipelineView {
    let mut v = PipelineView::from_nodes("ETL", dag(), EDGES.to_vec())
        .with_animation(false)
        .with_clock(0.0)
        .with_flowsim(true);
    let fs = v.flowsim_mut().unwrap();
    fs.update(FlowMsg::SetMode(mode));
    fs.set_gpu_active(gpu);
    v
}

fn frame(mode: FlowMode, gpu: bool) -> Option<image::RgbaImage> {
    let mut h = egui_kittest::Harness::builder()
        .with_size(egui::vec2(900.0, 620.0))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            view(mode, gpu).ui(ui);
        });
    h.run_steps(3);
    match h.render() {
        Ok(img) => Some(img),
        Err(e) => {
            eprintln!("[failsafe] no wgpu backend ({e}) — pixel leg skipped");
            None
        }
    }
}

fn diff_px(a: &image::RgbaImage, b: &image::RgbaImage) -> usize {
    assert_eq!(a.dimensions(), b.dimensions());
    a.pixels()
        .zip(b.pixels())
        .filter(|(p, q)| (0..3).map(|i| (p.0[i] as i32 - q.0[i] as i32).abs()).sum::<i32>() > 12)
        .count()
}

/// **RED-when-broken:** opting into the GPU lane without installing the renderer must
/// fall back to the CPU painter, **pixel for pixel** — not render a bare DAG with the
/// particles quietly gone.
#[test]
fn opting_into_the_gpu_lane_without_installing_the_renderer_still_paints_on_the_cpu() {
    assert!(
        !flow_renderer_installed(),
        "PREMISE: this binary must never install the flow renderer, or it stops testing the \
         no-renderer path. Something in this file installed one."
    );
    let before = flow_frames_drawn();

    let Some(off) = frame(FlowMode::Off, false) else { return };
    let opted_in = frame(FlowMode::Particles, true).expect("opted-in frame");
    let plain_cpu = frame(FlowMode::Particles, false).expect("cpu frame");

    let n = view(FlowMode::Particles, false).state_json()["flowsim"]["total_particles"]
        .as_u64()
        .unwrap() as usize;
    let floor = n * 15;
    let opted_px = diff_px(&opted_in, &off);
    eprintln!(
        "[failsafe] gpu_active(true) with NO renderer installed: {opted_px} px of overlay \
         ({n} particles planned, derived floor {floor})"
    );
    assert!(
        opted_px > floor,
        "FAIL-SAFE: `set_gpu_active(true)` with no installed renderer must fall back to the CPU \
         painter, not leave the overlay missing — only {opted_px} px differ from the Off frame, \
         for {n} planned particles. The CPU painter was skipped for a GPU lane that could not \
         draw."
    );

    // …and the fallback is the CPU path EXACTLY. `assert_eq!(0)`, not "close": a
    // degraded fallback would still pass the assertion above.
    let drift = diff_px(&opted_in, &plain_cpu);
    eprintln!("[failsafe] opted-in vs plain CPU frame: {drift} px differ (must be 0)");
    assert_eq!(
        drift, 0,
        "the fallback IS the unchanged CPU painter — {drift} px differ from the frame of a host \
         that never opted in at all"
    );

    // And nothing reached the device, which is what makes the two claims above a proof of
    // the FALLBACK rather than of a lane that happened to work.
    let frames = flow_frames_drawn() - before;
    assert_eq!(
        frames, 0,
        "no advect+draw frame can have been encoded without an installed renderer — the counter \
         moved by {frames}"
    );

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-pipeline.flowsim_gpu_failsafe",
        "gpu_optin_without_renderer_falls_back_to_cpu",
        opted_px > floor && drift == 0 && frames == 0,
        &format!("overlay={opted_px}px drift={drift}px device_frames={frames}"),
    );
}