fix(core): hold map_state across the HAL map in Buffer::map - #9959
Open
AdrianEddy wants to merge 4 commits into
Open
fix(core): hold map_state across the HAL map in Buffer::map#9959AdrianEddy wants to merge 4 commits into
map_state across the HAL map in Buffer::map#9959AdrianEddy wants to merge 4 commits into
Conversation
`Buffer::map` took the `map_state` lock as a temporary to swap the state out, then re-locked to store `Active` after the HAL map completed. That left `map_state` observably `Idle` for the whole duration of the map, so a `Buffer::unmap` running concurrently on another thread saw `Idle` and failed with `BufferAccessError::NotMapped` for a buffer that was merely mid-map. Hold one guard for the function instead. A concurrent unmap now blocks and then unmaps the `Active` mapping this call installs. The re-lock that previously forced the temporary guard is gone — the stores reuse the same guard — so the deadlock the old comment warned about is no longer possible. Holding the guard across `device::map_buffer` means holding it across that function's `initialization_status` write, so `BUFFER_MAP_STATE` gains a `BUFFER_INITIALIZATION_STATUS` edge. The edge is acyclic: `initialization_status` is a leaf rank and no path takes `map_state` while holding it.
`Device::handle_hal_error` calls `Device::lose`, which invokes the user's device-lost callback inline. Running it from `Buffer::map` while the `map_state` guard is held means a callback that touches the same buffer — `unmap`, `destroy` (which calls `unmap`), or `map_async` — re-locks a non-reentrant mutex on the same thread and deadlocks. It also acquires `DEVICE_LOST_CLOSURE`, which is not a declared follower of `BUFFER_MAP_STATE`. Split out `map_buffer_deferred_error`, which returns the HAL error untranslated. `Buffer::map` releases the guard first and translates after; `map_state` is already `Idle` on that path, so nothing depends on holding it. `map_buffer` keeps its previous signature and behaviour for the two buffer-creation call sites, which hold no such guard.
Member
|
#9960 is reviewed and mostly ready to merge. 🎉 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Connections
None.
Description
Buffer::unmapcan fail withBufferAccessError::NotMappedfor a buffer that is simply mid-map, when map and unmap run on different threads.Buffer::maptook themap_statelock as a temporary to swap the state out, ran the HAL map, then re-locked to storeActive. Between those two acquisitions — which spans the entire HAL map —map_stateis observablyIdle. A concurrentunmapreads thatIdleand lands inunmap_inner'sIdle => Err(NotMapped)arm. TheActivearm had the same shape, so there were two observable-Idlewindows.The fix holds a single guard for the function. A concurrent unmap now blocks and then unmaps the
Activemapping the call installs — a valid linearization, whereNotMappedcorresponded to no legal ordering at all.The old comment warned that holding the lock across the match would deadlock "when we try to re-lock the buffer back to active". That re-lock is what this change removes — the stores reuse the guard already held.
Holding the guard across
device::map_bufferalso holds it across that function'sinitialization_statuswrite, soBUFFER_MAP_STATEgains aBUFFER_INITIALIZATION_STATUSedge inlock::rank. The edge is acyclic:initialization_statusis a leaf rank, and no path acquiresmap_statewhile holding a buffer's init tracker.The third commit addresses a hazard the guard would otherwise introduce.
Device::handle_hal_errorcallsDevice::lose, which invokes the user's device-lost callback inline. On the HAL-map failure path that ran under the new guard, so a callback touching the same buffer (unmap,destroy→unmap, ormap_async) would re-lock a non-reentrant mutex on the same thread; it also acquiresDEVICE_LOST_CLOSURE, which is not a declared follower ofBUFFER_MAP_STATE.map_buffer_deferred_errorreturns the HAL error untranslated soBuffer::mapcan release the guard first and translate after —map_stateis alreadyIdlethere.map_bufferkeeps its signature for the two buffer-creation call sites, which hold no such guard.Testing
Reproduced on unmodified
trunkwith a stress harness using only the public API on thenoopbackend — thread Amap_async(Read)thenpoll(Wait), thread B oneunmap()per iteration at swept phase offsets:NotMappedwhile the same pendingmapAsyncresolvesOk), and every one confirmed by an immediate secondunmap()succeeding with no interveningmap_async— i.e. the state wentIdle → Activewith no legal writer.Under
--cfg wgpu_validate_locks: trunk 178 hits / 200k, this PR 0 / 200k with no rank panics — the new edge is exercised on every successful map and the ranked validator accepts the guard discipline.Those validator runs need #9960 applied on top; without it the mapping path aborts on a pre-existing release-order violation in
PendingSubmissionbefore reaching any of this. #9960 is independent of this PR and can land in either order.cargo xtask test buffer— 330/331 on a discrete and an integrated GPU (real Vulkan and DX12 HAL maps), no hangs. The one failure,wgpu_examples::big_compute_buffers::tests::two_bufferson the integrated GPU, reproduces identically on unmodifiedtrunk.cargo test -p wgpu-core --all-features— 65 pass.The harness is not included here. It is ~130 lines of public API and could be trimmed into a regression test; happy to add it if wanted.
Note for reviewers: on GLES,
map_bufferforMAP_READdoes aglGetBufferSubDatareadback under the global context lock, so on that backend a concurrent same-bufferunmap/map_asynccan now block for the duration of a GPU sync rather than returning early. It is per-buffer and bounded, and the early return it replaces was a spurious error, but it is a real behavioural change on that backend.Squash or Rebase?
Three commits, each independently reviewable; ready to rebase. Happy to squash if preferred.
Checklist
wgpumay be affected behaviorally.CHANGELOG.mdentries for the user-facing effects of this change are present.