Skip to main content

vk/mem/
budget.rs

1//! Explicit memory-budget snapshots and local per-heap policy limits.
2//! Donor: pinned VMA GetHeapBudgets/SetCurrentFrameIndex budget separation.
3//! Adaptation: queries are explicit; no hot-path driver queries or stale-budget promises.
4//! Empty-block eviction follows `VmaBlockVector::Free`; explicit trim is an
5//! additional Rust maintenance boundary. See license/vma/MIT.txt.
6use super::Allocator;
7use crate::{
8	self as vk,
9	mem::{AllocError, BudgetSnapshot, BudgetSource, HeapBudget},
10};
11
12/// Native cache released by one explicit maintenance call. Live regions never move.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub struct TrimStats {
15	pub block_count: usize,
16	pub block_bytes: u64,
17}
18
19impl TrimStats {
20	/// Construct the record with its default state.
21	#[inline]
22	#[must_use]
23	pub fn new() -> Self {
24		Self::default()
25	}
26
27	/// Set [`block_count`](Self::block_count) and return the updated record.
28	#[inline]
29	#[must_use]
30	pub fn set_block_count(mut self, value: usize) -> Self {
31		self.block_count = value;
32		self
33	}
34
35	/// Set [`block_bytes`](Self::block_bytes) and return the updated record.
36	#[inline]
37	#[must_use]
38	pub fn set_block_bytes(mut self, value: u64) -> Self {
39		self.block_bytes = value;
40		self
41	}
42}
43
44impl Allocator<'_> {
45	/// Set a local native-byte ceiling. Existing allocations are never evicted.
46	pub fn set_heap_limit(&mut self, heap: u32, limit: u64) -> Result<(), AllocError> {
47		set_heap_limit(self, heap, limit)
48	}
49
50	/// Heap-size/local-accounting estimate, without querying the driver.
51	pub fn budget_snapshot(&self) -> BudgetSnapshot {
52		budget_snapshot(self)
53	}
54
55	/// Release empty, unmapped cached blocks above their pool minimums.
56	/// No driver budget query, allocation, synchronization or live-object movement.
57	/// # Errors
58	/// Returns `Closed` after allocator close.
59	pub fn trim(&mut self) -> Result<TrimStats, AllocError> {
60		trim(self)
61	}
62
63	/// Query budget pressure and release eligible empty blocks on pressured heaps.
64	/// Minimum blocks and all live/mapped blocks remain owned by their pools.
65	/// # Errors
66	/// Returns closed/context/capability/native query errors before freeing memory.
67	/// # Safety
68	/// Supply the live instance and physical device used to create this allocator's device.
69	pub unsafe fn trim_with_budget(
70		&mut self,
71		instance: &vk::Instance,
72		physical: vk::PhysicalDevice,
73	) -> Result<TrimStats, AllocError> {
74		// SAFETY: identical native query identity/lifetime contract.
75		unsafe { trim_with_budget(self, instance, physical) }
76	}
77
78	/// Query enabled EXT_memory_budget; otherwise return an explicitly local estimate.
79	/// # Safety
80	/// Instance/physical device must be the live parent used to create this allocator's device.
81	pub unsafe fn query_budget(
82		&self,
83		instance: &vk::Instance,
84		physical: vk::PhysicalDevice,
85	) -> Result<BudgetSnapshot, AllocError> {
86		// SAFETY: identical native identity/lifetime contract.
87		unsafe { query_budget(self, instance, physical) }
88	}
89}
90
91/// Release surplus empty, unmapped cached blocks, preserving each pool minimum.
92///
93/// Free/method forms share this operation. Pools remain live and can grow again.
94/// No native budget query, host allocation, submit/wait or relocation is performed.
95/// Native resource uses must already have ended through the existing unsafe free/
96/// destroy contract before a block can become empty. Dropping anything never trims.
97/// Includes explicit-size default blocks retained by an abandoned Extensive context.
98/// # Errors
99/// Returns `Closed` after allocator close.
100pub fn trim(allocator: &mut Allocator<'_>) -> Result<TrimStats, AllocError> {
101	allocator.ensure_allocation()?;
102	allocator.ensure_open()?;
103	let mut snapshot = budget_snapshot(allocator);
104	Ok(super::pool::trim_blocks(allocator, &mut snapshot, true))
105}
106
107/// Query an explicit budget snapshot, then trim surplus empty blocks on pressured heaps.
108///
109/// Pressure means usage >= budget or native bytes >= the local heap ceiling.
110/// Without enabled EXT_memory_budget the snapshot is explicitly local. The
111/// selection subtracts released native bytes from that one snapshot, stopping
112/// once projected pressure ends or no eligible block remains. External usage
113/// may change immediately; this does not promise allocation success or budget
114/// compliance. No snapshot is cached and no live resource is evicted or moved.
115/// # Errors
116/// Returns closed/context/capability/native query errors before freeing memory.
117/// # Safety
118/// Instance/physical device must be the live parent used to create this allocator's device.
119pub unsafe fn trim_with_budget(
120	allocator: &mut Allocator<'_>,
121	instance: &vk::Instance,
122	physical: vk::PhysicalDevice,
123) -> Result<TrimStats, AllocError> {
124	allocator.ensure_allocation()?;
125	// SAFETY: caller supplies the exact live physical identity; query validates metadata.
126	let mut snapshot = unsafe { query_budget(allocator, instance, physical) }?;
127	Ok(super::pool::trim_blocks(allocator, &mut snapshot, false))
128}
129
130/// Change a local policy ceiling; this never reserves native heap capacity.
131pub fn set_heap_limit(
132	allocator: &mut Allocator<'_>,
133	heap: u32,
134	limit: u64,
135) -> Result<(), AllocError> {
136	allocator.ensure_global()?;
137	allocator.ensure_open()?;
138	if heap >= allocator.selector.properties.memory_heap_count {
139		return Err(AllocError::InvalidRequest("absent memory heap"));
140	}
141	let index = heap as usize;
142	if limit > allocator.selector.properties.memory_heaps[index].size
143		|| limit < allocator.heap_bytes[index]
144	{
145		return Err(AllocError::InvalidRequest(
146			"heap limit exceeds size or live native bytes",
147		));
148	}
149	allocator.heap_limits[index] = Some(limit);
150	Ok(())
151}
152
153/// Local accounting is always distinguished from implementation-reported usage.
154pub fn budget_snapshot(allocator: &Allocator<'_>) -> BudgetSnapshot {
155	if let Some(shared) = allocator.parallel_state() {
156		return shared.budget_snapshot();
157	}
158	let mut snapshot = BudgetSnapshot {
159		heap_count: allocator.selector.properties.memory_heap_count,
160		..Default::default()
161	};
162	for (index, heap) in snapshot.heaps[..snapshot.heap_count as usize]
163		.iter_mut()
164		.enumerate()
165	{
166		*heap = HeapBudget {
167			size: allocator.selector.properties.memory_heaps[index].size,
168			limit: allocator.heap_limits[index]
169				.unwrap_or(allocator.selector.properties.memory_heaps[index].size),
170			budget: allocator.heap_limits[index]
171				.unwrap_or(allocator.selector.properties.memory_heaps[index].size),
172			usage: allocator.heap_bytes[index],
173			native_bytes: allocator.heap_bytes[index],
174		};
175	}
176	snapshot
177}
178
179/// Return an explicit native budget snapshot without caching admission decisions.
180/// # Safety
181/// Supply the matching live instance and physical device. Metadata checks do not prove identity.
182pub unsafe fn query_budget(
183	allocator: &Allocator<'_>,
184	instance: &vk::Instance,
185	physical: vk::PhysicalDevice,
186) -> Result<BudgetSnapshot, AllocError> {
187	allocator.ensure_open()?;
188	let snapshot = budget_snapshot(allocator);
189	if !crate::mem::resource::extension(allocator.device.as_device(), c"VK_EXT_memory_budget") {
190		return Ok(snapshot);
191	}
192	let parent = instance
193		.capabilities()
194		.ok_or(AllocError::UnsupportedCapability(
195			"instance metadata is required",
196		))?;
197	let capabilities = allocator
198		.device
199		.as_device()
200		.capabilities()
201		.expect("extension requires metadata");
202	if physical.is_null() || !capabilities.matches_instance(parent) {
203		return Err(AllocError::InvalidProperties(
204			"budget context metadata disagree",
205		));
206	}
207	let mut budget = vk::PhysicalDeviceMemoryBudgetPropertiesEXT::new();
208	let mut output = vk::PhysicalDeviceMemoryProperties2 {
209		p_next: std::ptr::from_mut(&mut budget).cast(),
210		..Default::default()
211	};
212	// SAFETY: matching live physical device and initialized output chain.
213	unsafe {
214		if parent.instance_api_version() >= vk::API_VERSION_1_1 {
215			instance.get_physical_device_memory_properties2(physical, &mut output)?;
216		} else if parent
217			.instance_extensions()
218			.any(|name| name == c"VK_KHR_get_physical_device_properties2")
219		{
220			instance.get_physical_device_memory_properties2_khr(physical, &mut output)?;
221		} else {
222			return Err(AllocError::UnsupportedCapability(
223				"budget requires properties2",
224			));
225		}
226	}
227	validate_budget(allocator, &output, &budget)
228}
229
230fn validate_budget(
231	allocator: &Allocator<'_>,
232	output: &vk::PhysicalDeviceMemoryProperties2,
233	budget: &vk::PhysicalDeviceMemoryBudgetPropertiesEXT,
234) -> Result<BudgetSnapshot, AllocError> {
235	let mut snapshot = budget_snapshot(allocator);
236
237	if output.memory_properties.memory_heap_count != snapshot.heap_count {
238		return Err(AllocError::InvalidProperties(
239			"budget heap inventory changed",
240		));
241	}
242	for (index, heap) in snapshot.heaps[..snapshot.heap_count as usize]
243		.iter_mut()
244		.enumerate()
245	{
246		if output.memory_properties.memory_heaps[index].size != heap.size
247			|| budget.heap_budget[index] == 0
248			|| budget.heap_budget[index] > heap.size
249		{
250			return Err(AllocError::InvalidProperties("invalid native heap budget"));
251		}
252		heap.budget = budget.heap_budget[index];
253		heap.usage = budget.heap_usage[index];
254	}
255	if budget.heap_budget[snapshot.heap_count as usize..]
256		.iter()
257		.any(|&value| value != 0)
258		|| budget.heap_usage[snapshot.heap_count as usize..]
259			.iter()
260			.any(|&value| value != 0)
261	{
262		return Err(AllocError::InvalidProperties(
263			"native budget references absent heaps",
264		));
265	}
266	snapshot.source = BudgetSource::Extension;
267	Ok(snapshot)
268}
269
270#[derive(Clone, Copy)]
271pub(super) struct BudgetQuery {
272	physical: vk::PhysicalDevice,
273	function: vk::PFN_vkGetPhysicalDeviceMemoryProperties2,
274	name: &'static str,
275}
276
277pub(super) fn capture_query(
278	instance: &vk::Instance,
279	physical: vk::PhysicalDevice,
280) -> Option<BudgetQuery> {
281	let parent = instance.capabilities()?;
282	if parent.instance_api_version() >= vk::API_VERSION_1_1 {
283		Some(BudgetQuery {
284			physical,
285			function: instance.fp().get_physical_device_memory_properties2,
286			name: "vkGetPhysicalDeviceMemoryProperties2",
287		})
288	} else if parent
289		.instance_extensions()
290		.any(|name| name == c"VK_KHR_get_physical_device_properties2")
291	{
292		Some(BudgetQuery {
293			physical,
294			function: instance.fp().get_physical_device_memory_properties2_khr,
295			name: "vkGetPhysicalDeviceMemoryProperties2KHR",
296		})
297	} else {
298		None
299	}
300}
301
302/// Refresh an allocator-owned budget snapshot from the retained native context.
303///
304/// Without EXT_memory_budget this records local accounting. A manually constructed
305/// EXT allocator needs `refresh_budget_with_context` before automatic refresh.
306/// # Errors
307/// Missing context/PFN or invalid native outputs leave the previous cache intact.
308pub fn refresh_budget(allocator: &mut Allocator<'_>) -> Result<BudgetSnapshot, AllocError> {
309	allocator.ensure_global()?;
310	allocator.ensure_open()?;
311	let snapshot =
312		if crate::mem::resource::extension(allocator.device.as_device(), c"VK_EXT_memory_budget") {
313			let query = allocator
314				.budget_query
315				.ok_or(AllocError::UnsupportedCapability(
316					"retained physical budget context is required",
317				))?;
318			let function = query
319				.function
320				.ok_or(crate::load::CallError::MissingCommand(query.name))?;
321			let mut budget = vk::PhysicalDeviceMemoryBudgetPropertiesEXT::new();
322			let mut output = vk::PhysicalDeviceMemoryProperties2 {
323				p_next: std::ptr::from_mut(&mut budget).cast(),
324				..Default::default()
325			};
326			// SAFETY: context was captured under the allocator's native parent-lifetime/identity contract;
327			// the borrowed Device retains the loader library and both outputs are initialized stack records.
328			unsafe { function(query.physical, &mut output) };
329			validate_budget(allocator, &output, &budget)?
330		} else {
331			budget_snapshot(allocator)
332		};
333	allocator.budget_cache = Some(snapshot);
334	allocator.budget_operations = 0;
335	Ok(snapshot)
336}
337
338/// Capture a checked budget context for future refreshes, including manual constructors.
339/// # Errors
340/// Preserves context/query errors before changing retained budget state.
341/// # Safety
342/// Instance/physical device must be the exact live parents of this allocator's
343/// device and remain live for its complete remaining lifetime. Returned usage is
344/// a snapshot, never a reservation. Device retention keeps PFN code loaded only.
345pub unsafe fn refresh_budget_with_context(
346	allocator: &mut Allocator<'_>,
347	instance: &vk::Instance,
348	physical: vk::PhysicalDevice,
349) -> Result<BudgetSnapshot, AllocError> {
350	allocator.ensure_global()?;
351	// SAFETY: caller supplies the exact current and future native parent lifetime/identity contract.
352	let snapshot = unsafe { query_budget(allocator, instance, physical) }?;
353	allocator.budget_query = capture_query(instance, physical);
354	allocator.budget_cache = Some(snapshot);
355	allocator.budget_operations = 0;
356	Ok(snapshot)
357}
358
359/// Return an owned cached budget, refreshed on frame changes or after 30 region operations.
360///
361/// Local native-byte deltas are added to the last reported external usage. External
362/// pressure can change immediately; this does not guarantee allocation success.
363/// No arbitrary caller-supplied snapshot is admitted to the cache.
364pub fn heap_budgets(allocator: &mut Allocator<'_>) -> Result<BudgetSnapshot, AllocError> {
365	allocator.ensure_open()?;
366	if let Some(shared) = allocator.parallel_state() {
367		return Ok(shared.budget_snapshot());
368	}
369	if allocator.budget_cache.is_none() || allocator.budget_operations >= 30 {
370		refresh_budget(allocator)?;
371	}
372	let mut snapshot = allocator.budget_cache.expect("refreshed owned budget");
373	for (index, heap) in snapshot.heaps[..snapshot.heap_count as usize]
374		.iter_mut()
375		.enumerate()
376	{
377		let external = heap.usage.saturating_sub(heap.native_bytes);
378		heap.native_bytes = allocator.heap_bytes[index];
379		heap.usage = external
380			.checked_add(heap.native_bytes)
381			.ok_or(AllocError::InvalidProperties("budget usage overflow"))?;
382		heap.limit = allocator.heap_limits[index].unwrap_or(heap.size);
383		if snapshot.source == BudgetSource::Local {
384			heap.budget = heap.limit;
385		}
386	}
387	Ok(snapshot)
388}
389
390/// Mark a frame boundary for budget refresh; this never retires resources or waits.
391pub fn set_current_frame_index(
392	allocator: &mut Allocator<'_>,
393	index: u32,
394) -> Result<(), AllocError> {
395	allocator.ensure_global()?;
396	allocator.ensure_open()?;
397	if allocator.current_frame_index != index {
398		allocator.current_frame_index = index;
399		allocator.budget_cache = None;
400	}
401	Ok(())
402}
403
404pub(super) fn admit_native(
405	allocator: &mut Allocator<'_>,
406	index: u32,
407	size: u64,
408) -> Result<(), AllocError> {
409	let snapshot = heap_budgets(allocator)?;
410	let heap =
411		snapshot.heaps[allocator.selector.properties.memory_types[index as usize].heap_index as usize];
412	if heap
413		.usage
414		.checked_add(size)
415		.is_none_or(|usage| usage > heap.budget)
416	{
417		return Err(AllocError::OutOfSpace);
418	}
419	Ok(())
420}
421
422impl Allocator<'_> {
423	/// Explicitly refresh allocator-owned native/local budgets.
424	pub fn refresh_budget(&mut self) -> Result<BudgetSnapshot, AllocError> {
425		refresh_budget(self)
426	}
427
428	/// Capture a native query context under a retained parent-lifetime contract.
429	/// # Safety
430	/// Same identity/lifetime contract as `mem::refresh_budget_with_context`.
431	pub unsafe fn refresh_budget_with_context(
432		&mut self,
433		instance: &vk::Instance,
434		physical: vk::PhysicalDevice,
435	) -> Result<BudgetSnapshot, AllocError> {
436		// SAFETY: caller supplies the canonical retained native context contract.
437		unsafe { refresh_budget_with_context(self, instance, physical) }
438	}
439
440	/// Get owned cached/periodically refreshed heap budgets with local native-byte deltas.
441	pub fn heap_budgets(&mut self) -> Result<BudgetSnapshot, AllocError> {
442		heap_budgets(self)
443	}
444
445	/// Mark an explicit budget frame boundary; no completion or retirement is implied.
446	pub fn set_current_frame_index(&mut self, index: u32) -> Result<(), AllocError> {
447		set_current_frame_index(self, index)
448	}
449
450	pub fn current_frame_index(&self) -> u32 {
451		self.current_frame_index
452	}
453}
454
455pub(super) fn validate_profile(allocator: &Allocator<'_>) -> Result<(), AllocError> {
456	if crate::mem::resource::extension(allocator.device.as_device(), c"VK_EXT_memory_budget") {
457		let query = allocator
458			.budget_query
459			.ok_or(AllocError::UnsupportedCapability(
460				"retained physical budget context is required",
461			))?;
462		if query.function.is_none() {
463			return Err(crate::load::CallError::MissingCommand(query.name).into());
464		}
465	}
466	Ok(())
467}
468
469// Native growth refreshes the owned estimate; release never performs a fallible
470// driver query after a caller has already destroyed its resource. Correct local
471// deltas against that estimate, treating overflow conservatively as pressure.
472pub(super) fn cached_pressure(allocator: &Allocator<'_>, heap: usize) -> bool {
473	if let Some(shared) = allocator.parallel_state() {
474		let snapshot = shared.budget_snapshot();
475		return snapshot.heaps[heap].usage >= snapshot.heaps[heap].budget;
476	}
477	let Some(snapshot) = allocator.budget_cache else {
478		return false;
479	};
480	let old = snapshot.heaps[heap];
481	old
482		.usage
483		.saturating_sub(old.native_bytes)
484		.saturating_add(allocator.heap_bytes[heap])
485		>= old.budget
486}