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

nordisk/facett

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