diff --git a/API_DESIGN.md b/API_DESIGN.md index 24a908f..69c6e05 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -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 @@ -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 { @@ -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 { @@ -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 @@ -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. diff --git a/README.md b/README.md index 67a008d..d47cefe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ergonomic/ergonomic.v b/ergonomic/ergonomic.v index 0d4163a..554df91 100644 --- a/ergonomic/ergonomic.v +++ b/ergonomic/ergonomic.v @@ -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 { diff --git a/ergonomic/ergonomic_test.v b/ergonomic/ergonomic_test.v index a1a5b7f..f75f2d0 100644 --- a/ergonomic/ergonomic_test.v +++ b/ergonomic/ergonomic_test.v @@ -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 +}