facett-flow/tests/gpu_panes.rs
//! **A flow pane that stops being drawn gives its GPU memory back, and a new
//! generation keeps the pane's offscreen target.**
//!
//! `FlowRenderer` keeps one resident entry per pane (`egui::Id`): two storage buffers,
//! a uniform and an offscreen colour+depth target with its blit pipeline. The entry was
//! insert-only, so a closed tab or a dropped facet held all of it for the life of the
//! process, and every upload (a graph change, a cluster/raw toggle) built a fresh
//! target. `prepare` now runs facett-map's pane rule — a pane prepared neither in this
//! frame nor the one before is dropped — and an upload reuses the pane's target.
//!
//! Modelled on facett-map's
//! `gpu_lane_two_panes::two_panes_allocate_two_targets_once_and_a_gone_pane_frees_its_target`.
//!
//! Run (needs a real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=discrete \
//! cargo test -p facett-flow --features wgpu --test gpu_panes -- --nocapture
//! ```
#![cfg(feature = "wgpu")]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use facett_core::{Elm, Facet};
use facett_flow::{FlowGraph, Graph, Msg, gpu};
/// The device opt-in the GPU suites share: unset means skip, said loudly.
const GATE: &str = "FACETT_TEST_GPU";
const SURFACE: egui::Vec2 = egui::vec2(900.0, 640.0);
const PANE_A: egui::Rect =
egui::Rect::from_min_max(egui::pos2(20.0, 20.0), egui::pos2(440.0, 620.0));
const PANE_B: egui::Rect =
egui::Rect::from_min_max(egui::pos2(460.0, 20.0), egui::pos2(880.0, 620.0));
fn gated() -> bool {
let mode = std::env::var(GATE).unwrap_or_default();
if ["discrete", "integrated", "software"].contains(&mode.as_str()) {
return true;
}
eprintln!(
"SKIPPED and therefore PROVED NOTHING: set {GATE}=discrete|integrated|software so \
the flow pane rule runs on this box's GPU (deliberately not a silent pass)"
);
false
}
/// Pane A (`raw_a` flips it between the raw and the clustered geometry) and pane B
/// (shown while `show_b`), drawn through the flow renderer installed on `rs`.
struct Panes {
h: egui_kittest::Harness<'static>,
rs: egui_wgpu::RenderState,
show_b: Arc<AtomicBool>,
raw_a: Arc<AtomicBool>,
}
impl Panes {
fn new() -> Self {
use egui_kittest::wgpu::WgpuTestRenderer;
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),
"the flow renderer was refused on this device"
);
let show_b = Arc::new(AtomicBool::new(true));
let raw_a = Arc::new(AtomicBool::new(false));
let (sb, ra) = (Arc::clone(&show_b), Arc::clone(&raw_a));
let mut a = FlowGraph::new("pane-a", Graph::hub_demo(500)).expect("valid fixture");
let mut b = FlowGraph::new("pane-b", Graph::demo(500)).expect("valid fixture");
Elm::update(&mut a, Msg::Clustered(true));
let h = egui_kittest::Harness::builder()
.with_size(SURFACE)
.renderer(WgpuTestRenderer::from_render_state(rs.clone()))
.build_ui(move |ui| {
let clustered = !ra.load(Ordering::Relaxed);
if a.state().clustered != clustered {
Elm::update(&mut a, Msg::Clustered(clustered));
}
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(PANE_A));
child.set_clip_rect(PANE_A);
a.ui(&mut child);
if sb.load(Ordering::Relaxed) {
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(PANE_B));
child.set_clip_rect(PANE_B);
b.ui(&mut child);
}
});
Self {
h,
rs,
show_b,
raw_a,
}
}
/// Render `n` frames; hand back how many the flow lane encoded.
fn frames(&mut self, n: u32) -> u64 {
let before = facett_core::render::lane::frames_of(gpu::lane_name());
for _ in 0..n {
self.h.render().expect("the wgpu backend produced a frame");
self.h.step();
}
facett_core::render::lane::frames_of(gpu::lane_name()) - before
}
/// `(resident panes, offscreen targets ever made)`.
fn residency(&self) -> (usize, u64) {
gpu::residency(&self.rs)
}
}
#[test]
fn a_gone_pane_frees_its_resources_and_a_new_generation_keeps_the_target() {
if !gated() {
return;
}
let _gpu = facett_core::render::gputurn::hold();
let mut p = Panes::new();
assert!(
p.frames(3) > 0,
"the flow lane encoded no frames — this is the CPU preview"
);
let (panes, made) = p.residency();
assert_eq!(panes, 2, "two panes in the frame, {panes} resident");
assert_eq!(made, 2, "two panes made {made} offscreen targets");
// Pane A switches from the clustered overview to the raw graph: a new generation,
// so its buffers are uploaded again — into the target it already has.
p.raw_a.store(true, Ordering::Relaxed);
assert!(
p.frames(3) > 0,
"the flow lane encoded no frames after the toggle"
);
let (panes, made) = p.residency();
assert_eq!(
panes, 2,
"a re-upload changed the resident count to {panes}"
);
assert_eq!(
made,
2,
"a cluster/raw toggle built {} new offscreen target(s) — a new generation must \
keep the pane's target",
made - 2
);
// Pane B stops being drawn: one frame later it is gone, buffers and target with it.
p.show_b.store(false, Ordering::Relaxed);
assert!(
p.frames(3) > 0,
"the flow lane encoded no frames with pane A alone"
);
let (panes, _) = p.residency();
assert_eq!(
panes, 1,
"pane B is no longer drawn and {panes} panes are still resident — its storage \
buffers and offscreen target never came back"
);
}