use std::sync::atomic::{AtomicU32, Ordering};
pub const MAX_ATTEMPTS: u32 = 4;
#[must_use]
pub fn backoff_ms(next: u32) -> u32 {
250 << next.saturating_sub(2).min(4)
}
#[must_use]
pub fn is_no_adapter(err: &str) -> bool {
err.contains("No suitable graphics adapter") || err.contains("NotFound")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Preflight {
NoWebGpu,
NotSecure,
}
#[must_use]
pub fn preflight(has_navigator_gpu: bool, secure_context: bool) -> Option<Preflight> {
if !secure_context {
Some(Preflight::NotSecure)
} else if !has_navigator_gpu {
Some(Preflight::NoWebGpu)
} else {
None
}
}
#[must_use]
pub fn preflight_message(p: Preflight) -> &'static str {
match p {
Preflight::NoWebGpu => {
"no GPU: this browser exposes no WebGPU (navigator.gpu missing) — Linux Chrome needs \
chrome://flags/#enable-unsafe-webgpu and #enable-vulkan"
}
Preflight::NotSecure => {
"no GPU: this page is not a secure context, so the browser hides WebGPU — open it via \
localhost or https"
}
}
}
#[must_use]
pub fn software_adapter(
vendor: &str,
architecture: &str,
device: &str,
description: &str,
is_fallback: bool,
) -> Option<String> {
use crate::render::adapter::{facts_from_browser_info, is_software_rasteriser};
let facts = facts_from_browser_info(vendor, architecture, device, description, "BrowserWebGpu");
if is_software_rasteriser(&facts) {
return Some(facts.name);
}
is_fallback.then(|| if facts.name.is_empty() { "fallback adapter".to_string() } else { format!("fallback adapter {}", facts.name) })
}
#[must_use]
pub fn software_adapter_message(what: &str) -> String {
format!(
"no GPU: software adapter (SwiftShader/llvmpipe) — the browser's only WebGPU adapter is a CPU \
rasteriser ({what}), and facett does not draw on one. Linux Chrome reaches the card with \
--enable-gpu --enable-features=Vulkan --use-angle=vulkan (headless: also \
--render-node-override=/dev/dri/renderD*), and VK_LOADER_DRIVERS_SELECT must not exclude \
the card's driver"
)
}
#[must_use]
pub fn no_gpu_state(msg: &str) -> serde_json::Value {
serde_json::json!({ "gpu": "no GPU", "gpu_reason": msg, "adapter_attempts": attempts() })
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Next {
RetryAfter(u32),
Fail(String),
}
#[must_use]
pub fn after_failure(attempt: u32, err: &str) -> Next {
if is_no_adapter(err) {
if attempt < MAX_ATTEMPTS {
Next::RetryAfter(backoff_ms(attempt + 1))
} else {
Next::Fail(format!(
"no GPU: WebGPU is exposed but gave this page no adapter after {attempt} attempts \
(requestAdapter() returned null) — Linux Chrome needs Vulkan: \
--enable-features=Vulkan --use-angle=vulkan, or chrome://flags/#enable-vulkan \
(last error: {err})"
))
}
} else {
Next::Fail(format!("WebGPU renderer failed to start: {err}"))
}
}
static REASON: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
pub fn record_reason(why: &str) {
if let Ok(mut r) = REASON.lock() {
r.clear();
r.push_str(why);
}
}
#[must_use]
pub fn reason() -> String {
REASON.lock().map(|r| r.clone()).unwrap_or_default()
}
static ATTEMPTS: AtomicU32 = AtomicU32::new(0);
pub fn record_attempts(n: u32) {
ATTEMPTS.store(n, Ordering::Relaxed);
}
#[must_use]
pub fn attempts() -> u32 {
ATTEMPTS.load(Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;
const NOT_FOUND: &str = "RequestAdapterError(NotFound { active_backends: BROWSER_WEBGPU })";
#[test]
fn no_adapter_is_retried_with_a_growing_backoff_then_says_no_gpu() {
assert_eq!(after_failure(1, NOT_FOUND), Next::RetryAfter(250));
assert_eq!(after_failure(2, NOT_FOUND), Next::RetryAfter(500));
assert_eq!(after_failure(3, NOT_FOUND), Next::RetryAfter(1000));
match after_failure(MAX_ATTEMPTS, NOT_FOUND) {
Next::Fail(m) => {
assert!(m.starts_with("no GPU"), "the final refusal says no GPU plainly: {m}");
assert!(m.contains("4 attempts"), "{m}");
}
other => panic!("retried past the bound: {other:?}"),
}
}
#[test]
fn the_wgpu_display_text_is_recognised() {
assert!(is_no_adapter("No suitable graphics adapter found; active backends: BROWSER_WEBGPU"));
}
#[test]
fn a_device_failure_is_not_retried() {
let err = "RequestDeviceError(LimitsExceeded)";
assert!(!is_no_adapter(err));
assert_eq!(after_failure(1, err), Next::Fail(format!("WebGPU renderer failed to start: {err}")));
}
#[test]
fn the_whole_wait_stays_short() {
let total: u32 = (2..=MAX_ATTEMPTS).map(backoff_ms).sum();
assert_eq!(total, 1750);
}
#[test]
fn a_browser_without_webgpu_or_off_https_fails_at_once_with_the_cause() {
assert_eq!(preflight(true, true), None, "may ask");
assert_eq!(preflight(false, true), Some(Preflight::NoWebGpu));
assert_eq!(preflight(false, false), Some(Preflight::NotSecure), "insecure hides navigator.gpu");
assert!(preflight_message(Preflight::NoWebGpu).contains("navigator.gpu missing"));
assert!(preflight_message(Preflight::NotSecure).contains("localhost or https"));
for p in [Preflight::NoWebGpu, Preflight::NotSecure] {
assert!(preflight_message(p).starts_with("no GPU"));
}
}
#[test]
fn the_final_null_adapter_message_names_the_vulkan_remedy() {
match after_failure(MAX_ATTEMPTS, NOT_FOUND) {
Next::Fail(m) => assert!(m.contains("--enable-features=Vulkan"), "{m}"),
other => panic!("{other:?}"),
}
}
#[test]
fn the_reason_is_recorded_and_cleared() {
record_reason("no GPU: x");
assert_eq!(reason(), "no GPU: x");
record_reason("");
assert_eq!(reason(), "");
}
#[test]
fn a_software_adapter_is_refused_with_no_gpu_and_a_card_is_not() {
let what = software_adapter("google", "swiftshader", "", "", false).expect("SwiftShader is software");
assert!(what.contains("swiftshader"), "{what}");
let msg = software_adapter_message(&what);
assert!(msg.starts_with("no GPU: software adapter (SwiftShader/llvmpipe)"), "{msg}");
assert!(msg.contains("swiftshader"), "the message names what the browser offered: {msg}");
let st = no_gpu_state(&msg);
assert_eq!(st["gpu"], "no GPU");
assert_eq!(st["gpu_reason"], msg.as_str());
assert!(software_adapter("mesa", "", "", "llvmpipe (LLVM 21.1.8, 256 bits)", false).is_some());
assert!(software_adapter("mesa", "lavapipe", "", "", false).is_some());
assert_eq!(software_adapter("", "", "", "", true).as_deref(), Some("fallback adapter"));
assert_eq!(software_adapter("nvidia", "blackwell", "", "", false), None);
assert_eq!(software_adapter("amd", "rdna-3", "", "", false), None);
assert_eq!(software_adapter("", "", "", "", false), None);
}
#[test]
fn attempts_are_recorded() {
record_attempts(3);
assert_eq!(attempts(), 3);
}
}