Skip to main content

vk/mem/
defragmentation.rs

1//! Explicit relocation passes derived from pinned VMA 3.4.0
2//! `ComputeDefragmentation_{Fast,Balanced,Full,Extensive}`, `UpdateVectorStatistics`,
3//! `AllocInOtherBlock` and `EndPass` (license/vma/MIT.txt).
4//! Adaptation: exclusive allocator borrow, bounded existing-block reservations,
5//! checked consumer replacement/copy/completion, cooperative stop predicates,
6//! raw/alias-group and counted mapping transfer, and no implicit work in Drop.
7//! Extensive keeps context-owned phase state and empty blocks for class packing.
8use super::*;
9
10#[path = "defragmentation/extensive.rs"]
11mod extensive;
12use extensive::ExtensiveState;
13
14/// Existing-block relocation policy, derived from the pinned VMA algorithms.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum DefragmentationAlgorithm {
17	/// Consolidate sparse blocks into fuller compatible blocks, without same-block moves.
18	Fast,
19	/// Also compact within a block when average allocation/free sizes justify the copy.
20	#[default]
21	Balanced,
22	/// Also try every eligible source at a strictly lower offset within its own block.
23	Full,
24	/// Evacuate blocks, pack compatible resource classes, then compact remaining gaps.
25	/// Requires a persistent `DefragmentationContext`; no native allocation/copy is implicit.
26	Extensive,
27}
28
29/// Bounded relocation planning. Zero limits produce an empty pass.
30#[derive(Clone, Copy, Debug)]
31pub struct DefragmentationInfo<'pool> {
32	pub pool: Option<&'pool Pool>,
33	pub max_bytes: u64,
34	pub max_allocations: usize,
35	pub algorithm: DefragmentationAlgorithm,
36}
37
38impl Default for DefragmentationInfo<'_> {
39	fn default() -> Self {
40		Self {
41			pool: None,
42			max_bytes: u64::MAX,
43			max_allocations: usize::MAX,
44			algorithm: DefragmentationAlgorithm::default(),
45		}
46	}
47}
48
49impl<'pool> DefragmentationInfo<'pool> {
50	/// Construct the record with its default state.
51	#[inline]
52	#[must_use]
53	pub fn new() -> Self {
54		Self::default()
55	}
56
57	/// Set [`pool`](Self::pool) and return the updated record.
58	#[inline]
59	#[must_use]
60	pub fn set_pool(mut self, value: Option<&'pool Pool>) -> Self {
61		self.pool = value;
62		self
63	}
64
65	/// Set [`max_bytes`](Self::max_bytes) and return the updated record.
66	#[inline]
67	#[must_use]
68	pub fn set_max_bytes(mut self, value: u64) -> Self {
69		self.max_bytes = value;
70		self
71	}
72
73	/// Set [`max_allocations`](Self::max_allocations) and return the updated record.
74	#[inline]
75	#[must_use]
76	pub fn set_max_allocations(mut self, value: usize) -> Self {
77		self.max_allocations = value;
78		self
79	}
80
81	/// Set [`algorithm`](Self::algorithm) and return the updated record.
82	#[inline]
83	#[must_use]
84	pub fn set_algorithm(mut self, value: DefragmentationAlgorithm) -> Self {
85		self.algorithm = value;
86		self
87	}
88}
89
90/// Allocator-recorded primary resource, or raw storage, associated with a move.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum DefragmentationResource {
93	Buffer(vk::Buffer),
94	Image(vk::Image),
95	/// Raw storage without an allocator-recorded resource. External bindings and
96	/// all resources using this storage remain the caller's responsibility.
97	Unassociated,
98}
99
100/// Borrow-free binding snapshots. They do not retain resources or mappings.
101/// Mapped pointers describe counted holds owned by the pass's live regions.
102/// Source pointers expire on commit/destroy, destination pointers on ignore/destroy;
103/// copied snapshots extend no lifetime.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub struct DefragmentationMove {
106	pub source: AllocationInfo,
107	pub destination: AllocationInfo,
108	pub resource: DefragmentationResource,
109	/// The whole alias group must be recreated, copied and retired together.
110	/// `resource` identifies only the recorded primary resource, if any.
111	pub can_alias: bool,
112}
113
114impl DefragmentationMove {
115	/// Set [`source`](Self::source) and return the updated record.
116	#[inline]
117	#[must_use]
118	pub fn set_source(mut self, value: AllocationInfo) -> Self {
119		self.source = value;
120		self
121	}
122
123	/// Set [`destination`](Self::destination) and return the updated record.
124	#[inline]
125	#[must_use]
126	pub fn set_destination(mut self, value: AllocationInfo) -> Self {
127		self.destination = value;
128		self
129	}
130
131	/// Set [`resource`](Self::resource) and return the updated record.
132	#[inline]
133	#[must_use]
134	pub fn set_resource(mut self, value: DefragmentationResource) -> Self {
135		self.resource = value;
136		self
137	}
138
139	/// Set [`can_alias`](Self::can_alias) and return the updated record.
140	#[inline]
141	#[must_use]
142	pub fn set_can_alias(mut self, value: bool) -> Self {
143		self.can_alias = value;
144		self
145	}
146}
147
148/// Actual completed work; proposed or ignored moves are not counted as moved bytes.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
150pub struct DefragmentationStats {
151	pub moved_allocations: usize,
152	pub moved_bytes: u64,
153	pub freed_blocks: usize,
154	pub freed_bytes: u64,
155	pub ignored_allocations: usize,
156	pub destroyed_allocations: usize,
157}
158
159impl DefragmentationStats {
160	/// Construct the record with its default state.
161	#[inline]
162	#[must_use]
163	pub fn new() -> Self {
164		Self::default()
165	}
166
167	/// Set [`moved_allocations`](Self::moved_allocations) and return the updated record.
168	#[inline]
169	#[must_use]
170	pub fn set_moved_allocations(mut self, value: usize) -> Self {
171		self.moved_allocations = value;
172		self
173	}
174
175	/// Set [`moved_bytes`](Self::moved_bytes) and return the updated record.
176	#[inline]
177	#[must_use]
178	pub fn set_moved_bytes(mut self, value: u64) -> Self {
179		self.moved_bytes = value;
180		self
181	}
182
183	/// Set [`freed_blocks`](Self::freed_blocks) and return the updated record.
184	#[inline]
185	#[must_use]
186	pub fn set_freed_blocks(mut self, value: usize) -> Self {
187		self.freed_blocks = value;
188		self
189	}
190
191	/// Set [`freed_bytes`](Self::freed_bytes) and return the updated record.
192	#[inline]
193	#[must_use]
194	pub fn set_freed_bytes(mut self, value: u64) -> Self {
195		self.freed_bytes = value;
196		self
197	}
198
199	/// Set [`ignored_allocations`](Self::ignored_allocations) and return the updated record.
200	#[inline]
201	#[must_use]
202	pub fn set_ignored_allocations(mut self, value: usize) -> Self {
203		self.ignored_allocations = value;
204		self
205	}
206
207	/// Set [`destroyed_allocations`](Self::destroyed_allocations) and return the updated record.
208	#[inline]
209	#[must_use]
210	pub fn set_destroyed_allocations(mut self, value: usize) -> Self {
211		self.destroyed_allocations = value;
212		self
213	}
214}
215
216struct Move {
217	source_serial: u64,
218	proposal: DefragmentationMove,
219	destination: Option<Allocation>,
220}
221
222/// Exclusively borrows the allocator while replacement resources are prepared.
223///
224/// Create/bind replacements, copy and complete GPU work, update consumer native
225/// references, destroy each old resource, then commit its move. Ignore/abort needs
226/// replacement resources destroyed first. `finish` requires every move resolved.
227/// Alias groups additionally require all members recreated and retired together.
228/// Source and destination mapping holds stay distinct until resolution. Old
229/// source pointers expire on commit; destination pointers expire on ignore/destroy.
230/// Drop does no native work: omitting explicit resolution can leak reservations.
231pub struct DefragmentationPass<'allocator, 'device> {
232	allocator: &'allocator mut Allocator<'device>,
233	moves: HostVec<'device, Move>,
234	stats: DefragmentationStats,
235	finished: bool,
236	interrupted: bool,
237	context: Option<&'allocator mut ContextState<'device>>,
238}
239
240/// Reserve earlier free storage without creating native resources, growing blocks or copying data.
241///
242/// Pinned, imported/exported, separate, disjoint-plane and linear-pool storage
243/// is excluded. Raw, aliasable and mapped storage retains its placement class
244/// and counted mapping holds. Every affected alias and pointer is caller-owned.
245/// Optional custom-pool identity, native type, priority, alignment and granularity
246/// remain fixed. Source metadata and handles remain unchanged during planning.
247/// # Errors
248/// Returns identity or host metadata errors; failed planning
249/// frees its unbound destinations, preserving source resources and live tokens.
250/// # Safety
251/// Source resources must remain live and ordinary on this device. Caller owns
252/// GPU completion and must explicitly commit or ignore every proposed move.
253/// End old mapped-pointer uses and recreate every alias before committing a move.
254/// All resources bound outside this allocator are part of that same obligation.
255/// Raw/alias placement retains the recorded alignment, not undisclosed stronger
256/// resource alignment; supply the complete constraint at allocation or pin it.
257pub unsafe fn begin_defragmentation<'allocator, 'device>(
258	allocator: &'allocator mut Allocator<'device>,
259	info: &DefragmentationInfo,
260) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
261	// SAFETY: caller provides the canonical source/pointer/alias and completion contract.
262	unsafe { begin_defragmentation_with_break(allocator, info, &mut || false) }
263}
264
265/// Plan a pass with a borrowed cooperative stop predicate, following VMA's
266/// `pfnBreakCallback` / `CheckCounters` contract. Returning true stops scheduling;
267/// already proposed moves still need explicit resolution. Use `was_interrupted`
268/// to distinguish a stop from an exhausted search. Polls occur between source
269/// candidates, after host snapshot/preflight, and never during consumer copies.
270/// A panic rolls back unpublished destinations before unwinding (with panic=unwind).
271/// The callback is used only during this call and cannot borrow this allocator.
272/// # Safety
273/// Same resource, alias, mapped-pointer and completion contract as `begin_defragmentation`.
274pub unsafe fn begin_defragmentation_with_break<'allocator, 'device>(
275	allocator: &'allocator mut Allocator<'device>,
276	info: &DefragmentationInfo,
277	should_break: &mut dyn FnMut() -> bool,
278) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
279	allocator.ensure_global()?;
280	if info.algorithm == DefragmentationAlgorithm::Extensive {
281		return Err(AllocError::UnsupportedCapability(
282			"Extensive relocation requires DefragmentationContext",
283		));
284	}
285	// Capture fallible scheduling metadata before any destination reservation.
286	let mut state = PlanningState::new(allocator, info)?;
287	// SAFETY: identical caller-owned source/move completion contract; no persistent exclusions.
288	unsafe { plan(allocator, info, &[], &mut state, should_break) }
289}
290
291// A vector in VMA is fixed to one memory type/pool. Explicit Rust block-size
292// policies additionally have distinct reuse domains; never merge them here.
293#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
294struct Domain {
295	memory_type_index: u32,
296	pool: Option<u64>,
297	automatic: bool,
298	priority: u32,
299	block_size: u64,
300}
301
302fn domain(block: &Block) -> Domain {
303	Domain {
304		memory_type_index: block.memory_type_index,
305		pool: block.pool,
306		automatic: block.automatic,
307		priority: block.priority.get().to_bits(),
308		block_size: if block.automatic { 0 } else { block.size },
309	}
310}
311
312#[derive(Clone, Copy)]
313struct OrderedBlock {
314	handle: vk::DeviceMemory,
315	domain: Domain,
316	free_bytes: u64,
317	rank: usize,
318	immovable: bool,
319}
320
321#[derive(Clone, Copy, Default, PartialEq, Eq)]
322struct BalancedState {
323	average_allocation: u64,
324	average_free: u64,
325}
326
327#[derive(Clone, Copy)]
328struct DomainState {
329	key: Domain,
330	balanced: BalancedState,
331	extensive: ExtensiveState,
332}
333
334struct PlanningState<'device> {
335	blocks: HostVec<'device, OrderedBlock>,
336	domains: HostVec<'device, DomainState>,
337}
338
339impl<'device> PlanningState<'device> {
340	fn new(allocator: &Allocator<'device>, info: &DefragmentationInfo) -> Result<Self, AllocError> {
341		allocator.ensure_open()?;
342		if let Some(pool) = info.pool
343			&& pool::validate(allocator, pool)?.info.algorithm != AllocationAlgorithm::Tlsf
344		{
345			return Err(AllocError::UnsupportedCapability(
346				"linear pools cannot defragment",
347			));
348		}
349		let mut blocks = HostVec::new(allocator.host);
350		blocks
351			.try_reserve(allocator.blocks.len())
352			.map_err(|_| AllocError::HostAllocationFailed)?;
353		for (rank, block) in allocator.blocks.iter().enumerate() {
354			if info
355				.pool
356				.is_some_and(|pool| block.pool != Some(pool.serial))
357				|| block.export_types.as_raw() != 0
358				|| !matches!(block.metadata, Metadata::Tlsf(_))
359			{
360				continue;
361			}
362			blocks.push_reserved(OrderedBlock {
363				handle: block.handle,
364				domain: domain(block),
365				free_bytes: block.metadata.statistics().free_bytes,
366				rank,
367				immovable: false,
368			});
369		}
370		// Donor SortByFreeSize: fuller compatible blocks are destinations first.
371		// The original rank breaks ties without depending on native handle values.
372		blocks.sort_unstable_by_key(|block| (block.domain, block.free_bytes, block.rank));
373		for (rank, block) in blocks.iter_mut().enumerate() {
374			block.rank = rank;
375		}
376		let mut domains: HostVec<'device, DomainState> = HostVec::new(allocator.host);
377		domains
378			.try_reserve(blocks.len())
379			.map_err(|_| AllocError::HostAllocationFailed)?;
380		for block in &blocks {
381			if domains.last().is_none_or(|state| state.key != block.domain) {
382				domains.push_reserved(DomainState {
383					key: block.domain,
384					balanced: vector_statistics(allocator, block.domain),
385					extensive: ExtensiveState::default(),
386				});
387			}
388		}
389		Ok(Self { blocks, domains })
390	}
391}
392
393fn vector_statistics(allocator: &Allocator<'_>, key: Domain) -> BalancedState {
394	// Aggregate in u128: valid per-block extents must not wrap a vector's totals.
395	let (mut allocated, mut free, mut allocations, mut regions) = (0_u128, 0_u128, 0_u128, 0_u128);
396	for block in &allocator.blocks {
397		if domain(block) == key {
398			let stats = block.metadata.statistics();
399			allocated += u128::from(stats.allocated_bytes);
400			free += u128::from(stats.free_bytes);
401			allocations += stats.allocation_count as u128;
402			regions += stats.free_region_count as u128;
403		}
404	}
405	BalancedState {
406		average_allocation: allocated.checked_div(allocations).unwrap_or(0) as u64,
407		average_free: free.checked_div(regions).unwrap_or(0) as u64,
408	}
409}
410
411fn worthwhile_same_block(
412	allocator: &Allocator<'_>,
413	source: &Allocation,
414	averages: BalancedState,
415) -> bool {
416	let block = allocator
417		.blocks
418		.iter()
419		.find(|block| block.handle == source.handle)
420		.expect("live source block");
421	let mut previous = 0;
422	let mut next = 0;
423	block.metadata.for_each_free(|offset, size| {
424		if offset + size == source.offset {
425			previous = size;
426		}
427		if offset == source.offset + source.reserved_size {
428			next = size;
429		}
430	});
431	previous >= averages.average_free / 2
432		|| next >= averages.average_free / 2
433		|| source.size <= averages.average_free
434		|| source.size <= averages.average_allocation
435}
436
437// All bounds/atom/guard/alignment/class extents were checked on source creation.
438// Reserve exactly that extent in one explicit compatible block. A same-block
439// miss is undone directly before publication, mapping events or debug effects.
440fn reserve_destination(
441	allocator: &mut Allocator<'_>,
442	source: &Allocation,
443	target: vk::DeviceMemory,
444) -> Result<Option<Allocation>, AllocError> {
445	let live = allocator.validate(source)?;
446	let alignment = live.alignment;
447	let class = live.class;
448	let pool = live.pool;
449	let can_alias = live.can_alias;
450	let guard_size = live.guard_size;
451	let guard_offset = live.guard_offset;
452	allocator.reserve()?;
453	let block = allocator
454		.blocks
455		.iter_mut()
456		.find(|block| block.handle == target)
457		.expect("snapshot target block");
458	let (placement, offset) = match block.metadata.allocate_resource(
459		source.reserved_size,
460		alignment,
461		super::super::PlacementStrategy::MinOffset,
462		false,
463		class,
464	) {
465		Ok(placement) => placement,
466		Err(AllocError::OutOfSpace) => return Ok(None),
467		Err(error) => return Err(error),
468	};
469	if target == source.handle && offset >= source.offset {
470		block
471			.metadata
472			.free(placement)
473			.expect("unpublished same-block placement");
474		return Ok(None);
475	}
476	let size = block.size;
477	block.mapping.write().post_alloc();
478	let serial = publish(
479		allocator,
480		Region {
481			handle: target,
482			index: source.memory_type_index,
483			size: source.size,
484			reserved_size: source.reserved_size,
485			offset,
486			alignment,
487			block_size: size,
488			placement: Some(placement),
489			pool: None,
490			class: ResourceClass::Unknown,
491		},
492	);
493	let live = allocator.live.get_mut(&serial).expect("new destination");
494	live.pool = pool;
495	live.class = class;
496	live.can_alias = can_alias;
497	live.guard_size = guard_size;
498	live.guard_offset = guard_offset;
499	Ok(Some(allocator.published(serial)))
500}
501
502#[derive(Clone, Copy)]
503struct Source {
504	rank: usize,
505	offset: u64,
506	serial: u64,
507}
508
509struct PassCounters<'callback> {
510	remaining: u64,
511	oversized: u8,
512	should_break: &'callback mut dyn FnMut() -> bool,
513	interrupted: bool,
514	panic: Option<Box<dyn std::any::Any + Send>>,
515}
516
517// Donor CheckCounters bounds consecutive oversized-source scans per pass.
518const MAX_OVERSIZED_ALLOCATIONS: u8 = 16;
519
520struct Scan<'a> {
521	blocks: &'a [OrderedBlock],
522	sources: &'a [Source],
523	max_allocations: usize,
524}
525
526#[derive(Clone, Copy)]
527enum Within {
528	Never,
529	Full,
530	Balanced(BalancedState),
531}
532
533#[derive(Clone, Copy)]
534enum ResourceGroup {
535	Textures,
536	Buffers,
537	All,
538}
539
540impl ResourceGroup {
541	fn admits(self, class: ResourceClass) -> bool {
542		match self {
543			Self::Textures => class == ResourceClass::OptimalImage,
544			Self::Buffers => matches!(class, ResourceClass::Buffer | ResourceClass::LinearImage),
545			Self::All => true,
546		}
547	}
548}
549
550struct Selection {
551	sources: std::ops::Range<usize>,
552	targets: std::ops::Range<usize>,
553	earlier_only: bool,
554	within: Within,
555	group: ResourceGroup,
556}
557
558impl PassCounters<'_> {
559	fn exhausted(&self, max_allocations: usize, moves: usize) -> bool {
560		self.interrupted
561			|| moves == max_allocations
562			|| self.remaining == 0
563			|| self.oversized == MAX_OVERSIZED_ALLOCATIONS
564	}
565
566	fn poll(&mut self) -> bool {
567		match std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut self.should_break)) {
568			Ok(false) => false,
569			Ok(true) => {
570				self.interrupted = true;
571				true
572			}
573			Err(payload) => {
574				self.interrupted = true;
575				self.panic = Some(payload);
576				true
577			}
578		}
579	}
580}
581
582// Destination holds exist before publication, so a failed map or later planner
583// failure never invalidates a source pointer. Same-block moves share one native
584// mapping; different-block moves may fail their single pre-commit map attempt.
585unsafe fn prepare_destination(
586	allocator: &mut Allocator<'_>,
587	source: &Allocation,
588	destination: &Allocation,
589) -> Result<(), AllocError> {
590	// SAFETY: new unbound reservation, without consumer data or live pointer uses.
591	unsafe { guard::initialize(allocator, destination) }?;
592	let live = allocator.validate(source)?;
593	let map_count = live.map_count;
594	let persistent = live.persistent;
595	let holds = map_count
596		.checked_add(u32::from(persistent))
597		.ok_or(AllocError::InvalidRange)?;
598	if holds != 0 {
599		let device = allocator.device.clone();
600		// SAFETY: host-visible source implies the identical native type is visible;
601		// exclusive allocator borrow serializes mapping and no destination uses exist.
602		unsafe {
603			allocator.mapping_mut(destination).acquire_many(
604				device.as_device(),
605				destination.handle,
606				destination.block_size,
607				holds,
608			)
609		}?;
610		let live = allocator
611			.live
612			.get_mut(&destination.serial)
613			.expect("new destination");
614		live.map_count = map_count;
615		live.persistent = persistent;
616	}
617	Ok(())
618}
619
620unsafe fn record_move(
621	allocator: &mut Allocator<'_>,
622	source: &Allocation,
623	mut destination: Allocation,
624	moves: &mut HostVec<'_, Move>,
625	counters: &mut PassCounters,
626) -> Result<(), AllocError> {
627	// SAFETY: new unbound destination; debug setup and duplicated mapping holds only.
628	if let Err(error) = unsafe { prepare_destination(allocator, source, &destination) } {
629		// SAFETY: unpublished reservation, no replacement/copy or user mappings exist.
630		unsafe { rollback_allocation(allocator, &mut destination) }
631			.expect("debug destination rollback");
632		return Err(error);
633	}
634	let live = &allocator.live[&source.serial];
635	let resource = live.resource;
636	let can_alias = live.can_alias;
637	let destination_info =
638		allocation_info(allocator, &destination).expect("new destination identity");
639	let source_info = allocation_info(allocator, source).expect("source unchanged during planning");
640	counters.remaining -= source.size;
641	moves.push_reserved(Move {
642		source_serial: source.serial,
643		destination: Some(destination),
644		proposal: DefragmentationMove {
645			source: source_info,
646			destination: destination_info,
647			resource: match resource {
648				Some(Resource::Buffer(buffer)) => DefragmentationResource::Buffer(buffer),
649				Some(Resource::Image(image)) => DefragmentationResource::Image(image),
650				None => DefragmentationResource::Unassociated,
651			},
652			can_alias,
653		},
654	});
655	Ok(())
656}
657
658unsafe fn plan_selected(
659	allocator: &mut Allocator<'_>,
660	scan: &Scan<'_>,
661	selection: &Selection,
662	moves: &mut HostVec<'_, Move>,
663	counters: &mut PassCounters,
664) -> Result<(), AllocError> {
665	for source in scan
666		.sources
667		.iter()
668		.filter(|source| selection.sources.contains(&source.rank))
669	{
670		if counters.exhausted(scan.max_allocations, moves.len()) {
671			break;
672		}
673		if moves
674			.iter()
675			.any(|movement| movement.source_serial == source.serial)
676		{
677			continue;
678		}
679		if counters.poll() {
680			break;
681		}
682		let token = allocator.token(source.serial)?;
683		if token.size > counters.remaining {
684			counters.oversized += 1;
685			continue;
686		}
687		counters.oversized = 0;
688		if !selection.group.admits(allocator.live[&source.serial].class) {
689			continue;
690		}
691		let mut destination = None;
692		for rank in selection.targets.clone() {
693			let block = &scan.blocks[rank];
694			if block.handle == token.handle
695				|| block.domain != scan.blocks[source.rank].domain
696				|| selection.earlier_only && rank >= source.rank
697			{
698				continue;
699			}
700			if let Some(allocation) = reserve_destination(allocator, &token, block.handle)? {
701				destination = Some(allocation);
702				break;
703			}
704		}
705		if destination.is_none()
706			&& token.offset != 0
707			&& match selection.within {
708				Within::Never => false,
709				Within::Full => true,
710				Within::Balanced(averages) => worthwhile_same_block(allocator, &token, averages),
711			} {
712			destination = reserve_destination(allocator, &token, token.handle)?;
713		}
714		if let Some(destination) = destination {
715			// SAFETY: private new reservation; identical source/move completion contract.
716			unsafe { record_move(allocator, &token, destination, moves, counters) }?;
717		}
718	}
719	Ok(())
720}
721
722unsafe fn plan<'allocator, 'device>(
723	allocator: &'allocator mut Allocator<'device>,
724	info: &DefragmentationInfo,
725	ignored: &[u64],
726	state: &mut PlanningState<'device>,
727	should_break: &mut dyn FnMut() -> bool,
728) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
729	allocator.ensure_open()?;
730	let mut blocks = HostVec::new(allocator.host);
731	blocks
732		.try_reserve(state.blocks.len())
733		.map_err(|_| AllocError::HostAllocationFailed)?;
734	for block in &state.blocks {
735		if allocator
736			.blocks
737			.iter()
738			.any(|live| live.handle == block.handle)
739		{
740			blocks.push_reserved(OrderedBlock {
741				immovable: ignored.iter().any(|serial| {
742					allocator
743						.live
744						.get(serial)
745						.is_some_and(|live| live.handle == block.handle)
746				}),
747				..*block
748			});
749		}
750	}
751	let mut domains = HostVec::new(allocator.host);
752	domains
753		.try_reserve(state.domains.len())
754		.map_err(|_| AllocError::HostAllocationFailed)?;
755	domains.extend_from_slice_reserved(&state.domains);
756	// Donor EndPass moves blocks containing an ignored source to the immovable
757	// prefix. They remain destination storage but no longer supply sources.
758	if info.algorithm == DefragmentationAlgorithm::Extensive && allocator.granularity.0 != Some(1) {
759		extensive::exclude_ignored(&mut blocks, &mut domains);
760	} else {
761		blocks.sort_unstable_by_key(|block| (block.domain, !block.immovable, block.rank));
762	}
763	let mut sources = HostVec::new(allocator.host);
764	sources
765		.try_reserve(allocator.live.len())
766		.map_err(|_| AllocError::HostAllocationFailed)?;
767	for (&serial, live) in &allocator.live {
768		if live.plane.is_some() || live.imported || live.pinned || live.placement.is_none() {
769			continue;
770		}
771		if let Some(rank) = blocks
772			.iter()
773			.position(|block| block.handle == live.handle && !block.immovable)
774		{
775			sources.push_reserved(Source {
776				rank,
777				offset: live.offset,
778				serial,
779			});
780		}
781	}
782	sources.sort_unstable_by(|left, right| {
783		right
784			.rank
785			.cmp(&left.rank)
786			.then(left.offset.cmp(&right.offset))
787			.then(left.serial.cmp(&right.serial))
788	});
789	let mut moves = HostVec::new(allocator.host);
790	moves
791		.try_reserve(sources.len().min(info.max_allocations))
792		.map_err(|_| AllocError::HostAllocationFailed)?;
793	let mut counters = PassCounters {
794		remaining: info.max_bytes,
795		oversized: 0,
796		should_break,
797		interrupted: false,
798		panic: None,
799	};
800	let planned = (|| -> Result<(), AllocError> {
801		for domain in &mut domains {
802			if counters.exhausted(info.max_allocations, moves.len()) {
803				break;
804			}
805			let Some(start) = blocks.iter().position(|block| block.domain == domain.key) else {
806				continue;
807			};
808			let end = blocks[start..]
809				.iter()
810				.position(|block| block.domain != domain.key)
811				.map_or(blocks.len(), |length| start + length);
812			let scan = Scan {
813				blocks: &blocks,
814				sources: &sources,
815				max_allocations: info.max_allocations,
816			};
817			if info.algorithm == DefragmentationAlgorithm::Extensive && allocator.granularity.0 != Some(1)
818			{
819				// SAFETY: same selected sources and explicit alias/pointer/move-resolution contract.
820				unsafe {
821					extensive::plan(
822						allocator,
823						&scan,
824						start..end,
825						&mut domain.extensive,
826						&mut moves,
827						&mut counters,
828					)
829				}?;
830				continue;
831			}
832			let within = match info.algorithm {
833				DefragmentationAlgorithm::Fast => Within::Never,
834				DefragmentationAlgorithm::Balanced => Within::Balanced(domain.balanced),
835				DefragmentationAlgorithm::Full | DefragmentationAlgorithm::Extensive => Within::Full,
836			};
837			let mut selection = Selection {
838				sources: start..end,
839				targets: start..end,
840				earlier_only: true,
841				within,
842				group: ResourceGroup::All,
843			};
844			let before = moves.len();
845			// SAFETY: selected live sources; reservations remain unpublished to the consumer.
846			unsafe { plan_selected(allocator, &scan, &selection, &mut moves, &mut counters) }?;
847			if before == moves.len()
848				&& info.algorithm == DefragmentationAlgorithm::Balanced
849				&& !counters.exhausted(info.max_allocations, moves.len())
850			{
851				let refreshed = vector_statistics(allocator, domain.key);
852				if refreshed != domain.balanced {
853					domain.balanced = refreshed;
854					selection.within = Within::Balanced(refreshed);
855					// SAFETY: no reservations were made by this domain's first scan.
856					unsafe { plan_selected(allocator, &scan, &selection, &mut moves, &mut counters) }?;
857				}
858			}
859		}
860		Ok(())
861	})();
862	if planned.is_err() || counters.panic.is_some() {
863		for movement in &mut moves {
864			// SAFETY: still unbound unpublished destinations; source objects/tokens are unchanged.
865			unsafe {
866				rollback_allocation(
867					allocator,
868					movement.destination.as_mut().expect("unpublished move"),
869				)
870			}
871			.expect("planning rollback");
872		}
873		if let Some(payload) = counters.panic.take() {
874			std::panic::resume_unwind(payload);
875		}
876		planned?;
877	}
878	state.blocks = blocks;
879	state.domains = domains;
880	Ok(DefragmentationPass {
881		allocator,
882		moves,
883		stats: DefragmentationStats::new(),
884		finished: false,
885		interrupted: counters.interrupted,
886		context: None,
887	})
888}
889
890impl DefragmentationPass<'_, '_> {
891	/// Proposed move count, including moves already explicitly resolved.
892	pub fn len(&self) -> usize {
893		self.moves.len()
894	}
895
896	pub fn is_empty(&self) -> bool {
897		self.moves.is_empty()
898	}
899
900	/// The stop predicate returned true. An interrupted empty pass does not mean
901	/// compaction is complete; a later context pass can resume scheduling.
902	pub fn was_interrupted(&self) -> bool {
903		self.interrupted
904	}
905
906	/// Borrow a proposal's stable original snapshots; no native ownership is acquired.
907	pub fn movement(&self, index: usize) -> Result<&DefragmentationMove, AllocError> {
908		Ok(
909			&self
910				.moves
911				.get(index)
912				.ok_or(AllocError::InvalidRange)?
913				.proposal,
914		)
915	}
916
917	/// Access the same borrowed device for explicit replacement/copy/completion operations.
918	pub fn device(&self) -> &vk::Device {
919		self.allocator.device.as_device()
920	}
921
922	/// Retained native callbacks for explicit old/replacement resource destruction.
923	pub fn allocation_callbacks(&self) -> Option<&vk::AllocationCallbacks> {
924		self.allocator.callbacks
925	}
926
927	/// Bind a caller-created replacement buffer to this move's reserved destination.
928	/// # Safety
929	/// Replacement must satisfy the source requirements and native valid usage;
930	/// no copy/completion/destruction is performed here.
931	pub unsafe fn bind_buffer(&mut self, index: usize, buffer: vk::Buffer) -> Result<(), AllocError> {
932		if self.finished {
933			return Err(AllocError::Closed);
934		}
935		let movement = self.moves.get(index).ok_or(AllocError::InvalidRange)?;
936		if !matches!(
937			movement.proposal.resource,
938			DefragmentationResource::Buffer(_) | DefragmentationResource::Unassociated
939		) {
940			return Err(AllocError::WrongResource);
941		}
942		let destination = movement
943			.destination
944			.as_ref()
945			.ok_or(AllocError::UnknownAllocation)?;
946		// SAFETY: caller supplies an ordinary matching unbound replacement and retains its lifetime.
947		unsafe { bind_buffer_memory(self.allocator, buffer, destination) }
948	}
949
950	/// Bind a caller-created replacement image to the reserved destination.
951	/// # Safety
952	/// Caller supplies matching ordinary image requirements/layout/tiling and valid enabled features.
953	pub unsafe fn bind_image(&mut self, index: usize, image: vk::Image) -> Result<(), AllocError> {
954		if self.finished {
955			return Err(AllocError::Closed);
956		}
957		let movement = self.moves.get(index).ok_or(AllocError::InvalidRange)?;
958		if !matches!(
959			movement.proposal.resource,
960			DefragmentationResource::Image(_) | DefragmentationResource::Unassociated
961		) {
962			return Err(AllocError::WrongResource);
963		}
964		let destination = movement
965			.destination
966			.as_ref()
967			.ok_or(AllocError::UnknownAllocation)?;
968		// SAFETY: caller supplies a matching ordinary unbound replacement and owns native completion.
969		unsafe { bind_image_memory(self.allocator, image, destination) }
970	}
971
972	/// Recreate one buffer alias on this move's destination, without allocating memory.
973	/// Binding/requirements checks and native failure cleanup use the ordinary alias owner.
974	/// The returned resource is caller-owned and does not replace a recorded primary.
975	/// # Safety
976	/// Same native creation/alias/barrier contract as `mem::create_aliasing_buffer`.
977	/// Recreate every group member at its retained local offset; complete copies,
978	/// update references and destroy all old aliases before committing the move.
979	pub unsafe fn create_aliasing_buffer(
980		&mut self,
981		index: usize,
982		local_offset: u64,
983		info: &vk::BufferCreateInfo,
984	) -> Result<vk::Buffer, AllocError> {
985		let destination = self.destination(index)?;
986		let token = self.allocator.token(destination.serial)?;
987		// SAFETY: caller supplies the canonical alias creation/lifetime contract.
988		unsafe { create_aliasing_buffer(self.allocator, &token, local_offset, info) }
989	}
990
991	/// Recreate one image alias on the reserved destination, preserving ALIAS_BIT
992	/// and the ordinary alias owner's checked requirements/offset/type admission.
993	/// # Safety
994	/// Same group completion/reference/destruction contract as `create_aliasing_buffer`,
995	/// plus `mem::create_aliasing_image` native image/layout requirements.
996	pub unsafe fn create_aliasing_image(
997		&mut self,
998		index: usize,
999		local_offset: u64,
1000		info: &vk::ImageCreateInfo,
1001	) -> Result<vk::Image, AllocError> {
1002		let destination = self.destination(index)?;
1003		let token = self.allocator.token(destination.serial)?;
1004		// SAFETY: caller supplies the canonical image alias/lifetime contract.
1005		unsafe { create_aliasing_image(self.allocator, &token, local_offset, info) }
1006	}
1007
1008	fn destination(&self, index: usize) -> Result<&Allocation, AllocError> {
1009		if self.finished {
1010			return Err(AllocError::Closed);
1011		}
1012		self
1013			.moves
1014			.get(index)
1015			.ok_or(AllocError::InvalidRange)?
1016			.destination
1017			.as_ref()
1018			.ok_or(AllocError::UnknownAllocation)
1019	}
1020
1021	/// Replace the caller's source token after completed replacement and old-resource destruction.
1022	///
1023	/// Keeps copied diagnostic name, opaque user data and pinning metadata. Publishes
1024	/// the already bound destination and frees the old placement; no resource is
1025	/// created/destroyed and no GPU work is submitted or waited on.
1026	/// # Errors
1027	/// Rejects wrong/stale source, unresolved primary destination binding, or a
1028	/// mapping/guard/native failure. Raw storage may commit without a recorded binding.
1029	/// # Safety
1030	/// Copy/initialization must be complete. Destroy the old source resource first;
1031	/// update all dependent descriptors, addresses, views and consumer references
1032	/// before subsequent use. Caller owns the replacement's identity/callback lifetime.
1033	/// For aliasable storage retire the entire old alias group. End all uses of old
1034	/// mapped pointers before this call; counted explicit/persistent holds transfer
1035	/// to the destination. Obtain its new pointer from the destination snapshot or
1036	/// `mapped_pointer` after the context ends. Old pointers never update themselves.
1037	pub unsafe fn commit_move(
1038		&mut self,
1039		index: usize,
1040		source: &mut Allocation,
1041	) -> Result<(), AllocError> {
1042		if self.finished {
1043			return Err(AllocError::Closed);
1044		}
1045		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1046		if source.serial != movement.source_serial {
1047			return Err(AllocError::WrongResource);
1048		}
1049		let old = self.allocator.validate(source)?;
1050		let destination = movement
1051			.destination
1052			.as_ref()
1053			.ok_or(AllocError::UnknownAllocation)?;
1054		if (old.bound || old.resource.is_some()) && !self.allocator.validate(destination)?.bound {
1055			return Err(AllocError::InvalidRequest("replacement is not bound"));
1056		}
1057		let user_data = old.user_data;
1058		let pinned = old.pinned;
1059		let name = self
1060			.allocator
1061			.live
1062			.get_mut(&source.serial)
1063			.expect("validated source")
1064			.name
1065			.take();
1066		let old_serial = source.serial;
1067		let before = statistics(self.allocator);
1068		// SAFETY: caller retired old resources, aliases and pointer uses after exact completion.
1069		if let Err(error) = unsafe { free_region(self.allocator, source, true) } {
1070			self
1071				.allocator
1072				.live
1073				.get_mut(&old_serial)
1074				.expect("retained failed source")
1075				.name = name;
1076			return Err(error);
1077		}
1078		let destination = movement.destination.take().expect("validated destination");
1079		let live = self
1080			.allocator
1081			.live
1082			.get_mut(&destination.serial)
1083			.expect("live destination");
1084		live.name = name;
1085		live.user_data = user_data;
1086		live.pinned = pinned;
1087		*source = destination;
1088		let after = statistics(self.allocator);
1089		self.stats.moved_allocations += 1;
1090		self.stats.moved_bytes += source.size;
1091		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1092		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1093		Ok(())
1094	}
1095
1096	/// Release one unused destination, retaining its original source unchanged.
1097	/// # Safety
1098	/// Destroy every bound replacement/alias and complete all its uses first.
1099	/// End every destination mapped-pointer use; source mapping holds stay intact.
1100	pub unsafe fn ignore_move(&mut self, index: usize) -> Result<(), AllocError> {
1101		if self.finished {
1102			return Err(AllocError::Closed);
1103		}
1104		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1105		let destination = movement
1106			.destination
1107			.as_mut()
1108			.ok_or(AllocError::UnknownAllocation)?;
1109		let before = statistics(self.allocator);
1110		// SAFETY: caller ends all replacement uses and destroys any replacement object.
1111		unsafe { free_region(self.allocator, destination, true) }?;
1112		let after = statistics(self.allocator);
1113		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1114		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1115		movement.destination = None;
1116		if let Some(context) = self.context.as_mut() {
1117			// Capacity for every proposed move was reserved before native planning.
1118			if !context.ignored.contains(&movement.source_serial) {
1119				context.ignored.push_reserved(movement.source_serial);
1120			}
1121		}
1122		self.stats.ignored_allocations += 1;
1123		Ok(())
1124	}
1125
1126	/// Discard a source allocation instead of moving it, releasing both reservations.
1127	/// # Errors
1128	/// Rejects stale/wrong source, resolved moves or guard/native release failures.
1129	/// # Safety
1130	/// Destroy the old source and any bound replacement resource first and complete
1131	/// all their uses. Caller must discard every dependent descriptor/address/view.
1132	/// Destroy every alias and end all source/destination mapped-pointer uses;
1133	/// counted mapping holds are explicitly retired with their storage.
1134	pub unsafe fn destroy_move(
1135		&mut self,
1136		index: usize,
1137		source: &mut Allocation,
1138	) -> Result<(), AllocError> {
1139		if self.finished {
1140			return Err(AllocError::Closed);
1141		}
1142		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1143		if source.serial != movement.source_serial {
1144			return Err(AllocError::WrongResource);
1145		}
1146		self.allocator.validate(source)?;
1147		let destination = movement
1148			.destination
1149			.as_mut()
1150			.ok_or(AllocError::UnknownAllocation)?;
1151		self.allocator.validate(destination)?;
1152		// Diagnose/poison both completed regions before either token is retired.
1153		// A guard or map failure must leave this move resolvable, including all
1154		// counted mapping holds. Successful preflight need not run twice in free.
1155		// SAFETY: caller ended every old/replacement alias, host and GPU use.
1156		unsafe {
1157			guard::before_free(self.allocator, destination)?;
1158			guard::before_free(self.allocator, source)?;
1159		}
1160		for serial in [destination.serial, source.serial] {
1161			self
1162				.allocator
1163				.live
1164				.get_mut(&serial)
1165				.expect("validated move region")
1166				.debug_ready = false;
1167		}
1168		let before = statistics(self.allocator);
1169		// SAFETY: caller destroyed all old/replacement objects after completion; holds are prevalidated.
1170		unsafe { free_region(self.allocator, destination, true) }?;
1171		movement.destination = None;
1172		// SAFETY: completed source and ended pointer uses; cached native inverses are immutable.
1173		unsafe { free_region(self.allocator, source, true) }?;
1174		let after = statistics(self.allocator);
1175		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1176		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1177		self.stats.destroyed_allocations += 1;
1178		Ok(())
1179	}
1180
1181	/// Release every unresolved destination; completed moves remain committed.
1182	/// # Safety
1183	/// Destroy all unresolved replacement resources/aliases and complete their uses
1184	/// first. End every unresolved destination mapped-pointer use before this call.
1185	pub unsafe fn abort(&mut self) -> Result<DefragmentationStats, AllocError> {
1186		for index in 0..self.moves.len() {
1187			if self.moves[index].destination.is_some() {
1188				// SAFETY: caller provides the same per-replacement completion/destruction contract.
1189				unsafe { self.ignore_move(index) }?;
1190			}
1191		}
1192		self.finish()
1193	}
1194
1195	/// Close the host pass after every proposed move has been committed or ignored.
1196	pub fn finish(&mut self) -> Result<DefragmentationStats, AllocError> {
1197		if self.finished {
1198			return Ok(self.stats);
1199		}
1200		if self
1201			.moves
1202			.iter()
1203			.any(|movement| movement.destination.is_some())
1204		{
1205			return Err(AllocError::LiveAllocations);
1206		}
1207		if let Some(context) = self.context.as_mut() {
1208			context.stats = add_statistics(context.stats, self.stats)?;
1209			context.active = false;
1210		}
1211		self.finished = true;
1212		Ok(self.stats)
1213	}
1214}
1215
1216/// Explicitly pin/unpin a region against future allocator relocation planning.
1217pub fn set_allocation_pinned(
1218	allocator: &mut Allocator<'_>,
1219	allocation: &Allocation,
1220	pinned: bool,
1221) -> Result<(), AllocError> {
1222	allocator.validate(allocation)?;
1223	allocator
1224		.live
1225		.get_mut(&allocation.serial)
1226		.expect("validated allocation")
1227		.pinned = pinned;
1228	Ok(())
1229}
1230
1231impl<'device> Allocator<'device> {
1232	/// Begin an explicitly resolved relocation pass over eligible ordinary TLSF allocations.
1233	/// # Safety
1234	/// Same native resource and explicit completion/commit contract as `mem::begin_defragmentation`.
1235	pub unsafe fn begin_defragmentation<'allocator>(
1236		&'allocator mut self,
1237		info: &DefragmentationInfo,
1238	) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
1239		// SAFETY: caller provides canonical live-resource and move-resolution guarantees.
1240		unsafe { begin_defragmentation(self, info) }
1241	}
1242
1243	/// Plan with a borrowed stop predicate; interrupted proposals still need resolution.
1244	/// # Safety
1245	/// Same source/alias/pointer/completion contract as `mem::begin_defragmentation_with_break`.
1246	pub unsafe fn begin_defragmentation_with_break<'allocator>(
1247		&'allocator mut self,
1248		info: &DefragmentationInfo,
1249		should_break: &mut dyn FnMut() -> bool,
1250	) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
1251		// SAFETY: caller provides canonical live-source and explicit resolution guarantees.
1252		unsafe { begin_defragmentation_with_break(self, info, should_break) }
1253	}
1254
1255	/// Prevent or admit an allocation in future explicit relocation passes.
1256	pub fn set_allocation_pinned(
1257		&mut self,
1258		allocation: &Allocation,
1259		pinned: bool,
1260	) -> Result<(), AllocError> {
1261		set_allocation_pinned(self, allocation, pinned)
1262	}
1263}
1264
1265struct ContextState<'device> {
1266	ignored: HostVec<'device, u64>,
1267	stats: DefragmentationStats,
1268	active: bool,
1269}
1270
1271/// Persistent multi-pass relocation state with one exclusive allocator borrow.
1272/// Ignored allocations stay immovable for subsequent passes in this context.
1273/// Each pass must be explicitly resolved and finished; neither context nor pass
1274/// Drop submits, waits or destroys native resources. An abandoned unresolved pass
1275/// leaks its reservations, as documented by `DefragmentationPass`.
1276///
1277/// Initial per-domain order, Balanced averages and Extensive evacuation/packing
1278/// phases persist across passes. Extensive holds its blocks for reuse until
1279/// explicit `finish`; Drop clears host holds but never frees native memory.
1280pub struct DefragmentationContext<'allocator, 'device> {
1281	allocator: &'allocator mut Allocator<'device>,
1282	pool: Option<Pool>,
1283	max_bytes: u64,
1284	max_allocations: usize,
1285	algorithm: DefragmentationAlgorithm,
1286	planning: PlanningState<'device>,
1287	state: ContextState<'device>,
1288	finished: bool,
1289}
1290
1291impl<'allocator, 'device> DefragmentationContext<'allocator, 'device> {
1292	/// Retain one allocator and optional TLSF pool for successive bounded passes.
1293	/// No placement reservation or native effect occurs until `begin_pass`.
1294	/// # Errors
1295	/// Rejects closed/foreign/stale pools, linear-pool relocation and host metadata failure.
1296	pub fn new(
1297		allocator: &'allocator mut Allocator<'device>,
1298		info: &DefragmentationInfo,
1299	) -> Result<Self, AllocError> {
1300		allocator.ensure_global()?;
1301		allocator.ensure_open()?;
1302		let pool = if let Some(pool) = info.pool {
1303			if pool::validate(allocator, pool)?.info.algorithm != AllocationAlgorithm::Tlsf {
1304				return Err(AllocError::UnsupportedCapability(
1305					"linear pools cannot defragment",
1306				));
1307			}
1308			Some(Pool {
1309				owner: allocator.owner,
1310				serial: pool.serial,
1311			})
1312		} else {
1313			None
1314		};
1315		let planning = PlanningState::new(allocator, info)?;
1316		if info.algorithm == DefragmentationAlgorithm::Extensive {
1317			for block in &mut allocator.blocks {
1318				if planning
1319					.blocks
1320					.iter()
1321					.any(|selected| selected.handle == block.handle)
1322				{
1323					block.defragmentation_hold = true;
1324				}
1325			}
1326		}
1327		let state = ContextState {
1328			ignored: HostVec::new(allocator.host),
1329			stats: DefragmentationStats::new(),
1330			active: false,
1331		};
1332		Ok(Self {
1333			allocator,
1334			pool,
1335			max_bytes: info.max_bytes,
1336			max_allocations: info.max_allocations,
1337			algorithm: info.algorithm,
1338			planning,
1339			state,
1340			finished: false,
1341		})
1342	}
1343
1344	/// Reserve another bounded pass, excluding previously ignored source identities.
1345	/// Extensive may move into a later evacuated block to pack compatible classes.
1346	/// `is_empty` means this pass proposed no move under its policy and copy limits.
1347	/// # Errors
1348	/// Preserves policy/host metadata/planning errors without changing source storage.
1349	/// # Safety
1350	/// Same live-source, completed-copy and explicit move-resolution obligations as
1351	/// `mem::begin_defragmentation`. Finish/abort every pass before beginning another.
1352	pub unsafe fn begin_pass(&mut self) -> Result<DefragmentationPass<'_, 'device>, AllocError> {
1353		// SAFETY: identical caller-owned source/alias/pointer and explicit completion contract.
1354		unsafe { self.begin_pass_with_break(&mut || false) }
1355	}
1356
1357	/// Plan the next pass with a stop predicate borrowed only for this call.
1358	/// True preserves already proposed moves for resolution; `was_interrupted`
1359	/// distinguishes a stopped empty pass from completed scheduling. A later pass
1360	/// can use a different predicate; this context retains policy and ignored state.
1361	/// # Safety
1362	/// Same source/alias/mapped-pointer and explicit resolution contract as `begin_pass`.
1363	pub unsafe fn begin_pass_with_break(
1364		&mut self,
1365		should_break: &mut dyn FnMut() -> bool,
1366	) -> Result<DefragmentationPass<'_, 'device>, AllocError> {
1367		if self.finished {
1368			return Err(AllocError::Closed);
1369		}
1370		if self.state.active {
1371			return Err(AllocError::LiveAllocations);
1372		}
1373		self
1374			.state
1375			.ignored
1376			.try_reserve(self.allocator.live.len().min(self.max_allocations))
1377			.map_err(|_| AllocError::HostAllocationFailed)?;
1378		let info = DefragmentationInfo {
1379			pool: self.pool.as_ref(),
1380			max_bytes: self.max_bytes,
1381			max_allocations: self.max_allocations,
1382			algorithm: self.algorithm,
1383		};
1384		// SAFETY: caller supplies the canonical native source/move contract; context excludes persistent ignored identities.
1385		let mut pass = unsafe {
1386			plan(
1387				self.allocator,
1388				&info,
1389				&self.state.ignored,
1390				&mut self.planning,
1391				should_break,
1392			)
1393		}?;
1394		self.state.active = true;
1395		pass.context = Some(&mut self.state);
1396		Ok(pass)
1397	}
1398
1399	/// Totals from explicitly finished passes, never uncommitted proposals.
1400	pub fn statistics(&self) -> DefragmentationStats {
1401		self.state.stats
1402	}
1403
1404	pub fn ignored_allocations(&self) -> usize {
1405		self.state.ignored.len()
1406	}
1407
1408	/// Complete the context after all passes are resolved and finished.
1409	/// Extensive explicitly releases held empty blocks above pool minimums here;
1410	/// freed native block/byte counts are included in the returned total statistics.
1411	pub fn finish(&mut self) -> Result<DefragmentationStats, AllocError> {
1412		if self.state.active {
1413			return Err(AllocError::LiveAllocations);
1414		}
1415		if !self.finished && self.algorithm == DefragmentationAlgorithm::Extensive {
1416			let freed = pool::finish_defragmentation_blocks(self.allocator);
1417			// No native growth is possible through this exclusive context. These
1418			// cumulative frees are bounded by its initial native count/bytes.
1419			self.state.stats.freed_blocks += freed.block_count;
1420			self.state.stats.freed_bytes += freed.block_bytes;
1421		}
1422		self.finished = true;
1423		Ok(self.state.stats)
1424	}
1425}
1426
1427impl Drop for DefragmentationContext<'_, '_> {
1428	fn drop(&mut self) {
1429		if self.algorithm == DefragmentationAlgorithm::Extensive {
1430			for block in &mut self.allocator.blocks {
1431				if self
1432					.planning
1433					.blocks
1434					.iter()
1435					.any(|selected| selected.handle == block.handle)
1436				{
1437					block.defragmentation_hold = false;
1438				}
1439			}
1440		}
1441	}
1442}
1443
1444fn add_statistics(
1445	a: DefragmentationStats,
1446	b: DefragmentationStats,
1447) -> Result<DefragmentationStats, AllocError> {
1448	Ok(DefragmentationStats {
1449		moved_allocations: a
1450			.moved_allocations
1451			.checked_add(b.moved_allocations)
1452			.ok_or(AllocError::InvalidRange)?,
1453		moved_bytes: a
1454			.moved_bytes
1455			.checked_add(b.moved_bytes)
1456			.ok_or(AllocError::InvalidRange)?,
1457		freed_blocks: a
1458			.freed_blocks
1459			.checked_add(b.freed_blocks)
1460			.ok_or(AllocError::InvalidRange)?,
1461		freed_bytes: a
1462			.freed_bytes
1463			.checked_add(b.freed_bytes)
1464			.ok_or(AllocError::InvalidRange)?,
1465		ignored_allocations: a
1466			.ignored_allocations
1467			.checked_add(b.ignored_allocations)
1468			.ok_or(AllocError::InvalidRange)?,
1469		destroyed_allocations: a
1470			.destroyed_allocations
1471			.checked_add(b.destroyed_allocations)
1472			.ok_or(AllocError::InvalidRange)?,
1473	})
1474}