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
8 changes: 4 additions & 4 deletions API_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ device.queue.submit([command_buffer], submit_options)!
println('${physical_device.name()}: queue family ${device.queue.family_index}')
```

`Queue` is borrowed from its parent `Device` and becomes invalid when that device is destroyed. `OwnedBuffer` exposes its raw buffer and memory handles, requested size, allocation size, and selected memory-type index. Its `destroy()` method always destroys the buffer before freeing its memory; callers must destroy every buffer before destroying the parent device. `PhysicalDevice.find_memory_type()` applies both the resource's allowed-memory-type bit mask and the complete required property mask.
`Queue` is borrowed from its parent `Device` and becomes invalid when that device is destroyed. `Device.queues` contains every requested queue in request and queue-index order, while `Device.queue` remains the selected legacy queue or the first multi-queue request for compatibility. Queue requests are grouped by distinct family, and each priority maps to the same zero-based queue index in that family. The convenience API creates ordinary queues with zero `VkDeviceQueueCreateFlags`; protected or otherwise flagged queue configurations remain available through the raw API. `OwnedBuffer` exposes its raw buffer and memory handles, requested size, allocation size, and selected memory-type index. Its `destroy()` method always destroys the buffer before freeing its memory; callers must destroy every buffer before destroying the parent device. `PhysicalDevice.find_memory_type()` applies both the resource's allowed-memory-type bit mask and the complete required property mask.

`CommandPool` belongs to its parent `Device` and is fixed to that device's selected queue-family index. `PrimaryCommandBuffer` retains the exact device and pool handles needed by `free()`, while its public raw `handle` remains available for recording and submission. `free()` is idempotent and clears that raw handle. Reset, begin, and end failures are returned as typed `VulkanError` values. Destroying a command pool implicitly frees and invalidates all command buffers still allocated from it; callers may either free buffers explicitly before pool destruction or rely on that Vulkan lifetime rule, but must never use or free a buffer after its pool is destroyed. Every command pool must be destroyed before its parent device.
`CommandPool` belongs to its parent `Device` and is fixed to one requested queue-family index. `new_command_pool()` uses `Device.queue`; `new_command_pool_for_queue()` accepts any queue borrowed from that device and rejects foreign queues. `PrimaryCommandBuffer` retains the exact device and pool handles needed by `free()`, while its public raw `handle` remains available for recording and submission. `free()` is idempotent and clears that raw handle. Reset, begin, and end failures are returned as typed `VulkanError` values. Destroying a command pool implicitly frees and invalidates all command buffers still allocated from it; callers may either free buffers explicitly before pool destruction or rely on that Vulkan lifetime rule, but must never use or free a buffer after its pool is destroyed. Every command pool must be destroyed before its parent device.

`Fence` exposes status, timeout-aware waiting, and reset while preserving positive Vulkan statuses such as `VK_NOT_READY` and `VK_TIMEOUT`. `Fence` and `Semaphore` expose their raw handles for submission structures, clear those handles during idempotent destruction, and must be destroyed before their parent device.

Expand All @@ -114,13 +114,13 @@ println('${physical_device.name()}: queue family ${device.queue.family_index}')

`ImageLayoutTransition` keeps the synchronization-1 source/destination stage masks, access masks, old/new layouts, dependency flags, and aspect mask explicit. `PrimaryCommandBuffer.transition_image_layout()` records one image-only `vkCmdPipelineBarrier` over the owned image's single mip level and array layer. It does not infer synchronization, track layout state, or perform queue-family ownership transfers; use the raw API for broader ranges, ownership transfers, or synchronization-2 barriers.

`InstanceOptions` validates and owns instance layer/extension name pointers through creation. `DeviceOptions` configures one queue index and priority plus device extensions, core features, and an application-owned feature `pNext` chain. `PhysicalDevice.find_present_queue_family()` layers surface support over the existing queue-flag selection. Multiple queue requests, custom allocation callbacks, and concurrent-sharing resources remain in the raw layer. A future allocator-aware owning wrapper must retain the allocator used at creation so the same callbacks are supplied during destruction.
`InstanceOptions` validates and owns instance layer/extension name pointers through creation. `DeviceOptions` accepts either its compatible single-queue fields or `DeviceQueueRequest` values for multiple queues and families, plus device extensions, core features, and an application-owned feature `pNext` chain. `PhysicalDevice.find_present_queue_family()` layers surface support over the existing queue-flag selection. Custom allocation callbacks and concurrent-sharing resources remain in the raw layer. A future allocator-aware owning wrapper must retain the allocator used at creation so the same callbacks are supplied during destruction.

## Next slices

1. Instance extension and layer enumeration with owned V strings. (Implemented.)
2. Presentation-support selection layered onto the core queue-flag helper. (Implemented.)
3. Configurable queue requests, extension validation, and enabled features. (Single-queue configuration implemented; multiple queue requests remain.)
3. Configurable queue requests, extension validation, and enabled features. (Implemented, including multiple queues and families.)
4. Owned fences and binary semaphores with explicit parent ownership and destruction ordering. (Implemented.)
5. Owned 2D images with explicit parent ownership and destruction ordering. (Implemented.)
6. Checked primary command-buffer queue submission with explicit synchronization. (Implemented.)
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ This changelog tracks the semantic version of the V module in `v.mod`.
Generated binding snapshots continue to use the Vulkan registry version stored
in `VERSION`.

## Unreleased

### Added

- Multiple logical-device queue requests across distinct queue families, with
per-queue priorities, all borrowed queues exposed through `Device.queues`,
and queue-specific command-pool creation.

### Fixed

- Legacy nonzero queue indices now request every Vulkan queue through the
selected index before retrieving that queue.

## 1.5.0 - 2026-09-08

### Added
Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ Using GLFW and Dear ImGui [antono2/v_imgui_examples](https://github.com/antono2/

The generated module remains the complete low-level binding. The opt-in
`antono2.vulkan.ergonomic` submodule adds typed errors, instance lifecycle
helpers, physical-device and queue-family discovery, and single-queue logical
device ownership. It also provides explicit memory-type selection and owned
buffer/device-memory allocation, owned command pools and primary command-buffer
lifecycle helpers, synchronization objects, checked queue submission, owned 2D
images and views, and explicit image-layout transition recording without modifying
generated files.
helpers, physical-device and queue-family discovery, and validated single- or
multi-queue logical-device ownership. It also provides explicit memory-type
selection and owned buffer/device-memory allocation, owned command pools and
primary command-buffer lifecycle helpers, synchronization objects, checked queue
submission, owned 2D images and views, and explicit image-layout transition
recording without modifying generated files.
See [the ergonomic API design](API_DESIGN.md).

Instance and device configuration can validate requested names before Vulkan is
Expand All @@ -90,11 +90,18 @@ family := physical_device.find_queue_family(u32(vk.QueueFlagBits.graphics)) or {
return error('no graphics queue')
}
device := physical_device.new_device_with_options(vke.DeviceOptions{
queue_family: family
queue_requests: [vke.DeviceQueueRequest{
queue_family: family
priorities: [f32(1.0)]
}]
extensions: ['VK_KHR_swapchain']
})!
```

Each priority requests the queue at the same zero-based index in its family.
Use one `DeviceQueueRequest` per distinct family; `device.queues` exposes the
created queues in request order and `device.queue` remains the first one.

The CI lifecycle smoke test runs the ergonomic path from instance creation
through queue submission and cleanup under `VK_LAYER_KHRONOS_validation`.
Run it locally with:
Expand Down
150 changes: 124 additions & 26 deletions ergonomic/configuration.v
Original file line number Diff line number Diff line change
Expand Up @@ -53,48 +53,132 @@ pub fn new_instance_with_options(options InstanceOptions) !Instance {
return new_instance(&create_info)
}

// DeviceOptions configures the single queue exposed by Device together with
// device extensions, core features, and an optional feature pNext chain.
// DeviceQueueRequest requests consecutive queues from one family. Each
// priority maps to the queue at the same zero-based index in that family.
pub struct DeviceQueueRequest {
pub:
queue_family QueueFamily
priorities []f32 = [f32(1.0)]
}

// DeviceOptions configures logical-device queues together with device
// extensions, core features, and an optional feature pNext chain.
//
// queue_family, queue_index, and queue_priority preserve the original
// single-queue API. Set queue_requests to request queues from one or more
// families; it cannot be combined with those legacy fields. The first queue
// in queue_requests becomes Device.queue.
pub struct DeviceOptions {
pub:
queue_family QueueFamily
queue_index u32
queue_priority f32 = 1.0
queue_requests []DeviceQueueRequest
extensions []string
enabled_features &vk.PhysicalDeviceFeatures = unsafe { nil }
p_next voidptr = unsafe { nil }
}

// new_device_with_options validates the queue request and device extensions,
// then creates a logical device while retaining all temporary pointer arrays
// through vkCreateDevice.
pub fn (physical_device PhysicalDevice) new_device_with_options(options DeviceOptions) !Device {
if options.queue_family.properties.queueCount == 0 {
return error('queue family ${options.queue_family.index} has no queues')
struct DeviceQueuePlan {
requests []DeviceQueueRequest
primary_family_index u32
primary_queue_index u32
}

fn validate_device_queue_request(request DeviceQueueRequest) ! {
if request.queue_family.properties.queueCount == 0 {
return error('queue family ${request.queue_family.index} has no queues')
}
if request.priorities.len == 0 {
return error('queue family ${request.queue_family.index} must request at least one queue')
}
if request.priorities.len > int(request.queue_family.properties.queueCount) {
return error('requested ${request.priorities.len} queues from family ${request.queue_family.index}, which exposes ${request.queue_family.properties.queueCount}')
}
for queue_index, priority in request.priorities {
// Express this as an accepted range so NaN is rejected as well.
if !(priority >= 0.0 && priority <= 1.0) {
return error('queue priority for family ${request.queue_family.index} index ${queue_index} must be between 0.0 and 1.0')
}
}
}

fn device_queue_plan(options DeviceOptions) !DeviceQueuePlan {
if options.queue_requests.len == 0 {
if options.queue_family.properties.queueCount == 0 {
return error('queue family ${options.queue_family.index} has no queues')
}
if options.queue_index >= options.queue_family.properties.queueCount {
return error('queue index ${options.queue_index} is outside queue family ${options.queue_family.index}')
}
if !(options.queue_priority >= 0.0 && options.queue_priority <= 1.0) {
return error('queue priority must be between 0.0 and 1.0')
}

// Vulkan creates queues consecutively from index zero. Request every
// index through the selected legacy queue so vkGetDeviceQueue never
// targets a queue which was not created.
priorities := []f32{len: int(options.queue_index) + 1, init: options.queue_priority}
return DeviceQueuePlan{
requests: [
DeviceQueueRequest{
queue_family: options.queue_family
priorities: priorities
},
]
primary_family_index: options.queue_family.index
primary_queue_index: options.queue_index
}
}

if options.queue_family.properties.queueCount != 0 || options.queue_index != 0
|| options.queue_priority != 1.0 {
return error('queue_requests cannot be combined with queue_family, queue_index, or queue_priority')
}
if options.queue_index >= options.queue_family.properties.queueCount {
return error('queue index ${options.queue_index} is outside queue family ${options.queue_family.index}')

mut seen_families := map[u32]bool{}
for request in options.queue_requests {
validate_device_queue_request(request)!
if seen_families[request.queue_family.index] {
return error('queue family ${request.queue_family.index} is requested more than once')
}
seen_families[request.queue_family.index] = true
}
if options.queue_priority < 0.0 || options.queue_priority > 1.0 {
return error('queue priority must be between 0.0 and 1.0')
return DeviceQueuePlan{
requests: options.queue_requests.clone()
primary_family_index: options.queue_requests[0].queue_family.index
primary_queue_index: 0
}
}

// new_device_with_options validates queue requests and device extensions,
// then creates a logical device while retaining all temporary priority and
// pointer arrays through vkCreateDevice.
pub fn (physical_device PhysicalDevice) new_device_with_options(options DeviceOptions) !Device {
queue_plan := device_queue_plan(options)!
available_extensions := extension_names(physical_device.extensions()!)
validate_requested_names('device extensions', options.extensions, available_extensions)!

mut extension_pointers := []&char{cap: options.extensions.len}
for extension in options.extensions {
extension_pointers << extension.str
}
priority := options.queue_priority
queue_info := vk.DeviceQueueCreateInfo{
queueFamilyIndex: options.queue_family.index
queueCount: 1
pQueuePriorities: &priority
mut priority_groups := [][]f32{cap: queue_plan.requests.len}
for request in queue_plan.requests {
priority_groups << request.priorities.clone()
}
mut queue_infos := []vk.DeviceQueueCreateInfo{cap: queue_plan.requests.len}
for request_index, request in queue_plan.requests {
queue_infos << vk.DeviceQueueCreateInfo{
queueFamilyIndex: request.queue_family.index
queueCount: u32(priority_groups[request_index].len)
pQueuePriorities: priority_groups[request_index].data
}
}
create_info := vk.DeviceCreateInfo{
pNext: options.p_next
queueCreateInfoCount: 1
pQueueCreateInfos: &queue_info
queueCreateInfoCount: u32(queue_infos.len)
pQueueCreateInfos: queue_infos.data
enabledExtensionCount: u32(extension_pointers.len)
ppEnabledExtensionNames: extension_pointers.data
pEnabledFeatures: options.enabled_features
Expand All @@ -103,16 +187,30 @@ pub fn (physical_device PhysicalDevice) new_device_with_options(options DeviceOp
require_success(vk.create_device(physical_device.handle, &create_info, unsafe { nil }, &handle), 'vkCreateDevice')!
vk.load_device_commands(handle)

mut queue_handle := vk.Queue(unsafe { nil })
vk.get_device_queue(handle, options.queue_family.index, options.queue_index, &queue_handle)
mut queues := []Queue{}
mut primary_queue := Queue{}
for request in queue_plan.requests {
for queue_index in 0 .. request.priorities.len {
mut queue_handle := vk.Queue(unsafe { nil })
vk.get_device_queue(handle, request.queue_family.index, u32(queue_index), &queue_handle)
queue := Queue{
device: handle
handle: queue_handle
family_index: request.queue_family.index
index: u32(queue_index)
}
queues << queue
if queue.family_index == queue_plan.primary_family_index
&& queue.index == queue_plan.primary_queue_index {
primary_queue = queue
}
}
}
return Device{
physical_device: physical_device
handle: handle
queue: Queue{
handle: queue_handle
family_index: options.queue_family.index
index: options.queue_index
}
queue: primary_queue
queues: queues
}
}

Expand Down
Loading
Loading