From e7d02a076a11a8732b364f0b6c34c6a1d246c1e8 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Thu, 30 Jul 2026 15:12:52 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20Queue.LastSubmissionIndex=20deadlock=20?= =?UTF-8?q?=E2=80=94=20atomic=20instead=20of=20mutex=20(ADR-056)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onZero callbacks from Triage() called lastSubmissionIndex() which locked Queue.mu, already held by Submit(). Deadlock chain: Submit → postSubmit → Triage → onZero → lastSubmissionIndex → Queue.mu Fix: lastSubmissionIndex changed from mutex-protected uint64 to atomic.Uint64. Single writer (Submit under mu), lock-free readers (onZero callbacks). Safe because Store happens under mu (sequential) and Load is always-valid. --- CHANGELOG.md | 10 ++++++++++ queue_native.go | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8866164e..711931ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.30.29] - 2026-07-30 + +### Fixed + +- **Queue.LastSubmissionIndex deadlock** — `onZero` callbacks from `Triage()` + called `lastSubmissionIndex()` which locked `Queue.mu`, already held by + `Submit()`. Changed `lastSubmissionIndex` from `mutex`-protected `uint64` to + `atomic.Uint64` — single writer (Submit), lock-free readers (onZero callbacks). + ADR-056 deadlock chain: Submit→Triage→onZero→lastSubmissionIndex→mu. + ## [0.30.28] - 2026-07-30 ### Fixed diff --git a/queue_native.go b/queue_native.go index 9ae31ed5..6cc9e334 100644 --- a/queue_native.go +++ b/queue_native.go @@ -5,6 +5,7 @@ package wgpu import ( "fmt" "sync" + "sync/atomic" "github.com/gogpu/wgpu/core" "github.com/gogpu/wgpu/hal" @@ -27,8 +28,10 @@ type Queue struct { // lastSubmissionIndex is the most recent submission index returned by // hal.Queue.Submit(). Used by DestroyQueue to conservatively defer // resource destruction until after the latest known submission completes. - // Protected by mu. - lastSubmissionIndex uint64 + // Atomic: written by Submit (under mu), read by onZero callbacks which + // may fire during Triage while Queue.mu is held. Using atomic avoids + // deadlock: Submit→Triage→onZero→lastSubmissionIndex→mu (ADR-056). + lastSubmissionIndex atomic.Uint64 } // Submit submits command buffers for execution. Non-blocking. @@ -97,7 +100,7 @@ func (q *Queue) Submit(commandBuffers ...*CommandBuffer) (uint64, error) { } // Track the latest submission index for deferred resource destruction. - q.lastSubmissionIndex = subIdx + q.lastSubmissionIndex.Store(subIdx) // Record inflight resources and clean up completed ones. // dstTextures/dstBuffers prevent premature Release (BUG-DX12-006: use-after-free). @@ -346,13 +349,11 @@ func (q *Queue) SetSwapchainSuppressed(suppressed bool) { } // LastSubmissionIndex returns the most recent submission index. -// Used by resource Release() methods to schedule deferred destruction. -// Safe for concurrent use — reads under the queue mutex. +// Used by resource Release() and onZero callbacks to schedule deferred destruction. +// Lock-free via atomic — safe to call from onZero callbacks during Triage +// while Queue.mu is held by Submit. ADR-056 deadlock fix. func (q *Queue) LastSubmissionIndex() uint64 { - q.mu.Lock() - idx := q.lastSubmissionIndex - q.mu.Unlock() - return idx + return q.lastSubmissionIndex.Load() } // destroyQueue returns the device's DestroyQueue, or nil if unavailable.