From a0551ad50667ecbc8fb9a2a6e714074212ce7ed4 Mon Sep 17 00:00:00 2001 From: Anton Oreskin Date: Thu, 24 Sep 2026 18:49:42 +0200 Subject: [PATCH] Support mip ranges in ergonomic images and transitions --- ergonomic/ergonomic.v | 64 +++++++++++++++++++++++++------ ergonomic/ergonomic_test.v | 77 +++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/ergonomic/ergonomic.v b/ergonomic/ergonomic.v index 6d6a0ca..984b94a 100644 --- a/ergonomic/ergonomic.v +++ b/ergonomic/ergonomic.v @@ -432,7 +432,7 @@ pub fn (mut buffer OwnedBuffer) destroy() { } // OwnedImage owns a two-dimensional VkImage and its bound VkDeviceMemory. -// The image uses one mip level, one array layer, and one sample. +// The image uses one array layer and one sample. @[nocopy] pub struct OwnedImage { device vk.Device @@ -443,6 +443,7 @@ pub mut: pub: format vk.Format extent vk.Extent3D + mip_levels u32 = 1 tiling vk.ImageTiling usage vk.ImageUsageFlags allocation_size vk.DeviceSize @@ -453,9 +454,19 @@ pub: // all required properties, allocates it, and binds it at offset zero. pub fn (device &OwnedDevice) new_image_2d(width u32, height u32, format vk.Format, tiling vk.ImageTiling, usage vk.ImageUsageFlags, required_memory_properties vk.MemoryPropertyFlags) !&OwnedImage { + return device.new_image_2d_mips(width, height, 1, format, tiling, usage, required_memory_properties) +} + +// new_image_2d_mips creates an image with a complete or partial mip chain. +pub fn (device &OwnedDevice) new_image_2d_mips(width u32, height u32, mip_levels u32, + format vk.Format, tiling vk.ImageTiling, usage vk.ImageUsageFlags, + required_memory_properties vk.MemoryPropertyFlags) !&OwnedImage { if width == 0 || height == 0 { return error('image width and height must be greater than zero') } + if mip_levels == 0 || mip_levels > max_image_mip_levels(width, height) { + return error('image mip level count exceeds extent') + } if usage == 0 { return error('image usage must not be empty') } @@ -468,7 +479,7 @@ pub fn (device &OwnedDevice) new_image_2d(width u32, height u32, format vk.Forma imageType: ._2d format: format extent: extent - mipLevels: 1 + mipLevels: mip_levels arrayLayers: 1 samples: ._1 tiling: tiling @@ -510,6 +521,7 @@ pub fn (device &OwnedDevice) new_image_2d(width u32, height u32, format vk.Forma memory: memory format: format extent: extent + mip_levels: mip_levels tiling: tiling usage: usage allocation_size: requirements.size @@ -517,6 +529,16 @@ pub fn (device &OwnedDevice) new_image_2d(width u32, height u32, format vk.Forma } } +fn max_image_mip_levels(width u32, height u32) u32 { + mut largest := if width > height { width } else { height } + mut levels := u32(0) + for largest > 0 { + levels++ + largest >>= 1 + } + return levels +} + // destroy first destroys the image, then frees its bound memory. Repeated // calls are harmless while the parent OwnedDevice remains alive. pub fn (mut image OwnedImage) destroy() { @@ -530,11 +552,11 @@ pub fn (mut image OwnedImage) destroy() { } } -fn single_image_subresource_range(aspect_mask vk.ImageAspectFlags) vk.ImageSubresourceRange { +fn image_subresource_range(aspect_mask vk.ImageAspectFlags, base_mip_level u32, level_count u32) vk.ImageSubresourceRange { return vk.ImageSubresourceRange{ aspectMask: aspect_mask - baseMipLevel: 0 - levelCount: 1 + baseMipLevel: base_mip_level + levelCount: level_count baseArrayLayer: 0 layerCount: 1 } @@ -560,17 +582,25 @@ pub: 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. +// new_view creates an identity-swizzled 2D view over every mip level. pub fn (image &OwnedImage) new_view(aspect_mask vk.ImageAspectFlags) !&OwnedImageView { + return image.new_view_mips(aspect_mask, 0, image.mip_levels) +} + +// new_view_mips creates a view over a validated contiguous mip range. +pub fn (image &OwnedImage) new_view_mips(aspect_mask vk.ImageAspectFlags, base_mip_level u32, + level_count u32) !&OwnedImageView { if aspect_mask == 0 { return error('image view aspect mask must not be empty') } + if level_count == 0 || base_mip_level >= image.mip_levels + || level_count > image.mip_levels - base_mip_level { + return error('image view mip range exceeds image') + } if !image_usage_supports_view(image.usage) { return error('image usage does not support image views') } - subresource_range := single_image_subresource_range(aspect_mask) + subresource_range := image_subresource_range(aspect_mask, base_mip_level, level_count) create_info := vk.ImageViewCreateInfo{ image: image.handle viewType: ._2d @@ -620,11 +650,18 @@ pub: dst_access_mask vk.AccessFlags dependency_flags vk.DependencyFlags aspect_mask vk.ImageAspectFlags + base_mip_level u32 + level_count u32 // zero selects all levels from base_mip_level } // image_memory_barrier builds the raw barrier used by transition_image_layout. -// It covers the OwnedImage's single mip level and array layer. +// It covers the selected mip levels and the image's single array layer. pub fn (transition ImageLayoutTransition) image_memory_barrier(image &OwnedImage) vk.ImageMemoryBarrier { + level_count := if transition.level_count == 0 { + image.mip_levels - transition.base_mip_level + } else { + transition.level_count + } return vk.ImageMemoryBarrier{ srcAccessMask: transition.src_access_mask dstAccessMask: transition.dst_access_mask @@ -633,7 +670,8 @@ pub fn (transition ImageLayoutTransition) image_memory_barrier(image &OwnedImage srcQueueFamilyIndex: vk.queue_family_ignored dstQueueFamilyIndex: vk.queue_family_ignored image: image.handle - subresourceRange: single_image_subresource_range(transition.aspect_mask) + subresourceRange: image_subresource_range(transition.aspect_mask, transition.base_mip_level, + level_count) } } @@ -644,6 +682,10 @@ pub fn (buffer &PrimaryCommandBuffer) transition_image_layout(image &OwnedImage, if transition.aspect_mask == 0 { return error('image transition aspect mask must not be empty') } + if transition.base_mip_level >= image.mip_levels || (transition.level_count > 0 + && transition.level_count > image.mip_levels - transition.base_mip_level) { + return error('image transition mip range exceeds image') + } 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) diff --git a/ergonomic/ergonomic_test.v b/ergonomic/ergonomic_test.v index 28ac16f..9692cd2 100644 --- a/ergonomic/ergonomic_test.v +++ b/ergonomic/ergonomic_test.v @@ -354,6 +354,22 @@ fn test_new_image_2d_rejects_empty_usage_before_calling_vulkan() { assert false } +fn test_new_image_2d_mips_rejects_invalid_count_before_calling_vulkan() { + device := OwnedDevice{ + handle: vk.Device(unsafe { nil }) + } + usage := u32(vk.ImageUsageFlagBits.sampled) + for count in [u32(0), 5] { + device.new_image_2d_mips(8, 4, count, .r8g8b8a8_unorm, .optimal, usage, + 0) or { + assert err.msg() == 'image mip level count exceeds extent' + continue + } + assert false + } + assert max_image_mip_levels(8, 4) == 4 +} + fn test_owned_image_exposes_creation_and_allocation_metadata() { extent := vk.Extent3D{ width: 640 @@ -374,6 +390,7 @@ fn test_owned_image_exposes_creation_and_allocation_metadata() { assert image.format == .r8g8b8a8_unorm assert image.extent.width == 640 assert image.extent.height == 480 + assert image.mip_levels == 1 assert image.allocation_size == 4096 assert image.memory_type_index == 2 } @@ -405,6 +422,22 @@ fn test_new_image_view_rejects_transfer_only_usage_before_calling_vulkan() { assert false } +fn test_new_image_view_rejects_invalid_mip_range_before_calling_vulkan() { + image := OwnedImage{ + device: vk.Device(unsafe { nil }) + handle: vk.Image(unsafe { nil }) + format: .r8g8b8a8_unorm + mip_levels: 4 + } + for count in [u32(0), 4] { + image.new_view_mips(u32(vk.ImageAspectFlagBits.color), 1, count) or { + assert err.msg() == 'image view mip range exceeds image' + continue + } + assert false + } +} + fn test_image_view_accepts_every_usage_permitted_by_vuid_04441() { compatible_usages := [ u32(vk.ImageUsageFlagBits.sampled), @@ -440,7 +473,7 @@ fn test_owned_image_view_exposes_parent_and_subresource_metadata() { image: image_handle format: .r8g8b8a8_unorm view_type: ._2d - subresource_range: single_image_subresource_range(color) + subresource_range: image_subresource_range(color, 0, 1) } assert view.handle == view_handle @@ -482,6 +515,26 @@ fn test_image_layout_transition_builds_explicit_single_subresource_barrier() { assert barrier.subresourceRange.layerCount == 1 } +fn test_image_layout_transition_selects_mip_range() { + image := OwnedImage{ + handle: vk.Image(unsafe { nil }) + mip_levels: 4 + } + transition := ImageLayoutTransition{ + aspect_mask: u32(vk.ImageAspectFlagBits.color) + base_mip_level: 1 + level_count: 2 + } + barrier := transition.image_memory_barrier(image) + assert barrier.subresourceRange.baseMipLevel == 1 + assert barrier.subresourceRange.levelCount == 2 + all_remaining := ImageLayoutTransition{ + aspect_mask: u32(vk.ImageAspectFlagBits.color) + base_mip_level: 1 + } + assert all_remaining.image_memory_barrier(image).subresourceRange.levelCount == 3 +} + fn test_transition_image_layout_rejects_empty_aspect_before_calling_vulkan() { buffer := PrimaryCommandBuffer{ handle: vk.CommandBuffer(unsafe { nil }) @@ -499,3 +552,25 @@ fn test_transition_image_layout_rejects_empty_aspect_before_calling_vulkan() { } assert false } + +fn test_transition_image_layout_rejects_invalid_mip_range_before_calling_vulkan() { + buffer := PrimaryCommandBuffer{ + handle: vk.CommandBuffer(unsafe { nil }) + } + image := OwnedImage{ + handle: vk.Image(unsafe { nil }) + mip_levels: 4 + } + for base in [u32(4), 3] { + transition := ImageLayoutTransition{ + aspect_mask: u32(vk.ImageAspectFlagBits.color) + base_mip_level: base + level_count: 2 + } + buffer.transition_image_layout(image, transition) or { + assert err.msg() == 'image transition mip range exceeds image' + continue + } + assert false + } +}