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}"),
);
}