gfx-rs/wgpu · critical

Could not lock adapter context. This is most-likely a deadlo

Error message

Could not lock adapter context. This is most-likely a deadlock.

What it means

`AdapterContext::lock` on the WGL (Windows GL) backend tries to acquire the adapter's shared GL context mutex with a 1-second timeout (`try_lock_for`). If the lock cannot be acquired within `CONTEXT_LOCK_TIMEOUT_SECS`, it panics with this message to surface a suspected deadlock instead of hanging forever. The adapter context is shared between all devices/surfaces of one adapter, so any thread holding it longer than ~1 second triggers this in another thread.

Source

Thrown at wgpu-hal/src/gles/wgl.rs:70

    }

    pub fn raw_context(&self) -> *mut c_void {
        match self.inner.lock().context {
            Some(ref wgl) => wgl.context.0,
            None => ptr::null_mut(),
        }
    }

    /// Obtain a lock to the WGL context and get handle to the [`glow::Context`] that can be used to
    /// do rendering.
    #[track_caller]
    pub fn lock(&self) -> AdapterContextLock<'_> {
        let inner = self
            .inner
            // Don't lock forever. If it takes longer than 1 second to get the lock we've got a
            // deadlock and should panic to show where we got stuck
            .try_lock_for(Duration::from_secs(CONTEXT_LOCK_TIMEOUT_SECS))
            .expect("Could not lock adapter context. This is most-likely a deadlock.");

        if let Some(wgl) = &inner.context {
            wgl.make_current(inner.device.dc).unwrap()
        };

        AdapterContextLock { inner }
    }

    /// Obtain a lock to the WGL context and get handle to the [`glow::Context`] that can be used to
    /// do rendering.
    ///
    /// Unlike [`lock`](Self::lock), this accepts a device to pass to `make_current` and exposes the error
    /// when `make_current` fails.
    #[track_caller]
    fn lock_with_dc(&self, device: Gdi::HDC) -> windows::core::Result<AdapterContextLock<'_>> {
        let inner = self
            .inner
            .try_lock_for(Duration::from_secs(CONTEXT_LOCK_TIMEOUT_SECS))

View on GitHub (pinned to 3e11ff59bf)

Solutions

  1. Audit for recursive locking: never call another wgpu method that takes the context while you already hold `AdapterContextLock` (e.g. inside `lock()`-based accessors like `GlTexture::raw`).
  2. Ensure every `AdapterContextLock` is dropped promptly; never hold it across `.await` or long-running GL calls.
  3. Route all GL/wgpu calls for that adapter through a single thread or serialize them with your own synchronization so contention stays below the 1s timeout.
  4. If contention is legitimately heavy, restructure to drop-and-reacquire frequently, or file an issue to raise `CONTEXT_LOCK_TIMEOUT_SECS` via a patched build.

Example fix

// before: nested lock deadlocks (same thread, non-reentrant)
let tex = unsafe { texture.lock() };
unsafe { queue.submit(...) }; // panics: submit locks the same context
// after
let tex_raw = unsafe { texture.lock() }.raw().clone();
drop(tex_raw_owner); // ensure lock released before further wgpu calls
unsafe { queue.submit(...) };
Defensive patterns

Strategy: validation

Validate before calling

// Rust: detect re-entrant lock attempts before they deadlock
// wgpu's AdapterContext uses a timed try_lock; emulate the guard discipline:
fn assert_not_holding<T>(guard_slot: &Option<std::sync::MutexGuard<'_, T>>) {
    assert!(guard_slot.is_none(), "wgpu GL context lock already held on this thread: recursive call will deadlock");
}

Type guard

// Rust: structural guard - refuse to run wgpu calls while a lock guard exists
struct GlGuardScope<'a>(Option<wgpu_hal::gles::AdapterContextLock<'a>>);
impl<'a> GlGuardScope<'a> {
    fn enter(&mut self, ctx: &'a wgpu_hal::gles::AdapterContext) {
        assert!(self.0.is_none(), "cannot nest AdapterContext locks on one thread");
        self.0 = Some(ctx.lock());
    }
}

Try / catch

// panic must be caught at the FFI/task boundary, not around every call
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    device_queue_do_work();
}));
match result {
    Ok(v) => v,
    Err(p) => {
        let msg = p.downcast_ref::<String>().map(String::as_str)
            .or_else(|| p.downcast_ref::<&str>().copied()).unwrap_or("");
        if msg.contains("Could not lock adapter context") {
            restart_gl_thread(); // rebuild device after suspected deadlock
        } else { std::panic::resume_unwind(p); }
    }
}

Prevention

When it happens

Trigger: Any public API that needs the GL context (buffer/texture/queue operations, instance->adapter enumeration) while another thread holds `AdapterContextLock` for more than 1 second, or recursively locking the adapter context on the same thread (std sync RwLock/Mutex non-reentrant), or a lock leaked (not dropped) on another thread.

Common situations: Calling wgpu GL APIs from the same thread that already holds a lock (re-entrant call e.g. inside a winit callback that itself calls queue.submit); holding the lock across an await point; doing long GL work inside a closure that keeps `AdapterContextLock` alive; window teardown racing with rendering on another thread.

Related errors


AI-assisted analysis of gfx-rs/wgpu@3e11ff59bf (2026-09-03). Data as JSON: /api/errors/8a3c73c98683f4b1. Report an issue: GitHub.