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
33 changes: 29 additions & 4 deletions API_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ The generated `vulkan.v` and `vulkan_video.v` files remain the complete, low-lev
- Two-call enumerations return V arrays and internally retry `VK_INCOMPLETE`.
- Generated names and signatures are never edited to improve ergonomics. New wrappers compose them from the submodule.

## Discovery through queue submission
## Discovery through resources and queue submission

The first slice covers loader initialization, default-allocator instance creation and destruction, physical-device enumeration, core property snapshots, and owned device-name strings. Later slices add queue-family and logical-device selection, owned memory-backed buffers and images, command pools and primary command buffers, fences and binary semaphores, and checked queue submission:
The first slice covers loader initialization, default-allocator instance creation and destruction, physical-device enumeration, core property snapshots, and owned device-name strings. Later slices add queue-family and logical-device selection, owned memory-backed buffers and images, image views and explicit layout-transition recording, command pools and primary command buffers, fences and binary semaphores, and checked queue submission:

```v
import antono2.vulkan as vk
Expand Down Expand Up @@ -45,6 +45,16 @@ buffer := device.new_buffer(4096, usage, memory_properties)!
defer {
buffer.destroy()
}
image_usage := u32(vk.ImageUsageFlagBits.sampled) | u32(vk.ImageUsageFlagBits.transfer_dst)
image := device.new_image_2d(640, 480, .r8g8b8a8_unorm, .optimal, image_usage,
memory_properties)!
defer {
image.destroy()
}
view := image.new_view(u32(vk.ImageAspectFlagBits.color))!
defer {
view.destroy()
}
pool_flags := u32(vk.CommandPoolCreateFlagBits.reset_command_buffer)
pool := device.new_command_pool(pool_flags)!
defer {
Expand All @@ -55,7 +65,17 @@ defer {
command_buffer.free()
}
command_buffer.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))!
// Record commands with command_buffer.handle.
transition := vke.ImageLayoutTransition{
old_layout: .undefined
new_layout: .transfer_dst_optimal
src_stage_mask: u32(vk.PipelineStageFlagBits.top_of_pipe)
dst_stage_mask: u32(vk.PipelineStageFlagBits.transfer)
src_access_mask: 0
dst_access_mask: u32(vk.AccessFlagBits.transfer_write)
dependency_flags: 0
aspect_mask: u32(vk.ImageAspectFlagBits.color)
}
command_buffer.transition_image_layout(image, transition)!
command_buffer.end()!
mut fence := device.new_fence(false)!
defer {
Expand Down Expand Up @@ -89,6 +109,10 @@ println('${physical_device.name()}: queue family ${device.queue.family_index}')

`OwnedImage` creates a simple exclusive-sharing 2D image with one mip level, one array layer, and one sample. It exposes the raw image and memory handles plus its format, extent, tiling, usage, allocation size, and selected memory type. Destruction releases the image before its bound allocation, and must happen before destroying the parent device. More specialized image creation remains available through the raw layer.

`OwnedImageView` creates an identity-swizzled 2D view using the image's format and explicit aspect mask. It exposes the raw view and parent-image handles, view type, format, and complete subresource range. Every view must be destroyed before its image.

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

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
Expand All @@ -99,4 +123,5 @@ Custom allocation callbacks, concurrent-sharing buffers, queue priorities other
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.)
7. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.
7. Owned 2D image views and focused synchronization-1 layout-transition recording. (Implemented.)
8. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ The generated module remains the complete low-level binding. The opt-in
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, and checked queue submission without
modifying generated files.
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).

## Generate
Expand Down
101 changes: 101 additions & 0 deletions ergonomic/ergonomic.v
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,107 @@ pub fn (image OwnedImage) destroy() {
vk.free_memory(image.device, image.memory, unsafe { nil })
}

fn single_image_subresource_range(aspect_mask vk.ImageAspectFlags) vk.ImageSubresourceRange {
return vk.ImageSubresourceRange{
aspectMask: aspect_mask
baseMipLevel: 0
levelCount: 1
baseArrayLayer: 0
layerCount: 1
}
}

// OwnedImageView owns a two-dimensional view of one OwnedImage. The view must
// be destroyed before its image and parent Device.
pub struct OwnedImageView {
device vk.Device
pub:
handle vk.ImageView
image vk.Image
format vk.Format
view_type vk.ImageViewType
subresource_range vk.ImageSubresourceRange
}

// new_view creates an identity-swizzled 2D view over the image's single mip
// level and array layer. The aspect mask remains explicit because it depends
// on how the image format will be used.
pub fn (image OwnedImage) new_view(aspect_mask vk.ImageAspectFlags) !OwnedImageView {
if aspect_mask == 0 {
return error('image view aspect mask must not be empty')
}
subresource_range := single_image_subresource_range(aspect_mask)
create_info := vk.ImageViewCreateInfo{
image: image.handle
viewType: ._2d
format: image.format
components: vk.ComponentMapping{
r: .identity
g: .identity
b: .identity
a: .identity
}
subresourceRange: subresource_range
}
mut handle := vk.ImageView(unsafe { nil })
require_success(vk.create_image_view(image.device, &create_info, unsafe { nil }, &handle), 'vkCreateImageView')!
return OwnedImageView{
device: image.device
handle: handle
image: image.handle
format: image.format
view_type: ._2d
subresource_range: subresource_range
}
}

// destroy releases the view. Call it exactly once before destroying its image
// or parent Device.
pub fn (view OwnedImageView) destroy() {
vk.destroy_image_view(view.device, view.handle, unsafe { nil })
}

// ImageLayoutTransition describes one synchronization-1 image barrier. Stage
// masks, access masks, layouts, dependency flags, and aspects all remain
// explicit. Queue-family ownership transfers are intentionally out of scope.
pub struct ImageLayoutTransition {
pub:
old_layout vk.ImageLayout
new_layout vk.ImageLayout
src_stage_mask vk.PipelineStageFlags
dst_stage_mask vk.PipelineStageFlags
src_access_mask vk.AccessFlags
dst_access_mask vk.AccessFlags
dependency_flags vk.DependencyFlags
aspect_mask vk.ImageAspectFlags
}

// image_memory_barrier builds the raw barrier used by transition_image_layout.
// It covers the OwnedImage's single mip level and array layer.
pub fn (transition ImageLayoutTransition) image_memory_barrier(image OwnedImage) vk.ImageMemoryBarrier {
return vk.ImageMemoryBarrier{
srcAccessMask: transition.src_access_mask
dstAccessMask: transition.dst_access_mask
oldLayout: transition.old_layout
newLayout: transition.new_layout
srcQueueFamilyIndex: vk.queue_family_ignored
dstQueueFamilyIndex: vk.queue_family_ignored
image: image.handle
subresourceRange: single_image_subresource_range(transition.aspect_mask)
}
}

// transition_image_layout records one vkCmdPipelineBarrier for an OwnedImage.
// The command buffer must be recording. This helper does not track image state
// or perform queue-family ownership transfer.
pub fn (buffer PrimaryCommandBuffer) transition_image_layout(image OwnedImage, transition ImageLayoutTransition) ! {
if transition.aspect_mask == 0 {
return error('image transition aspect mask must not be empty')
}
barrier := transition.image_memory_barrier(image)
vk.cmd_pipeline_barrier(buffer.handle, transition.src_stage_mask, transition.dst_stage_mask, transition.dependency_flags, 0, unsafe { nil }, 0, unsafe { nil }, 1, &barrier)
}

// 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 {
Expand Down
83 changes: 83 additions & 0 deletions ergonomic/ergonomic_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,86 @@ fn test_owned_image_exposes_creation_and_allocation_metadata() {
assert image.allocation_size == 4096
assert image.memory_type_index == 2
}

fn test_new_image_view_rejects_empty_aspect_before_calling_vulkan() {
image := OwnedImage{
device: vk.Device(unsafe { nil })
handle: vk.Image(unsafe { nil })
format: .r8g8b8a8_unorm
}
image.new_view(0) or {
assert err.msg() == 'image view aspect mask must not be empty'
return
}
assert false
}

fn test_owned_image_view_exposes_parent_and_subresource_metadata() {
color := u32(vk.ImageAspectFlagBits.color)
view_handle := vk.ImageView(unsafe { nil })
image_handle := vk.Image(unsafe { nil })
view := OwnedImageView{
device: vk.Device(unsafe { nil })
handle: view_handle
image: image_handle
format: .r8g8b8a8_unorm
view_type: ._2d
subresource_range: single_image_subresource_range(color)
}

assert view.handle == view_handle
assert view.image == image_handle
assert view.format == .r8g8b8a8_unorm
assert view.view_type == ._2d
assert view.subresource_range.aspectMask == color
assert view.subresource_range.baseMipLevel == 0
assert view.subresource_range.levelCount == 1
assert view.subresource_range.baseArrayLayer == 0
assert view.subresource_range.layerCount == 1
}

fn test_image_layout_transition_builds_explicit_single_subresource_barrier() {
image := OwnedImage{
handle: vk.Image(unsafe { nil })
}
transition := ImageLayoutTransition{
old_layout: .undefined
new_layout: .transfer_dst_optimal
src_stage_mask: u32(vk.PipelineStageFlagBits.top_of_pipe)
dst_stage_mask: u32(vk.PipelineStageFlagBits.transfer)
src_access_mask: 0
dst_access_mask: u32(vk.AccessFlagBits.transfer_write)
dependency_flags: u32(vk.DependencyFlagBits.by_region)
aspect_mask: u32(vk.ImageAspectFlagBits.color)
}
barrier := transition.image_memory_barrier(image)

assert barrier.image == image.handle
assert barrier.oldLayout == .undefined
assert barrier.newLayout == .transfer_dst_optimal
assert barrier.srcAccessMask == 0
assert barrier.dstAccessMask == u32(vk.AccessFlagBits.transfer_write)
assert barrier.srcQueueFamilyIndex == vk.queue_family_ignored
assert barrier.dstQueueFamilyIndex == vk.queue_family_ignored
assert barrier.subresourceRange.aspectMask == u32(vk.ImageAspectFlagBits.color)
assert barrier.subresourceRange.levelCount == 1
assert barrier.subresourceRange.layerCount == 1
}

fn test_transition_image_layout_rejects_empty_aspect_before_calling_vulkan() {
buffer := PrimaryCommandBuffer{
handle: vk.CommandBuffer(unsafe { nil })
}
image := OwnedImage{
handle: vk.Image(unsafe { nil })
}
transition := ImageLayoutTransition{
old_layout: .undefined
new_layout: .general
}
buffer.transition_image_layout(image, transition) or {
assert err.msg() == 'image transition aspect mask must not be empty'
return
}
assert false
}
Loading