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).
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 = 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	let offset = block
470		.metadata
471		.info(placement)
472		.expect("new destination placement")
473		.offset;
474	if target == source.handle && offset >= source.offset {
475		block
476			.metadata
477			.free(placement)
478			.expect("unpublished same-block placement");
479		return Ok(None);
480	}
481	let size = block.size;
482	let allocation = publish(
483		allocator,
484		Region {
485			handle: target,
486			index: source.memory_type_index,
487			size: source.size,
488			reserved_size: source.reserved_size,
489			offset,
490			alignment,
491			block_size: size,
492			placement: Some(placement),
493		},
494	);
495	let live = allocator
496		.live
497		.get_mut(&allocation.serial)
498		.expect("new destination");
499	live.pool = pool;
500	live.class = class;
501	live.can_alias = can_alias;
502	live.guard_size = guard_size;
503	live.guard_offset = guard_offset;
504	Ok(Some(allocation))
505}
506
507#[derive(Clone, Copy)]
508struct Source {
509	rank: usize,
510	offset: u64,
511	serial: u64,
512}
513
514struct PassCounters<'callback> {
515	remaining: u64,
516	oversized: u8,
517	should_break: &'callback mut dyn FnMut() -> bool,
518	interrupted: bool,
519	panic: Option<Box<dyn std::any::Any + Send>>,
520}
521
522// Donor CheckCounters bounds consecutive oversized-source scans per pass.
523const MAX_OVERSIZED_ALLOCATIONS: u8 = 16;
524
525struct Scan<'a> {
526	blocks: &'a [OrderedBlock],
527	sources: &'a [Source],
528	max_allocations: usize,
529}
530
531#[derive(Clone, Copy)]
532enum Within {
533	Never,
534	Full,
535	Balanced(BalancedState),
536}
537
538#[derive(Clone, Copy)]
539enum ResourceGroup {
540	Textures,
541	Buffers,
542	All,
543}
544
545impl ResourceGroup {
546	fn admits(self, class: ResourceClass) -> bool {
547		match self {
548			Self::Textures => class == ResourceClass::OptimalImage,
549			Self::Buffers => matches!(class, ResourceClass::Buffer | ResourceClass::LinearImage),
550			Self::All => true,
551		}
552	}
553}
554
555struct Selection {
556	sources: std::ops::Range<usize>,
557	targets: std::ops::Range<usize>,
558	earlier_only: bool,
559	within: Within,
560	group: ResourceGroup,
561}
562
563impl PassCounters<'_> {
564	fn exhausted(&self, max_allocations: usize, moves: usize) -> bool {
565		self.interrupted
566			|| moves == max_allocations
567			|| self.remaining == 0
568			|| self.oversized == MAX_OVERSIZED_ALLOCATIONS
569	}
570
571	fn poll(&mut self) -> bool {
572		match std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut self.should_break)) {
573			Ok(false) => false,
574			Ok(true) => {
575				self.interrupted = true;
576				true
577			}
578			Err(payload) => {
579				self.interrupted = true;
580				self.panic = Some(payload);
581				true
582			}
583		}
584	}
585}
586
587// Destination holds exist before publication, so a failed map or later planner
588// failure never invalidates a source pointer. Same-block moves share one native
589// mapping; different-block moves may fail their single pre-commit map attempt.
590unsafe fn prepare_destination(
591	allocator: &mut Allocator<'_>,
592	source: &Allocation,
593	destination: &Allocation,
594) -> Result<(), AllocError> {
595	// SAFETY: new unbound reservation, without consumer data or live pointer uses.
596	unsafe { guard::initialize(allocator, destination) }?;
597	let live = allocator.validate(source)?;
598	let map_count = live.map_count;
599	let persistent = live.persistent;
600	let holds = map_count
601		.checked_add(u32::from(persistent))
602		.ok_or(AllocError::InvalidRange)?;
603	if holds != 0 {
604		let device = allocator.device.clone();
605		// SAFETY: host-visible source implies the identical native type is visible;
606		// exclusive allocator borrow serializes mapping and no destination uses exist.
607		unsafe {
608			allocator.mapping_mut(destination).acquire_many(
609				device.as_device(),
610				destination.handle,
611				destination.block_size,
612				holds,
613			)
614		}?;
615		let live = allocator
616			.live
617			.get_mut(&destination.serial)
618			.expect("new destination");
619		live.map_count = map_count;
620		live.persistent = persistent;
621	}
622	Ok(())
623}
624
625unsafe fn record_move(
626	allocator: &mut Allocator<'_>,
627	source: &Allocation,
628	mut destination: Allocation,
629	moves: &mut HostVec<'_, Move>,
630	counters: &mut PassCounters,
631) -> Result<(), AllocError> {
632	// SAFETY: new unbound destination; debug setup and duplicated mapping holds only.
633	if let Err(error) = unsafe { prepare_destination(allocator, source, &destination) } {
634		// SAFETY: unpublished reservation, no replacement/copy or user mappings exist.
635		unsafe { rollback_allocation(allocator, &mut destination) }
636			.expect("debug destination rollback");
637		return Err(error);
638	}
639	let live = &allocator.live[&source.serial];
640	let resource = live.resource;
641	let can_alias = live.can_alias;
642	let destination_info =
643		allocation_info(allocator, &destination).expect("new destination identity");
644	let source_info = allocation_info(allocator, source).expect("source unchanged during planning");
645	counters.remaining -= source.size;
646	moves.push_reserved(Move {
647		source_serial: source.serial,
648		destination: Some(destination),
649		proposal: DefragmentationMove {
650			source: source_info,
651			destination: destination_info,
652			resource: match resource {
653				Some(Resource::Buffer(buffer)) => DefragmentationResource::Buffer(buffer),
654				Some(Resource::Image(image)) => DefragmentationResource::Image(image),
655				None => DefragmentationResource::Unassociated,
656			},
657			can_alias,
658		},
659	});
660	Ok(())
661}
662
663unsafe fn plan_selected(
664	allocator: &mut Allocator<'_>,
665	scan: &Scan<'_>,
666	selection: &Selection,
667	moves: &mut HostVec<'_, Move>,
668	counters: &mut PassCounters,
669) -> Result<(), AllocError> {
670	for source in scan
671		.sources
672		.iter()
673		.filter(|source| selection.sources.contains(&source.rank))
674	{
675		if counters.exhausted(scan.max_allocations, moves.len()) {
676			break;
677		}
678		if moves
679			.iter()
680			.any(|movement| movement.source_serial == source.serial)
681		{
682			continue;
683		}
684		if counters.poll() {
685			break;
686		}
687		let token = allocator.token(source.serial)?;
688		if token.size > counters.remaining {
689			counters.oversized += 1;
690			continue;
691		}
692		counters.oversized = 0;
693		if !selection.group.admits(allocator.live[&source.serial].class) {
694			continue;
695		}
696		let mut destination = None;
697		for rank in selection.targets.clone() {
698			let block = &scan.blocks[rank];
699			if block.handle == token.handle
700				|| block.domain != scan.blocks[source.rank].domain
701				|| selection.earlier_only && rank >= source.rank
702			{
703				continue;
704			}
705			if let Some(allocation) = reserve_destination(allocator, &token, block.handle)? {
706				destination = Some(allocation);
707				break;
708			}
709		}
710		if destination.is_none()
711			&& token.offset != 0
712			&& match selection.within {
713				Within::Never => false,
714				Within::Full => true,
715				Within::Balanced(averages) => worthwhile_same_block(allocator, &token, averages),
716			} {
717			destination = reserve_destination(allocator, &token, token.handle)?;
718		}
719		if let Some(destination) = destination {
720			// SAFETY: private new reservation; identical source/move completion contract.
721			unsafe { record_move(allocator, &token, destination, moves, counters) }?;
722		}
723	}
724	Ok(())
725}
726
727unsafe fn plan<'allocator, 'device>(
728	allocator: &'allocator mut Allocator<'device>,
729	info: &DefragmentationInfo,
730	ignored: &[u64],
731	state: &mut PlanningState<'device>,
732	should_break: &mut dyn FnMut() -> bool,
733) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
734	allocator.ensure_open()?;
735	let mut blocks = HostVec::new(allocator.host);
736	blocks
737		.try_reserve(state.blocks.len())
738		.map_err(|_| AllocError::HostAllocationFailed)?;
739	for block in &state.blocks {
740		if allocator
741			.blocks
742			.iter()
743			.any(|live| live.handle == block.handle)
744		{
745			blocks.push_reserved(OrderedBlock {
746				immovable: ignored.iter().any(|serial| {
747					allocator
748						.live
749						.get(serial)
750						.is_some_and(|live| live.handle == block.handle)
751				}),
752				..*block
753			});
754		}
755	}
756	let mut domains = HostVec::new(allocator.host);
757	domains
758		.try_reserve(state.domains.len())
759		.map_err(|_| AllocError::HostAllocationFailed)?;
760	domains.extend_from_slice_reserved(&state.domains);
761	// Donor EndPass moves blocks containing an ignored source to the immovable
762	// prefix. They remain destination storage but no longer supply sources.
763	if info.algorithm == DefragmentationAlgorithm::Extensive && allocator.granularity.0 != Some(1) {
764		extensive::exclude_ignored(&mut blocks, &mut domains);
765	} else {
766		blocks.sort_unstable_by_key(|block| (block.domain, !block.immovable, block.rank));
767	}
768	let mut sources = HostVec::new(allocator.host);
769	sources
770		.try_reserve(allocator.live.len())
771		.map_err(|_| AllocError::HostAllocationFailed)?;
772	for (&serial, live) in &allocator.live {
773		if live.plane.is_some() || live.imported || live.pinned || live.placement.is_none() {
774			continue;
775		}
776		if let Some(rank) = blocks
777			.iter()
778			.position(|block| block.handle == live.handle && !block.immovable)
779		{
780			sources.push_reserved(Source {
781				rank,
782				offset: live.offset,
783				serial,
784			});
785		}
786	}
787	sources.sort_unstable_by(|left, right| {
788		right
789			.rank
790			.cmp(&left.rank)
791			.then(left.offset.cmp(&right.offset))
792			.then(left.serial.cmp(&right.serial))
793	});
794	let mut moves = HostVec::new(allocator.host);
795	moves
796		.try_reserve(sources.len().min(info.max_allocations))
797		.map_err(|_| AllocError::HostAllocationFailed)?;
798	let mut counters = PassCounters {
799		remaining: info.max_bytes,
800		oversized: 0,
801		should_break,
802		interrupted: false,
803		panic: None,
804	};
805	let planned = (|| -> Result<(), AllocError> {
806		for domain in &mut domains {
807			if counters.exhausted(info.max_allocations, moves.len()) {
808				break;
809			}
810			let Some(start) = blocks.iter().position(|block| block.domain == domain.key) else {
811				continue;
812			};
813			let end = blocks[start..]
814				.iter()
815				.position(|block| block.domain != domain.key)
816				.map_or(blocks.len(), |length| start + length);
817			let scan = Scan {
818				blocks: &blocks,
819				sources: &sources,
820				max_allocations: info.max_allocations,
821			};
822			if info.algorithm == DefragmentationAlgorithm::Extensive && allocator.granularity.0 != Some(1)
823			{
824				// SAFETY: same selected sources and explicit alias/pointer/move-resolution contract.
825				unsafe {
826					extensive::plan(
827						allocator,
828						&scan,
829						start..end,
830						&mut domain.extensive,
831						&mut moves,
832						&mut counters,
833					)
834				}?;
835				continue;
836			}
837			let within = match info.algorithm {
838				DefragmentationAlgorithm::Fast => Within::Never,
839				DefragmentationAlgorithm::Balanced => Within::Balanced(domain.balanced),
840				DefragmentationAlgorithm::Full | DefragmentationAlgorithm::Extensive => Within::Full,
841			};
842			let mut selection = Selection {
843				sources: start..end,
844				targets: start..end,
845				earlier_only: true,
846				within,
847				group: ResourceGroup::All,
848			};
849			let before = moves.len();
850			// SAFETY: selected live sources; reservations remain unpublished to the consumer.
851			unsafe { plan_selected(allocator, &scan, &selection, &mut moves, &mut counters) }?;
852			if before == moves.len()
853				&& info.algorithm == DefragmentationAlgorithm::Balanced
854				&& !counters.exhausted(info.max_allocations, moves.len())
855			{
856				let refreshed = vector_statistics(allocator, domain.key);
857				if refreshed != domain.balanced {
858					domain.balanced = refreshed;
859					selection.within = Within::Balanced(refreshed);
860					// SAFETY: no reservations were made by this domain's first scan.
861					unsafe { plan_selected(allocator, &scan, &selection, &mut moves, &mut counters) }?;
862				}
863			}
864		}
865		Ok(())
866	})();
867	if planned.is_err() || counters.panic.is_some() {
868		for movement in &mut moves {
869			// SAFETY: still unbound unpublished destinations; source objects/tokens are unchanged.
870			unsafe {
871				rollback_allocation(
872					allocator,
873					movement.destination.as_mut().expect("unpublished move"),
874				)
875			}
876			.expect("planning rollback");
877		}
878		if let Some(payload) = counters.panic.take() {
879			std::panic::resume_unwind(payload);
880		}
881		planned?;
882	}
883	state.blocks = blocks;
884	state.domains = domains;
885	Ok(DefragmentationPass {
886		allocator,
887		moves,
888		stats: DefragmentationStats::new(),
889		finished: false,
890		interrupted: counters.interrupted,
891		context: None,
892	})
893}
894
895impl DefragmentationPass<'_, '_> {
896	/// Proposed move count, including moves already explicitly resolved.
897	pub fn len(&self) -> usize {
898		self.moves.len()
899	}
900
901	pub fn is_empty(&self) -> bool {
902		self.moves.is_empty()
903	}
904
905	/// The stop predicate returned true. An interrupted empty pass does not mean
906	/// compaction is complete; a later context pass can resume scheduling.
907	pub fn was_interrupted(&self) -> bool {
908		self.interrupted
909	}
910
911	/// Borrow a proposal's stable original snapshots; no native ownership is acquired.
912	pub fn movement(&self, index: usize) -> Result<&DefragmentationMove, AllocError> {
913		Ok(
914			&self
915				.moves
916				.get(index)
917				.ok_or(AllocError::InvalidRange)?
918				.proposal,
919		)
920	}
921
922	/// Access the same borrowed device for explicit replacement/copy/completion operations.
923	pub fn device(&self) -> &vk::Device {
924		self.allocator.device.as_device()
925	}
926
927	/// Retained native callbacks for explicit old/replacement resource destruction.
928	pub fn allocation_callbacks(&self) -> Option<&vk::AllocationCallbacks> {
929		self.allocator.callbacks
930	}
931
932	/// Bind a caller-created replacement buffer to this move's reserved destination.
933	/// # Safety
934	/// Replacement must satisfy the source requirements and native valid usage;
935	/// no copy/completion/destruction is performed here.
936	pub unsafe fn bind_buffer(&mut self, index: usize, buffer: vk::Buffer) -> Result<(), AllocError> {
937		if self.finished {
938			return Err(AllocError::Closed);
939		}
940		let movement = self.moves.get(index).ok_or(AllocError::InvalidRange)?;
941		if !matches!(
942			movement.proposal.resource,
943			DefragmentationResource::Buffer(_) | DefragmentationResource::Unassociated
944		) {
945			return Err(AllocError::WrongResource);
946		}
947		let destination = movement
948			.destination
949			.as_ref()
950			.ok_or(AllocError::UnknownAllocation)?;
951		// SAFETY: caller supplies an ordinary matching unbound replacement and retains its lifetime.
952		unsafe { bind_buffer_memory(self.allocator, buffer, destination) }
953	}
954
955	/// Bind a caller-created replacement image to the reserved destination.
956	/// # Safety
957	/// Caller supplies matching ordinary image requirements/layout/tiling and valid enabled features.
958	pub unsafe fn bind_image(&mut self, index: usize, image: vk::Image) -> Result<(), AllocError> {
959		if self.finished {
960			return Err(AllocError::Closed);
961		}
962		let movement = self.moves.get(index).ok_or(AllocError::InvalidRange)?;
963		if !matches!(
964			movement.proposal.resource,
965			DefragmentationResource::Image(_) | DefragmentationResource::Unassociated
966		) {
967			return Err(AllocError::WrongResource);
968		}
969		let destination = movement
970			.destination
971			.as_ref()
972			.ok_or(AllocError::UnknownAllocation)?;
973		// SAFETY: caller supplies a matching ordinary unbound replacement and owns native completion.
974		unsafe { bind_image_memory(self.allocator, image, destination) }
975	}
976
977	/// Recreate one buffer alias on this move's destination, without allocating memory.
978	/// Binding/requirements checks and native failure cleanup use the ordinary alias owner.
979	/// The returned resource is caller-owned and does not replace a recorded primary.
980	/// # Safety
981	/// Same native creation/alias/barrier contract as `mem::create_aliasing_buffer`.
982	/// Recreate every group member at its retained local offset; complete copies,
983	/// update references and destroy all old aliases before committing the move.
984	pub unsafe fn create_aliasing_buffer(
985		&mut self,
986		index: usize,
987		local_offset: u64,
988		info: &vk::BufferCreateInfo,
989	) -> Result<vk::Buffer, AllocError> {
990		let destination = self.destination(index)?;
991		let token = self.allocator.token(destination.serial)?;
992		// SAFETY: caller supplies the canonical alias creation/lifetime contract.
993		unsafe { create_aliasing_buffer(self.allocator, &token, local_offset, info) }
994	}
995
996	/// Recreate one image alias on the reserved destination, preserving ALIAS_BIT
997	/// and the ordinary alias owner's checked requirements/offset/type admission.
998	/// # Safety
999	/// Same group completion/reference/destruction contract as `create_aliasing_buffer`,
1000	/// plus `mem::create_aliasing_image` native image/layout requirements.
1001	pub unsafe fn create_aliasing_image(
1002		&mut self,
1003		index: usize,
1004		local_offset: u64,
1005		info: &vk::ImageCreateInfo,
1006	) -> Result<vk::Image, AllocError> {
1007		let destination = self.destination(index)?;
1008		let token = self.allocator.token(destination.serial)?;
1009		// SAFETY: caller supplies the canonical image alias/lifetime contract.
1010		unsafe { create_aliasing_image(self.allocator, &token, local_offset, info) }
1011	}
1012
1013	fn destination(&self, index: usize) -> Result<&Allocation, AllocError> {
1014		if self.finished {
1015			return Err(AllocError::Closed);
1016		}
1017		self
1018			.moves
1019			.get(index)
1020			.ok_or(AllocError::InvalidRange)?
1021			.destination
1022			.as_ref()
1023			.ok_or(AllocError::UnknownAllocation)
1024	}
1025
1026	/// Replace the caller's source token after completed replacement and old-resource destruction.
1027	///
1028	/// Keeps copied diagnostic name, opaque user data and pinning metadata. Publishes
1029	/// the already bound destination and frees the old placement; no resource is
1030	/// created/destroyed and no GPU work is submitted or waited on.
1031	/// # Errors
1032	/// Rejects wrong/stale source, unresolved primary destination binding, or a
1033	/// mapping/guard/native failure. Raw storage may commit without a recorded binding.
1034	/// # Safety
1035	/// Copy/initialization must be complete. Destroy the old source resource first;
1036	/// update all dependent descriptors, addresses, views and consumer references
1037	/// before subsequent use. Caller owns the replacement's identity/callback lifetime.
1038	/// For aliasable storage retire the entire old alias group. End all uses of old
1039	/// mapped pointers before this call; counted explicit/persistent holds transfer
1040	/// to the destination. Obtain its new pointer from the destination snapshot or
1041	/// `mapped_pointer` after the context ends. Old pointers never update themselves.
1042	pub unsafe fn commit_move(
1043		&mut self,
1044		index: usize,
1045		source: &mut Allocation,
1046	) -> Result<(), AllocError> {
1047		if self.finished {
1048			return Err(AllocError::Closed);
1049		}
1050		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1051		if source.serial != movement.source_serial {
1052			return Err(AllocError::WrongResource);
1053		}
1054		let old = self.allocator.validate(source)?;
1055		let destination = movement
1056			.destination
1057			.as_ref()
1058			.ok_or(AllocError::UnknownAllocation)?;
1059		if (old.bound || old.resource.is_some()) && !self.allocator.validate(destination)?.bound {
1060			return Err(AllocError::InvalidRequest("replacement is not bound"));
1061		}
1062		let user_data = old.user_data;
1063		let pinned = old.pinned;
1064		let name = self
1065			.allocator
1066			.live
1067			.get_mut(&source.serial)
1068			.expect("validated source")
1069			.name
1070			.take();
1071		let old_serial = source.serial;
1072		let before = statistics(self.allocator);
1073		// SAFETY: caller retired old resources, aliases and pointer uses after exact completion.
1074		if let Err(error) = unsafe { free_region(self.allocator, source, true) } {
1075			self
1076				.allocator
1077				.live
1078				.get_mut(&old_serial)
1079				.expect("retained failed source")
1080				.name = name;
1081			return Err(error);
1082		}
1083		let destination = movement.destination.take().expect("validated destination");
1084		let live = self
1085			.allocator
1086			.live
1087			.get_mut(&destination.serial)
1088			.expect("live destination");
1089		live.name = name;
1090		live.user_data = user_data;
1091		live.pinned = pinned;
1092		*source = destination;
1093		let after = statistics(self.allocator);
1094		self.stats.moved_allocations += 1;
1095		self.stats.moved_bytes += source.size;
1096		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1097		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1098		Ok(())
1099	}
1100
1101	/// Release one unused destination, retaining its original source unchanged.
1102	/// # Safety
1103	/// Destroy every bound replacement/alias and complete all its uses first.
1104	/// End every destination mapped-pointer use; source mapping holds stay intact.
1105	pub unsafe fn ignore_move(&mut self, index: usize) -> Result<(), AllocError> {
1106		if self.finished {
1107			return Err(AllocError::Closed);
1108		}
1109		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1110		let destination = movement
1111			.destination
1112			.as_mut()
1113			.ok_or(AllocError::UnknownAllocation)?;
1114		let before = statistics(self.allocator);
1115		// SAFETY: caller ends all replacement uses and destroys any replacement object.
1116		unsafe { free_region(self.allocator, destination, true) }?;
1117		let after = statistics(self.allocator);
1118		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1119		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1120		movement.destination = None;
1121		if let Some(context) = self.context.as_mut() {
1122			// Capacity for every proposed move was reserved before native planning.
1123			if !context.ignored.contains(&movement.source_serial) {
1124				context.ignored.push_reserved(movement.source_serial);
1125			}
1126		}
1127		self.stats.ignored_allocations += 1;
1128		Ok(())
1129	}
1130
1131	/// Discard a source allocation instead of moving it, releasing both reservations.
1132	/// # Errors
1133	/// Rejects stale/wrong source, resolved moves or guard/native release failures.
1134	/// # Safety
1135	/// Destroy the old source and any bound replacement resource first and complete
1136	/// all their uses. Caller must discard every dependent descriptor/address/view.
1137	/// Destroy every alias and end all source/destination mapped-pointer uses;
1138	/// counted mapping holds are explicitly retired with their storage.
1139	pub unsafe fn destroy_move(
1140		&mut self,
1141		index: usize,
1142		source: &mut Allocation,
1143	) -> Result<(), AllocError> {
1144		if self.finished {
1145			return Err(AllocError::Closed);
1146		}
1147		let movement = self.moves.get_mut(index).ok_or(AllocError::InvalidRange)?;
1148		if source.serial != movement.source_serial {
1149			return Err(AllocError::WrongResource);
1150		}
1151		self.allocator.validate(source)?;
1152		let destination = movement
1153			.destination
1154			.as_mut()
1155			.ok_or(AllocError::UnknownAllocation)?;
1156		self.allocator.validate(destination)?;
1157		// Diagnose/poison both completed regions before either token is retired.
1158		// A guard or map failure must leave this move resolvable, including all
1159		// counted mapping holds. Successful preflight need not run twice in free.
1160		// SAFETY: caller ended every old/replacement alias, host and GPU use.
1161		unsafe {
1162			guard::before_free(self.allocator, destination)?;
1163			guard::before_free(self.allocator, source)?;
1164		}
1165		for serial in [destination.serial, source.serial] {
1166			self
1167				.allocator
1168				.live
1169				.get_mut(&serial)
1170				.expect("validated move region")
1171				.debug_ready = false;
1172		}
1173		let before = statistics(self.allocator);
1174		// SAFETY: caller destroyed all old/replacement objects after completion; holds are prevalidated.
1175		unsafe { free_region(self.allocator, destination, true) }?;
1176		movement.destination = None;
1177		// SAFETY: completed source and ended pointer uses; cached native inverses are immutable.
1178		unsafe { free_region(self.allocator, source, true) }?;
1179		let after = statistics(self.allocator);
1180		self.stats.freed_blocks += before.native_allocation_count - after.native_allocation_count;
1181		self.stats.freed_bytes += before.native_bytes - after.native_bytes;
1182		self.stats.destroyed_allocations += 1;
1183		Ok(())
1184	}
1185
1186	/// Release every unresolved destination; completed moves remain committed.
1187	/// # Safety
1188	/// Destroy all unresolved replacement resources/aliases and complete their uses
1189	/// first. End every unresolved destination mapped-pointer use before this call.
1190	pub unsafe fn abort(&mut self) -> Result<DefragmentationStats, AllocError> {
1191		for index in 0..self.moves.len() {
1192			if self.moves[index].destination.is_some() {
1193				// SAFETY: caller provides the same per-replacement completion/destruction contract.
1194				unsafe { self.ignore_move(index) }?;
1195			}
1196		}
1197		self.finish()
1198	}
1199
1200	/// Close the host pass after every proposed move has been committed or ignored.
1201	pub fn finish(&mut self) -> Result<DefragmentationStats, AllocError> {
1202		if self.finished {
1203			return Ok(self.stats);
1204		}
1205		if self
1206			.moves
1207			.iter()
1208			.any(|movement| movement.destination.is_some())
1209		{
1210			return Err(AllocError::LiveAllocations);
1211		}
1212		if let Some(context) = self.context.as_mut() {
1213			context.stats = add_statistics(context.stats, self.stats)?;
1214			context.active = false;
1215		}
1216		self.finished = true;
1217		Ok(self.stats)
1218	}
1219}
1220
1221/// Explicitly pin/unpin a region against future allocator relocation planning.
1222pub fn set_allocation_pinned(
1223	allocator: &mut Allocator<'_>,
1224	allocation: &Allocation,
1225	pinned: bool,
1226) -> Result<(), AllocError> {
1227	allocator.validate(allocation)?;
1228	allocator
1229		.live
1230		.get_mut(&allocation.serial)
1231		.expect("validated allocation")
1232		.pinned = pinned;
1233	Ok(())
1234}
1235
1236impl<'device> Allocator<'device> {
1237	/// Begin an explicitly resolved relocation pass over eligible ordinary TLSF allocations.
1238	/// # Safety
1239	/// Same native resource and explicit completion/commit contract as `mem::begin_defragmentation`.
1240	pub unsafe fn begin_defragmentation<'allocator>(
1241		&'allocator mut self,
1242		info: &DefragmentationInfo,
1243	) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
1244		// SAFETY: caller provides canonical live-resource and move-resolution guarantees.
1245		unsafe { begin_defragmentation(self, info) }
1246	}
1247
1248	/// Plan with a borrowed stop predicate; interrupted proposals still need resolution.
1249	/// # Safety
1250	/// Same source/alias/pointer/completion contract as `mem::begin_defragmentation_with_break`.
1251	pub unsafe fn begin_defragmentation_with_break<'allocator>(
1252		&'allocator mut self,
1253		info: &DefragmentationInfo,
1254		should_break: &mut dyn FnMut() -> bool,
1255	) -> Result<DefragmentationPass<'allocator, 'device>, AllocError> {
1256		// SAFETY: caller provides canonical live-source and explicit resolution guarantees.
1257		unsafe { begin_defragmentation_with_break(self, info, should_break) }
1258	}
1259
1260	/// Prevent or admit an allocation in future explicit relocation passes.
1261	pub fn set_allocation_pinned(
1262		&mut self,
1263		allocation: &Allocation,
1264		pinned: bool,
1265	) -> Result<(), AllocError> {
1266		set_allocation_pinned(self, allocation, pinned)
1267	}
1268}
1269
1270struct ContextState<'device> {
1271	ignored: HostVec<'device, u64>,
1272	stats: DefragmentationStats,
1273	active: bool,
1274}
1275
1276/// Persistent multi-pass relocation state with one exclusive allocator borrow.
1277/// Ignored allocations stay immovable for subsequent passes in this context.
1278/// Each pass must be explicitly resolved and finished; neither context nor pass
1279/// Drop submits, waits or destroys native resources. An abandoned unresolved pass
1280/// leaks its reservations, as documented by `DefragmentationPass`.
1281///
1282/// Initial per-domain order, Balanced averages and Extensive evacuation/packing
1283/// phases persist across passes. Extensive holds its blocks for reuse until
1284/// explicit `finish`; Drop clears host holds but never frees native memory.
1285pub struct DefragmentationContext<'allocator, 'device> {
1286	allocator: &'allocator mut Allocator<'device>,
1287	pool: Option<Pool>,
1288	max_bytes: u64,
1289	max_allocations: usize,
1290	algorithm: DefragmentationAlgorithm,
1291	planning: PlanningState<'device>,
1292	state: ContextState<'device>,
1293	finished: bool,
1294}
1295
1296impl<'allocator, 'device> DefragmentationContext<'allocator, 'device> {
1297	/// Retain one allocator and optional TLSF pool for successive bounded passes.
1298	/// No placement reservation or native effect occurs until `begin_pass`.
1299	/// # Errors
1300	/// Rejects closed/foreign/stale pools, linear-pool relocation and host metadata failure.
1301	pub fn new(
1302		allocator: &'allocator mut Allocator<'device>,
1303		info: &DefragmentationInfo,
1304	) -> Result<Self, AllocError> {
1305		allocator.ensure_global()?;
1306		allocator.ensure_open()?;
1307		let pool = if let Some(pool) = info.pool {
1308			if pool::validate(allocator, pool)?.info.algorithm != AllocationAlgorithm::Tlsf {
1309				return Err(AllocError::UnsupportedCapability(
1310					"linear pools cannot defragment",
1311				));
1312			}
1313			Some(Pool {
1314				owner: allocator.owner,
1315				serial: pool.serial,
1316			})
1317		} else {
1318			None
1319		};
1320		let planning = PlanningState::new(allocator, info)?;
1321		if info.algorithm == DefragmentationAlgorithm::Extensive {
1322			for block in &mut allocator.blocks {
1323				if planning
1324					.blocks
1325					.iter()
1326					.any(|selected| selected.handle == block.handle)
1327				{
1328					block.defragmentation_hold = true;
1329				}
1330			}
1331		}
1332		let state = ContextState {
1333			ignored: HostVec::new(allocator.host),
1334			stats: DefragmentationStats::new(),
1335			active: false,
1336		};
1337		Ok(Self {
1338			allocator,
1339			pool,
1340			max_bytes: info.max_bytes,
1341			max_allocations: info.max_allocations,
1342			algorithm: info.algorithm,
1343			planning,
1344			state,
1345			finished: false,
1346		})
1347	}
1348
1349	/// Reserve another bounded pass, excluding previously ignored source identities.
1350	/// Extensive may move into a later evacuated block to pack compatible classes.
1351	/// `is_empty` means this pass proposed no move under its policy and copy limits.
1352	/// # Errors
1353	/// Preserves policy/host metadata/planning errors without changing source storage.
1354	/// # Safety
1355	/// Same live-source, completed-copy and explicit move-resolution obligations as
1356	/// `mem::begin_defragmentation`. Finish/abort every pass before beginning another.
1357	pub unsafe fn begin_pass(&mut self) -> Result<DefragmentationPass<'_, 'device>, AllocError> {
1358		// SAFETY: identical caller-owned source/alias/pointer and explicit completion contract.
1359		unsafe { self.begin_pass_with_break(&mut || false) }
1360	}
1361
1362	/// Plan the next pass with a stop predicate borrowed only for this call.
1363	/// True preserves already proposed moves for resolution; `was_interrupted`
1364	/// distinguishes a stopped empty pass from completed scheduling. A later pass
1365	/// can use a different predicate; this context retains policy and ignored state.
1366	/// # Safety
1367	/// Same source/alias/mapped-pointer and explicit resolution contract as `begin_pass`.
1368	pub unsafe fn begin_pass_with_break(
1369		&mut self,
1370		should_break: &mut dyn FnMut() -> bool,
1371	) -> Result<DefragmentationPass<'_, 'device>, AllocError> {
1372		if self.finished {
1373			return Err(AllocError::Closed);
1374		}
1375		if self.state.active {
1376			return Err(AllocError::LiveAllocations);
1377		}
1378		self
1379			.state
1380			.ignored
1381			.try_reserve(self.allocator.live.len().min(self.max_allocations))
1382			.map_err(|_| AllocError::HostAllocationFailed)?;
1383		let info = DefragmentationInfo {
1384			pool: self.pool.as_ref(),
1385			max_bytes: self.max_bytes,
1386			max_allocations: self.max_allocations,
1387			algorithm: self.algorithm,
1388		};
1389		// SAFETY: caller supplies the canonical native source/move contract; context excludes persistent ignored identities.
1390		let mut pass = unsafe {
1391			plan(
1392				self.allocator,
1393				&info,
1394				&self.state.ignored,
1395				&mut self.planning,
1396				should_break,
1397			)
1398		}?;
1399		self.state.active = true;
1400		pass.context = Some(&mut self.state);
1401		Ok(pass)
1402	}
1403
1404	/// Totals from explicitly finished passes, never uncommitted proposals.
1405	pub fn statistics(&self) -> DefragmentationStats {
1406		self.state.stats
1407	}
1408
1409	pub fn ignored_allocations(&self) -> usize {
1410		self.state.ignored.len()
1411	}
1412
1413	/// Complete the context after all passes are resolved and finished.
1414	/// Extensive explicitly releases held empty blocks above pool minimums here;
1415	/// freed native block/byte counts are included in the returned total statistics.
1416	pub fn finish(&mut self) -> Result<DefragmentationStats, AllocError> {
1417		if self.state.active {
1418			return Err(AllocError::LiveAllocations);
1419		}
1420		if !self.finished && self.algorithm == DefragmentationAlgorithm::Extensive {
1421			let freed = pool::finish_defragmentation_blocks(self.allocator);
1422			// No native growth is possible through this exclusive context. These
1423			// cumulative frees are bounded by its initial native count/bytes.
1424			self.state.stats.freed_blocks += freed.block_count;
1425			self.state.stats.freed_bytes += freed.block_bytes;
1426		}
1427		self.finished = true;
1428		Ok(self.state.stats)
1429	}
1430}
1431
1432impl Drop for DefragmentationContext<'_, '_> {
1433	fn drop(&mut self) {
1434		if self.algorithm == DefragmentationAlgorithm::Extensive {
1435			for block in &mut self.allocator.blocks {
1436				if self
1437					.planning
1438					.blocks
1439					.iter()
1440					.any(|selected| selected.handle == block.handle)
1441				{
1442					block.defragmentation_hold = false;
1443				}
1444			}
1445		}
1446	}
1447}
1448
1449fn add_statistics(
1450	a: DefragmentationStats,
1451	b: DefragmentationStats,
1452) -> Result<DefragmentationStats, AllocError> {
1453	Ok(DefragmentationStats {
1454		moved_allocations: a
1455			.moved_allocations
1456			.checked_add(b.moved_allocations)
1457			.ok_or(AllocError::InvalidRange)?,
1458		moved_bytes: a
1459			.moved_bytes
1460			.checked_add(b.moved_bytes)
1461			.ok_or(AllocError::InvalidRange)?,
1462		freed_blocks: a
1463			.freed_blocks
1464			.checked_add(b.freed_blocks)
1465			.ok_or(AllocError::InvalidRange)?,
1466		freed_bytes: a
1467			.freed_bytes
1468			.checked_add(b.freed_bytes)
1469			.ok_or(AllocError::InvalidRange)?,
1470		ignored_allocations: a
1471			.ignored_allocations
1472			.checked_add(b.ignored_allocations)
1473			.ok_or(AllocError::InvalidRange)?,
1474		destroyed_allocations: a
1475			.destroyed_allocations
1476			.checked_add(b.destroyed_allocations)
1477			.ok_or(AllocError::InvalidRange)?,
1478	})
1479}