facett-flow/tests/gpu_host.rs
#![cfg(all(
feature = "wgpu",
feature = "boxes",
feature = "arrows",
feature = "status",
feature = "rate"
))]
use facett_core::render::lane;
use facett_flow::{
Animation, Edge, FlowScene, Graph, Node, PaintOpts, View,
attr::{Rates, Sizes, Status, StatusTable},
gpu,
};
use std::sync::{Arc, Mutex};
fn graph() -> Graph {
Graph {
nodes: vec![
Node {
id: "a".into(),
label: "a".into(),
group: "g".into(),
position: [0.0, 0.0],
kind: 0,
},
Node {
id: "b".into(),
label: "b".into(),
group: "g".into(),
position: [200.0, 0.0],
kind: 2,
},
],
edges: vec![Edge {
from: "a".into(),
to: "b".into(),
}],
}
}
fn diff(a: &image::RgbaImage, b: &image::RgbaImage) -> usize {
a.pixels().zip(b.pixels()).filter(|(a, b)| a != b).count()
}
#[test]
fn host_variants_pixels_cache_attributes_and_idle_eviction() {
for ppp in [1_u32, 2] {
run_variants(ppp);
}
}
fn run_variants(ppp: u32) {
if !["integrated", "discrete", "software"].contains(
&std::env::var("FACETT_TEST_GPU")
.unwrap_or_default()
.as_str(),
) {
eprintln!("SKIPPED: set FACETT_TEST_GPU to prove host variant pixels/cache on a device");
return;
}
let _turn = facett_core::render::gputurn::hold();
let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
assert!(gpu::install_renderer(&rs));
let data = Arc::new(Mutex::new((
FlowScene::with_key(graph(), 88).unwrap(),
PaintOpts {
moving: false,
..Default::default()
},
)));
let state = data.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(420.0, 240.0))
.with_pixels_per_point(ppp as f32)
.renderer(egui_kittest::wgpu::WgpuTestRenderer::from_render_state(
rs.clone(),
))
.build_ui(move |ui| {
let d = state.lock().unwrap();
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(420.0, 240.0));
assert!(
facett_flow::paint(
ui,
egui::Id::new("host-test"),
rect,
&d.0,
&View {
scale: 1.0,
offset: egui::vec2(100.0, 100.0)
},
&d.1
)
.gpu
);
});
let frame = |h: &mut egui_kittest::Harness<'_>| {
h.step();
h.render().unwrap()
};
frame(&mut h);
let plain = frame(&mut h);
let bytes = lane::clock_stats_of(gpu::lane_name()).upload_bytes_total;
assert_eq!(diff(&plain, &frame(&mut h)), 0);
assert_eq!(
bytes,
lane::clock_stats_of(gpu::lane_name()).upload_bytes_total
);
data.lock()
.unwrap()
.0
.set_status(StatusTable {
nodes: vec![None; 2],
edges: vec![None],
})
.unwrap();
assert_eq!(
diff(&plain, &frame(&mut h)),
0,
"all-None table must retain plain shader"
);
assert_eq!(
bytes,
lane::clock_stats_of(gpu::lane_name()).upload_bytes_total
);
data.lock()
.unwrap()
.0
.set_sizes(Sizes {
nodes: vec![[40.0, 30.0]; 2],
})
.unwrap();
let boxes = frame(&mut h);
// Explicit picking/pixel agreement: use the same world-space node interior
// and curve midpoint returned by the host API, at both tested pixel densities.
{
let d = data.lock().unwrap();
let view = View {
scale: 1.0,
offset: egui::vec2(100.0, 100.0),
};
for (world, expected) in [
(egui::pos2(15.0, 10.0), facett_flow::Hit::Node(0)),
(d.0.edge_point(0, 0.5).unwrap(), facett_flow::Hit::Edge(0)),
] {
assert_eq!(d.0.hit(world, 0.5), Some(expected));
let screen = view.screen(world);
let sample = boxes.get_pixel(
(screen.x * ppp as f32) as u32,
(screen.y * ppp as f32) as u32,
);
assert_ne!(
sample,
boxes.get_pixel(50 * ppp, 50 * ppp),
"picked {expected:?} must coincide with a lit pixel at {screen:?}"
);
}
}
assert!(diff(&plain, &boxes) > 600, "box area must be visible");
assert_ne!(
plain.get_pixel(115 * ppp, 110 * ppp),
boxes.get_pixel(115 * ppp, 110 * ppp)
);
assert_eq!(
boxes.get_pixel(80 * ppp, 85 * ppp),
boxes.get_pixel(75 * ppp, 85 * ppp),
"rounded-box outer corner must be transparent"
);
let before = lane::clock_stats_of(gpu::lane_name()).upload_bytes_total;
data.lock()
.unwrap()
.0
.set_status(StatusTable {
nodes: vec![Some(Status::Error); 2],
edges: vec![Some(Status::Slow)],
})
.unwrap();
let status = frame(&mut h);
assert!(diff(&boxes, &status) > 600);
assert_eq!(
lane::clock_stats_of(gpu::lane_name()).upload_bytes_total - before,
40,
"only two 16B nodes + one packed 8B edge attribute, no geometry"
);
data.lock().unwrap().0.set_arrows(true);
let arrow = frame(&mut h);
assert!(diff(&status, &arrow) > 10);
let changed: Vec<_> = status
.enumerate_pixels()
.filter(|(x, y, p)| **p != *arrow.get_pixel(*x, *y))
.map(|(x, y, _)| (x, y))
.collect();
assert!(
changed
.iter()
.all(|&(x, y)| (272 * ppp..=280 * ppp).contains(&x)
&& (96 * ppp..=104 * ppp).contains(&y)),
"arrow must end at consumer box side: {changed:?}"
);
{
let mut d = data.lock().unwrap();
d.0.set_rates(Rates {
edges: vec![Some(0.0)],
})
.unwrap();
d.1.moving = true;
d.1.seconds = 0.0;
}
let dead = frame(&mut h);
data.lock().unwrap().1.seconds = 0.43;
assert_eq!(
diff(&dead, &frame(&mut h)),
0,
"zero throughput must have neither trail nor packets"
);
{
let mut d = data.lock().unwrap();
d.0.set_rates(Rates {
edges: vec![Some(1.0)],
})
.unwrap();
d.1.seconds = 0.0;
}
let live = frame(&mut h);
data.lock().unwrap().1.seconds = 0.43;
assert!(diff(&live, &frame(&mut h)) > 10);
#[cfg(feature = "lod")]
{
data.lock().unwrap().1.min_packet_px = 500.0;
let culled = frame(&mut h);
data.lock().unwrap().1.seconds = 0.65;
assert_eq!(
diff(&culled, &frame(&mut h)),
0,
"short-edge packets and trails culled under opt-in threshold"
);
data.lock().unwrap().1.min_packet_px = 0.0;
}
data.lock().unwrap().1.animation = Animation::External;
let external = frame(&mut h);
data.lock().unwrap().1.seconds = 0.85;
assert_eq!(diff(&external, &frame(&mut h)), 0);
let targets = gpu::residency(&rs).1;
{
let mut g = graph();
g.nodes[0].position[1] += 20.0;
data.lock().unwrap().0.replace(g).unwrap();
}
assert!(diff(&external, &frame(&mut h)) > 20);
assert_eq!(gpu::residency(&rs).1, targets);
gpu::evict_idle(&rs, u64::MAX);
assert_eq!(gpu::residency(&rs).0, 0);
}