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}