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
16 changes: 14 additions & 2 deletions API_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,31 @@ defer {
command_buffer.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))!
// Record commands with command_buffer.handle.
command_buffer.end()!
mut fence := device.new_fence(false)!
defer {
fence.destroy()
}
mut image_ready := device.new_semaphore()!
defer {
image_ready.destroy()
}
// Submit using fence.handle and image_ready.handle.
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.

`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.

`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.

Custom allocation callbacks, concurrent-sharing buffers, queue priorities other than 1.0, enabled features, and device extensions deliberately remain in the raw layer for now. A future configurable 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.
2. Presentation-support selection layered onto the core queue-flag helper.
3. Configurable queue requests, extension validation, and enabled features.
4. Owned images and synchronization objects, each with explicit parent ownership and destruction ordering.
5. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.
4. Owned fences and binary semaphores with explicit parent ownership and destruction ordering. (Implemented.)
5. Owned images with explicit parent ownership and destruction ordering.
6. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.
82 changes: 82 additions & 0 deletions ergonomic/ergonomic.v
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,88 @@ pub fn (buffer OwnedBuffer) destroy() {
vk.free_memory(buffer.device, buffer.memory, unsafe { nil })
}

// Fence owns a VkFence created by one Device. Its parent device must outlive
// it. The raw handle remains public for queue submission.
pub struct Fence {
device vk.Device
pub mut:
handle vk.Fence
}

// new_fence creates a fence, optionally in the signaled state.
pub fn (device Device) new_fence(signaled bool) !Fence {
flags := if signaled { u32(vk.FenceCreateFlagBits.signaled) } else { vk.FenceCreateFlags(0) }
create_info := vk.FenceCreateInfo{
flags: flags
}
mut handle := vk.Fence(unsafe { nil })
require_success(vk.create_fence(device.handle, &create_info, unsafe { nil }, &handle), 'vkCreateFence')!
return Fence{
device: device.handle
handle: handle
}
}

// status returns VK_SUCCESS when signaled and VK_NOT_READY otherwise.
pub fn (fence Fence) status() !vk.Result {
return check(vk.get_fence_status(fence.device, fence.handle), 'vkGetFenceStatus')
}

// is_signaled reports the current fence state.
pub fn (fence Fence) is_signaled() !bool {
return fence.status()! == .success
}

// wait blocks for at most timeout nanoseconds and returns VK_SUCCESS or
// VK_TIMEOUT so callers can distinguish completion from expiration.
pub fn (fence Fence) wait(timeout u64) !vk.Result {
return check(vk.wait_for_fences(fence.device, 1, &fence.handle, vk.Bool32(1), timeout), 'vkWaitForFences')
}

// reset returns the fence to the unsignaled state.
pub fn (fence Fence) reset() ! {
require_success(vk.reset_fences(fence.device, 1, &fence.handle), 'vkResetFences')!
}

// destroy releases the fence and clears its handle. Repeated calls are
// harmless, but the parent Device must still be alive.
pub fn (mut fence Fence) destroy() {
if isnil(fence.handle) {
return
}
vk.destroy_fence(fence.device, fence.handle, unsafe { nil })
fence.handle = vk.Fence(unsafe { nil })
}

// Semaphore owns a binary VkSemaphore created by one Device. Its raw handle
// remains public for submission and presentation structures.
pub struct Semaphore {
device vk.Device
pub mut:
handle vk.Semaphore
}

// new_semaphore creates a core binary semaphore.
pub fn (device Device) new_semaphore() !Semaphore {
create_info := vk.SemaphoreCreateInfo{}
mut handle := vk.Semaphore(unsafe { nil })
require_success(vk.create_semaphore(device.handle, &create_info, unsafe { nil }, &handle), 'vkCreateSemaphore')!
return Semaphore{
device: device.handle
handle: handle
}
}

// destroy releases the semaphore and clears its handle. Repeated calls are
// harmless, but the parent Device must still be alive.
pub fn (mut semaphore Semaphore) destroy() {
if isnil(semaphore.handle) {
return
}
vk.destroy_semaphore(semaphore.device, semaphore.handle, unsafe { nil })
semaphore.handle = vk.Semaphore(unsafe { nil })
}

// physical_devices performs Vulkan's count/fill enumeration pattern and
// retries when the available device set changes and VK_INCOMPLETE is returned.
pub fn (instance Instance) physical_devices() ![]PhysicalDevice {
Expand Down
36 changes: 36 additions & 0 deletions ergonomic/ergonomic_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,42 @@ fn test_free_is_idempotent_for_an_already_cleared_command_buffer() {
assert isnil(buffer.handle)
}

fn test_fence_and_semaphore_retain_parent_device_and_raw_handles() {
device_handle := vk.Device(unsafe { nil })
fence_handle := vk.Fence(unsafe { nil })
semaphore_handle := vk.Semaphore(unsafe { nil })
fence := Fence{
device: device_handle
handle: fence_handle
}
semaphore := Semaphore{
device: device_handle
handle: semaphore_handle
}

assert fence.device == device_handle
assert fence.handle == fence_handle
assert semaphore.device == device_handle
assert semaphore.handle == semaphore_handle
}

fn test_sync_destroy_is_idempotent_for_cleared_handles() {
mut fence := Fence{
device: vk.Device(unsafe { nil })
handle: vk.Fence(unsafe { nil })
}
mut semaphore := Semaphore{
device: vk.Device(unsafe { nil })
handle: vk.Semaphore(unsafe { nil })
}
fence.destroy()
fence.destroy()
semaphore.destroy()
semaphore.destroy()
assert isnil(fence.handle)
assert isnil(semaphore.handle)
}

fn memory_properties(types []vk.MemoryPropertyFlags) vk.PhysicalDeviceMemoryProperties {
mut properties := vk.PhysicalDeviceMemoryProperties{
memoryTypeCount: u32(types.len)
Expand Down
Loading