facett_graphview/backend.rs
1//! **Runtime backend selection** — mirrors nornir's embedder probe→pick pattern
2//! (`probe the hardware, then choose`). vello is *not* GPU-only: the same scene
3//! rasterizes on the GPU (`vello` on wgpu) when a usable adapter exists, and on
4//! the CPU (`vello_cpu`, multithreaded SIMD) when it doesn't. The caller probes
5//! once and renders through whichever [`Backend`] [`decide`] returns.
6
7/// What a hardware probe found. The caller fills this (e.g. from a wgpu adapter
8/// enumeration, an env override, or a forced-CPU test flag); we keep the *policy*
9/// — turning a probe into a pick — in one place so every graph surface decides
10/// identically.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct GpuProbe {
13 /// A usable GPU adapter was found (a real wgpu device can be created).
14 pub usable_gpu: bool,
15 /// The caller forces the CPU path regardless (CI determinism, golden PNGs,
16 /// `FACETT_GRAPHVIEW_CPU=1`, a known-bad driver allowlist hit).
17 pub force_cpu: bool,
18}
19
20impl GpuProbe {
21 /// A probe asserting a usable GPU is present.
22 pub fn gpu() -> Self {
23 Self { usable_gpu: true, force_cpu: false }
24 }
25 /// A probe with no GPU (the fallback path).
26 pub fn cpu_only() -> Self {
27 Self { usable_gpu: false, force_cpu: false }
28 }
29 /// A probe that reads the `FACETT_GRAPHVIEW_CPU` env override on top of a
30 /// detected-GPU flag. The standard wiring a host uses.
31 pub fn from_env(usable_gpu: bool) -> Self {
32 Self { usable_gpu, force_cpu: std::env::var_os("FACETT_GRAPHVIEW_CPU").is_some() }
33 }
34}
35
36/// The chosen rasterizer.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Backend {
39 /// `vello` on a wgpu device — GPU compute rasterization. The fast path for
40 /// large graphs. Wired behind the `gpu` cargo feature; see [`crate::gpu`].
41 GpuVello,
42 /// `vello_cpu` — multithreaded SIMD software rasterization. Always available
43 /// (it ships transitively inside epaint 0.34). The reference path here.
44 CpuVello,
45}
46
47impl Backend {
48 pub fn is_gpu(self) -> bool {
49 matches!(self, Backend::GpuVello)
50 }
51 /// Human label for a status line / about box.
52 pub fn label(self) -> &'static str {
53 match self {
54 Backend::GpuVello => "vello (GPU/wgpu)",
55 Backend::CpuVello => "vello_cpu (SIMD/threads)",
56 }
57 }
58}
59
60/// THE picker: turn a [`GpuProbe`] into a [`Backend`]. GPU when usable and not
61/// forced off **and** the `gpu` feature is compiled in; CPU otherwise. Keeping
62/// this pure (probe in, enum out) makes the policy trivially testable.
63pub fn decide(probe: GpuProbe) -> Backend {
64 let want_gpu = probe.usable_gpu && !probe.force_cpu;
65 // The GPU path only exists when compiled with `gpu`; without it, any probe
66 // resolves to CPU. This is what lets a default build stay GPU-dep-free while
67 // the seam is real.
68 if want_gpu && cfg!(feature = "gpu") {
69 Backend::GpuVello
70 } else {
71 Backend::CpuVello
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn cpu_only_probe_always_picks_cpu() {
81 assert_eq!(decide(GpuProbe::cpu_only()), Backend::CpuVello);
82 }
83
84 #[test]
85 fn force_cpu_overrides_a_present_gpu() {
86 let p = GpuProbe { usable_gpu: true, force_cpu: true };
87 assert_eq!(decide(p), Backend::CpuVello);
88 }
89
90 #[test]
91 fn gpu_probe_picks_gpu_only_when_feature_on() {
92 let got = decide(GpuProbe::gpu());
93 if cfg!(feature = "gpu") {
94 assert_eq!(got, Backend::GpuVello);
95 } else {
96 // No GPU path compiled in → the probe still resolves, to CPU.
97 assert_eq!(got, Backend::CpuVello);
98 }
99 }
100}