screenpipe/screenpipe · error · anyhow::Error

latest frame mutex poisoned

Error message

latest frame mutex poisoned

What it means

Raised when the Mutex guarding the LatestFrame slot is poisoned, i.e. another thread panicked while holding the lock on latest.0. std Mutex poisoning means shared frame state may be inconsistent, so store_frame refuses to proceed.

Source

Thrown at crates/screenpipe-screen/src/wgc_capture.rs:597

    /// one-time "latest" texture creation. Returns whether a new frame was stored
    /// (false for stale-generation frames and pool-recreate transitions).
    fn store_frame(
        frame: &Direct3D11CaptureFrame,
        frame_pool: &Direct3D11CaptureFramePool,
        d3d: &Arc<D3dContext>,
        latest: &Arc<(Mutex<LatestFrame>, Condvar)>,
        closed: &Arc<AtomicBool>,
        stats: &Arc<CaptureCounters>,
    ) -> Result<bool> {
        let content = frame
            .ContentSize()
            .map_err(|e| anyhow!("ContentSize failed: {}", e))?;
        let content_size = (content.Width, content.Height);

        let mut slot = latest
            .0
            .lock()
            .map_err(|_| anyhow!("latest frame mutex poisoned"))?;

        if content_size != slot.pool_size {
            // Display mode changed (resolution/scaling): recreate the pool at the new
            // size in place. Keeping the session alive avoids re-flashing the capture
            // border on Windows 10 and rides out the change without a reinit.
            tracing::info!(
                "display size changed {:?} -> {:?}, recreating WGC frame pool",
                slot.pool_size,
                content_size
            );
            let recreate = create_winrt_device(&d3d.dxgi).and_then(|device| {
                frame_pool
                    .Recreate(
                        &device,
                        DirectXPixelFormat::B8G8R8A8UIntNormalized,
                        FRAME_POOL_BUFFERS,
                        SizeInt32 {
                            Width: content.Width,

View on GitHub (pinned to 4ebf712990)

Solutions

  1. Find and fix the original panic in the code that locks latest.0 (check panic backtraces before this error)
  2. Use lock().unwrap_or_else(|p| p.into_inner()) if you decide state is recoverable, since LatestFrame is just a slot
  3. Consider parking_lot::Mutex (no poisoning) if the slot can be safely rebuilt after a panic
  4. Reinitialize the capture pipeline after poisoning instead of failing every subsequent frame

Example fix

// before
let mut slot = latest.0.lock().map_err(|_| anyhow!("latest frame mutex poisoned"))?;
// after
let mut slot = match latest.0.lock() {
    Ok(g) => g,
    Err(poisoned) => {
        tracing::warn!("latest frame mutex poisoned; recovering slot");
        poisoned.into_inner()
    }
};
Defensive patterns

Strategy: try-catch

Try / catch

let mut slot = match latest.0.lock() {
    Ok(g) => g,
    Err(p) => p.into_inner(), // recover the slot; it is a plain value
};

Prevention

When it happens

Trigger: Any thread that previously locked latest.0 panicked while holding the lock (e.g. a bug in the pool-resize path, condvar wait misuse, or an unwrap inside the critical section), leaving the mutex poisoned for all later frame arrivals.

Common situations: A panic inside the display-mode-change pool-resize code under the lock; an assertion/unwrap failing in the consumer thread reading LatestFrame; tests panicking on the shared state.

Related errors


AI-assisted analysis of screenpipe/screenpipe@4ebf712990 (2026-09-01). Data as JSON: /api/errors/e64d8496d9638f0e. Report an issue: GitHub.