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: 31 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
ref: v1.4.0
path: source/modules/antono2/memory
- uses: prantlf/setup-v-action@v4
with:
version: 0.5.2
- name: Install Vulkan development library
run: sudo apt-get update && sudo apt-get install -y libvulkan-dev libvulkan-volk-dev mesa-vulkan-drivers
- name: Check formatting
Expand All @@ -38,7 +40,7 @@ jobs:
- name: Vet allocator sources
working-directory: source/modules/antono2/vkmemalloc
# V 0.5.2's directory-mode vet parser does not accept aliased imports.
run: v vet block_pool.v block_pool_test.v upload_ring.v upload_ring_test.v vulkan_memory_allocator.v vulkan_memory_allocator_test.v
run: v vet block_pool.v block_pool_test.v mapped_memory.v mapped_memory_test.v memory_policy.v memory_policy_test.v upload_ring.v upload_ring_test.v vulkan_memory_allocator.v vulkan_memory_allocator_test.v
- name: Run allocator tests
working-directory: source/modules/antono2/vkmemalloc
run: v run setup.vsh --check
Expand All @@ -48,3 +50,31 @@ jobs:
lavapipe_icd=$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit)
test -n "$lavapipe_icd"
VK_ICD_FILENAMES="$lavapipe_icd" v run examples/buffer_suballocation

sanitizers:
runs-on: ubuntu-24.04
env:
VMODULES: ${{ github.workspace }}/source/modules
VULKAN_SDK: /usr
ASAN_OPTIONS: detect_leaks=0
steps:
- uses: actions/checkout@v7
with:
path: source/modules/antono2/vkmemalloc
- uses: actions/checkout@v7
with:
repository: antono2/vulkan
path: source/modules/antono2/vulkan
- uses: actions/checkout@v7
with:
repository: antono2/memory
ref: v1.4.0
path: source/modules/antono2/memory
- uses: prantlf/setup-v-action@v4
with:
version: 0.5.2
- name: Install native compiler and Vulkan development library
run: sudo apt-get update && sudo apt-get install -y clang libvulkan-dev libvulkan-volk-dev
- name: Run CPU policy tests with AddressSanitizer and UndefinedBehaviorSanitizer
working-directory: source/modules/antono2/vkmemalloc
run: v -cc clang -cflags -fsanitize=address,undefined -cflags -fno-omit-frame-pointer -ldflags -fsanitize=address,undefined test .
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

All notable changes to this project will be documented in this file.

## 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
per-heap diagnostics with a portable allocator-commitment fallback.
- 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
allocation.
- 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.

## 2.3.2 - 2026-09-12

- Promote the allocation-policy dependency to the production-hardened
Expand Down
121 changes: 115 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ 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
enough for examples while avoiding one Vulkan allocation per resource.

## How it fits together

The allocator has three 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
readback use. The selected `MemoryTypeChoice` explains the heap, flags,
budget state, and score.
2. **Block planning** uses `antono2.memory.RangeAllocator` to place compatible
buffers into larger memory-type-specific blocks. 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
private ownership record together.

Images remain dedicated. That conservative rule avoids hiding the additional
tiling and buffer-image granularity rules that a safe image suballocator would
need to model. Existing `MemType` APIs remain available for short examples;
new applications should normally use `AllocationOptions`.

## Install
```sh
v install antono2.vkmemalloc
Expand Down Expand Up @@ -60,6 +80,40 @@ The lower-level `allocate()` method also uses an isolated block because raw
`VkMemoryRequirements` do not identify whether the caller will bind a buffer or
image. Use `create_buffer()` when automatic buffer suballocation is desired.

## Policy-based allocation

The policy API expresses how the resource will be used and keeps hard
requirements distinct from preferences:

```v
options := vma.AllocationOptions{
usage: .upload
// These are optional refinements. Required flags are never dropped.
preferred_flags: vk.MemoryPropertyFlags(vk.MemoryPropertyFlagBits.host_cached)
}
result := allocator.create_buffer_with_options(&buffer_info, options, &buffer,
mut allocation)
```

- `.gpu_only` requires device-local memory.
- `.upload` requires host-visible memory and prefers coherent, device-local
types.
- `.readback` requires host-visible memory and prefers cached, coherent types.
- `.automatic` has no implicit hard requirement and prefers device-local
memory.

`required_flags` is a hard filter. `preferred_flags` improves a candidate's
rank, while `avoided_flags` lowers it without making the type unusable. The
default `.prefer_within` budget policy moves a heap with enough estimated room
ahead of an otherwise better match. `.require_within` filters over-budget
heaps for new Vulkan blocks while still permitting reuse of compatible blocks
that are already committed. `.ignore` ranks without considering room.

Use `allocator.select_memory_type(...)` when you need to inspect the choice
before creating a resource. Policy allocation tries compatible types in rank
order after reclaiming empty cached blocks on memory pressure. It never relaxes
required flags.

Allocate and bind a buffer, checking the returned Vulkan result:

```v
Expand All @@ -79,9 +133,18 @@ if allocator.map(mut allocation, &mapped) != .success {
return error('could not map buffer memory')
}
// Copy data to mapped here.
if allocator.flush(allocation) != .success {
return error('could not flush buffer memory')
}
allocator.unmap(mut allocation)
```

For host-coherent memory, `flush()` and `invalidate()` are checked no-ops. For
non-coherent memory they call Vulkan with ranges expanded to
`nonCoherentAtomSize`. The `_range` variants accept allocation-relative offsets
and sizes. Flush after host writes before device access; invalidate only after
device writes have completed and before reading them on the host.

Destroy the Vulkan buffer or image before freeing its memory:

```v
Expand Down Expand Up @@ -134,6 +197,39 @@ reported, `trim_empty_blocks()` can return it to Vulkan before retrying another
memory class. The allocator may still create a new compatible block when its
configured block limit and the Vulkan device allow it.

### Heap budgets

`VK_EXT_memory_budget` exposes driver estimates for current heap usage and the
amount the process can reasonably consume. The current integration uses Vulkan
1.1's properties query. Opt in only after confirming and enabling the device
extension:

```v
budget_supported := vma.supports_memory_budget(physical_device)
// Add vk.ext_memory_budget_extension_name to VkDeviceCreateInfo when true.
mut allocator := vma.new(vma.AllocatorCreateInfo{
physical_device: physical_device
device: device
memory_budget_enabled: budget_supported
})

_ = allocator.refresh_memory_budget()
for heap in allocator.memory_heaps() {
println('heap ${heap.heap_index}: ${heap.usage}/${heap.budget}')
}
```

The [runnable example](examples/buffer_suballocation/main.v) shows the complete
extension-name array and logical-device creation sequence.

`new()` obtains an initial enabled budget snapshot. Call
`refresh_memory_budget()` periodically (for example, once per frame or every
few seconds); policy selection uses the latest snapshot without adding a driver
query to every allocation. Without the extension, the same APIs fall back to
physical heap sizes and this allocator's own committed blocks. Budgets are
changing estimates, not reservations; Vulkan allocation can still fail and the
returned `vk.Result` remains authoritative.

## Persistent upload ring

`UploadRing` owns one dedicated, persistently mapped, host-coherent staging
Expand All @@ -149,6 +245,9 @@ slice := uploads.allocate(4096, 256) or { panic(err) }
unsafe {
copy(&u8(slice.data), source.data, source.len)
}
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.

// After the protecting fence or timeline value has completed:
Expand All @@ -163,7 +262,13 @@ must not retire a slice until the GPU has finished reading it. Call
`uploads.stats()` returns `UploadRingStats`, keeping this module's public API
independent of the internal allocation-policy type.

## Memory classes
`new_upload_ring()` deliberately requires host-coherent memory, so its
`flush()` calls are checked no-ops. Advanced callers can use
`new_upload_ring_with_options(..., AllocationOptions{ usage: .upload })` to
permit other host-visible types; flushing each written slice then handles
non-coherent memory correctly.

## Legacy memory classes

- `.staging` requires host-visible and host-coherent memory and may be mapped.
- `.gpu` requires device-local memory and normally cannot be mapped.
Expand All @@ -181,8 +286,10 @@ independent of the internal allocation-policy type.
when multiple threads can allocate or free concurrently.
- 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, enforce heap budgets, choose
between equivalent heaps, or automatically flush non-coherent memory.
- The allocator does not relocate live resources or suballocate 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.
- Vulkan objects must not outlive the memory bound to them.

All allocation and binding functions return `vk.Result`; callers should handle
Expand All @@ -196,9 +303,11 @@ The bookkeeping tests do not require a Vulkan-capable GPU:
v test .
```

The runnable example creates two real buffers, verifies that they share a
memory block, maps one range, creates a dedicated image, then exercises a
persistently mapped upload ring through wraparound and FIFO retirement:
The 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, prints heap diagnostics,
then exercises a persistently mapped upload ring through wraparound and FIFO
retirement:

```sh
v run examples/buffer_suballocation
Expand Down
33 changes: 33 additions & 0 deletions block_pool.v
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module vkmemalloc

import antono2.memory
import antono2.vulkan as vk

struct BlockReservation {
owner voidptr
Expand Down Expand Up @@ -77,6 +78,38 @@ fn (pool &MemoryBlockPool) block_is_dedicated(block_id u64) ?bool {
return none
}

fn (pool &MemoryBlockPool) block_capacity(block_id u64) ?u64 {
for block in pool.blocks {
if block.id == block_id {
return block.capacity
}
}
return none
}

fn (pool &MemoryBlockPool) block_memory_type(block_id u64) ?u32 {
for block in pool.blocks {
if block.id == block_id {
return block.memory_type
}
}
return none
}

fn (pool &MemoryBlockPool) heap_stats(props &vk.PhysicalDeviceMemoryProperties, heap_index u32) (u64, u64) {
mut committed := u64(0)
mut used := u64(0)
for block in pool.blocks {
if block.memory_type >= props.memoryTypeCount
|| props.memoryTypes[block.memory_type].heapIndex != heap_index {
continue
}
committed += block.capacity
used += block.ranges.stats().used
}
return committed, used
}

fn (pool &MemoryBlockPool) recommended_block_size(requested_size u64) !u64 {
if requested_size == 0 {
return error('allocation size must be greater than zero')
Expand Down
Loading