From d6829fb506d233974cba78d063bf9b8bb06eda1e Mon Sep 17 00:00:00 2001 From: antono2 Date: Thu, 10 Sep 2026 18:19:15 +0200 Subject: [PATCH 1/3] Use canonical antono2.memory dependency --- .github/workflows/test.yml | 4 ++-- CHANGELOG.md | 7 +++++++ README.md | 7 +++++-- block_pool.v | 8 ++++---- upload_ring.v | 40 ++++++++++++++++++++++++++++++-------- upload_ring_test.v | 8 ++++---- v.mod | 4 ++-- 7 files changed, 56 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9460cc1..d4e148e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,8 @@ jobs: - uses: actions/checkout@v7 with: repository: antono2/memory - ref: v0.2.0 - path: source/modules/generic_pool + ref: v1.0.2 + path: source/modules/antono2/memory - uses: prantlf/setup-v-action@v4 - name: Install Vulkan development library run: sudo apt-get update && sudo apt-get install -y libvulkan-dev libvulkan-volk-dev mesa-vulkan-drivers diff --git a/CHANGELOG.md b/CHANGELOG.md index b2003ca..52d8f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## 2.2.0 - 2026-09-10 + +- Migrate the allocation-policy dependency to the canonical `antono2.memory` + module and pin it to the immutable v1.0.2 release. +- Return a local `UploadRingStats` value from `UploadRing.stats()` so the public + Vulkan API does not expose the underlying policy module's type. + ## 2.1.1 - 2026-09-10 - Pin the general allocation dependency to `memory` v0.2.0 so released diff --git a/README.md b/README.md index 71946f7..ade34ef 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ enough for examples while avoiding one Vulkan allocation per resource. v install antono2.vkmemalloc ``` -VPM installs the Vulkan bindings and `generic_pool` dependencies automatically. +VPM installs the Vulkan bindings and `antono2.memory` dependencies automatically. The Vulkan loader, headers, and a working GPU driver must also be installed. -The allocator uses [`generic_pool.RangeAllocator`](https://github.com/antono2/memory) +The allocator uses [`antono2.memory`](https://github.com/antono2/memory), +specifically `memory.RangeAllocator`, for its dependency-free block suballocation policy. Vulkan handles remain isolated in this module. @@ -129,6 +130,8 @@ Slices are strictly FIFO and never cross the end of the buffer. Retirement is rejected when attempted out of order. The caller owns submission tracking and must not retire a slice until the GPU has finished reading it. Call `uploads.destroy()` before destroying the allocator or Vulkan device. +`uploads.stats()` returns `UploadRingStats`, keeping this module's public API +independent of the internal allocation-policy type. ## Memory classes diff --git a/block_pool.v b/block_pool.v index 2c7e56c..87ffe5d 100644 --- a/block_pool.v +++ b/block_pool.v @@ -1,11 +1,11 @@ module vkmemalloc -import generic_pool +import antono2.memory struct BlockReservation { owner voidptr block_id u64 - allocation generic_pool.RangeAllocation + allocation memory.RangeAllocation pub: memory_type u32 offset u64 @@ -17,7 +17,7 @@ struct MemoryBlock { memory_type u32 capacity u64 dedicated bool - ranges &generic_pool.RangeAllocator @[required] + ranges &memory.RangeAllocator @[required] } struct BlockPoolStats { @@ -106,7 +106,7 @@ fn (mut pool MemoryBlockPool) add_block_with_policy(memory_type u32, capacity u6 memory_type: memory_type capacity: capacity dedicated: dedicated - ranges: generic_pool.new_range_allocator(capacity) + ranges: memory.new_range_allocator(capacity) } return id } diff --git a/upload_ring.v b/upload_ring.v index cebf126..910337a 100644 --- a/upload_ring.v +++ b/upload_ring.v @@ -1,19 +1,33 @@ module vkmemalloc -import generic_pool +import antono2.memory import antono2.vulkan as vk // UploadSlice identifies one persistently mapped staging-buffer range. Slices // must be retired in allocation order after the GPU no longer reads them. pub struct UploadSlice { owner voidptr - allocation generic_pool.RingAllocation + allocation memory.RingAllocation pub: offset u64 size u64 data voidptr } +// UploadRingStats describes current payload, alignment/wrap padding, free +// space, and peak occupancy without exposing the underlying policy type. +pub struct UploadRingStats { +pub: + capacity u64 + used u64 + payload u64 + padding u64 + free u64 + peak_used u64 + allocation_count int + largest_contiguous_free u64 +} + // UploadRing owns one dedicated, persistently mapped Vulkan staging buffer and // suballocates it with FIFO ring semantics. It is not internally synchronized. pub struct UploadRing { @@ -23,8 +37,8 @@ pub: mut: allocator &Allocator = unsafe { nil } backing AllocationInfo - mapped voidptr = unsafe { nil } - ranges &generic_pool.RingAllocator = unsafe { nil } + mapped voidptr = unsafe { nil } + ranges &memory.RingAllocator = unsafe { nil } destroyed bool } @@ -63,7 +77,7 @@ pub fn new_upload_ring(mut allocator Allocator, capacity u64) !&UploadRing { allocator: allocator backing: backing mapped: mapped - ranges: generic_pool.new_ring_allocator(capacity) + ranges: memory.new_ring_allocator(capacity) } } @@ -104,11 +118,21 @@ pub fn (mut ring UploadRing) retire(slice UploadSlice) bool { } // stats returns current payload, padding, free-space, and peak ring occupancy. -pub fn (ring &UploadRing) stats() generic_pool.RingStats { +pub fn (ring &UploadRing) stats() UploadRingStats { if ring.destroyed { - return generic_pool.RingStats{} + return UploadRingStats{} + } + stats := ring.ranges.stats() + return UploadRingStats{ + capacity: stats.capacity + used: stats.used + payload: stats.payload + padding: stats.padding + free: stats.free + peak_used: stats.peak_used + allocation_count: stats.allocation_count + largest_contiguous_free: stats.largest_contiguous_free } - return ring.ranges.stats() } // destroy invalidates all slices, unmaps and destroys the buffer, and releases diff --git a/upload_ring_test.v b/upload_ring_test.v index b84105d..40230d7 100644 --- a/upload_ring_test.v +++ b/upload_ring_test.v @@ -1,13 +1,13 @@ module vkmemalloc -import generic_pool +import antono2.memory fn test_upload_ring_returns_aligned_host_pointers_and_wraps() { mut storage := []u8{len: 64} mut uploads := UploadRing{ capacity: 64 mapped: storage.data - ranges: generic_pool.new_ring_allocator(64) + ranges: memory.new_ring_allocator(64) } first := uploads.allocate(24, 16) or { panic(err) } second := uploads.allocate(24, 16) or { panic(err) } @@ -37,12 +37,12 @@ fn test_upload_ring_rejects_foreign_forged_and_destroyed_slices() { mut first_ring := UploadRing{ capacity: 32 mapped: first_storage.data - ranges: generic_pool.new_ring_allocator(32) + ranges: memory.new_ring_allocator(32) } mut second_ring := UploadRing{ capacity: 32 mapped: second_storage.data - ranges: generic_pool.new_ring_allocator(32) + ranges: memory.new_ring_allocator(32) } allocation := first_ring.allocate(8, 1) or { panic(err) } foreign := second_ring.allocate(8, 1) or { panic(err) } diff --git a/v.mod b/v.mod index 08cd8c9..aff825d 100644 --- a/v.mod +++ b/v.mod @@ -2,9 +2,9 @@ Module { name: 'antono2.vkmemalloc' author: 'Anton Oreskin' description: 'Vulkan block suballocation helpers for buffers and images' - version: '2.1.1' + version: '2.2.0' license: 'MIT' repo_url: 'https://github.com/antono2/vulkan_memory_allocator' tags: ['V','vulkan','allocator'] - dependencies: ['antono2.vulkan','https://github.com/antono2/memory@v0.2.0'] + dependencies: ['antono2.vulkan','antono2.memory@v1.0.2'] } From 69f3ac81fc3ecf96a05fde11c92a2ae77338ff40 Mon Sep 17 00:00:00 2001 From: antono2 Date: Thu, 10 Sep 2026 19:06:06 +0200 Subject: [PATCH 2/3] Support concurrent mappings in shared blocks --- CHANGELOG.md | 3 ++ README.md | 4 +- examples/buffer_suballocation/main.v | 13 +++-- vulkan_memory_allocator.v | 71 +++++++++++++++++++++++++--- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d8f1a..179d992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. module and pin it to the immutable v1.0.2 release. - Return a local `UploadRingStats` value from `UploadRing.stats()` so the public Vulkan API does not expose the underlying policy module's type. +- Share one reference-counted Vulkan mapping across mapped suballocations in the + same block, allowing production consumers to keep multiple staging ranges + mapped concurrently. ## 2.1.1 - 2026-09-10 diff --git a/README.md b/README.md index ade34ef..53579ee 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ independent of the internal allocation-policy type. contain many suballocations. - The allocator is not internally synchronized. Externally synchronize access when multiple threads can allocate or free concurrently. -- Do not map two allocations sharing one memory block concurrently; Vulkan - permits a device-memory object to be mapped only once at a time. +- 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. - Vulkan objects must not outlive the memory bound to them. diff --git a/examples/buffer_suballocation/main.v b/examples/buffer_suballocation/main.v index 3e12511..18f42a4 100644 --- a/examples/buffer_suballocation/main.v +++ b/examples/buffer_suballocation/main.v @@ -106,12 +106,19 @@ fn run() ! { assert stats.block_count == 1 assert stats.allocation_count == 2 println('two buffers share one block: committed=${stats.committed}, used=${stats.used}') - mut mapped := voidptr(unsafe { nil }) - require_success(allocator.map(mut second_allocation, &mapped), 'map second staging buffer')! + mut first_mapped := voidptr(unsafe { nil }) + mut second_mapped := voidptr(unsafe { nil }) + require_success(allocator.map(mut first_allocation, &first_mapped), 'map first staging buffer')! + require_success(allocator.map(mut second_allocation, &second_mapped), + 'map second staging buffer')! unsafe { - *(&u8(mapped)) = 42 + *(&u8(first_mapped)) = 21 + *(&u8(second_mapped)) = 42 } + assert usize(second_mapped) - usize(first_mapped) == second_allocation.offset - first_allocation.offset + allocator.unmap(mut first_allocation) allocator.unmap(mut second_allocation) + println('shared staging suballocations mapped concurrently') image_info := vk.ImageCreateInfo{ imageType: ._2d diff --git a/vulkan_memory_allocator.v b/vulkan_memory_allocator.v index 3e06f9f..1a3aaa9 100644 --- a/vulkan_memory_allocator.v +++ b/vulkan_memory_allocator.v @@ -13,6 +13,8 @@ mut: planner &MemoryBlockPool = unsafe { nil } pools [max_pools]vk.DeviceMemory block_ids [max_pools]u64 + mapped [max_pools]voidptr + map_refs [max_pools]u32 pool_size u32 } @@ -36,6 +38,8 @@ fn (mut a Allocator) remember_block(memory vk.DeviceMemory, block_id u64) bool { if isnil(a.pools[i]) { a.pools[i] = memory a.block_ids[i] = block_id + a.mapped[i] = unsafe { nil } + a.map_refs[i] = 0 return true } } @@ -44,6 +48,8 @@ fn (mut a Allocator) remember_block(memory vk.DeviceMemory, block_id u64) bool { } a.pools[a.pool_size] = memory a.block_ids[a.pool_size] = block_id + a.mapped[a.pool_size] = unsafe { nil } + a.map_refs[a.pool_size] = 0 a.pool_size++ return true } @@ -54,6 +60,8 @@ fn (mut a Allocator) forget_block(block_id u64) ?vk.DeviceMemory { memory := a.pools[i] a.pools[i] = unsafe { nil } a.block_ids[i] = 0 + a.mapped[i] = unsafe { nil } + a.map_refs[i] = 0 for a.pool_size > 0 && isnil(a.pools[a.pool_size - 1]) { a.pool_size-- } @@ -63,6 +71,15 @@ fn (mut a Allocator) forget_block(block_id u64) ?vk.DeviceMemory { return none } +fn (a &Allocator) block_index(block_id u64) ?int { + for i in 0 .. a.pool_size { + if a.block_ids[i] == block_id && !isnil(a.pools[i]) { + return int(i) + } + } + return none +} + fn (a &Allocator) memory_for_block(block_id u64) ?vk.DeviceMemory { for i in 0 .. a.pool_size { if a.block_ids[i] == block_id && !isnil(a.pools[i]) { @@ -95,6 +112,7 @@ pub mut: size u64 mut: reservation BlockReservation + mapped bool } pub struct MemNode { @@ -418,20 +436,53 @@ pub fn (mut a Allocator) create_image(p_image_create_info &vk.ImageCreateInfo, t return vk.Result.success } -// map maps the allocation's byte range for host access. +// map maps the allocation's byte range for host access. Compatible allocations +// sharing one VkDeviceMemory block share one Vulkan mapping internally. pub fn (mut a Allocator) map(mut alloc_info AllocationInfo, data &voidptr) vk.Result { if !a.owns_allocation(alloc_info) { + eprintln('Cannot map an allocation not owned by this allocator') + return .error_memory_map_failed + } + if alloc_info.mapped { + eprintln('Cannot map an allocation that is already mapped') + return .error_memory_map_failed + } + index := a.block_index(alloc_info.reservation.block_id) or { + eprintln('Cannot map an allocation whose memory block is unavailable') return .error_memory_map_failed } - return vk.map_memory(a.create_info.device, alloc_info.memory, alloc_info.offset, - alloc_info.size, 0, data) + if isnil(a.mapped[index]) { + mut base := voidptr(unsafe { nil }) + result := vk.map_memory(a.create_info.device, alloc_info.memory, 0, vk.whole_size, 0, &base) + if result != .success { + eprintln('Could not map Vulkan memory block ${alloc_info.reservation.block_id}: ${result}') + return result + } + a.mapped[index] = base + } + unsafe { + *data = voidptr(usize(a.mapped[index]) + usize(alloc_info.offset)) + } + a.map_refs[index]++ + alloc_info.mapped = true + return .success } -// unmap unmaps the memory block containing an allocation. +// unmap releases this allocation's mapping reference. The Vulkan memory block +// remains mapped until every mapped suballocation has been unmapped. pub fn (mut a Allocator) unmap(mut alloc_info AllocationInfo) { - if a.owns_allocation(alloc_info) { - vk.unmap_memory(a.create_info.device, alloc_info.memory) + if !a.owns_allocation(alloc_info) || !alloc_info.mapped { + return + } + index := a.block_index(alloc_info.reservation.block_id) or { return } + if a.map_refs[index] > 0 { + a.map_refs[index]-- + } + if a.map_refs[index] == 0 && !isnil(a.mapped[index]) { + vk.unmap_memory(a.create_info.device, a.pools[index]) + a.mapped[index] = unsafe { nil } } + alloc_info.mapped = false } // release returns a tracked suballocation to its VkDeviceMemory block. Empty @@ -440,6 +491,9 @@ pub fn (mut a Allocator) release(mut alloc_info AllocationInfo) bool { if !a.owns_allocation(alloc_info) { return false } + if alloc_info.mapped { + a.unmap(mut alloc_info) + } block_id := alloc_info.reservation.block_id dedicated := a.planner.block_is_dedicated(block_id) or { return false } if !a.planner.release(alloc_info.reservation) { @@ -529,6 +583,11 @@ pub fn (a &Allocator) stats() AllocatorStats { pub fn (mut a Allocator) destroy() { for i in 0 .. a.pool_size { if !isnil(a.pools[i]) { + if !isnil(a.mapped[i]) { + vk.unmap_memory(a.create_info.device, a.pools[i]) + a.mapped[i] = unsafe { nil } + a.map_refs[i] = 0 + } vk.free_memory(a.create_info.device, a.pools[i], unsafe { nil }) a.pools[i] = unsafe { nil } a.block_ids[i] = 0 From 974b23529a5ae3ed324b8ace72b0e826ae6008f9 Mon Sep 17 00:00:00 2001 From: antono2 Date: Thu, 10 Sep 2026 23:34:29 +0200 Subject: [PATCH 3/3] Use antono2.mem distribution package --- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 4 ++-- README.md | 6 +++--- block_pool.v | 8 ++++---- upload_ring.v | 10 +++++----- upload_ring_test.v | 8 ++++---- v.mod | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4e148e..744900d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,9 +26,9 @@ jobs: path: source/modules/antono2/vulkan - uses: actions/checkout@v7 with: - repository: antono2/memory - ref: v1.0.2 - path: source/modules/antono2/memory + repository: antono2/mem + ref: v1.0.3 + path: source/modules/antono2/mem - uses: prantlf/setup-v-action@v4 - name: Install Vulkan development library run: sudo apt-get update && sudo apt-get install -y libvulkan-dev libvulkan-volk-dev mesa-vulkan-drivers diff --git a/CHANGELOG.md b/CHANGELOG.md index 179d992..4aeec22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ All notable changes to this project will be documented in this file. ## 2.2.0 - 2026-09-10 -- Migrate the allocation-policy dependency to the canonical `antono2.memory` - module and pin it to the immutable v1.0.2 release. +- Migrate the allocation-policy dependency to the canonical `antono2.mem` + module and pin it to the immutable v1.0.3 release. - Return a local `UploadRingStats` value from `UploadRing.stats()` so the public Vulkan API does not expose the underlying policy module's type. - Share one reference-counted Vulkan mapping across mapped suballocations in the diff --git a/README.md b/README.md index 53579ee..39fb3f2 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ enough for examples while avoiding one Vulkan allocation per resource. v install antono2.vkmemalloc ``` -VPM installs the Vulkan bindings and `antono2.memory` dependencies automatically. +VPM installs the Vulkan bindings and `antono2.mem` dependencies automatically. The Vulkan loader, headers, and a working GPU driver must also be installed. -The allocator uses [`antono2.memory`](https://github.com/antono2/memory), -specifically `memory.RangeAllocator`, +The allocator uses [`antono2.mem`](https://github.com/antono2/mem), +specifically `mem.RangeAllocator`, for its dependency-free block suballocation policy. Vulkan handles remain isolated in this module. diff --git a/block_pool.v b/block_pool.v index 87ffe5d..b5da791 100644 --- a/block_pool.v +++ b/block_pool.v @@ -1,11 +1,11 @@ module vkmemalloc -import antono2.memory +import antono2.mem struct BlockReservation { owner voidptr block_id u64 - allocation memory.RangeAllocation + allocation mem.RangeAllocation pub: memory_type u32 offset u64 @@ -17,7 +17,7 @@ struct MemoryBlock { memory_type u32 capacity u64 dedicated bool - ranges &memory.RangeAllocator @[required] + ranges &mem.RangeAllocator @[required] } struct BlockPoolStats { @@ -106,7 +106,7 @@ fn (mut pool MemoryBlockPool) add_block_with_policy(memory_type u32, capacity u6 memory_type: memory_type capacity: capacity dedicated: dedicated - ranges: memory.new_range_allocator(capacity) + ranges: mem.new_range_allocator(capacity) } return id } diff --git a/upload_ring.v b/upload_ring.v index 910337a..d73410d 100644 --- a/upload_ring.v +++ b/upload_ring.v @@ -1,13 +1,13 @@ module vkmemalloc -import antono2.memory +import antono2.mem import antono2.vulkan as vk // UploadSlice identifies one persistently mapped staging-buffer range. Slices // must be retired in allocation order after the GPU no longer reads them. pub struct UploadSlice { owner voidptr - allocation memory.RingAllocation + allocation mem.RingAllocation pub: offset u64 size u64 @@ -37,8 +37,8 @@ pub: mut: allocator &Allocator = unsafe { nil } backing AllocationInfo - mapped voidptr = unsafe { nil } - ranges &memory.RingAllocator = unsafe { nil } + mapped voidptr = unsafe { nil } + ranges &mem.RingAllocator = unsafe { nil } destroyed bool } @@ -77,7 +77,7 @@ pub fn new_upload_ring(mut allocator Allocator, capacity u64) !&UploadRing { allocator: allocator backing: backing mapped: mapped - ranges: memory.new_ring_allocator(capacity) + ranges: mem.new_ring_allocator(capacity) } } diff --git a/upload_ring_test.v b/upload_ring_test.v index 40230d7..24fecce 100644 --- a/upload_ring_test.v +++ b/upload_ring_test.v @@ -1,13 +1,13 @@ module vkmemalloc -import antono2.memory +import antono2.mem fn test_upload_ring_returns_aligned_host_pointers_and_wraps() { mut storage := []u8{len: 64} mut uploads := UploadRing{ capacity: 64 mapped: storage.data - ranges: memory.new_ring_allocator(64) + ranges: mem.new_ring_allocator(64) } first := uploads.allocate(24, 16) or { panic(err) } second := uploads.allocate(24, 16) or { panic(err) } @@ -37,12 +37,12 @@ fn test_upload_ring_rejects_foreign_forged_and_destroyed_slices() { mut first_ring := UploadRing{ capacity: 32 mapped: first_storage.data - ranges: memory.new_ring_allocator(32) + ranges: mem.new_ring_allocator(32) } mut second_ring := UploadRing{ capacity: 32 mapped: second_storage.data - ranges: memory.new_ring_allocator(32) + ranges: mem.new_ring_allocator(32) } allocation := first_ring.allocate(8, 1) or { panic(err) } foreign := second_ring.allocate(8, 1) or { panic(err) } diff --git a/v.mod b/v.mod index aff825d..e7a6ddf 100644 --- a/v.mod +++ b/v.mod @@ -6,5 +6,5 @@ Module { license: 'MIT' repo_url: 'https://github.com/antono2/vulkan_memory_allocator' tags: ['V','vulkan','allocator'] - dependencies: ['antono2.vulkan','antono2.memory@v1.0.2'] + dependencies: ['antono2.vulkan','antono2.mem@v1.0.3'] }