Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,52 @@ All notable changes to this project will be documented in this file.
`create_suballocated_image_with_options()` APIs while retaining dedicated
allocation in the existing image APIs.
- Separate reusable blocks by Vulkan memory type and resource class so buffers,
linear images, and optimal images never share a block or create a
linear images and optimal images never share a block or create a
`bufferImageGranularity` boundary.
- Query Vulkan 1.1 dedicated-allocation requirements and honor both required and
preferred dedicated memory; conservatively use dedicated memory on Vulkan
1.0.
- Expose the resource class in allocations, events, and class-filtered
- Expose the resource class in allocations, events and class-filtered
fragmentation statistics.
- Reject sparse, disjoint, and DRM-format-modifier images in the ordinary image
- Reject sparse, disjoint and DRM-format-modifier images in the ordinary image
suballocation path, which cannot provide their specialized binding rules.
- Add CPU isolation/ownership tests and a lavapipe example proving real optimal
image sharing alongside separate linear-image and buffer blocks; run every
real Vulkan workload with the Khronos validation layer in CI.

## 2.5.0 - 2026-09-12

- Add cumulative allocation, fallback, block-reuse, trim, and high-water-mark
- Add cumulative allocation, fallback, block-reuse, trim and high-water-mark
counters alongside current allocator statistics.
- Add an opt-in bounded lifecycle event trace with deterministic sequence
numbers, chronological snapshots, overwrite accounting, and resettable
numbers, chronological snapshots, overwrite accounting and resettable
measurement windows.
- Add a 30,000-operation deterministic block-planner workload that continuously
verifies ownership, overlap, accounting, memory-type isolation, and complete
verifies ownership, overlap, accounting, memory-type isolation and complete
coalescing.
- Extend the lavapipe integration example with 1,536 real Vulkan buffer
allocations, mapped writes, flushes, fragmented reuse, and trace validation.
allocations, mapped writes, flushes, fragmented reuse and trace validation.
- Keep tracing disabled by default and retain the allocator's existing external
synchronization contract.

## 2.4.0 - 2026-09-12

- Add explainable, deterministic memory-type ranking for GPU-only, upload,
readback, and automatic usage, with required, preferred, and avoided flags.
- Add optional `VK_EXT_memory_budget` discovery, refresh, selection policy, and
readback and automatic usage, with required, preferred and avoided flags.
- Add optional `VK_EXT_memory_budget` discovery, refresh, selection policy and
per-heap diagnostics with a portable allocator-commitment fallback.
- Add policy-based raw, buffer, dedicated-buffer, and image allocation APIs
- Add policy-based raw, buffer, dedicated-buffer and image allocation APIs
while preserving the existing `MemType` entry points.
- Retry allocation after trimming empty blocks and fall through to compatible
lower-ranked memory types on host/device out-of-memory results.
- Record the selected heap, property flags, and containing block size in every
- Record the selected heap, property flags and containing block size in every
allocation.
- Add checked `flush`, `flush_range`, `invalidate`, and `invalidate_range`
- Add checked `flush`, `flush_range`, `invalidate` and `invalidate_range`
helpers aligned to the device's `nonCoherentAtomSize`.
- Add policy-selected upload rings and slice-level flush/invalidate helpers
while retaining the coherent default constructor.
- Document the allocator from a high-level policy/planning/ownership viewpoint
and exercise policy selection, live budgets, and flushing with lavapipe.
and exercise policy selection, live budgets and flushing with lavapipe.

## 2.3.2 - 2026-09-12

Expand All @@ -64,7 +64,7 @@ All notable changes to this project will be documented in this file.

## 2.3.1 - 2026-09-11

- Expose global and per-memory-type free-range, largest-contiguous-range, and
- Expose global and per-memory-type free-range, largest-contiguous-range and
empty-block diagnostics without changing allocation policy.

## 2.3.0 - 2026-09-11
Expand Down Expand Up @@ -92,7 +92,7 @@ All notable changes to this project will be documented in this file.
- Free dedicated allocation blocks immediately on release while retaining
reusable shared buffer blocks.
- Add `UploadRing`, a dedicated persistently mapped staging buffer with aligned
FIFO allocation, checked retirement, wraparound, and occupancy statistics.
FIFO allocation, checked retirement, wraparound and occupancy statistics.

## 2.0.0 - 2026-09-10

Expand All @@ -102,5 +102,5 @@ All notable changes to this project will be documented in this file.
- Add configurable preferred block size and maximum block count.
- Add allocator commitment and occupancy statistics.
- Add explicit empty-block trimming while retaining blocks for reuse by default.
- Validate allocation ownership before mapping, unmapping, or releasing ranges.
- Validate allocation ownership before mapping, unmapping or releasing ranges.
- Add deterministic CPU-only policy tests and a real Vulkan buffer smoke test.
43 changes: 21 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This module provides small, explicit helpers for selecting Vulkan memory types,
allocating and binding memory for buffers and images, mapping host-visible
allocations, and suballocating shared `VkDeviceMemory` blocks.
allocations and suballocating shared `VkDeviceMemory` blocks.

Despite the repository name, this is not a binding to AMD's Vulkan Memory
Allocator. It is a compact V-native allocator intended to remain understandable
Expand All @@ -15,17 +15,17 @@ enough for examples while avoiding one Vulkan allocation per resource.
The allocator has four deliberately separate layers:

1. **Policy** filters the memory types allowed by Vulkan, applies required
property flags, and ranks the remaining types for GPU-only, upload, or
property flags and ranks the remaining types for GPU-only, upload or
readback use. The selected `MemoryTypeChoice` explains the heap, flags,
budget state, and score.
budget state and score.
2. **Block planning** uses `antono2.memory.RangeAllocator` to place compatible
resources into larger blocks. Every block has both a Vulkan memory type and
a resource class (`buffer`, `linear_image`, or `optimal_image`), so classes
a resource class (`buffer`, `linear_image` or `optimal_image`), so classes
with a [`bufferImageGranularity`](https://docs.vulkan.org/spec/latest/chapters/limits.html#limits-bufferimagegranularity)
boundary rule never become adjacent. This
CPU-only layer is deterministic and independently tested.
3. **Vulkan ownership** creates, maps, binds, and frees `VkDeviceMemory` while
`AllocationInfo` keeps the selected type, heap, properties, block size, and
3. **Vulkan ownership** creates, maps, binds and frees `VkDeviceMemory` while
`AllocationInfo` keeps the selected type, heap, properties, block size and
private ownership record together.
4. **Observability** exposes current occupancy separately from cumulative
activity and an optional bounded event trace. Normal applications pay no
Expand All @@ -42,8 +42,8 @@ new applications should normally use `AllocationOptions`.
| File | Responsibility |
| --- | --- |
| [memory_policy.v](memory_policy.v) | Memory-type ranking and heap-budget policy |
| [block_pool.v](block_pool.v) | CPU-only placement, ownership, and resource-class isolation |
| [vulkan_memory_allocator.v](vulkan_memory_allocator.v) | Vulkan block lifetime, resource creation/binding, mapping, and release |
| [block_pool.v](block_pool.v) | CPU-only placement, ownership and resource-class isolation |
| [vulkan_memory_allocator.v](vulkan_memory_allocator.v) | Vulkan block lifetime, resource creation/binding, mapping and release |
| [mapped_memory.v](mapped_memory.v) | Non-coherent flush/invalidate range validation |
| [upload_ring.v](upload_ring.v) | Persistent staging storage and FIFO slice retirement |
| [diagnostics.v](diagnostics.v) | Activity counters and bounded lifecycle events |
Expand All @@ -60,10 +60,9 @@ v install antono2.vkmemalloc

VPM installs the Vulkan bindings and `antono2.memory` dependencies automatically.

The Vulkan loader, headers, and a working GPU driver must also be installed.
The Vulkan loader, headers and a working GPU driver must also be installed.

For a fresh machine, install the native Vulkan prerequisites, V dependencies,
and run the compile checks with one command:
For a fresh machine, install the native Vulkan prerequisites and V dependencies, then run the compile checks with one command:

```sh
v run setup.vsh
Expand All @@ -72,10 +71,10 @@ v run setup.vsh
Use `v run setup.vsh --check` for a read-only diagnostic pass.

Windows 10/11 x64 is a tested target. The setup delegates Vulkan SDK discovery
to `antono2.vulkan`, reuses an existing compatible `VULKAN_SDK`, and does not
to `antono2.vulkan`, reuses an existing compatible `VULKAN_SDK` and does not
replace an installed SDK. CI runs the complete allocator test suite on Windows
Server 2022 with MSVC and SwiftShader. Linux CI additionally runs the real
Vulkan buffer, image, and sustained-allocation example workloads.
Vulkan buffer, image and sustained-allocation example workloads.

The allocator uses the production-hardened v1.4 release of
[`antono2.memory`](https://github.com/antono2/memory), specifically
Expand Down Expand Up @@ -241,7 +240,7 @@ println('largest free range: ${stats.largest_free_range}')
```

`committed` is memory obtained through `vkAllocateMemory`; `used` is the sum of
live resource ranges. `free_range_count`, `largest_free_range`, and
live resource ranges. `free_range_count`, `largest_free_range` and
`empty_block_count` make cached capacity and external fragmentation visible.
The largest range is measured before applying the alignment of a future
request, so it is diagnostic rather than a guarantee that an allocation will
Expand Down Expand Up @@ -290,9 +289,9 @@ println('fallback attempts: ${diagnostics.counters.fallback_attempts}')
```

Set `event_trace_capacity` in `AllocatorCreateInfo` to retain the latest
allocation success, failure, release, and trim events. The storage is a bounded
allocation success, failure, release and trim events. The storage is a bounded
ring: old records are overwritten, `dropped_event_count` reports how many were
replaced, and `recent_events()` always returns the retained records in
replaced and `recent_events()` always returns the retained records in
chronological order. Events use a monotonic sequence rather than a wall-clock
timestamp so traces remain deterministic and callers can add the timing system
appropriate to their application.
Expand Down Expand Up @@ -359,7 +358,7 @@ unsafe {
if uploads.flush(slice) != .success {
return error('could not flush upload slice')
}
// Record a copy from uploads.buffer at slice.offset, submit it, and keep slice.
// Record a copy from uploads.buffer at slice.offset, submit it and keep slice.

// After the protecting fence or timeline value has completed:
retired := uploads.retire(slice)
Expand Down Expand Up @@ -397,13 +396,13 @@ non-coherent memory correctly.
- The allocator tracks at most 256 memory blocks by default. Each block can
contain many suballocations.
- The allocator is not internally synchronized. Externally synchronize access
when multiple threads can allocate, free, query diagnostics, or reset the
when multiple threads can allocate, free, query diagnostics or reset the
diagnostic window concurrently. This keeps synchronization ownership with
the renderer, which normally already serializes Vulkan device-memory calls.
- Concurrently mapped allocations in one shared block reuse a single underlying
Vulkan mapping. Each successful `map()` must have a matching `unmap()`.
- The allocator does not relocate live resources. Image sharing is opt-in and
excludes sparse, disjoint, and DRM-format-modifier images.
excludes sparse, disjoint and DRM-format-modifier images.
- Heap budgets guide selection but cannot enforce a process-wide or system-wide
limit because other allocators can change process usage and external system
activity can change the budget concurrently.
Expand All @@ -416,7 +415,7 @@ errors instead of assuming allocation succeeds.

The bookkeeping tests do not require a Vulkan-capable GPU. They include a
30,000-operation mixed-size, mixed-alignment workload across multiple memory
types and continuously verify ownership, non-overlap, accounting, and
types and continuously verify ownership, non-overlap, accounting and
coalescing invariants:

```sh
Expand All @@ -425,7 +424,7 @@ v test .

The introductory runnable example enables live budgets when available, creates
two real policy-selected upload buffers, verifies that they share a memory
block, maps and flushes them, creates a dedicated GPU-only image, and prints
block, maps and flushes them, creates a dedicated GPU-only image and prints
heap diagnostics and allocation counters. It also exercises a persistently
mapped upload ring through wraparound and FIFO retirement:

Expand All @@ -435,7 +434,7 @@ v run examples/buffer_suballocation

The [separate sustained workload](examples/stress/main.v) performs 1,536 real
Vulkan buffer allocations with mapped writes, flushes, fragmenting
release/refill cycles, bounded-trace wraparound, and final coalescing checks:
release/refill cycles, bounded-trace wraparound and final coalescing checks:

```sh
v run examples/stress
Expand Down
2 changes: 1 addition & 1 deletion block_pool_stress_test.v
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Runs sustained mixed allocation/release workloads across memory blocks.
// Checks non-overlap, accounting, and per-memory-type invariants after each operation.
// Checks non-overlap, accounting and per-memory-type invariants after each operation.
module vkmemalloc

fn assert_block_pool_invariants(pool &MemoryBlockPool, active []BlockReservation, committed u64) {
Expand Down
2 changes: 1 addition & 1 deletion block_pool_test.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Checks block selection, alignment, dedicated isolation, and release coalescing.
// Checks block selection, alignment, dedicated isolation and release coalescing.
// Rejects stale or forged reservations using CPU-only allocation metadata.
module vkmemalloc

Expand Down
2 changes: 1 addition & 1 deletion diagnostics.v
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ fn (mut a Allocator) note_block_trimmed(memory_type u32, heap_index u32, resourc
}

// diagnostics returns a consistent single-threaded snapshot of current state,
// lifetime counters, and bounded-trace retention. Like the allocator itself,
// lifetime counters and bounded-trace retention. Like the allocator itself,
// callers must externally synchronize this query with concurrent mutations.
pub fn (a &Allocator) diagnostics() AllocatorDiagnostics {
return AllocatorDiagnostics{
Expand Down
2 changes: 1 addition & 1 deletion examples/stress/main.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Runs repeated buffer, image, and upload-ring workloads on a Vulkan device.
// Runs repeated buffer, image and upload-ring workloads on a Vulkan device.
// Checks allocation cleanup and diagnostics across sustained resource churn.
module main

Expand Down
2 changes: 1 addition & 1 deletion mapped_memory_test.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Checks atom-aligned mapped ranges, bounds rejection, and ownership validation.
// Checks atom-aligned mapped ranges, bounds rejection and ownership validation.
// Uses synthetic allocations for coherent-memory cases that need no driver calls.
module vkmemalloc

Expand Down
2 changes: 1 addition & 1 deletion memory_policy.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Ranks Vulkan memory types using usage hints, required properties, and heap budgets.
// Ranks Vulkan memory types using usage hints, required properties and heap budgets.
// Keeps selection policy separate from driver allocation and block reuse.
module vkmemalloc

Expand Down
6 changes: 3 additions & 3 deletions upload_ring.v
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub:
}

// UploadRingStats describes current payload, alignment/wrap padding, free
// space, and peak occupancy without exposing the underlying policy type.
// space and peak occupancy without exposing the underlying policy type.
pub struct UploadRingStats {
pub:
capacity u64
Expand Down Expand Up @@ -176,7 +176,7 @@ pub fn (ring &UploadRing) invalidate(slice UploadSlice) vk.Result {
return ring.allocator.invalidate_range(ring.backing, slice.offset, slice.size)
}

// stats returns current payload, padding, free-space, and peak ring occupancy.
// stats returns current payload, padding, free-space and peak ring occupancy.
pub fn (ring &UploadRing) stats() UploadRingStats {
if ring.destroyed {
return UploadRingStats{}
Expand All @@ -194,7 +194,7 @@ pub fn (ring &UploadRing) stats() UploadRingStats {
}
}

// destroy invalidates all slices, unmaps and destroys the buffer, and releases
// destroy invalidates all slices, unmaps and destroys the buffer and releases
// its dedicated memory. The allocator itself remains usable.
pub fn (mut ring UploadRing) destroy() bool {
if ring.destroyed {
Expand Down
2 changes: 1 addition & 1 deletion upload_ring_test.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Checks staging-slice alignment, wraparound, FIFO retirement, and ownership.
// Checks staging-slice alignment, wraparound, FIFO retirement and ownership.
// Uses host byte storage instead of a real mapped Vulkan buffer.
module vkmemalloc

Expand Down
12 changes: 6 additions & 6 deletions vulkan_memory_allocator.v
Original file line number Diff line number Diff line change
Expand Up @@ -689,13 +689,13 @@ fn (mut a Allocator) create_image_with_options_policy(image_info &vk.ImageCreate
return result
}

// create_buffer creates a buffer, suballocates compatible memory, and binds it.
// create_buffer creates a buffer, suballocates compatible memory and binds it.
pub fn (mut a Allocator) create_buffer(buffer_info &vk.BufferCreateInfo, type MemType, buffer &vk.Buffer, mut alloc_info AllocationInfo) vk.Result {
return a.create_buffer_with_policy(buffer_info, type, false, buffer, mut alloc_info)
}

// create_dedicated_buffer creates a buffer with an isolated memory block. Use
// it for persistent mapping, external memory, or explicit lifetime isolation.
// it for persistent mapping, external memory or explicit lifetime isolation.
pub fn (mut a Allocator) create_dedicated_buffer(buffer_info &vk.BufferCreateInfo, type MemType, buffer &vk.Buffer, mut alloc_info AllocationInfo) vk.Result {
return a.create_buffer_with_policy(buffer_info, type, true, buffer, mut alloc_info)
}
Expand Down Expand Up @@ -789,15 +789,15 @@ pub fn (mut a Allocator) create_image_with_options(image_info &vk.ImageCreateInf
}

// create_suballocated_image creates an ordinary linear- or optimal-tiling
// image, shares memory only with images in the same resource class, and binds
// image, shares memory only with images in the same resource class and binds
// it. A Vulkan 1.1+ driver request for dedicated memory is always honored;
// Vulkan 1.0 conservatively falls back to a dedicated allocation.
pub fn (mut a Allocator) create_suballocated_image(image_info &vk.ImageCreateInfo, type MemType, image &vk.Image, mut alloc_info AllocationInfo) vk.Result {
return a.create_image_with_policy(image_info, type, false, image, mut alloc_info)
}

// create_suballocated_image_with_options is the policy-based counterpart of
// create_suballocated_image(). Sparse, disjoint, and DRM-format-modifier images
// create_suballocated_image(). Sparse, disjoint and DRM-format-modifier images
// require specialized binding paths and return error_feature_not_present here.
pub fn (mut a Allocator) create_suballocated_image_with_options(image_info &vk.ImageCreateInfo, options AllocationOptions, image &vk.Image, mut alloc_info AllocationInfo) vk.Result {
return a.create_image_with_options_policy(image_info, options, false, image, mut alloc_info)
Expand Down Expand Up @@ -959,7 +959,7 @@ fn (mut a Allocator) trim_empty_blocks_filtered(heap_index u32, filter_by_heap b
return removed
}

// AllocatorStats reports Vulkan block commitment, live suballocation use, and
// AllocatorStats reports Vulkan block commitment, live suballocation use and
// free-range fragmentation. largest_free_range is the largest raw contiguous
// range and does not account for the alignment of a future request.
pub struct AllocatorStats {
Expand Down Expand Up @@ -992,7 +992,7 @@ pub fn (a &Allocator) stats() AllocatorStats {
}
}

// stats_for_memory_type returns commitment, occupancy, and fragmentation for
// stats_for_memory_type returns commitment, occupancy and fragmentation for
// one Vulkan memory-type index. Use this instead of global stats when
// diagnosing whether compatible blocks can satisfy a resource request.
pub fn (a &Allocator) stats_for_memory_type(memory_type u32) AllocatorStats {
Expand Down
Loading