facett-core/src/render/backend.rs
//! **Runtime backend selection** — the galileo-style Renderer backend-swap policy,
//! lifted into the L0 kernel from `facett-graphview::backend` so map skins and the
//! graph engine decide identically. (graphview will re-export this in Phase C; this
//! milestone just adds it to core without breaking graphview, which keeps its own
//! copy.)
//!
//! The same scene rasterizes on the GPU ([`Backend::Wgpu`], the L0 SDF wgpu
//! pipeline) when a usable adapter exists, and on the CPU ([`Backend::VelloCpu`],
//! [`super::cpu::CpuRenderer`] / `vello_cpu`, multithreaded SIMD) when it doesn't.
//! The caller probes once and renders through whichever [`Backend`] [`decide`]
//! returns.
/// What a hardware probe found. The caller fills this (e.g. from a wgpu adapter
/// enumeration, an env override, or a forced-CPU test flag); the *policy* — turning
/// a probe into a pick — lives in [`decide`] so every surface decides identically.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GpuProbe {
/// A usable GPU adapter was found (a real wgpu device can be created).
pub usable_gpu: bool,
/// The caller forces the CPU path regardless (CI determinism, golden PNGs,
/// `FACETT_RENDER_CPU=1`, a known-bad driver allowlist hit).
pub force_cpu: bool,
}
impl GpuProbe {
/// A probe asserting a usable GPU is present.
pub fn gpu() -> Self {
Self { usable_gpu: true, force_cpu: false }
}
/// A probe with no GPU (the fallback path).
pub fn cpu_only() -> Self {
Self { usable_gpu: false, force_cpu: false }
}
/// A probe that reads the `FACETT_RENDER_CPU` env override on top of a
/// detected-GPU flag. The standard wiring a host uses.
pub fn from_env(usable_gpu: bool) -> Self {
Self { usable_gpu, force_cpu: std::env::var_os("FACETT_RENDER_CPU").is_some() }
}
}
/// The chosen rasterizer, named by the technology that draws.
///
/// Renamed 2026-10-02 (owner): the variants were `GpuVello`/`CpuVello`, but the GPU
/// arm is mostly the L0 SDF **wgpu** pipeline — vello is only one of the rasterizers
/// it covers — so it is named by the GPU API. The CPU arm is vello_cpu. The old
/// names remain as deprecated associated consts below so published downstream code
/// still compiles.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Backend {
/// GPU rasterization on a wgpu device, the fast path for large scenes: the L0
/// SDF pipeline (plus, with `l1-vello`, the vello L1 overlay), and in
/// facett-graphview the vello GPU rasterizer. Named by the GPU API because it
/// spans more than one rasterizer; vello runs on the `wgpu29` alias until vello
/// ships for wgpu 30. Wired behind the core `wgpu` feature; see [`super::gpu`].
Wgpu,
/// `vello_cpu` — multithreaded SIMD software rasterization. Always available.
/// The reference path here.
VelloCpu,
}
#[allow(non_upper_case_globals)]
impl Backend {
/// Deprecated name of [`Backend::Wgpu`].
#[deprecated(note = "renamed to Backend::Wgpu — named by the GPU API, not by one rasterizer")]
pub const GpuVello: Backend = Backend::Wgpu;
/// Deprecated name of [`Backend::VelloCpu`].
#[deprecated(note = "renamed to Backend::VelloCpu — named for the vello_cpu rasterizer it is")]
pub const CpuVello: Backend = Backend::VelloCpu;
pub fn is_gpu(self) -> bool {
matches!(self, Backend::Wgpu)
}
/// Human label for a status line / about box.
pub fn label(self) -> &'static str {
match self {
Backend::Wgpu => "L0 SDF wgpu (GPU)",
Backend::VelloCpu => "vello_cpu (SIMD/threads)",
}
}
}
/// THE picker: turn a [`GpuProbe`] into a [`Backend`]. GPU when usable and not
/// forced off **and** the `wgpu` feature is compiled in; CPU otherwise. Keeping
/// this pure (probe in, enum out) makes the policy trivially testable.
pub fn decide(probe: GpuProbe) -> Backend {
let want_gpu = probe.usable_gpu && !probe.force_cpu;
// The GPU path only exists when compiled with `wgpu`; without it, any probe
// resolves to CPU. This is what lets a default build stay GPU-dep-free while
// the seam is real.
if want_gpu && gpu_arm_compiled() {
Backend::Wgpu
} else {
Backend::VelloCpu
}
}
/// Whether the GPU rasterization arm is compiled into this build of `facett-core`
/// (i.e. the `wgpu` feature is on). The single source of truth for "can [`decide`]
/// ever return [`Backend::Wgpu`]". Re-pointed skins/engines (e.g.
/// `facett-graphview`) should key their expectations off THIS, not their own
/// crate's feature flag — workspace feature-unification can light up
/// `facett-core/wgpu` independently of a downstream crate's own `gpu` feature.
pub const fn gpu_arm_compiled() -> bool {
cfg!(feature = "wgpu")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_only_probe_always_picks_cpu() {
assert_eq!(decide(GpuProbe::cpu_only()), Backend::VelloCpu);
}
#[test]
fn force_cpu_overrides_a_present_gpu() {
let p = GpuProbe { usable_gpu: true, force_cpu: true };
assert_eq!(decide(p), Backend::VelloCpu);
}
#[test]
fn gpu_probe_picks_gpu_only_when_feature_on() {
let got = decide(GpuProbe::gpu());
if cfg!(feature = "wgpu") {
assert_eq!(got, Backend::Wgpu);
} else {
assert_eq!(got, Backend::VelloCpu);
}
}
}