pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(180);
static ONE_GPU_AT_A_TIME: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub struct Turn {
_across_processes: Option<std::fs::File>,
_in_process: std::sync::MutexGuard<'static, ()>,
}
#[must_use]
pub fn hold() -> Turn {
let in_process = ONE_GPU_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
Turn { _across_processes: flock_turn(), _in_process: in_process }
}
#[must_use]
pub fn lock_path() -> std::path::PathBuf {
if let Some(p) = std::env::var_os("FACETT_GPU_LOCK") {
return std::path::PathBuf::from(p);
}
std::env::var_os("XDG_RUNTIME_DIR")
.map(std::path::PathBuf::from)
.filter(|d| d.is_dir())
.unwrap_or_else(std::env::temp_dir)
.join("facett-one-gpu.lock")
}
#[cfg(unix)]
fn flock_turn() -> Option<std::fs::File> {
use std::io::Write as _;
let path = lock_path();
let file =
std::fs::OpenOptions::new().create(true).truncate(false).write(true).open(&path).ok()?;
let fd = std::os::fd::AsRawFd::as_raw_fd(&file);
let deadline = std::time::Instant::now() + WAIT_LIMIT;
loop {
if unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) } == 0 {
return Some(file);
}
if std::time::Instant::now() >= deadline {
let _ = writeln!(
std::io::stderr(),
"[one-gpu] waited {}s for {} and gave up — this bring-up is UNSERIALISED \
against other processes. Something is holding the GPU turn far longer \
than a render should.",
WAIT_LIMIT.as_secs(),
path.display()
);
return None;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
#[cfg(not(unix))]
fn flock_turn() -> Option<std::fs::File> {
None
}
#[cfg(feature = "wgpu")]
pub use probe::{open, OnSoftware, Probe};
#[cfg(feature = "wgpu")]
pub mod probe {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OnSoftware {
Refuse,
Skip,
}
pub struct Probe {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub adapter: wgpu::Adapter,
pub named: String,
_turn: super::Turn,
}
pub struct ProbeGuard {
pub adapter: wgpu::Adapter,
pub named: String,
_turn: super::Turn,
}
impl Probe {
#[must_use]
pub fn is_software(&self) -> bool {
crate::render::adapter::is_software_rasteriser(&crate::render::gpu::facts_of(
&self.adapter.get_info(),
))
}
#[must_use]
pub fn split(self) -> (wgpu::Device, wgpu::Queue, ProbeGuard) {
(
self.device,
self.queue,
ProbeGuard { adapter: self.adapter, named: self.named, _turn: self._turn },
)
}
}
fn software_allowed() -> bool {
matches!(
std::env::var("FACETT_ALLOW_SOFTWARE_GPU").ok().as_deref(),
Some("1" | "true" | "on" | "yes")
)
}
pub fn open(
who: &str,
on_software: OnSoftware,
limits: impl FnOnce(&wgpu::Adapter) -> Option<wgpu::Limits>,
) -> Option<Probe> {
let turn = super::hold();
let instance = wgpu::Instance::default();
let adapter = crate::render::gpu::request_best_adapter(
&instance,
crate::render::gpu::preferred_backends(),
)?;
let info = adapter.get_info();
let facts = crate::render::gpu::facts_of(&info);
let named = facts.describe();
eprintln!("[{who}] adapter — {named}");
if crate::render::adapter::is_software_rasteriser(&facts) && !software_allowed() {
match on_software {
OnSoftware::Refuse => {
crate::render::gpu::refuse_software_adapter(who, &info);
}
OnSoftware::Skip => {
eprintln!(
"[{who}] SKIPPING — {named} is a SOFTWARE rasteriser. Whatever \
this rendered would be about llvmpipe, not about the GPU lane. \
Set FACETT_ALLOW_SOFTWARE_GPU=1 if this host really has no card."
);
return None;
}
}
}
let required_limits = limits(&adapter)?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some(who),
required_features: wgpu::Features::empty(),
required_limits,
memory_hints: wgpu::MemoryHints::default(),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
}))
.ok()?;
Some(Probe { device, queue, adapter, named, _turn: turn })
}
#[must_use]
pub fn downlevel(_: &wgpu::Adapter) -> Option<wgpu::Limits> {
Some(wgpu::Limits::downlevel_defaults())
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
fn overlap_seen<G: 'static>(take: fn() -> G) -> bool {
let inside = Arc::new(AtomicBool::new(false));
let overlapped = Arc::new(AtomicBool::new(false));
let first = take();
inside.store(true, Ordering::SeqCst);
let (i2, o2) = (inside.clone(), overlapped.clone());
let t = std::thread::spawn(move || {
let _second = take();
if i2.load(Ordering::SeqCst) {
o2.store(true, Ordering::SeqCst);
}
});
std::thread::sleep(std::time::Duration::from_millis(150));
inside.store(false, Ordering::SeqCst);
drop(first);
t.join().expect("the second taker finished");
overlapped.load(Ordering::SeqCst)
}
#[test]
fn a_second_taker_waits_for_the_first_to_let_go() {
assert!(
overlap_seen(|| ()),
"RED ARM DID NOT FIRE: with NO guard at all the second thread still failed to \
get inside, so this experiment cannot detect a lock that excludes nothing. \
Whatever the green arm below reports is meaningless until this fires."
);
assert!(
!overlap_seen(super::hold),
"two holders were inside the turn at once — the lock excludes nothing, which \
is exactly the shape six twinned mutexes had on 2026-08-27"
);
}
#[cfg(unix)]
#[test]
fn the_cross_process_half_really_excludes() {
fn a_stranger_can_take_it(path: &std::path::Path) -> bool {
let f = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
.expect("the lock path is writable");
let fd = std::os::fd::AsRawFd::as_raw_fd(&f);
unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) == 0 }
}
let mine =
std::env::temp_dir().join(format!("facett-turn-selftest-{}.lock", std::process::id()));
assert!(
a_stranger_can_take_it(&mine),
"RED ARM DID NOT FIRE: a fresh flock on an unheld file {} was refused, so the \
green arm below cannot tell an flock that works from one that never ran",
mine.display()
);
let _ = std::fs::remove_file(&mine);
let turn = super::hold();
assert!(
turn._across_processes.is_some(),
"hold() did not take the cross-process half at all ({} — unwritable? or \
another process has held the box's turn for over {}s), so every run is \
serialised only against its own threads",
super::lock_path().display(),
super::WAIT_LIMIT.as_secs()
);
assert!(
!a_stranger_can_take_it(&super::lock_path()),
"a second open() of {} took the lock while the turn was held: the \
cross-process half excludes nothing, and two `cargo test` invocations on \
this box will bring up devices concurrently again",
super::lock_path().display()
);
drop(turn);
}
#[cfg(feature = "wgpu")]
#[test]
fn two_device_probes_never_hold_the_card_at_once() {
use std::sync::atomic::AtomicUsize;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
fn one_probe() -> bool {
let Some(p) = super::probe::open(
"gputurn-overlap-probe",
super::probe::OnSoftware::Skip,
super::probe::downlevel,
) else {
return false;
};
let now = LIVE.fetch_add(1, Ordering::SeqCst) + 1;
PEAK.fetch_max(now, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(120));
LIVE.fetch_sub(1, Ordering::SeqCst);
drop(p);
true
}
let t = std::thread::spawn(one_probe);
let a = one_probe();
let b = t.join().expect("the second probe finished");
if !a || !b {
eprintln!("[gputurn] no usable GPU adapter — skipping the probe overlap proof");
return;
}
assert_eq!(
PEAK.load(Ordering::SeqCst),
1,
"two probes held a device at the same time — `probe::open` is not taking the \
turn, or the `Turn` is being dropped before the `Probe` reaches the caller"
);
}
#[test]
fn the_lock_path_is_writable_so_the_cross_process_half_is_not_silently_off() {
let _turn = super::hold();
let p = super::lock_path();
assert!(p.is_file(), "taking the turn did not create {} — flock never ran", p.display());
}
}