Skip to main content

vk/mem/
synchronized.rs

1//! Opt-in internally synchronized native access, following VMA 3.4.0's vector
2//! and map/bind locks. Rust adaptation: one placement owner, stable per-backing
3//! mutexes, checked native leases, explicit scope partitioning and poison recovery.
4use super::{AllocError, Allocator, AllocatorStats};
5use std::sync::{Condvar, Mutex, MutexGuard, PoisonError, TryLockError};
6
7#[path = "synchronized/access.rs"]
8mod access;
9#[path = "synchronized/virtual_block.rs"]
10mod virtual_block;
11pub use virtual_block::SynchronizedVirtualBlock;
12
13/// One allocator with synchronized direct operations and concurrent pool scopes.
14///
15/// Placement uses one mutex per owner. Direct map/unmap/bind operations acquire
16/// a live allocation lease, then execute under that backing's stable mutex
17/// without the placement mutex. Other backings can map/bind, allocate and free
18/// concurrently, including inside the same pool. Regions sharing one backing
19/// serialize native map/bind work and retain exact explicit/persistent counts.
20/// The first such operation fallibly reserves callback-owned backing metadata.
21///
22/// Borrowed results, relocation and global services use exclusive access and
23/// wait for outstanding native leases. `try_with_allocator` reports busy during
24/// those leases. Borrowed pool/type/block scopes add independent placement
25/// owners for their documented domains. There is no process-global lock or
26/// new Send/Sync assertion on ordinary [`Allocator`]. Performance is unmeasured.
27/// Callbacks must never reenter this owner or create cross-owner lock cycles.
28/// Vulkan completion and mapped-byte synchronization remain caller obligations.
29/// Drop frees CPU metadata only; explicitly close native storage before Drop.
30/// A scoped operation cannot return its allocator borrow:
31/// ```compile_fail
32/// # fn escape(shared: &vk::SynchronizedAllocator<'_>) {
33/// let escaped = unsafe { shared.with_allocator(|allocator| allocator) };
34/// # }
35/// ```
36/// An existing ordinary allocator's device borrow may still be shortened:
37/// ```
38/// fn shorten<'device: 'scope, 'scope>(owner: vk::Allocator<'device>) -> vk::Allocator<'scope> {
39///     owner
40/// }
41/// ```
42/// The ordinary allocator remains non-Send:
43/// ```compile_fail
44/// fn require_send<T: Send>() {}
45/// require_send::<vk::Allocator<'static>>();
46/// ```
47///
48/// ```no_run
49/// # fn example(allocator: vk::Allocator<'_>) -> Result<(), vk::AllocError> {
50/// // SAFETY: retained chains/callbacks and mapped storage satisfy new's
51/// // any-thread contract through final host cleanup.
52/// let shared = unsafe { vk::SynchronizedAllocator::new(allocator) };
53/// std::thread::scope(|scope| {
54///     scope.spawn(|| {
55///         // SAFETY: no replacement/thread-affine state is installed; no
56///         // native work is live and ordinary close preconditions hold.
57///         unsafe { shared.with_allocator(|allocator| allocator.close()) }??;
58///         Ok::<_, vk::AllocError>(())
59///     }).join().unwrap()
60/// })?;
61/// # Ok(())
62/// # }
63/// ```
64pub struct SynchronizedAllocator<'device> {
65	pub(in crate::mem) inner: Mutex<TransferableAllocator<'device>>,
66	idle: Condvar,
67}
68
69pub(in crate::mem) struct TransferableAllocator<'device> {
70	pub(in crate::mem) allocator: Allocator<'device>,
71	pub(in crate::mem) poisoned: bool,
72	pub(in crate::mem) active: usize,
73}
74
75// SAFETY: this wrapper is created only by unsafe new, which establishes
76// any-thread validity of every retained native/callback/chain/map reference.
77// CPU containers own their initialized storage; raw mapping pointers are opaque
78// addresses, not Rust references or permission to dereference. Device is Sync.
79// Moving changes no native identity. The wrapper is private and never exposed;
80// the placement mutex covers registries, budgets, native counts and leases;
81// stable backing mutexes cover native mapping/binding transitions. with_allocator's unsafe contract preserves
82// these invariants for replacement/registration and all escaping raw pointers.
83// No Sync is asserted for the underlying allocator. Drop only releases CPU/OS
84// metadata, whose any-thread callback contract is retained by the device borrow.
85unsafe impl Send for TransferableAllocator<'_> {}
86
87impl<'device> SynchronizedAllocator<'device> {
88	/// Adopt one allocator for synchronized access from any host thread.
89	/// No native operation, allocation, mapping or thread is started.
90	/// # Safety
91	/// Every retained callback record/code/user datum, pool allocation chain and
92	/// nested external reference must remain valid for the device borrow and
93	/// support access on any participating thread, including final CPU cleanup.
94	/// Retained records must be immutable across threads; callback-owned mutable
95	/// state needs its own synchronization when shared with independent owners.
96	/// Existing mapped-byte uses must be synchronized against subsequent operations.
97	/// Native contexts must meet Vulkan's host-thread lifetime/valid-usage contract.
98	/// No callback may unwind or reenter this same allocator.
99	pub unsafe fn new(allocator: Allocator<'device>) -> Self {
100		Self {
101			inner: Mutex::new(TransferableAllocator {
102				allocator,
103				poisoned: false,
104				active: 0,
105			}),
106			idle: Condvar::new(),
107		}
108	}
109
110	/// Adopt one device-retained allocator for synchronized access from any
111	/// host thread. Every retained reference is owned or `'static`, so the
112	/// unsafe contract of [`Self::new`] is satisfied by construction; the
113	/// remaining caller obligations are the documented host-thread,
114	/// non-reentrancy and mapped-byte synchronization rules.
115	pub fn new_owned(allocator: Allocator<'static>) -> Self {
116		// SAFETY: an 'static allocator retains no caller borrow whose validity
117		// for the device borrow must be proven here.
118		unsafe { Self::new(allocator) }
119	}
120
121	pub(in crate::mem) fn lock_owner(
122		&self,
123	) -> Result<MutexGuard<'_, TransferableAllocator<'device>>, AllocError> {
124		let guard = self
125			.inner
126			.lock()
127			.map_err(|_| AllocError::SynchronizationPoisoned)?;
128		if guard.poisoned {
129			return Err(AllocError::SynchronizationPoisoned);
130		}
131		Ok(guard)
132	}
133
134	pub(in crate::mem) fn wait_owner<'owner>(
135		&'owner self,
136		guard: MutexGuard<'owner, TransferableAllocator<'device>>,
137	) -> Result<MutexGuard<'owner, TransferableAllocator<'device>>, AllocError> {
138		let guard = self
139			.idle
140			.wait(guard)
141			.map_err(|_| AllocError::SynchronizationPoisoned)?;
142		if guard.poisoned {
143			return Err(AllocError::SynchronizationPoisoned);
144		}
145		Ok(guard)
146	}
147
148	pub(in crate::mem) fn lock_idle(
149		&self,
150	) -> Result<MutexGuard<'_, TransferableAllocator<'device>>, AllocError> {
151		let mut guard = self.lock_owner()?;
152		while guard.active != 0 {
153			guard = self.wait_owner(guard)?;
154		}
155		Ok(guard)
156	}
157
158	pub(in crate::mem) fn notify_idle(&self) {
159		self.idle.notify_all();
160	}
161
162	/// Run one operation/batch with exclusive access to the canonical allocator.
163	/// The borrow cannot escape the closure. Returned tokens/handles remain raw
164	/// native identities; no lock covers their later GPU or mapped-byte uses.
165	/// # Errors
166	/// Returns SynchronizationPoisoned without running the closure after a panic.
167	/// Operation errors remain the closure's result and do not poison the mutex.
168	/// # Panics
169	/// A closure panic propagates and poisons subsequent access. Reentrant use of
170	/// this same allocator can deadlock and violates the unsafe access contract.
171	/// # Safety
172	/// Preserve new's any-thread contract for every retained registration/state
173	/// change. Do not replace the allocator with thread-affine state. All native
174	/// operation/callback/completion rules apply. Inside any partitioned scope, never move
175	/// out or replace the borrowed child allocator. Synchronize any escaping mapped
176	/// pointer before unmap/free/relocation; the mutex protects metadata only.
177	pub unsafe fn with_allocator<R>(
178		&self,
179		operation: impl FnOnce(&mut Allocator<'device>) -> R,
180	) -> Result<R, AllocError> {
181		let mut guard = self.lock_idle()?;
182		Ok(operation(&mut guard.allocator))
183	}
184
185	/// Attempt one exclusive operation without waiting for another host user.
186	/// `None` means busy; poison is an error and never bypassed.
187	/// # Errors
188	/// Same poison refusal as with_allocator; operation failures stay in R.
189	/// # Safety
190	/// The complete with_allocator contract applies, including any-thread retained
191	/// state, callback non-reentrancy and synchronization of escaping mappings.
192	pub unsafe fn try_with_allocator<R>(
193		&self,
194		operation: impl FnOnce(&mut Allocator<'device>) -> R,
195	) -> Result<Option<R>, AllocError> {
196		let mut guard = match self.inner.try_lock() {
197			Ok(guard) => guard,
198			Err(TryLockError::WouldBlock) => return Ok(None),
199			Err(TryLockError::Poisoned(_)) => return Err(AllocError::SynchronizationPoisoned),
200		};
201		if guard.poisoned {
202			return Err(AllocError::SynchronizationPoisoned);
203		}
204		if guard.active != 0 {
205			return Ok(None);
206		}
207		Ok(Some(operation(&mut guard.allocator)))
208	}
209
210	pub(in crate::mem) fn owner_mut(&mut self) -> &mut Allocator<'device> {
211		&mut self
212			.inner
213			.get_mut()
214			.unwrap_or_else(PoisonError::into_inner)
215			.allocator
216	}
217
218	pub(in crate::mem) fn is_poisoned(&self) -> bool {
219		self.inner.is_poisoned() || self.inner.lock().is_ok_and(|owner| owner.poisoned)
220	}
221
222	/// Run independent custom pools concurrently through borrowed child owners.
223	///
224	/// Children correspond to `pools` in input order, preserve allocation/pool
225	/// identities and use the same canonical methods through `with_allocator`.
226	/// Each child serializes its own pool; different pools can execute native
227	/// operations concurrently, even on the same memory type. A shared reservation
228	/// lock enforces aggregate native-count, heap and within-budget limits, but
229	/// never covers native PFNs. The parent remains locked for the whole scope.
230	/// Its ordinary operations, statistics and relocation resume after restoration.
231	///
232	/// All scope metadata uses the allocator's CPU callbacks. Merge capacity is
233	/// reserved before native effects; scope exit and panic restoration allocate
234	/// no host memory and perform no native cleanup. Existing mappings remain live.
235	/// Budget pressure is sampled at entry and corrected for all local reservations
236	/// through this batch; split long-lived workloads into bounded scopes to refresh.
237	///
238	/// ```no_run
239	/// # fn example(shared: &vk::SynchronizedAllocator<'_>, first: &vk::Pool, second: &vk::Pool) -> Result<(), vk::AllocError> {
240	/// // SAFETY: all retained references/callbacks support concurrent workers;
241	/// // this batch installs no thread-affine state and leaves native cleanup explicit.
242	/// unsafe { shared.with_pools(&[first, second], |children| {
243	///     std::thread::scope(|scope| {
244	///         for (child, pool) in children.iter().zip([first, second]) {
245	///             scope.spawn(move || child.with_allocator(|allocator| {
246	///                 allocator.pool_statistics(pool)
247	///             }).unwrap().unwrap());
248	///         }
249	///     });
250	/// })?; }
251	/// # Ok(())
252	/// # }
253	/// ```
254	/// # Errors
255	/// Rejects foreign/stale/duplicate pools, nested pool scopes, preparation OOM,
256	/// and poisoned access. A child panic poisons the parent even if caught by
257	/// the operation. Cached export-root ownership follows its backing memory across the scope.
258	/// # Safety
259	/// The complete `with_allocator` contract applies to every child. Use only
260	/// its selected pool for allocations; allocator-wide changes are rejected.
261	/// Never replace, move out or retain a child's allocator/context. All borrowed
262	/// workers must finish before returning; do not reenter the locked parent.
263	/// Synchronize shared callbacks/user data across pools. Mapped-byte and GPU
264	/// ownership/completion remain explicit, including after a worker panic.
265	pub unsafe fn with_pools<R>(
266		&self,
267		pools: &[&super::Pool],
268		operation: impl FnOnce(&[Self]) -> R,
269	) -> Result<R, AllocError> {
270		// SAFETY: same any-thread, non-reentrancy and borrowed-worker contract.
271		unsafe { self.with_domains(super::allocator::Selection::Pools(pools), operation) }
272	}
273
274	/// Run independent default memory-type vectors concurrently.
275	///
276	/// Children correspond to distinct native memory-type indices. They own only
277	/// default storage of that type, including separate allocations and cached
278	/// blocks; custom pools remain with the parent. Native type selection cannot
279	/// escape its selected type. Tokens/maps and accounting retain parent identity.
280	///
281	/// ```no_run
282	/// # fn example(shared: &vk::SynchronizedAllocator<'_>) -> Result<(), vk::AllocError> {
283	/// // SAFETY: retained device/callback state supports these joined workers;
284	/// // no replacement, registration or mapped byte access occurs in this batch.
285	/// unsafe { shared.with_memory_types(&[0], |children| {
286	///     std::thread::scope(|scope| {
287	///         scope.spawn(|| children[0].with_allocator(|allocator| {
288	///             allocator.memory_type_properties(0)
289	///         }).unwrap().unwrap()).join().unwrap();
290	///     });
291	/// })?; }
292	/// # Ok(())
293	/// # }
294	/// ```
295	/// # Errors
296	/// Rejects absent/duplicate types, nested scopes, poison and metadata OOM.
297	/// # Safety
298	/// The complete `with_pools` worker/callback/native-lifetime contract applies.
299	/// Use default allocation policies, without a custom pool. Never move out or
300	/// replace child owners, reenter the parent or form cross-child lock cycles.
301	pub unsafe fn with_memory_types<R>(
302		&self,
303		memory_types: &[u32],
304		operation: impl FnOnce(&[Self]) -> R,
305	) -> Result<R, AllocError> {
306		// SAFETY: same borrowed scope and any-thread retained-state contract.
307		unsafe {
308			self.with_domains(
309				super::allocator::Selection::MemoryTypes(memory_types),
310				operation,
311			)
312		}
313	}
314
315	/// Run mapping/cache/binding work concurrently on distinct native blocks.
316	///
317	/// Each seed is a live allocation; seeds must select different backing memory
318	/// objects. A child contains every live region on that backing object, including
319	/// regions not used as seeds. Regions sharing a block share its one mutex/map
320	/// count. Different blocks of one custom/default pool can progress independently.
321	/// Allocation/free, pool changes, trim and relocation are excluded for this
322	/// borrowed phase. Map holds, metadata, cached exports and original block order
323	/// return to the parent, including after panic. No native cleanup occurs on exit.
324	/// A child borrow cannot escape this phase:
325	/// ```compile_fail
326	/// # fn escape(shared: &vk::SynchronizedAllocator<'_>, seed: &vk::Allocation) {
327	/// let child = unsafe { shared.with_blocks(&[seed], |children| &children[0]) };
328	/// # }
329	/// ```
330	/// # Errors
331	/// Rejects stale/foreign seeds, duplicate backings, nested scopes, poison and OOM.
332	/// # Safety
333	/// The complete `with_pools` borrowing/any-thread/native contract applies.
334	/// Use each token only in its matching child. No worker/reference may escape;
335	/// synchronize mapped bytes and external object uses, and avoid lock cycles.
336	pub unsafe fn with_blocks<R>(
337		&self,
338		allocations: &[&super::Allocation],
339		operation: impl FnOnce(&[Self]) -> R,
340	) -> Result<R, AllocError> {
341		// SAFETY: matching live seeds and the same bounded borrowed-worker contract.
342		unsafe { self.with_domains(super::allocator::Selection::Blocks(allocations), operation) }
343	}
344
345	unsafe fn with_domains<R>(
346		&self,
347		selection: super::allocator::Selection<'_>,
348		operation: impl FnOnce(&[Self]) -> R,
349	) -> Result<R, AllocError> {
350		let mut guard = self.lock_idle()?;
351		// SAFETY: the parent lock excludes all ordinary access; caller retains
352		// every worker inside the borrowed scope and preserves any-thread state.
353		let (result, poisoned) =
354			unsafe { super::allocator::with_domains(&mut guard.allocator, selection, operation) }?;
355		if poisoned {
356			guard.poisoned = true;
357			return Err(AllocError::SynchronizationPoisoned);
358		}
359		Ok(result)
360	}
361
362	/// Observe a serialized allocator-owned statistics snapshot.
363	/// # Errors
364	/// Returns SynchronizationPoisoned after a closure panic.
365	pub fn statistics(&self) -> Result<AllocatorStats, AllocError> {
366		let guard = self.lock_idle()?;
367		Ok(guard.allocator.statistics())
368	}
369
370	/// Recover the exclusive allocator without performing native cleanup.
371	/// # Errors
372	/// PoisonError retains the allocator for deliberate inspection/recovery using
373	/// into_inner; do not assume panic-interrupted native transactions completed.
374	// Keep the original native owner in the poison result without a new fallible
375	// allocation during recovery. Boxing here could lose explicit cleanup on OOM.
376	#[allow(clippy::result_large_err)]
377	pub fn into_inner(self) -> Result<Allocator<'device>, PoisonError<Allocator<'device>>> {
378		match self.inner.into_inner() {
379			Ok(owner) if !owner.poisoned => Ok(owner.allocator),
380			Ok(owner) => Err(PoisonError::new(owner.allocator)),
381			Err(error) => Err(PoisonError::new(error.into_inner().allocator)),
382		}
383	}
384}