use crate::hw_probe::{forced_backend_from_env, ScanBackend};
use super::CompiledScanner;
static RUNTIME_DEGRADE_WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
static MEGASCAN_DEGRADE_WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
fn cached_gpu_env_flags() -> (bool, bool) {
static FLAGS: std::sync::OnceLock<(bool, bool)> = std::sync::OnceLock::new();
*FLAGS.get_or_init(|| {
let no_gpu = crate::gpu::env_no_gpu();
let require_gpu = std::env::var("KEYHOG_REQUIRE_GPU").as_deref() == Ok("1");
(no_gpu, require_gpu)
})
}
#[must_use]
pub fn gpu_forced_unavailable_message(
scanner: &CompiledScanner,
backend: ScanBackend,
) -> Option<String> {
let forced = forced_backend_from_env()?;
if !matches!(forced, ScanBackend::Gpu | ScanBackend::MegaScan) {
return None;
}
if !matches!(backend, ScanBackend::Gpu | ScanBackend::MegaScan) {
return None;
}
if scanner.gpu_stack_usable() {
return None;
}
Some(format!(
"KEYHOG_BACKEND={} but GPU stack unavailable (gpu_literals={}, gpu_backend={}, gpu_matcher={}) - \
silent CPU fallback is forbidden; unset KEYHOG_BACKEND or install a compatible GPU adapter",
forced.label(),
scanner.gpu_literals.is_some(),
scanner.gpu_backend.is_some(),
scanner.gpu_matcher().is_some(),
))
}
pub fn deny_silent_gpu_degrade(scanner: &CompiledScanner, backend: ScanBackend) {
if let Some(msg) = gpu_forced_unavailable_message(scanner, backend) {
panic!("{msg}");
}
if !matches!(backend, ScanBackend::Gpu | ScanBackend::MegaScan) {
return;
}
let (no_gpu, require_gpu) = cached_gpu_env_flags();
if require_gpu {
eprintln!(
"keyhog: KEYHOG_REQUIRE_GPU=1 but the GPU dispatch failed at runtime \
(literals={}, backend={}, matcher={}). Refusing to silently degrade.",
scanner.gpu_literals.is_some(),
scanner.gpu_backend.is_some(),
scanner.gpu_matcher().is_some(),
);
std::process::exit(2);
}
if no_gpu {
return;
}
if RUNTIME_DEGRADE_WARNED.set(()).is_ok() {
eprintln!(
"keyhog: GPU dispatch failed at runtime; this scan and any subsequent \
ones in this process degrade to CPU/SIMD. Often a transient driver issue or a \
program the GPU lowering pipeline rejects (check the preceding tracing::error \
line for the underlying message). Set KEYHOG_NO_GPU=1 to silence, or \
KEYHOG_REQUIRE_GPU=1 to hard-fail next time."
);
}
}
pub fn deny_silent_megascan_degrade(reason: &str) {
let (no_gpu, require_gpu) = cached_gpu_env_flags();
if require_gpu {
eprintln!(
"keyhog: KEYHOG_REQUIRE_GPU=1 but MegaScan rule-pipeline dispatch failed ({reason}). \
Refusing to silently fall back to literal-set."
);
std::process::exit(2);
}
if no_gpu {
return;
}
if MEGASCAN_DEGRADE_WARNED.set(()).is_ok() {
eprintln!(
"keyhog: MegaScan rule-pipeline unavailable ({reason}); this scan and any \
subsequent ones in this process degrade to the literal-set GPU dispatch. \
Set KEYHOG_NO_GPU=1 to silence, or KEYHOG_REQUIRE_GPU=1 to hard-fail next time."
);
}
}
impl CompiledScanner {
pub(crate) fn gpu_stack_usable(&self) -> bool {
self.gpu_literals.is_some() && self.gpu_backend.is_some() && self.gpu_matcher().is_some()
}
}