Skip to main content

vk/mem/
pool.rs

1//! Fixed-type TLSF/linear pools following pinned VMA 3.4.0 `CreatePool`,
2//! `VmaBlockVector::{CreateMinBlocks,AllocatePage,Free}` and `GetPoolStatistics`.
3//! See license/vma/MIT.txt.
4//! Adaptation: checked tokens, fallible metadata, explicit cleanup, and the
5//! shared resource-aware placement, atom isolation and native mapping/accounting.
6
7use super::*;
8use crate::mem::BudgetSnapshot;
9
10/// Identity of one live custom pool. Dropping it never destroys native storage.
11#[derive(Debug)]
12pub struct Pool {
13	pub(super) owner: Identity,
14	pub(super) serial: u64,
15}
16
17impl Pool {
18	pub fn is_destroyed(&self) -> bool {
19		self.serial == 0
20	}
21}
22
23/// Fixed-type ordinary-memory pool; not a Vulkan ABI record.
24///
25/// Uses the selected placement algorithm. Priority requires captured enabled
26/// EXT feature metadata. Use `create_pool_with_chain` for a retained borrowed native chain.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct PoolCreateInfo {
29	pub priority: MemoryPriority,
30	pub memory_type_index: u32,
31	/// TLSF by default; linear supports explicit free-at-once and stack lifetimes.
32	/// Ring and upper-address placement require `max_block_count = 1`.
33	pub algorithm: AllocationAlgorithm,
34	/// Exact native block size in bytes, or zero for VMA's automatic sizing policy.
35	pub block_size: u64,
36	/// Preallocate this many blocks transactionally and retain this floor.
37	pub min_block_count: usize,
38	/// Pooled-block ceiling; zero means unlimited. Automatic pools can additionally
39	/// own dedicated allocations, still subject to native allocation/heap limits.
40	pub max_block_count: usize,
41	/// Zero means no additional constraint; otherwise a power of two.
42	pub min_allocation_alignment: u64,
43}
44
45impl PoolCreateInfo {
46	/// Construct the record with its default state.
47	#[inline]
48	#[must_use]
49	pub fn new() -> Self {
50		Self::default()
51	}
52
53	/// Set [`priority`](Self::priority) and return the updated record.
54	#[inline]
55	#[must_use]
56	pub fn set_priority(mut self, value: MemoryPriority) -> Self {
57		self.priority = value;
58		self
59	}
60
61	/// Set [`memory_type_index`](Self::memory_type_index) and return the updated record.
62	#[inline]
63	#[must_use]
64	pub fn set_memory_type_index(mut self, value: u32) -> Self {
65		self.memory_type_index = value;
66		self
67	}
68
69	/// Set [`algorithm`](Self::algorithm) and return the updated record.
70	#[inline]
71	#[must_use]
72	pub fn set_algorithm(mut self, value: AllocationAlgorithm) -> Self {
73		self.algorithm = value;
74		self
75	}
76
77	/// Set [`block_size`](Self::block_size) and return the updated record.
78	#[inline]
79	#[must_use]
80	pub fn set_block_size(mut self, value: u64) -> Self {
81		self.block_size = value;
82		self
83	}
84
85	/// Set [`min_block_count`](Self::min_block_count) and return the updated record.
86	#[inline]
87	#[must_use]
88	pub fn set_min_block_count(mut self, value: usize) -> Self {
89		self.min_block_count = value;
90		self
91	}
92
93	/// Set [`max_block_count`](Self::max_block_count) and return the updated record.
94	#[inline]
95	#[must_use]
96	pub fn set_max_block_count(mut self, value: usize) -> Self {
97		self.max_block_count = value;
98		self
99	}
100
101	/// Set [`min_allocation_alignment`](Self::min_allocation_alignment) and return the updated record.
102	#[inline]
103	#[must_use]
104	pub fn set_min_allocation_alignment(mut self, value: u64) -> Self {
105		self.min_allocation_alignment = value;
106		self
107	}
108}
109
110/// Local pool statistics. No native budget query or host allocation is performed.
111/// Free spans describe physical holes; alignment/granularity may prevent their use.
112/// Linear holes additionally wait for their stack/ring lifetime to retire.
113/// Counts/bytes include pool-owned dedicated allocations and retained empty blocks.
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
115pub struct PoolStats {
116	pub allocation_count: usize,
117	pub allocation_bytes: u64,
118	pub reserved_bytes: u64,
119	pub block_count: usize,
120	pub block_bytes: u64,
121	pub free_bytes: u64,
122	pub largest_free_region: u64,
123}
124
125impl PoolStats {
126	/// Construct the record with its default state.
127	#[inline]
128	#[must_use]
129	pub fn new() -> Self {
130		Self::default()
131	}
132
133	/// Set [`allocation_count`](Self::allocation_count) and return the updated record.
134	#[inline]
135	#[must_use]
136	pub fn set_allocation_count(mut self, value: usize) -> Self {
137		self.allocation_count = value;
138		self
139	}
140
141	/// Set [`allocation_bytes`](Self::allocation_bytes) and return the updated record.
142	#[inline]
143	#[must_use]
144	pub fn set_allocation_bytes(mut self, value: u64) -> Self {
145		self.allocation_bytes = value;
146		self
147	}
148
149	/// Set [`reserved_bytes`](Self::reserved_bytes) and return the updated record.
150	#[inline]
151	#[must_use]
152	pub fn set_reserved_bytes(mut self, value: u64) -> Self {
153		self.reserved_bytes = value;
154		self
155	}
156
157	/// Set [`block_count`](Self::block_count) and return the updated record.
158	#[inline]
159	#[must_use]
160	pub fn set_block_count(mut self, value: usize) -> Self {
161		self.block_count = value;
162		self
163	}
164
165	/// Set [`block_bytes`](Self::block_bytes) and return the updated record.
166	#[inline]
167	#[must_use]
168	pub fn set_block_bytes(mut self, value: u64) -> Self {
169		self.block_bytes = value;
170		self
171	}
172
173	/// Set [`free_bytes`](Self::free_bytes) and return the updated record.
174	#[inline]
175	#[must_use]
176	pub fn set_free_bytes(mut self, value: u64) -> Self {
177		self.free_bytes = value;
178		self
179	}
180
181	/// Set [`largest_free_region`](Self::largest_free_region) and return the updated record.
182	#[inline]
183	#[must_use]
184	pub fn set_largest_free_region(mut self, value: u64) -> Self {
185		self.largest_free_region = value;
186		self
187	}
188}
189
190pub(super) struct PoolState<'callbacks> {
191	pub allocation_chain: super::super::chain::ChainData,
192	pub name: Option<HostString<'callbacks>>,
193	pub info: PoolCreateInfo,
194	pub preferred_block_size: u64,
195}
196
197// Donor: CalcAllocationParams, AllocateMemoryOfType and the pool's own dedicated
198// list. Fixed pools never dedicate; automatic pools keep that storage in the
199// same identity/type/accounting domain rather than escaping to a default pool.
200pub(super) unsafe fn allocate(
201	allocator: &mut Allocator<'_>,
202	request: &AllocRequest,
203	storage: StorageOptions<'_>,
204	dedication: Dedication,
205	options: &AllocationCreateInfo,
206	class: ResourceClass,
207	avoided: vk::MemoryPropertyFlags,
208) -> Result<u64, AllocError> {
209	let pool = options.pool.expect("selected custom pool");
210	let state = validate(allocator, pool)?;
211	let info = state.info;
212	let preferred_block_size = state.preferred_block_size;
213	if !request.alignment.is_power_of_two() {
214		return Err(AllocError::InvalidRequest("invalid native alignment"));
215	}
216	let request = AllocRequest {
217		memory_type_bits: request.memory_type_bits & (1 << info.memory_type_index),
218		alignment: request.alignment.max(info.min_allocation_alignment),
219		..*request
220	};
221	// Never relax mandatory properties/masks or select outside this fixed type.
222	let selected_type = allocator
223		.selector
224		.find_memory_type_with_preferences(&request, avoided)?;
225	let blocks = BlockOptions {
226		selected_type: Some(selected_type),
227		algorithm: info.algorithm,
228		allow_ring: info.max_block_count == 1,
229		automatic: info.block_size == 0,
230		block_size: preferred_block_size,
231		strategy: options.strategy,
232		class,
233		grow: !options.never_allocate,
234		pool: Some(pool.serial),
235		upper_address: options.upper_address,
236		priority: info.priority,
237		within_budget: options.within_budget,
238		prefer_mapped: super::super::policy::prefers_mapped(options),
239	};
240	if info.block_size != 0 {
241		if dedication == Dedication::Required {
242			return Err(AllocError::UnsupportedCapability(
243				"fixed-size pools cannot dedicate resources",
244			));
245		}
246		// SAFETY: exact pool/type, queried requirements and checked resource page separation.
247		// Pooled publication records this pool identity.
248		return unsafe { allocate_pooled(allocator, &request, avoided, blocks) };
249	}
250	// SAFETY: identical native obligations; every success is attached below.
251	let serial =
252		unsafe { allocate_automatic(allocator, &request, storage, dedication, avoided, blocks) }?;
253	allocator
254		.live
255		.get_mut(&serial)
256		.expect("new pool allocation")
257		.pool = Some(pool.serial);
258	Ok(serial)
259}
260
261// Pinned VMA AllocateMemoryOfType and AllocatePage: dedicated preference,
262// existing blocks first, growth from 1/8 towards the preferred size and at most
263// three halving steps. Rust retries only recoverable native/storage failures;
264// device loss, missing commands and metadata failures keep their original error.
265pub(super) unsafe fn allocate_automatic(
266	allocator: &mut Allocator<'_>,
267	request: &AllocRequest,
268	storage: StorageOptions<'_>,
269	dedication: Dedication,
270	avoided: vk::MemoryPropertyFlags,
271	blocks: BlockOptions,
272) -> Result<u64, AllocError> {
273	if blocks.upper_address && dedication == Dedication::Required {
274		return Err(AllocError::UnsupportedCapability(
275			"upper address cannot use required dedication",
276		));
277	}
278	if dedication == Dedication::Required {
279		if !blocks.grow {
280			return Err(AllocError::OutOfSpace);
281		}
282		// SAFETY: required dedication keeps the exact resource and pool type.
283		return unsafe { allocate_storage(allocator, request, storage, avoided) };
284	}
285	let high_count = allocator.allocation_limit < u32::MAX / 4
286		&& allocator.total_native_count() > allocator.allocation_limit as usize * 3 / 4;
287	let prefer_dedicated = blocks.grow
288		&& !blocks.upper_address
289		&& !high_count
290		&& (dedication == Dedication::Preferred || request.size > blocks.block_size / 2);
291	if prefer_dedicated {
292		// SAFETY: matching resource-specific chain and fixed compatible type.
293		match unsafe { allocate_storage(allocator, request, storage, avoided) } {
294			Ok(serial) => return Ok(serial),
295			Err(error) if retryable(&error) => {}
296			Err(error) => return Err(error),
297		}
298	}
299	// SAFETY: reuse can publish a placement but performs no native block acquisition.
300	let mut last = match unsafe {
301		allocate_pooled(
302			allocator,
303			request,
304			avoided,
305			BlockOptions {
306				grow: false,
307				..blocks
308			},
309		)
310	} {
311		Ok(serial) => return Ok(serial),
312		Err(error) if retryable(&error) => error,
313		Err(error) => return Err(error),
314	};
315	if !blocks.grow {
316		return Err(last);
317	}
318	let index = request.memory_type_bits.trailing_zeros() as usize;
319	let flags = allocator.selector.properties.memory_types[index].property_flags;
320	let atom = if flags.as_raw()
321		& (vk::MemoryPropertyFlagBits::HOST_VISIBLE_BIT | vk::MemoryPropertyFlagBits::HOST_COHERENT_BIT)
322			.as_raw()
323		== vk::MemoryPropertyFlagBits::HOST_VISIBLE_BIT.as_raw()
324	{
325		allocator.selector.atom
326	} else {
327		1
328	};
329	let reserved = request
330		.size
331		.checked_add(atom - 1)
332		.map(|value| value & !(atom - 1))
333		.ok_or(AllocError::InvalidRange)?;
334	let max_existing = allocator
335		.blocks
336		.iter()
337		.filter(|block| {
338			block.pool == blocks.pool
339				&& block.memory_type_index as usize == index
340				&& block.priority == blocks.priority
341				&& block.automatic == blocks.automatic
342		})
343		.map(|block| block.size)
344		.max()
345		.unwrap_or(0);
346	let mut size = blocks.block_size;
347	let mut shift = 0;
348	while shift < 3 && size / 2 > max_existing && size / 4 >= reserved {
349		size /= 2;
350		shift += 1;
351	}
352	// VMA's growth admission uses free budget to choose pooled block sizes.
353	// It does not turn an ordinary dedicated fallback into WITHIN_BUDGET.
354	let snapshot = super::budget::heap_budgets(allocator)?;
355	let heap = allocator.selector.properties.memory_types[index].heap_index as usize;
356	let available = snapshot.heaps[heap]
357		.budget
358		.saturating_sub(snapshot.heaps[heap].usage)
359		.min(
360			snapshot.heaps[heap]
361				.limit
362				.saturating_sub(snapshot.heaps[heap].native_bytes),
363		);
364	while size >= reserved {
365		if !blocks.upper_address && size > available {
366			last = AllocError::OutOfSpace;
367		} else {
368			// SAFETY: compatible fixed pool/type, checked padding and bounded native sizes.
369			match unsafe {
370				allocate_pooled(
371					allocator,
372					request,
373					avoided,
374					BlockOptions {
375						block_size: size,
376						..blocks
377					},
378				)
379			} {
380				Ok(serial) => return Ok(serial),
381				Err(error) if retryable(&error) => last = error,
382				Err(error) => return Err(error),
383			}
384		}
385		if shift == 3 {
386			break;
387		}
388		size /= 2;
389		shift += 1;
390	}
391	if !prefer_dedicated && !blocks.upper_address {
392		// SAFETY: fallback remains owned by this pool and uses its fixed type.
393		return unsafe { allocate_storage(allocator, request, storage, avoided) };
394	}
395	Err(last)
396}
397
398fn retryable(error: &AllocError) -> bool {
399	matches!(
400		error,
401		AllocError::OutOfSpace | AllocError::AllocationLimit | AllocError::AllocationTooLarge { .. }
402	) || matches!(error, AllocError::Call(error) if recoverable(error))
403}
404
405impl Allocator<'_> {
406	/// Create a fixed-type pool with the selected placement algorithm and minimum blocks.
407	/// # Errors
408	/// Rejects invalid policy/types/limits and preserves metadata/native failures.
409	/// # Safety
410	/// The device/physical metadata and native allocation limits must remain valid.
411	pub unsafe fn create_pool(&mut self, info: &PoolCreateInfo) -> Result<Pool, AllocError> {
412		// SAFETY: identical native metadata/lifetime obligations.
413		unsafe { create_pool(self, info) }
414	}
415
416	/// Release an empty pool and invalidate its identity. Live pooled/dedicated regions are rejected.
417	pub fn destroy_pool(&mut self, pool: &mut Pool) -> Result<(), AllocError> {
418		destroy_pool(self, pool)
419	}
420
421	/// Observe checked local pool accounting without native queries.
422	pub fn pool_statistics(&self, pool: &Pool) -> Result<PoolStats, AllocError> {
423		pool_statistics(self, pool)
424	}
425}
426
427pub(super) fn validate_options<'a, 'device>(
428	allocator: &'a Allocator<'device>,
429	pool: &Pool,
430	options: &AllocationCreateInfo,
431) -> Result<&'a PoolState<'device>, AllocError> {
432	let state = validate(allocator, pool)?;
433	if options.upper_address
434		&& (state.info.algorithm != AllocationAlgorithm::Linear || state.info.max_block_count != 1)
435	{
436		return Err(AllocError::UnsupportedCapability(
437			"upper address requires a single-block linear pool",
438		));
439	}
440	Ok(state)
441}
442
443pub(super) fn validate<'a, 'device>(
444	allocator: &'a Allocator<'device>,
445	pool: &Pool,
446) -> Result<&'a PoolState<'device>, AllocError> {
447	allocator.ensure_open()?;
448	if allocator.owner != pool.owner {
449		return Err(AllocError::WrongAllocator);
450	}
451	allocator
452		.pools
453		.get(&pool.serial)
454		.ok_or(AllocError::UnknownPool)
455}
456
457/// Create a fixed-memory-type TLSF pool transactionally.
458///
459/// A zero maximum follows VMA's unlimited convention. Minimum blocks stay
460/// allocated until explicit pool destruction. Above the minimum, one empty
461/// block can be retained to avoid repeated native allocation/free churn.
462/// Zero block size selects VMA's heap-based preferred size, bounded growth and
463/// pool-owned dedicated fallback. Minimum blocks use the full preferred size.
464/// # Errors
465/// Returns checked invalid-policy/type/state/limit errors or the original native
466/// failure. A failed minimum-block acquisition rolls back only this new pool.
467/// # Safety
468/// Context/physical metadata must be valid; enforce any unqueried native limits.
469pub unsafe fn create_pool(
470	allocator: &mut Allocator<'_>,
471	info: &PoolCreateInfo,
472) -> Result<Pool, AllocError> {
473	// SAFETY: ordinary pool has no retained caller chain.
474	unsafe { create_pool_inner(allocator, info, Default::default()) }
475}
476
477/// Create a pool retaining a borrowed native allocation chain for every block.
478/// Import chains are rejected: a single imported payload cannot back repeated growth.
479/// # Errors
480/// Same policy/native errors as `create_pool`, plus conflicting or import chains.
481/// # Safety
482/// The complete chain and nested storage remain valid and immutable until this
483/// pool is destroyed. The borrow lasts for the allocator's device lifetime.
484/// Every future block allocation must meet the chain's native valid usage.
485pub unsafe fn create_pool_with_chain<'device>(
486	allocator: &mut Allocator<'device>,
487	info: &PoolCreateInfo,
488	chain: super::super::AllocationChain<'device>,
489) -> Result<Pool, AllocError> {
490	if chain.data.imports {
491		return Err(AllocError::InvalidRequest(
492			"pool chains cannot import memory",
493		));
494	}
495	// SAFETY: caller retains the complete chain for every future pool acquisition.
496	unsafe { create_pool_inner(allocator, info, chain.data) }
497}
498
499unsafe fn create_pool_inner(
500	allocator: &mut Allocator<'_>,
501	info: &PoolCreateInfo,
502	chain: super::super::chain::ChainData,
503) -> Result<Pool, AllocError> {
504	allocator.ensure_open()?;
505	allocator.ensure_global()?;
506	if info.priority != MemoryPriority::default() && !allocator.memory_priority {
507		return Err(AllocError::UnsupportedCapability(
508			"pool priority requires enabled EXT feature metadata",
509		));
510	}
511	if (info.max_block_count != 0 && info.min_block_count > info.max_block_count)
512		|| (info.min_allocation_alignment != 0 && !info.min_allocation_alignment.is_power_of_two())
513	{
514		return Err(AllocError::InvalidRequest("invalid pool policy"));
515	}
516	if info.memory_type_index >= allocator.selector.properties.memory_type_count {
517		return Err(AllocError::InvalidRequest("absent pool memory type"));
518	}
519	if info.block_size != 0 {
520		allocator.validate_size(info.block_size)?;
521	}
522	let flags =
523		allocator.selector.properties.memory_types[info.memory_type_index as usize].property_flags;
524	let protected = flags.as_raw() & vk::MemoryPropertyFlagBits::PROTECTED_BIT.as_raw() != 0;
525	if protected && !allocator.protected_memory {
526		return Err(AllocError::UnsupportedCapability(
527			"protected pool requires enabled feature metadata",
528		));
529	}
530	super::external::allocation_exports(allocator, info.memory_type_index, chain)?;
531	// Native type/capability exclusions remain shared with all ordinary selection.
532	allocator.selector.find_memory_type(&AllocRequest {
533		size: info.block_size.max(1),
534		alignment: 1,
535		memory_type_bits: 1 << info.memory_type_index,
536		required_flags: if protected {
537			vk::MemoryPropertyFlagBits::PROTECTED_BIT
538		} else {
539			vk::MemoryPropertyFlags::empty()
540		},
541		preferred_flags: vk::MemoryPropertyFlags::empty(),
542	})?;
543	let preferred_block_size = if info.block_size != 0 {
544		info.block_size
545	} else {
546		preferred_block_size(allocator, info.memory_type_index)?
547	};
548	allocator
549		.next_serial
550		.checked_add(1)
551		.ok_or(AllocError::SerialExhausted)?;
552	allocator
553		.pools
554		.try_reserve(1)
555		.map_err(|_| AllocError::HostAllocationFailed)?;
556	let serial = allocator.next_serial;
557	for _ in 0..info.min_block_count {
558		// SAFETY: validated ordinary type, inverse PFN preflighted at allocator creation.
559		if let Err(error) = unsafe {
560			create_block(
561				allocator,
562				serial,
563				info.memory_type_index,
564				preferred_block_size,
565				info,
566				chain,
567			)
568		} {
569			remove_blocks(allocator, serial);
570			return Err(error);
571		}
572	}
573	allocator.next_serial += 1;
574	allocator.pools.insert_reserved(
575		serial,
576		PoolState {
577			allocation_chain: chain,
578			name: None,
579			info: *info,
580			preferred_block_size,
581		},
582	);
583	Ok(Pool {
584		owner: allocator.owner,
585		serial,
586	})
587}
588
589unsafe fn create_block(
590	allocator: &mut Allocator<'_>,
591	serial: u64,
592	memory_type_index: u32,
593	block_size: u64,
594	info: &PoolCreateInfo,
595	chain: super::super::chain::ChainData,
596) -> Result<(), AllocError> {
597	allocator.check_native(block_size, memory_type_index)?;
598	allocator
599		.blocks
600		.try_reserve(1)
601		.map_err(|_| AllocError::HostAllocationFailed)?;
602	let metadata = Metadata::new_native(
603		block_size,
604		info.algorithm,
605		info.max_block_count == 1,
606		allocator.granularity,
607		allocator.host,
608	)?;
609	let export_types = super::external::allocation_exports(allocator, memory_type_index, chain)?;
610	// SAFETY: exact pool type/size, admitted priority/address metadata and preflighted bookkeeping.
611	let handle = unsafe {
612		allocate_native(
613			allocator,
614			block_size,
615			memory_type_index,
616			StorageOptions {
617				chain: &chain,
618				dedicated: None,
619				device_address: allocator.buffer_device_address,
620				priority: info.priority,
621				within_budget: false,
622			},
623		)
624	}?;
625	if handle.is_null() || allocator.contains_memory(handle) {
626		return Err(
627			CallError::InvalidOutput("allocator returned a null or already-live memory handle").into(),
628		);
629	}
630	allocator.blocks.push_reserved(Block {
631		defragmentation_hold: false,
632		automatic: info.block_size == 0,
633		export_types,
634		priority: info.priority,
635		pool: Some(serial),
636		handle,
637		size: block_size,
638		memory_type_index,
639		metadata,
640		mapping: MappingOwner::new_with_hysteresis(allocator.mapping_hysteresis),
641	});
642	allocator.add_native(block_size, memory_type_index, handle);
643	Ok(())
644}
645
646fn remove_block(allocator: &mut Allocator<'_>, index: usize) {
647	let block = &mut allocator.blocks[index];
648	debug_assert!(block.metadata.is_empty());
649	debug_assert_eq!(block.mapping.read().references, 0);
650	// SAFETY: empty unmapped block; any cached mapping has no user pointer holds.
651	unsafe {
652		block
653			.mapping
654			.write()
655			.clear_cache(allocator.device.as_device(), block.handle)
656	}
657	.expect("preflighted cached mapping cleanup");
658	// SAFETY: no regions/maps remain; native inverse PFN is immutable and preflighted.
659	unsafe {
660		allocator
661			.device
662			.as_device()
663			.free_memory(block.handle, allocator.callbacks)
664	}
665	.expect("preflighted pool cleanup");
666	let block = allocator.blocks.remove(index);
667	allocator.remove_native(block.size, block.memory_type_index, block.handle);
668}
669
670fn remove_blocks(allocator: &mut Allocator<'_>, serial: u64) {
671	for index in (0..allocator.blocks.len()).rev() {
672		if allocator.blocks[index].pool == Some(serial) {
673			remove_block(allocator, index);
674		}
675	}
676}
677
678/// Destroy an empty pool, releasing its retained native blocks explicitly.
679/// # Errors
680/// Rejects closed, foreign, stale or live-allocation pools before native effects.
681pub fn destroy_pool(allocator: &mut Allocator<'_>, pool: &mut Pool) -> Result<(), AllocError> {
682	allocator.ensure_allocation()?;
683	validate(allocator, pool)?;
684	if allocator
685		.live
686		.values()
687		.any(|live| live.pool == Some(pool.serial))
688	{
689		return Err(AllocError::LiveAllocations);
690	}
691	remove_blocks(allocator, pool.serial);
692	allocator.pools.remove(&pool.serial);
693	pool.serial = 0;
694	Ok(())
695}
696
697/// Query a pool's local native bytes, placements and free ranges.
698/// # Errors
699/// Rejects closed, foreign and destroyed pool identities.
700pub fn pool_statistics(allocator: &Allocator<'_>, pool: &Pool) -> Result<PoolStats, AllocError> {
701	validate(allocator, pool)?;
702	let mut result = PoolStats::new();
703	for block in allocator
704		.blocks
705		.iter()
706		.filter(|block| block.pool == Some(pool.serial))
707	{
708		let stats = block.metadata.statistics();
709		result.block_count += 1;
710		result.block_bytes += block.size;
711		result.reserved_bytes += stats.allocated_bytes;
712		result.free_bytes += stats.free_bytes;
713		result.largest_free_region = result.largest_free_region.max(stats.largest_free_region);
714	}
715	for live in allocator
716		.live
717		.values()
718		.filter(|live| live.pool == Some(pool.serial))
719	{
720		result.allocation_count += 1;
721		result.allocation_bytes += live.size;
722		if live.placement.is_none() {
723			result.block_count += 1;
724			result.block_bytes += live.size;
725			result.reserved_bytes += live.reserved_size;
726		}
727	}
728	Ok(result)
729}
730
731// Pinned VMA Free: keep one empty spare unless under budget pressure. Partial
732// release can discard the vector's trailing empty spare. Canonical region
733// retirement performs one incremental free-space swap afterward. No driver query here.
734pub(super) fn release_block(allocator: &mut Allocator<'_>, index: usize) {
735	let selected = &allocator.blocks[index];
736	if selected.defragmentation_hold {
737		return;
738	}
739	let domain = BlockDomain::new_from_block(selected);
740	let min = domain
741		.pool
742		.map_or(0, |serial| allocator.pools[&serial].info.min_block_count);
743	let same_domain = |block: &Block| domain.contains(block);
744	let count = allocator
745		.blocks
746		.iter()
747		.filter(|block| same_domain(block))
748		.count();
749	if count <= min {
750		return;
751	}
752	let empty = allocator.blocks[index].metadata.is_empty();
753	let other_empty = allocator
754		.blocks
755		.iter()
756		.enumerate()
757		.rfind(|(other, block)| {
758			*other != index
759				&& !block.defragmentation_hold
760				&& same_domain(block)
761				&& block.metadata.is_empty()
762				&& block.mapping.read().references == 0
763		})
764		.map(|(index, _)| index);
765	if empty {
766		let heap = allocator.selector.properties.memory_types
767			[allocator.blocks[index].memory_type_index as usize]
768			.heap_index as usize;
769		let ceiling =
770			allocator.heap_limits[heap].unwrap_or(allocator.selector.properties.memory_heaps[heap].size);
771		if other_empty.is_some()
772			|| allocator.heap_bytes[heap] >= ceiling
773			|| super::budget::cached_pressure(allocator, heap)
774		{
775			remove_block(allocator, index);
776		}
777	} else if other_empty.is_some() {
778		// Donor Free inspects only the vector's last block after partial free.
779		// Incremental sorting does not promise an entirely sorted vector.
780		let last = allocator
781			.blocks
782			.iter()
783			.rposition(same_domain)
784			.expect("live block vector");
785		let block = &allocator.blocks[last];
786		if !block.defragmentation_hold
787			&& block.metadata.is_empty()
788			&& block.mapping.read().references == 0
789		{
790			remove_block(allocator, last);
791		}
792	}
793}
794
795// Explicit maintenance shares the same native block removal/accounting owner.
796// Reverse vector order preserves pool floors and never caches external budgets.
797pub(super) fn trim_blocks(
798	allocator: &mut Allocator<'_>,
799	snapshot: &mut BudgetSnapshot,
800	all: bool,
801) -> TrimStats {
802	let mut result = TrimStats::new();
803	for index in (0..allocator.blocks.len()).rev() {
804		let block = &allocator.blocks[index];
805		let serial = block.pool;
806		if block.defragmentation_hold
807			|| block.mapping.read().references != 0
808			|| !block.metadata.is_empty()
809		{
810			continue;
811		}
812		let min = serial.map_or(0, |serial| allocator.pools[&serial].info.min_block_count);
813		if allocator
814			.blocks
815			.iter()
816			.filter(|other| {
817				other.pool == serial
818					&& other.memory_type_index == block.memory_type_index
819					&& other.priority == block.priority
820					&& other.automatic == block.automatic
821			})
822			.count()
823			<= min
824		{
825			continue;
826		}
827		let heap = allocator.selector.properties.memory_types[block.memory_type_index as usize]
828			.heap_index as usize;
829		let budget = &mut snapshot.heaps[heap];
830		if !all && budget.usage < budget.budget && budget.native_bytes < budget.limit {
831			continue;
832		}
833		// Totals are bounded by the allocator's already checked native count/bytes.
834		result.block_count += 1;
835		result.block_bytes += block.size;
836		budget.usage = budget.usage.saturating_sub(block.size);
837		budget.native_bytes -= block.size;
838		remove_block(allocator, index);
839	}
840	result
841}
842
843// Extensive contexts retain evacuated blocks for subsequent class packing.
844// Explicit context completion releases their surplus empty storage; Drop only
845// clears the host holds, leaving trim/reuse/close as native cleanup boundaries.
846pub(super) fn finish_defragmentation_blocks(allocator: &mut Allocator<'_>) -> TrimStats {
847	let mut result = TrimStats::new();
848	for index in (0..allocator.blocks.len()).rev() {
849		let block = &mut allocator.blocks[index];
850		if !block.defragmentation_hold {
851			continue;
852		}
853		block.defragmentation_hold = false;
854		if !block.metadata.is_empty() || block.mapping.read().references != 0 {
855			continue;
856		}
857		let pool = block.pool;
858		let minimum = pool.map_or(0, |serial| allocator.pools[&serial].info.min_block_count);
859		if pool.is_some()
860			&& allocator
861				.blocks
862				.iter()
863				.filter(|block| block.pool == pool)
864				.count()
865				<= minimum
866		{
867			continue;
868		}
869		result.block_count += 1;
870		result.block_bytes += allocator.blocks[index].size;
871		remove_block(allocator, index);
872	}
873	result
874}
875
876// Pinned VMA CalcPreferredBlockSize. One owner for custom and default vectors.
877fn preferred_block_size(allocator: &Allocator<'_>, index: u32) -> Result<u64, AllocError> {
878	let heap = allocator.selector.properties.memory_types[index as usize].heap_index as usize;
879	let heap_size = allocator.selector.properties.memory_heaps[heap].size;
880	let size = if heap_size <= 1024 * 1024 * 1024 {
881		heap_size / 8
882	} else {
883		allocator.preferred_large_heap_block_size
884	};
885	size
886		.checked_add(31)
887		.map(|size| size & !31)
888		.filter(|&size| size != 0)
889		.ok_or(AllocError::InvalidProperties("zero automatic block size"))
890}
891
892pub(super) unsafe fn allocate_default(
893	allocator: &mut Allocator<'_>,
894	request: &AllocRequest,
895	storage: StorageOptions<'_>,
896	dedication: Dedication,
897	options: &AllocationCreateInfo,
898	class: ResourceClass,
899	avoided: vk::MemoryPropertyFlags,
900) -> Result<u64, AllocError> {
901	let mut bits = request.memory_type_bits;
902	let mut last = None;
903	loop {
904		let candidate = AllocRequest {
905			memory_type_bits: bits,
906			..*request
907		};
908		let index = match allocator
909			.selector
910			.find_memory_type_with_preferences(&candidate, avoided)
911		{
912			Ok(index) => index,
913			Err(error) => return Err(last.unwrap_or(error)),
914		};
915		bits &= !(1 << index);
916		let request = AllocRequest {
917			memory_type_bits: 1 << index,
918			..*request
919		};
920		let blocks = BlockOptions {
921			selected_type: Some(index),
922			algorithm: AllocationAlgorithm::Tlsf,
923			allow_ring: false,
924			automatic: true,
925			block_size: preferred_block_size(allocator, index)?,
926			strategy: options.strategy,
927			class,
928			grow: !options.never_allocate,
929			pool: None,
930			upper_address: false,
931			priority: options.priority,
932			within_budget: options.within_budget,
933			prefer_mapped: super::super::policy::prefers_mapped(options),
934		};
935		// SAFETY: one compatible type at a time; reuse/growth/fallback have the same native requirements.
936		match unsafe { allocate_automatic(allocator, &request, storage, dedication, avoided, blocks) } {
937			Ok(serial) => return Ok(serial),
938			Err(error) if retryable(&error) => last = Some(error),
939			Err(error) => return Err(error),
940		}
941	}
942}
943
944/// Set preferred block bytes for heaps larger than 1 GiB before pool/storage creation.
945/// Small heaps use one eighth of their heap size; both paths round up to 32 bytes.
946/// Does not alter explicit `Pooled`/`Adaptive` policies or query/reserve a budget.
947/// # Errors
948/// Rejects zero/overflowing sizes, closed state and existing native/pool domains.
949pub fn set_preferred_block_size(
950	allocator: &mut Allocator<'_>,
951	size: u64,
952) -> Result<(), AllocError> {
953	allocator.ensure_global()?;
954	allocator.ensure_open()?;
955	if allocator.native_count != 0 || !allocator.live.is_empty() {
956		return Err(AllocError::LiveAllocations);
957	}
958	if !allocator.pools.is_empty() {
959		return Err(AllocError::LivePools);
960	}
961	let size = size
962		.checked_add(31)
963		.map(|size| size & !31)
964		.filter(|&size| size != 0)
965		.ok_or(AllocError::InvalidRequest(
966			"preferred block size must be nonzero and roundable",
967		))?;
968	allocator.preferred_large_heap_block_size = size;
969	Ok(())
970}
971
972impl Allocator<'_> {
973	/// Configure large-heap automatic block sizing before any pool/storage creation.
974	pub fn set_preferred_block_size(&mut self, size: u64) -> Result<(), AllocError> {
975		set_preferred_block_size(self, size)
976	}
977}
978
979/// Configure native pooled mapping retention before creating storage or pools.
980/// Enabled by default. Separate allocations never cache mappings. This affects
981/// native map/unmap call counts, never allocation-level pointer access rights.
982/// # Errors
983/// Rejects a closed allocator or any existing native/pool domain.
984pub fn set_mapping_hysteresis(
985	allocator: &mut Allocator<'_>,
986	enabled: bool,
987) -> Result<(), AllocError> {
988	allocator.ensure_global()?;
989	allocator.ensure_open()?;
990	if allocator.native_count != 0 || !allocator.live.is_empty() {
991		return Err(AllocError::LiveAllocations);
992	}
993	if !allocator.pools.is_empty() {
994		return Err(AllocError::LivePools);
995	}
996	allocator.mapping_hysteresis = enabled;
997	Ok(())
998}
999
1000impl Allocator<'_> {
1001	/// Configure pooled mapping retention before native/pool acquisition.
1002	pub fn set_mapping_hysteresis(&mut self, enabled: bool) -> Result<(), AllocError> {
1003		set_mapping_hysteresis(self, enabled)
1004	}
1005}