From 3f3d49f04c84812ed6547d9f7198d341e65a9fb6 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 18:20:15 +0200 Subject: [PATCH 1/4] Validate completion-driven upload retirement against candidate memory --- docs/deferred-retirement-validation.md | 51 ++++++ .../buffer_suballocation/deferred_uploads.v | 153 ++++++++++++++++++ examples/buffer_suballocation/main.v | 5 +- 3 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 docs/deferred-retirement-validation.md create mode 100644 examples/buffer_suballocation/deferred_uploads.v diff --git a/docs/deferred-retirement-validation.md b/docs/deferred-retirement-validation.md new file mode 100644 index 0000000..3fc9f7a --- /dev/null +++ b/docs/deferred-retirement-validation.md @@ -0,0 +1,51 @@ +# Deferred upload retirement integration fixture + +This branch demonstrates a candidate integration for +[memory PR #34](https://github.com/antono2/memory/pull/34). It is ready for review +with that source revision in an isolated module overlay. The released allocator +still declares `antono2.memory@v1.4.0`; the candidate example must not be merged +or advertised as working with that release. Update the dependency and tested +manifest together after the new memory API is released. + +Allocator baseline: `b85f1ec7f29d3eb0442bff57e5cc3b1ee2ea7a26`. +Memory candidate: `8b2c2d8a59e2bb1cbb60b87a4e6212861f422f72`. +Vulkan dependency: `a8ca830921ca77a6826f5040054b7c330f2dfa06` (`v3.2.2`). + +`examples/buffer_suballocation/deferred_uploads.v` uses the existing example's +device and allocator. It registers upload slices in a two-entry retirement queue, +submits real buffer copies, observes Vulkan fences before reporting completion, +and releases slices in allocation order. A rejected full-queue registration +leaves upload accounting unchanged. After observing the first fence, another +allocation wraps to offset zero. A final fence and transfer-to-host barriers +permit independent mapped readback of values `21,22,23` before shutdown. + +The example performs no premature reuse even if the GPU finishes before the +application polls. Cleanup waits for actual device completion before draining +and destroying pending resources. Device-loss recovery was not fault-injected. +The existing example uses queue family zero; the tested devices provide a usable +transfer-capable queue there. The helper accepts the created queue family index. + +## Verified locally + +- Official released V 0.5.2 (`7647ce1`), Lavapipe, validation layer enabled: pass. +- Official released V 0.5.2, NVIDIA GeForce GTX 1060, validation layer enabled: pass. +- Pinned V `efae23e` with explicit V3, Lavapipe, validation layer enabled: pass. +- Output: `fenced retirement: copies=3 readback=21,22,23 wrap=0 pending=0`. +- No validation-layer errors were reported by these runs. + +The two existing instance/device descriptor locals are mutable for compatibility +with the pinned V3 compiler's pointer checking. No public UploadRing API changes +are required. This is Linux device evidence, not native Windows/macOS coverage. + +Reproduce with an overlay containing this allocator, candidate memory and Vulkan: + +```sh +export VMODULES=${VMODULES:?Set VMODULES to the isolated module directory} +compiler=${VEXE:-v} +VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \ + "$compiler" run examples/buffer_suballocation +``` + +Use the pinned current compiler and add `-new-compiler` for the strict V3 run. +To select a specific driver, set `VK_DRIVER_FILES` to an installed ICD manifest +on the current host. Otherwise the Vulkan loader selects the available driver. diff --git a/examples/buffer_suballocation/deferred_uploads.v b/examples/buffer_suballocation/deferred_uploads.v new file mode 100644 index 0000000..66e38ce --- /dev/null +++ b/examples/buffer_suballocation/deferred_uploads.v @@ -0,0 +1,153 @@ +// Candidate integration: requires the memory revision with RetirementQueue. +// Exercises real ordered queue submissions, fence observation and host readback. +module main + +import antono2.memory +import antono2.vkmemalloc as vma +import antono2.vulkan as vk + +fn record_upload(command vk.CommandBuffer, source vk.Buffer, destination vk.Buffer, slice vma.UploadSlice, offset u64) ! { + begin := vk.CommandBufferBeginInfo{ flags: u32(vk.CommandBufferUsageFlagBits.one_time_submit) } + require_success(vk.begin_command_buffer(command, &begin), 'begin deferred copy')! + region := vk.BufferCopy{ srcOffset: slice.offset, dstOffset: offset, size: 4 } + vk.cmd_copy_buffer(command, source, destination, 1, ®ion) + barrier := vk.BufferMemoryBarrier{ + srcAccessMask: u32(vk.AccessFlagBits.transfer_write) + dstAccessMask: u32(vk.AccessFlagBits.host_read) + srcQueueFamilyIndex: vk.queue_family_ignored + dstQueueFamilyIndex: vk.queue_family_ignored + buffer: destination + offset: offset + size: 4 + } + vk.cmd_pipeline_barrier(command, u32(vk.PipelineStageFlagBits.transfer), u32(vk.PipelineStageFlagBits.host), 0, + 0, unsafe { nil }, 1, &barrier, 0, unsafe { nil }) + require_success(vk.end_command_buffer(command), 'end deferred copy')! +} + +fn submit_copy(queue vk.Queue, command vk.CommandBuffer, fence vk.Fence) ! { + submit := vk.SubmitInfo{ commandBufferCount: 1, pCommandBuffers: &command } + require_success(vk.queue_submit(queue, 1, &submit, fence), 'submit deferred copy')! +} + +fn retire_completed(mut pending memory.RetirementQueue[vma.UploadSlice], mut uploads vma.UploadRing) int { + mut retired := 0 + for { + slice := pending.try_take_ready() or { break } + if !uploads.retire(slice) { panic('upload FIFO retirement failed') } + retired++ + } + return retired +} + +fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocator) ! { + mut queue := vk.Queue(unsafe { nil }) + vk.get_device_queue(device, family, 0, &queue) + pool_info := vk.CommandPoolCreateInfo{ queueFamilyIndex: family } + mut command_pool := vk.CommandPool(unsafe { nil }) + require_success(vk.create_command_pool(device, &pool_info, unsafe { nil }, &command_pool), 'create copy pool')! + defer { vk.destroy_command_pool(device, command_pool, unsafe { nil }) } + mut commands := unsafe { []vk.CommandBuffer{len: 3} } + allocate_info := vk.CommandBufferAllocateInfo{ commandPool: command_pool, level: .primary, commandBufferCount: 3 } + require_success(vk.allocate_command_buffers(device, &allocate_info, commands.data), 'allocate copy commands')! + mut fences := []vk.Fence{len: 3, init: vk.Fence(unsafe { nil })} + defer { + for fence in fences { + if !isnil(fence) { vk.destroy_fence(device, fence, unsafe { nil }) } + } + } + for index in 0 .. fences.len { + info := vk.FenceCreateInfo{} + mut fence := vk.Fence(unsafe { nil }) + require_success(vk.create_fence(device, &info, unsafe { nil }, &fence), 'create copy fence')! + fences[index] = fence + } + buffer_info := vk.BufferCreateInfo{ size: 16, usage: u32(vk.BufferUsageFlagBits.transfer_dst), sharingMode: .exclusive } + mut destination := vk.Buffer(unsafe { nil }) + mut destination_allocation := vma.AllocationInfo{} + require_success(allocator.create_buffer_with_options(&buffer_info, vma.AllocationOptions{ usage: .readback }, + &destination, mut destination_allocation), 'create readback destination')! + defer { + vk.destroy_buffer(device, destination, unsafe { nil }) + _ = allocator.release(mut destination_allocation) + } + mut uploads := vma.new_upload_ring_with_options(mut allocator, 1024, vma.AllocationOptions{ usage: .upload })! + mut pending := memory.new_retirement_queue[vma.UploadSlice](2)! + defer { + // This runs before buffer/fence/command cleanup, including on error paths. + idle := vk.device_wait_idle(device) + if idle == .success { + pending.advance_completed(~u64(0)) or { panic(err) } + _ = retire_completed(mut pending, mut uploads) + _ = uploads.destroy() + } else { + // A lost device is not completion evidence. Destroy the API object as + // part of device teardown without falsely reporting completed tokens. + vk.destroy_buffer(device, uploads.buffer, unsafe { nil }) + } + } + for index in 0 .. 2 { + if pending.is_full() { + return error('unexpected queue backpressure') + } + slice := uploads.allocate(400, 16)! + pending.enqueue(slice, u64(index + 1))! + unsafe { *(&u32(slice.data)) = u32(21 + index) } + require_success(uploads.flush(slice), 'flush queued upload')! + record_upload(commands[index], uploads.buffer, destination, slice, u64(index * 4))! + submit_copy(queue, commands[index], fences[index])! + } + // No reuse before the application observes completion, even if the GPU has + // already finished. A full queue must not cause another ring allocation. + if pending.try_take_ready() != none { + return error('premature retirement') + } + if !pending.is_full() { + return error('expected bounded queue exhaustion') + } + before := uploads.stats() + if _ := pending.enqueue(vma.UploadSlice{}, 3) { + return error('expected backpressure') + } + if uploads.stats() != before { + return error('backpressure changed upload allocation state') + } + first_fence := fences[0] + require_success(vk.wait_for_fences(device, 1, &first_fence, vk._true, 10_000_000_000), 'wait first copy')! + pending.advance_completed(1)! + if retire_completed(mut pending, mut uploads) != 1 { + return error('expected first retirement only') + } + wrapped := uploads.allocate(300, 16)! + pending.enqueue(wrapped, 3)! + if wrapped.offset != 0 { + return error('expected upload wraparound') + } + unsafe { *(&u32(wrapped.data)) = 23 } + require_success(uploads.flush(wrapped), 'flush wrapped queued upload')! + record_upload(commands[2], uploads.buffer, destination, wrapped, 8)! + submit_copy(queue, commands[2], fences[2])! + // One queue executes these submissions in order. The final fence covers all + // three copies and their transfer-to-host barriers; unrelated queues would + // require separate completion domains or explicit synchronization. + last_fence := fences[2] + require_success(vk.wait_for_fences(device, 1, &last_fence, vk._true, 10_000_000_000), 'wait final copy')! + pending.advance_completed(3)! + if retire_completed(mut pending, mut uploads) != 2 { + return error('expected final two retirements') + } + mut mapped := voidptr(unsafe { nil }) + require_success(allocator.map(mut destination_allocation, &mapped), 'map copy readback')! + defer { allocator.unmap(mut destination_allocation) } + require_success(allocator.invalidate(destination_allocation), 'invalidate copy readback')! + for index in 0 .. 3 { + actual := unsafe { *(&u32(usize(mapped) + usize(index * 4))) } + if actual != u32(21 + index) { + return error('independent copy readback mismatch at ${index}: ${actual}') + } + } + if !pending.is_empty() || uploads.stats().allocation_count != 0 { + return error('pending work at shutdown') + } + println('fenced retirement: copies=3 readback=21,22,23 wrap=0 pending=0') +} diff --git a/examples/buffer_suballocation/main.v b/examples/buffer_suballocation/main.v index d917369..6035433 100644 --- a/examples/buffer_suballocation/main.v +++ b/examples/buffer_suballocation/main.v @@ -26,7 +26,7 @@ fn first_physical_device(instance vk.Instance) !vk.PhysicalDevice { fn run() ! { require_success(vk.initialize_loader(), 'initialize Vulkan loader')! - application_info := vk.ApplicationInfo{ + mut application_info := vk.ApplicationInfo{ pApplicationName: c'vkmemalloc suballocation example' applicationVersion: 1 pEngineName: c'none' @@ -50,7 +50,7 @@ fn run() ! { device_extensions << vk.ext_memory_budget_extension_name } mut priority := f32(1) - queue_info := vk.DeviceQueueCreateInfo{ + mut queue_info := vk.DeviceQueueCreateInfo{ queueFamilyIndex: 0 queueCount: 1 pQueuePriorities: &priority @@ -242,6 +242,7 @@ fn run() ! { uploads_destroyed := uploads.destroy() assert uploads_destroyed assert allocator.stats().block_count == 0 + run_deferred_uploads(device, 0, mut allocator)! } fn main() { From f150059aa8a0c3e82ebe5083aacb2af576778b95 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 18:33:55 +0200 Subject: [PATCH 2/4] Prepare fenced upload integration with memory v1.5.0 --- CHANGELOG.md | 10 +++ README.md | 7 +- compatibility.json | 2 +- ...t-validation.md => deferred-retirement.md} | 20 ++---- .../buffer_suballocation/deferred_uploads.v | 66 ++++++++++++++----- v.mod | 4 +- 6 files changed, 74 insertions(+), 35 deletions(-) rename docs/{deferred-retirement-validation.md => deferred-retirement.md} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e72fc2f..3f71b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## 2.7.0 - 2026-10-14 + +- Pin `antono2.memory` v1.5.0 and extend the buffer example with bounded, + completion-driven upload retirement. +- Submit real buffer copies, observe fences before reuse, exercise upload-ring + wraparound and backpressure, and verify independent host readback. +- Keep the existing `UploadRing` API and caller-owned synchronization contract. +- Document completion domains, cleanup ordering and portable reproduction steps. +- Make Vulkan descriptor locals compatible with current V pointer checking. + ## 2.6.0 - 2026-09-12 - Add explicit `create_suballocated_image()` and diff --git a/README.md b/README.md index 2d74c5e..c48a046 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,17 @@ 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. -The allocator uses the production-hardened v1.4 release of +The allocator uses the v1.5 release of [`antono2.memory`](https://github.com/antono2/memory), specifically `memory.RangeAllocator`, for its dependency-free block suballocation policy. Vulkan handles remain isolated in this module. +The buffer-suballocation example also demonstrates +[fenced upload retirement](docs/deferred-retirement.md) using +`memory.RetirementQueue`. It observes real Vulkan fence completion, preserves +FIFO slice release and verifies copied data through mapped readback. + ## Basic use Create one allocator after selecting a physical device and creating its logical diff --git a/compatibility.json b/compatibility.json index fa726e4..4b16e92 100644 --- a/compatibility.json +++ b/compatibility.json @@ -5,7 +5,7 @@ "ci_runtime": "v1.2", "modules": { "vulkan": "v3.2.2", - "memory": "v1.4.0" + "memory": "v1.5.0" }, "build_tools": [ "git", diff --git a/docs/deferred-retirement-validation.md b/docs/deferred-retirement.md similarity index 70% rename from docs/deferred-retirement-validation.md rename to docs/deferred-retirement.md index 3fc9f7a..3c943b0 100644 --- a/docs/deferred-retirement-validation.md +++ b/docs/deferred-retirement.md @@ -1,15 +1,9 @@ -# Deferred upload retirement integration fixture +# Fenced upload retirement -This branch demonstrates a candidate integration for -[memory PR #34](https://github.com/antono2/memory/pull/34). It is ready for review -with that source revision in an isolated module overlay. The released allocator -still declares `antono2.memory@v1.4.0`; the candidate example must not be merged -or advertised as working with that release. Update the dependency and tested -manifest together after the new memory API is released. - -Allocator baseline: `b85f1ec7f29d3eb0442bff57e5cc3b1ee2ea7a26`. -Memory candidate: `8b2c2d8a59e2bb1cbb60b87a4e6212861f422f72`. -Vulkan dependency: `a8ca830921ca77a6826f5040054b7c330f2dfa06` (`v3.2.2`). +This example uses `antono2.memory@v1.5.0` and the Vulkan dependency pinned in +`v.mod` and `compatibility.json`. Install the declared dependencies with the +repository setup script. If using an isolated module directory, set `VMODULES` +to that directory before setup and validation. `examples/buffer_suballocation/deferred_uploads.v` uses the existing example's device and allocator. It registers upload slices in a two-entry retirement queue, @@ -37,11 +31,11 @@ The two existing instance/device descriptor locals are mutable for compatibility with the pinned V3 compiler's pointer checking. No public UploadRing API changes are required. This is Linux device evidence, not native Windows/macOS coverage. -Reproduce with an overlay containing this allocator, candidate memory and Vulkan: +Run from the repository root with the native Vulkan prerequisites installed: ```sh -export VMODULES=${VMODULES:?Set VMODULES to the isolated module directory} compiler=${VEXE:-v} +"$compiler" run setup.vsh VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \ "$compiler" run examples/buffer_suballocation ``` diff --git a/examples/buffer_suballocation/deferred_uploads.v b/examples/buffer_suballocation/deferred_uploads.v index 66e38ce..5cd6d22 100644 --- a/examples/buffer_suballocation/deferred_uploads.v +++ b/examples/buffer_suballocation/deferred_uploads.v @@ -1,4 +1,4 @@ -// Candidate integration: requires the memory revision with RetirementQueue. +// Uses memory v1.5's bounded completion queue with actual Vulkan fences. // Exercises real ordered queue submissions, fence observation and host readback. module main @@ -7,9 +7,15 @@ import antono2.vkmemalloc as vma import antono2.vulkan as vk fn record_upload(command vk.CommandBuffer, source vk.Buffer, destination vk.Buffer, slice vma.UploadSlice, offset u64) ! { - begin := vk.CommandBufferBeginInfo{ flags: u32(vk.CommandBufferUsageFlagBits.one_time_submit) } + begin := vk.CommandBufferBeginInfo{ + flags: u32(vk.CommandBufferUsageFlagBits.one_time_submit) + } require_success(vk.begin_command_buffer(command, &begin), 'begin deferred copy')! - region := vk.BufferCopy{ srcOffset: slice.offset, dstOffset: offset, size: 4 } + region := vk.BufferCopy{ + srcOffset: slice.offset + dstOffset: offset + size: 4 + } vk.cmd_copy_buffer(command, source, destination, 1, ®ion) barrier := vk.BufferMemoryBarrier{ srcAccessMask: u32(vk.AccessFlagBits.transfer_write) @@ -20,13 +26,16 @@ fn record_upload(command vk.CommandBuffer, source vk.Buffer, destination vk.Buff offset: offset size: 4 } - vk.cmd_pipeline_barrier(command, u32(vk.PipelineStageFlagBits.transfer), u32(vk.PipelineStageFlagBits.host), 0, - 0, unsafe { nil }, 1, &barrier, 0, unsafe { nil }) + vk.cmd_pipeline_barrier(command, u32(vk.PipelineStageFlagBits.transfer), + u32(vk.PipelineStageFlagBits.host), 0, 0, unsafe { nil }, 1, &barrier, 0, unsafe { nil }) require_success(vk.end_command_buffer(command), 'end deferred copy')! } fn submit_copy(queue vk.Queue, command vk.CommandBuffer, fence vk.Fence) ! { - submit := vk.SubmitInfo{ commandBufferCount: 1, pCommandBuffers: &command } + submit := vk.SubmitInfo{ + commandBufferCount: 1 + pCommandBuffers: &command + } require_success(vk.queue_submit(queue, 1, &submit, fence), 'submit deferred copy')! } @@ -43,13 +52,21 @@ fn retire_completed(mut pending memory.RetirementQueue[vma.UploadSlice], mut upl fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocator) ! { mut queue := vk.Queue(unsafe { nil }) vk.get_device_queue(device, family, 0, &queue) - pool_info := vk.CommandPoolCreateInfo{ queueFamilyIndex: family } + pool_info := vk.CommandPoolCreateInfo{ + queueFamilyIndex: family + } mut command_pool := vk.CommandPool(unsafe { nil }) - require_success(vk.create_command_pool(device, &pool_info, unsafe { nil }, &command_pool), 'create copy pool')! + require_success(vk.create_command_pool(device, &pool_info, unsafe { nil }, &command_pool), + 'create copy pool')! defer { vk.destroy_command_pool(device, command_pool, unsafe { nil }) } mut commands := unsafe { []vk.CommandBuffer{len: 3} } - allocate_info := vk.CommandBufferAllocateInfo{ commandPool: command_pool, level: .primary, commandBufferCount: 3 } - require_success(vk.allocate_command_buffers(device, &allocate_info, commands.data), 'allocate copy commands')! + allocate_info := vk.CommandBufferAllocateInfo{ + commandPool: command_pool + level: .primary + commandBufferCount: 3 + } + require_success(vk.allocate_command_buffers(device, &allocate_info, commands.data), + 'allocate copy commands')! mut fences := []vk.Fence{len: 3, init: vk.Fence(unsafe { nil })} defer { for fence in fences { @@ -62,16 +79,23 @@ fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocato require_success(vk.create_fence(device, &info, unsafe { nil }, &fence), 'create copy fence')! fences[index] = fence } - buffer_info := vk.BufferCreateInfo{ size: 16, usage: u32(vk.BufferUsageFlagBits.transfer_dst), sharingMode: .exclusive } + buffer_info := vk.BufferCreateInfo{ + size: 16 + usage: u32(vk.BufferUsageFlagBits.transfer_dst) + sharingMode: .exclusive + } mut destination := vk.Buffer(unsafe { nil }) mut destination_allocation := vma.AllocationInfo{} - require_success(allocator.create_buffer_with_options(&buffer_info, vma.AllocationOptions{ usage: .readback }, - &destination, mut destination_allocation), 'create readback destination')! + require_success(allocator.create_buffer_with_options(&buffer_info, vma.AllocationOptions{ + usage: .readback + }, &destination, mut destination_allocation), 'create readback destination')! defer { vk.destroy_buffer(device, destination, unsafe { nil }) _ = allocator.release(mut destination_allocation) } - mut uploads := vma.new_upload_ring_with_options(mut allocator, 1024, vma.AllocationOptions{ usage: .upload })! + mut uploads := vma.new_upload_ring_with_options(mut allocator, 1024, vma.AllocationOptions{ + usage: .upload + })! mut pending := memory.new_retirement_queue[vma.UploadSlice](2)! defer { // This runs before buffer/fence/command cleanup, including on error paths. @@ -92,7 +116,9 @@ fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocato } slice := uploads.allocate(400, 16)! pending.enqueue(slice, u64(index + 1))! - unsafe { *(&u32(slice.data)) = u32(21 + index) } + unsafe { + *(&u32(slice.data)) = u32(21 + index) + } require_success(uploads.flush(slice), 'flush queued upload')! record_upload(commands[index], uploads.buffer, destination, slice, u64(index * 4))! submit_copy(queue, commands[index], fences[index])! @@ -113,7 +139,8 @@ fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocato return error('backpressure changed upload allocation state') } first_fence := fences[0] - require_success(vk.wait_for_fences(device, 1, &first_fence, vk._true, 10_000_000_000), 'wait first copy')! + require_success(vk.wait_for_fences(device, 1, &first_fence, vk._true, 10_000_000_000), + 'wait first copy')! pending.advance_completed(1)! if retire_completed(mut pending, mut uploads) != 1 { return error('expected first retirement only') @@ -123,7 +150,9 @@ fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocato if wrapped.offset != 0 { return error('expected upload wraparound') } - unsafe { *(&u32(wrapped.data)) = 23 } + unsafe { + *(&u32(wrapped.data)) = 23 + } require_success(uploads.flush(wrapped), 'flush wrapped queued upload')! record_upload(commands[2], uploads.buffer, destination, wrapped, 8)! submit_copy(queue, commands[2], fences[2])! @@ -131,7 +160,8 @@ fn run_deferred_uploads(device vk.Device, family u32, mut allocator vma.Allocato // three copies and their transfer-to-host barriers; unrelated queues would // require separate completion domains or explicit synchronization. last_fence := fences[2] - require_success(vk.wait_for_fences(device, 1, &last_fence, vk._true, 10_000_000_000), 'wait final copy')! + require_success(vk.wait_for_fences(device, 1, &last_fence, vk._true, 10_000_000_000), + 'wait final copy')! pending.advance_completed(3)! if retire_completed(mut pending, mut uploads) != 2 { return error('expected final two retirements') diff --git a/v.mod b/v.mod index 8784f39..9def3a4 100644 --- a/v.mod +++ b/v.mod @@ -2,9 +2,9 @@ Module { name: 'antono2.vkmemalloc' author: 'Anton Oreskin' description: 'Policy-driven Vulkan memory selection, suballocation, mapping, and diagnostics' - version: '2.6.2' + version: '2.7.0' license: 'MIT' repo_url: 'https://github.com/antono2/vulkan_memory_allocator' tags: ['V','vulkan','allocator'] - dependencies: ['antono2.vulkan@v3.2.2','antono2.memory@v1.4.0'] + dependencies: ['antono2.vulkan@v3.2.2','antono2.memory@v1.5.0'] } From bc3fda260a60a179ee70ee15aba78600380590eb Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 18:37:32 +0200 Subject: [PATCH 3/4] Discover a transfer-capable queue for fenced copy validation --- docs/deferred-retirement.md | 5 ++- .../buffer_suballocation/deferred_uploads.v | 24 ++++++++++++ examples/buffer_suballocation/main.v | 5 ++- .../buffer_suballocation/queue_family_test.v | 38 +++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 examples/buffer_suballocation/queue_family_test.v diff --git a/docs/deferred-retirement.md b/docs/deferred-retirement.md index 3c943b0..613a0ee 100644 --- a/docs/deferred-retirement.md +++ b/docs/deferred-retirement.md @@ -16,8 +16,9 @@ permit independent mapped readback of values `21,22,23` before shutdown. The example performs no premature reuse even if the GPU finishes before the application polls. Cleanup waits for actual device completion before draining and destroying pending resources. Device-loss recovery was not fault-injected. -The existing example uses queue family zero; the tested devices provide a usable -transfer-capable queue there. The helper accepts the created queue family index. +The example discovers an available transfer-capable queue before creating the +device. Graphics and compute queues qualify through their implicit transfer +support. The helper receives the created queue family index. ## Verified locally diff --git a/examples/buffer_suballocation/deferred_uploads.v b/examples/buffer_suballocation/deferred_uploads.v index 5cd6d22..9f0fa0a 100644 --- a/examples/buffer_suballocation/deferred_uploads.v +++ b/examples/buffer_suballocation/deferred_uploads.v @@ -6,6 +6,30 @@ import antono2.memory import antono2.vkmemalloc as vma import antono2.vulkan as vk +// Graphics and compute queues implicitly support transfer operations even if +// the driver omits the explicit transfer flag. Ignore unavailable queues. +fn select_transfer_family(properties []vk.QueueFamilyProperties) ?u32 { + capabilities := u32(vk.QueueFlagBits.graphics) | u32(vk.QueueFlagBits.compute) | u32(vk.QueueFlagBits.transfer) + for index, property in properties { + if property.queueCount > 0 && property.queueFlags & capabilities != 0 { + return u32(index) + } + } + return none +} + +fn transfer_queue_family(physical_device vk.PhysicalDevice) !u32 { + mut count := u32(0) + mut no_properties := unsafe { nil } + vk.get_physical_device_queue_family_properties(physical_device, &count, mut no_properties) + if count == 0 { return error('physical device has no queue families') } + mut properties := []vk.QueueFamilyProperties{len: int(count)} + vk.get_physical_device_queue_family_properties(physical_device, &count, mut properties[0]) + return select_transfer_family(properties[..int(count)]) or { + return error('physical device has no transfer-capable queue family') + } +} + fn record_upload(command vk.CommandBuffer, source vk.Buffer, destination vk.Buffer, slice vma.UploadSlice, offset u64) ! { begin := vk.CommandBufferBeginInfo{ flags: u32(vk.CommandBufferUsageFlagBits.one_time_submit) diff --git a/examples/buffer_suballocation/main.v b/examples/buffer_suballocation/main.v index 6035433..c85f67a 100644 --- a/examples/buffer_suballocation/main.v +++ b/examples/buffer_suballocation/main.v @@ -44,6 +44,7 @@ fn run() ! { vk.load_instance_commands(instance) physical_device := first_physical_device(instance)! + transfer_family := transfer_queue_family(physical_device)! memory_budget_supported := vma.supports_memory_budget(physical_device) mut device_extensions := []&char{} if memory_budget_supported { @@ -51,7 +52,7 @@ fn run() ! { } mut priority := f32(1) mut queue_info := vk.DeviceQueueCreateInfo{ - queueFamilyIndex: 0 + queueFamilyIndex: transfer_family queueCount: 1 pQueuePriorities: &priority } @@ -242,7 +243,7 @@ fn run() ! { uploads_destroyed := uploads.destroy() assert uploads_destroyed assert allocator.stats().block_count == 0 - run_deferred_uploads(device, 0, mut allocator)! + run_deferred_uploads(device, transfer_family, mut allocator)! } fn main() { diff --git a/examples/buffer_suballocation/queue_family_test.v b/examples/buffer_suballocation/queue_family_test.v new file mode 100644 index 0000000..3e5c7b8 --- /dev/null +++ b/examples/buffer_suballocation/queue_family_test.v @@ -0,0 +1,38 @@ +// Checks unavailable and non-transfer families before device creation. +module main + +import antono2.vulkan as vk + +fn test_transfer_family_selection_rejects_unavailable_and_sparse_only_queues() { + if _ := select_transfer_family([]vk.QueueFamilyProperties{}) { assert false + } + if _ := select_transfer_family([ + vk.QueueFamilyProperties{ queueCount: 0, queueFlags: u32(vk.QueueFlagBits.transfer) }, + vk.QueueFamilyProperties{ queueCount: 1, queueFlags: u32(vk.QueueFlagBits.sparse_binding) }, + ]) + { + assert false + } + families := [ + vk.QueueFamilyProperties{ + queueCount: 1 + queueFlags: u32(vk.QueueFlagBits.sparse_binding) + }, + vk.QueueFamilyProperties{ + queueCount: 0 + queueFlags: u32(vk.QueueFlagBits.graphics) + }, + vk.QueueFamilyProperties{ + queueCount: 1 + queueFlags: u32(vk.QueueFlagBits.compute) + }, + vk.QueueFamilyProperties{ + queueCount: 1 + queueFlags: u32(vk.QueueFlagBits.transfer) + }, + ] + assert (select_transfer_family(families) or { panic('missing compute transfer support') }) == 2 + assert (select_transfer_family([ + vk.QueueFamilyProperties{ queueCount: 1, queueFlags: u32(vk.QueueFlagBits.graphics) }, + ]) or { panic('missing graphics transfer support') }) == 0 +} From 8c65f6633e25f6d346d5e967e345b908f554adf2 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 18:47:19 +0200 Subject: [PATCH 4/4] Exercise fenced upload readback on native Windows with SwiftShader --- .github/workflows/test.yml | 6 +++++- CHANGELOG.md | 2 ++ docs/deferred-retirement.md | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2bfc55..ff4ba7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,7 +142,7 @@ jobs: - name: Verify the public setup diagnostics working-directory: source/modules/antono2/vkmemalloc run: v run setup.vsh --check - - name: Test with the Microsoft C compiler + - name: Test and run fenced uploads with the Microsoft C compiler working-directory: source/modules/antono2/vkmemalloc shell: pwsh run: | @@ -152,6 +152,10 @@ jobs: Import-Module (Join-Path $InstallPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') Enter-VsDevShell -VsInstallPath $InstallPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' v -cc msvc test . + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $env:VK_INSTANCE_LAYERS = 'VK_LAYER_KHRONOS_validation' + v -cc msvc run examples/buffer_suballocation + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } linux-v-master: name: Advisory current V master and Vulkan master diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f71b77..3184ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. - Keep the existing `UploadRing` API and caller-owned synchronization contract. - Document completion domains, cleanup ordering and portable reproduction steps. - Make Vulkan descriptor locals compatible with current V pointer checking. +- Discover a transfer-capable queue and run fenced upload checks on Linux with + Lavapipe and Windows with MSVC/SwiftShader and Vulkan validation enabled. ## 2.6.0 - 2026-09-12 diff --git a/docs/deferred-retirement.md b/docs/deferred-retirement.md index 613a0ee..f88980a 100644 --- a/docs/deferred-retirement.md +++ b/docs/deferred-retirement.md @@ -30,7 +30,9 @@ support. The helper receives the created queue family index. The two existing instance/device descriptor locals are mutable for compatibility with the pinned V3 compiler's pointer checking. No public UploadRing API changes -are required. This is Linux device evidence, not native Windows/macOS coverage. +are required. These local results are Linux device evidence. CI separately runs +the fenced-copy program on native Windows with MSVC, SwiftShader and validation +enabled. No native macOS runtime coverage is claimed. Run from the repository root with the native Vulkan prerequisites installed: