use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};
use super::adapter::{
gpu_unavailable_json, is_management_display, selected_adapter, AdapterSelection,
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LaneRecord {
pub name: String,
pub installed: bool,
pub reports_frames: bool,
pub frames: u64,
pub idle_reason: Option<String>,
}
impl LaneRecord {
#[must_use]
pub fn short_name(&self) -> &str {
self.name.rsplit("::").next().unwrap_or(self.name.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuLaneUse {
NotInstalled,
InUse {
frames: u64,
},
InstalledUnused,
Unknown {
silent_lanes: usize,
},
}
impl GpuLaneUse {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
GpuLaneUse::NotInstalled => "not_installed",
GpuLaneUse::InUse { .. } => "in_use",
GpuLaneUse::InstalledUnused => "installed_unused",
GpuLaneUse::Unknown { .. } => "unknown",
}
}
#[must_use]
pub fn frames(self) -> u64 {
match self {
GpuLaneUse::InUse { frames } => frames,
_ => 0,
}
}
}
static LANES: OnceLock<Mutex<BTreeMap<&'static str, LaneRecord>>> = OnceLock::new();
fn lanes_slot() -> &'static Mutex<BTreeMap<&'static str, LaneRecord>> {
LANES.get_or_init(|| Mutex::new(BTreeMap::new()))
}
fn with_lane(name: &'static str, f: impl FnOnce(&mut LaneRecord)) {
if let Ok(mut g) = lanes_slot().lock() {
let rec = g.entry(name).or_insert_with(|| LaneRecord {
name: name.to_owned(),
installed: false,
reports_frames: false,
frames: 0,
idle_reason: None,
});
f(rec);
}
}
pub fn note_installed(name: &'static str) {
with_lane(name, |r| r.installed = true);
}
pub fn note_reports_frames(name: &'static str) {
with_lane(name, |r| r.reports_frames = true);
}
pub fn note_frame(name: &'static str) {
with_lane(name, |r| {
r.installed = true;
r.reports_frames = true;
r.frames += 1;
});
}
pub const PANE_NOT_PAINTED_YET: &str = "pane_not_painted_yet";
pub const AWAITING_FIRST_ENCODE: &str = "awaiting_first_encode";
pub const NOT_YET_DRAWN_REASONS: [&str; 2] = [PANE_NOT_PAINTED_YET, AWAITING_FIRST_ENCODE];
pub fn note_idle_reason(name: &'static str, reason: Option<&str>) {
with_lane(name, |r| {
r.idle_reason = reason.map(str::to_owned);
});
}
#[must_use]
pub fn idle_reason_of(name: &str) -> Option<String> {
lanes_slot().lock().ok().and_then(|g| g.get(name).and_then(|r| r.idle_reason.clone()))
}
#[must_use]
pub fn lanes() -> Vec<LaneRecord> {
lanes_slot().lock().map(|g| g.values().cloned().collect()).unwrap_or_default()
}
#[must_use]
pub fn frames_of(name: &str) -> u64 {
lanes_slot().lock().map(|g| g.get(name).map_or(0, |r| r.frames)).unwrap_or(0)
}
#[must_use]
pub fn lane_use() -> GpuLaneUse {
fold_lane_use(&lanes())
}
#[must_use]
pub fn fold_lane_use(lanes: &[LaneRecord]) -> GpuLaneUse {
let live: Vec<&LaneRecord> = lanes.iter().filter(|l| l.installed || l.frames > 0).collect();
if live.is_empty() {
return GpuLaneUse::NotInstalled;
}
let frames: u64 = live.iter().map(|l| l.frames).sum();
if frames > 0 {
return GpuLaneUse::InUse { frames };
}
let silent = live.iter().filter(|l| !l.reports_frames).count();
if silent == 0 {
GpuLaneUse::InstalledUnused
} else {
GpuLaneUse::Unknown { silent_lanes: silent }
}
}
#[doc(hidden)]
pub fn reset_for_test() {
if let Ok(mut g) = lanes_slot().lock() {
g.clear();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuStatus {
pub adapter: Option<AdapterSelection>,
pub unavailable: Option<serde_json::Value>,
pub lane: GpuLaneUse,
pub lanes: Vec<LaneRecord>,
}
impl GpuStatus {
#[must_use]
pub fn probe() -> Self {
let lanes = lanes();
let un = gpu_unavailable_json();
Self {
adapter: selected_adapter(),
unavailable: if un.is_null() { None } else { Some(un) },
lane: fold_lane_use(&lanes),
lanes,
}
}
#[must_use]
pub fn none() -> Self {
Self { adapter: None, unavailable: None, lane: GpuLaneUse::NotInstalled, lanes: Vec::new() }
}
#[must_use]
pub fn of(adapter: Option<AdapterSelection>, lanes: Vec<LaneRecord>) -> Self {
Self { adapter, unavailable: None, lane: fold_lane_use(&lanes), lanes }
}
#[must_use]
pub fn is_software(&self) -> bool {
self.adapter.as_ref().is_some_and(|s| s.software)
}
#[must_use]
pub fn adapter_count(&self) -> usize {
self.adapter.as_ref().map_or(0, |s| s.considered.len())
}
#[must_use]
pub fn device_class(&self) -> String {
match &self.adapter {
None => "none".to_owned(),
Some(s) if s.software => "SOFTWARE (CPU rasteriser)".to_owned(),
Some(s) if is_management_display(&s.chosen) => "management console (BMC)".to_owned(),
Some(s) => s.chosen.kind.as_str().to_owned(),
}
}
#[must_use]
pub fn adapter_line(&self) -> String {
match &self.adapter {
Some(s) => format!("GPU: {}", s.chosen.name),
None => "GPU: none — no adapter was brought up in this process".to_owned(),
}
}
#[must_use]
pub fn detail_line(&self) -> String {
match &self.adapter {
Some(s) => format!(
"{} · {} · {} adapter(s) enumerated{}",
if s.chosen.backend.is_empty() { "?" } else { s.chosen.backend.as_str() },
self.device_class(),
s.considered.len(),
if s.forced { " · FORCED" } else { "" },
),
None => "no backend · no adapter enumerated".to_owned(),
}
}
#[must_use]
pub fn lane_line(&self) -> String {
let names = |only_used: bool| -> String {
let v: Vec<&str> = self
.lanes
.iter()
.filter(|l| if only_used { l.frames > 0 } else { l.installed })
.map(LaneRecord::short_name)
.collect();
if v.is_empty() { "—".to_owned() } else { v.join(", ") }
};
match self.lane {
GpuLaneUse::InUse { frames } => {
format!("GPU lane: IN USE — {frames} frame(s) encoded by {}", names(true))
}
GpuLaneUse::InstalledUnused if self.every_idle_lane_is_undrawn() => format!(
"GPU lane: installed, NOT YET DRAWN — {} has not been asked to paint (no CPU painter is doing its work either)",
names(false)
),
GpuLaneUse::InstalledUnused => format!(
"GPU lane: installed but UNUSED — {} drew 0 frames (the CPU painter is doing the work)",
names(false)
),
GpuLaneUse::NotInstalled => {
"GPU lane: not installed — the CPU painter owns every pane".to_owned()
}
GpuLaneUse::Unknown { silent_lanes } => format!(
"GPU lane: unknown — {silent_lanes} installed lane(s) ({}) do not report frames",
names(false)
),
}
}
#[must_use]
pub fn every_idle_lane_is_undrawn(&self) -> bool {
let mut idle = self.lanes.iter().filter(|l| l.installed && l.frames == 0).peekable();
idle.peek().is_some()
&& idle.all(|l| {
l.idle_reason.as_deref().is_some_and(|r| NOT_YET_DRAWN_REASONS.contains(&r))
})
}
#[must_use]
pub fn unavailable_line(&self) -> Option<String> {
let u = self.unavailable.as_ref()?;
Some(format!(
"{} — {}",
u["code"].as_str().unwrap_or("facet-gpu-?"),
u["remedy"].as_str().unwrap_or("(no remedy recorded)")
))
}
#[must_use]
pub fn rows(&self) -> Vec<String> {
let mut v = vec![self.adapter_line(), self.detail_line(), self.lane_line()];
v.extend(self.idle_reason_line());
v.extend(self.unavailable_line());
v
}
#[must_use]
pub fn idle_reason_line(&self) -> Option<String> {
let v: Vec<String> = self
.lanes
.iter()
.filter(|l| l.frames == 0)
.filter_map(|l| l.idle_reason.as_ref().map(|r| format!("{}: {r}", l.short_name())))
.collect();
if v.is_empty() { None } else { Some(format!("idle because — {}", v.join("; "))) }
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"adapter": match &self.adapter {
Some(s) => s.to_json(),
None => serde_json::Value::Null,
},
"adapter_count": self.adapter_count(),
"backend": self.adapter.as_ref().map(|s| s.chosen.backend.clone()),
"device_class": self.device_class(),
"software": self.is_software(),
"lane": self.lane.as_str(),
"lane_frames": self.lane.frames(),
"lanes": self.lanes,
"unavailable": self.unavailable.clone().unwrap_or(serde_json::Value::Null),
"rows": self.rows(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::adapter::{AdapterFacts, AdapterKind, VENDOR_MESA, VENDOR_NVIDIA};
fn nvidia() -> AdapterFacts {
AdapterFacts::new("NVIDIA GeForce RTX 4090", AdapterKind::DiscreteGpu, "Vulkan")
.with_vendor(VENDOR_NVIDIA)
}
fn llvmpipe() -> AdapterFacts {
AdapterFacts::new("llvmpipe (LLVM 21.1.8, 256 bits)", AdapterKind::Cpu, "Vulkan")
.with_vendor(VENDOR_MESA)
}
fn lane(name: &str, installed: bool, reports: bool, frames: u64) -> LaneRecord {
LaneRecord { name: name.to_owned(), installed, reports_frames: reports, frames, idle_reason: None }
}
#[test]
fn an_undrawn_lane_is_not_reported_as_the_cpu_painter_doing_the_work() {
let undrawn = |name: &str, reason: &str| LaneRecord {
idle_reason: Some(reason.to_owned()),
..lane(name, true, true, 0)
};
let status = |lanes: Vec<LaneRecord>| GpuStatus {
adapter: None,
unavailable: None,
lane: fold_lane_use(&lanes),
lanes,
};
let korp = status(vec![
undrawn("facett_core::render::gpu::graphcloud::cloud::CloudRenderer", PANE_NOT_PAINTED_YET),
undrawn("facett_map::gpu::GpuMapRenderer", PANE_NOT_PAINTED_YET),
]);
assert!(korp.every_idle_lane_is_undrawn());
let line = korp.lane_line();
assert!(
!line.contains("the CPU painter is doing the work"),
"nothing was drawing this lane's pane, so nothing was doing its work: {line}",
);
assert!(line.contains("NOT YET DRAWN"), "the row must say what is actually true: {line}");
assert!(
korp.rows().iter().any(|r| r.contains("GpuMapRenderer: pane_not_painted_yet")),
"{:?}",
korp.rows(),
);
let chosen = status(vec![undrawn("facett_map::gpu::GpuMapRenderer", AWAITING_FIRST_ENCODE)]);
assert!(!chosen.lane_line().contains("the CPU painter is doing the work"));
let declined = status(vec![undrawn("facett_map::gpu::GpuMapRenderer", "rotated_camera")]);
assert!(!declined.every_idle_lane_is_undrawn());
assert!(
declined.lane_line().contains("the CPU painter is doing the work"),
"a declined lane really did hand the work over: {}",
declined.lane_line(),
);
let mixed = status(vec![
lane("facett_core::render::gpu::graphcloud::cloud::CloudRenderer", true, true, 0),
undrawn("facett_map::gpu::GpuMapRenderer", PANE_NOT_PAINTED_YET),
]);
assert!(
!mixed.every_idle_lane_is_undrawn(),
"a lane that never explained itself cannot be counted as un-drawn",
);
}
#[test]
fn an_idle_lane_names_its_reason_in_the_row_a_human_reads() {
let mut rec = lane("facett_map::gpu::GpuMapRenderer", true, true, 0);
let bare = GpuStatus {
adapter: None,
unavailable: None,
lane: fold_lane_use(std::slice::from_ref(&rec)),
lanes: vec![rec.clone()],
};
let bare_rows = bare.rows();
assert!(bare_rows.iter().any(|r| r.contains("installed but UNUSED")), "{bare_rows:?}");
assert!(
!bare_rows.iter().any(|r| r.contains("idle because")),
"a lane that never explained itself must not have words put in its mouth: {bare_rows:?}"
);
assert_eq!(bare.idle_reason_line(), None);
rec.idle_reason = Some("pane_not_painted_yet".to_owned());
let told = GpuStatus {
adapter: None,
unavailable: None,
lane: fold_lane_use(std::slice::from_ref(&rec)),
lanes: vec![rec.clone()],
};
let told_rows = told.rows();
assert_ne!(bare_rows, told_rows, "the reason must CHANGE what a human reads, or it is not reaching them");
assert!(
told_rows.iter().any(|r| r == "idle because — GpuMapRenderer: pane_not_painted_yet"),
"the reason must be its OWN row, naming lane and reason: {told_rows:?}"
);
assert!(
!told.lane_line().contains("pane_not_painted_yet"),
"the reason belongs on its own row: {}",
told.lane_line()
);
let painting = lane("facett_map::gpu::GpuMapRenderer", true, true, 7);
let mut idle_other = lane("facett_graph3d::gpu::CloudRenderer", true, true, 0);
idle_other.idle_reason = Some("no_graph_open".to_owned());
let mixed = GpuStatus {
adapter: None,
unavailable: None,
lane: fold_lane_use(&[painting.clone(), idle_other.clone()]),
lanes: vec![painting, idle_other],
};
let mixed_rows = mixed.rows();
assert!(mixed_rows.iter().any(|r| r.contains("IN USE — 7 frame(s)")), "{mixed_rows:?}");
assert!(
!mixed_rows.iter().any(|r| r.contains("GpuMapRenderer: ")),
"the lane that PAINTED must not be listed as idle: {mixed_rows:?}"
);
assert!(
mixed_rows.iter().any(|r| r == "idle because — CloudRenderer: no_graph_open"),
"the idle lane's reason must survive alongside a painting one: {mixed_rows:?}"
);
}
#[test]
fn the_lane_fold_never_guesses() {
assert_eq!(fold_lane_use(&[]), GpuLaneUse::NotInstalled);
assert_eq!(fold_lane_use(&[lane("A", false, true, 0)]), GpuLaneUse::NotInstalled);
assert_eq!(fold_lane_use(&[lane("A", true, true, 0)]), GpuLaneUse::InstalledUnused);
assert_eq!(
fold_lane_use(&[lane("A", true, true, 7), lane("B", true, true, 5)]),
GpuLaneUse::InUse { frames: 12 }
);
assert_eq!(
fold_lane_use(&[lane("A", true, false, 0)]),
GpuLaneUse::Unknown { silent_lanes: 1 },
"a lane that never promised to count frames must fold to unknown, not to unused"
);
assert_eq!(
fold_lane_use(&[lane("A", true, true, 0), lane("B", true, false, 0)]),
GpuLaneUse::Unknown { silent_lanes: 1 }
);
assert_eq!(
fold_lane_use(&[lane("A", true, true, 3), lane("B", true, false, 0)]),
GpuLaneUse::InUse { frames: 3 }
);
}
#[test]
fn a_lying_software_adapter_still_reads_software() {
let liar = AdapterFacts::new("llvmpipe (LLVM 21.1.8)", AdapterKind::DiscreteGpu, "Vulkan")
.with_vendor(VENDOR_MESA);
let st = GpuStatus::of(AdapterSelection::decide(&[liar], None), vec![]);
assert!(st.is_software(), "llvmpipe is software however it classes itself");
assert_eq!(st.device_class(), "SOFTWARE (CPU rasteriser)");
assert!(
st.detail_line().contains("SOFTWARE"),
"the row must say SOFTWARE: {}",
st.detail_line()
);
assert!(
!st.detail_line().contains("discrete"),
"the row must NOT call a software rasteriser discrete: {}",
st.detail_line()
);
assert_eq!(st.to_json()["software"], serde_json::json!(true));
assert_eq!(st.to_json()["device_class"], serde_json::json!("SOFTWARE (CPU rasteriser)"));
}
#[test]
fn rows_carry_adapter_backend_class_and_count() {
let st = GpuStatus::of(
AdapterSelection::decide(&[llvmpipe(), nvidia()], None),
vec![lane("facett_map::gpu::GpuMapRenderer", true, true, 42)],
);
let rows = st.rows();
assert!(rows[0].contains("NVIDIA GeForce RTX 4090"), "{:?}", rows);
assert!(rows[1].contains("Vulkan"), "{:?}", rows);
assert!(rows[1].contains("discrete"), "{:?}", rows);
assert!(rows[1].contains("2 adapter(s)"), "the count of enumerated adapters: {:?}", rows);
assert!(rows[2].contains("IN USE") && rows[2].contains("42"), "{:?}", rows);
assert!(rows[2].contains("GpuMapRenderer"), "the lane is named: {:?}", rows);
assert!(!st.is_software());
let j = st.to_json();
assert_eq!(j["backend"], serde_json::json!("Vulkan"));
assert_eq!(j["device_class"], serde_json::json!("discrete"));
assert_eq!(j["adapter_count"], serde_json::json!(2));
assert_eq!(j["lane"], serde_json::json!("in_use"));
assert_eq!(j["lane_frames"], serde_json::json!(42));
assert_eq!(j["rows"].as_array().expect("rows").len(), 3);
}
#[test]
fn a_gpu_less_process_says_so() {
let st = GpuStatus::none();
assert!(st.adapter_line().contains("none"), "{}", st.adapter_line());
assert!(st.lane_line().contains("not installed"), "{}", st.lane_line());
let j = st.to_json();
assert!(j["adapter"].is_null());
assert_eq!(j["lane"], serde_json::json!("not_installed"));
assert_eq!(j["software"], serde_json::json!(false));
assert_eq!(j["adapter_count"], serde_json::json!(0));
}
#[test]
fn the_registry_records_what_the_lanes_report() {
reset_for_test();
note_installed("test::LaneA");
assert_eq!(lane_use(), GpuLaneUse::Unknown { silent_lanes: 1 });
note_reports_frames("test::LaneA");
assert_eq!(lane_use(), GpuLaneUse::InstalledUnused);
note_frame("test::LaneB");
assert_eq!(lane_use(), GpuLaneUse::InUse { frames: 1 });
let recs = lanes();
let b = recs.iter().find(|l| l.name == "test::LaneB").expect("LaneB registered");
assert!(b.installed && b.reports_frames && b.frames == 1);
assert_eq!(b.short_name(), "LaneB");
reset_for_test();
assert_eq!(lane_use(), GpuLaneUse::NotInstalled);
}
}