From 74ebf3d25e94b87a755d0ad0875332f962c8b92f Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Wed, 22 Jul 2026 01:07:29 -0700 Subject: generate mipmaps Force-Push: yes Change-Id: I2683fa6e3f290a812b74e5b64701c3094bb5cbee --- src/graphics_window_dressing.rs | 270 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 243 insertions(+), 27 deletions(-) (limited to 'src/graphics_window_dressing.rs') diff --git a/src/graphics_window_dressing.rs b/src/graphics_window_dressing.rs index e774177..fd6b8d6 100644 --- a/src/graphics_window_dressing.rs +++ b/src/graphics_window_dressing.rs @@ -1,7 +1,8 @@ #![deny(unsafe_code)] use crate::error::*; use crate::graphics_permanent::{ - PermanentGraphicsState, GraphicsStateForReinit, QueueFamilyIndices + PermanentGraphicsState, GraphicsStateForReinit, QueueFamilyIndices, + EnableAnisotropy }; use crate::model_loader::load_model; use crate::shader_data::{ Vertex, UniformBlock }; @@ -56,6 +57,8 @@ pub struct WindowDressing { texture_image: vk::Image, texture_image_memory: vk::DeviceMemory, texture_image_view: vk::ImageView, + mip_count: u32, + sampler: vk::Sampler, uniform_buffers: Vec, pub uniform_buffer_memory: Vec, @@ -105,14 +108,14 @@ pub struct Concurrency { impl WindowDressing { pub fn new(permanent: &PermanentGraphicsState, - for_reinit: &GraphicsStateForReinit) + for_reinit: &GraphicsStateForReinit, + enable_anisotropy: EnableAnisotropy) -> Result { let window = &permanent.window; let instance = &permanent.instance; let surface = &permanent.surface; let device = &permanent.device; - let sampler = &permanent.sampler; let graphics_queue = &permanent.graphics_queue; let physical_device = &for_reinit.physical_device; let indices = &for_reinit.indices; @@ -149,10 +152,12 @@ impl WindowDressing { = init_index_buffer(indices, instance, physical_device, device, graphics_queue, &transient_command_pool)?; - let (texture_image, texture_image_memory, texture_image_view) + let (texture_image, texture_image_memory, texture_image_view, mip_count) = init_texture(instance, physical_device, device, graphics_queue, &transient_command_pool)?; + let sampler = init_sampler(&device, &enable_anisotropy, mip_count)?; + let (uniform_buffers, uniform_buffer_memory) = init_uniform_buffers(instance, physical_device, device, swapchain.images.len())?; @@ -163,7 +168,7 @@ impl WindowDressing { = init_descriptor_sets(device, descriptor_set_layout, &uniform_buffers, &descriptor_pool, swapchain.images.len(), - &texture_image_view, sampler)?; + &texture_image_view, &sampler)?; let command_buffers = init_commands( device, &swapchain.extent, &framebuffers, &render_pass, @@ -190,6 +195,8 @@ impl WindowDressing { texture_image, texture_image_memory, texture_image_view, + mip_count, + sampler, uniform_buffers, uniform_buffer_memory, descriptor_pool, @@ -211,7 +218,6 @@ impl WindowDressing { let instance = &permanent.instance; let surface = &permanent.surface; let device = &permanent.device; - let sampler = &permanent.sampler; let physical_device = &for_reinit.physical_device; let indices = &for_reinit.indices; let descriptor_set_layout = &for_reinit.descriptor_set_layout; @@ -250,7 +256,7 @@ impl WindowDressing { = init_descriptor_sets(device, descriptor_set_layout, &uniform_buffers, &descriptor_pool, swapchain.images.len(), - &self.texture_image_view, sampler)?; + &self.texture_image_view, &self.sampler)?; // Notice that we reused the command pool. let command_buffers = init_commands( @@ -289,11 +295,14 @@ impl WindowDressing { unsafe { device.destroy_buffer(self.vertex_buffer, None) }; unsafe { device.free_memory(self.vertex_buffer_memory, None) }; + unsafe { device.destroy_buffer(self.index_buffer, None) }; unsafe { device.free_memory(self.index_buffer_memory, None) }; + unsafe { device.destroy_image(self.texture_image, None) }; unsafe { device.free_memory(self.texture_image_memory, None) }; unsafe { device.destroy_image_view(self.texture_image_view, None) }; + unsafe { device.destroy_sampler(self.sampler, None) }; for semaphore in self.concurrency.image_available_semaphores { unsafe { device.destroy_semaphore(semaphore, None) }; @@ -425,7 +434,7 @@ fn init_swapchain(window: &Window, instance: &Instance, let mut image_views = Vec::new(); for image in &images { - let view = init_image_view(device, image, format.format, + let view = init_image_view(device, image, 1, format.format, vk::ImageAspectFlags::COLOR)?; image_views.push(view); } @@ -447,13 +456,13 @@ fn init_depth(instance: &Instance, physical_device: &vk::PhysicalDevice, let (image, image_memory) = allocate_image(instance, physical_device, device, - extent.width, extent.height, + extent.width, extent.height, 1, format, vk::ImageTiling::OPTIMAL, vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, vk::MemoryPropertyFlags::DEVICE_LOCAL)?; - let image_view = init_image_view(device, &image, format, + let image_view = init_image_view(device, &image, 1, format, vk::ImageAspectFlags::DEPTH)?; Ok((image, image_memory, image_view, format)) @@ -719,7 +728,7 @@ fn init_index_buffer(indices: Vec, instance: &Instance, fn init_texture(instance: &Instance, physical_device: &vk::PhysicalDevice, device: &Device, queue: &vk::Queue, command_pool: &vk::CommandPool) - -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> + -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView, u32)> { let png = include_bytes!("../textures/forest_leaves_04_diff.png"); @@ -728,6 +737,21 @@ fn init_texture(instance: &Instance, let (width, height) = reader.info().size(); + let format_properties = unsafe { + instance.get_physical_device_format_properties(*physical_device, + vk::Format::R8G8B8A8_SRGB) + }; + let has_linear_filter = format_properties + .optimal_tiling_features + .contains(vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR); + let mip_count = if has_linear_filter { + // This will generate mips all the way down to 1x1. It is not clear + // whether there's a benefit to that. + (width.max(height)).ilog2() + 1 + } else { + 1 + }; + let mut pixels = vec![0; reader.info().raw_bytes()]; reader.next_frame(&mut pixels)?; @@ -735,14 +759,16 @@ fn init_texture(instance: &Instance, = stage_in_buffer(instance, physical_device, device, &pixels)?; let (image, image_memory) - = allocate_image(instance, physical_device, device, width, height, + = allocate_image(instance, physical_device, device, + width, height, mip_count, vk::Format::R8G8B8A8_SRGB, vk::ImageTiling::OPTIMAL, vk::ImageUsageFlags::SAMPLED + | vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST, vk::MemoryPropertyFlags::DEVICE_LOCAL)?; - change_image_layout(device, queue, command_pool, &image, + change_image_layout(device, queue, command_pool, &image, mip_count, vk::Format::R8G8B8A8_SRGB, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL)?; @@ -750,18 +776,19 @@ fn init_texture(instance: &Instance, copy_buffer_to_image(device, queue, command_pool, &staging_buffer, &image, width, height)?; - change_image_layout(device, queue, command_pool, &image, - vk::Format::R8G8B8A8_SRGB, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)?; + // This will also change the layout to SHADER_READ_ONLY_OPTIMAL. - let view = init_image_view(device, &image, vk::Format::R8G8B8A8_SRGB, + fill_mip_levels(device, queue, command_pool, &image, + width, height, mip_count)?; + + let view = init_image_view(device, &image, mip_count, + vk::Format::R8G8B8A8_SRGB, vk::ImageAspectFlags::COLOR)?; unsafe { device.destroy_buffer(staging_buffer, None) }; unsafe { device.free_memory(staging_memory, None) }; - Ok((image, image_memory, view)) + Ok((image, image_memory, view, mip_count)) } @@ -815,6 +842,39 @@ fn init_buffer(instance: &Instance, } +#[allow(unsafe_code)] +fn init_sampler(device: &Device, enable_anisotropy: &EnableAnisotropy, + mip_count: u32) + -> Result +{ + let mut sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false) + .compare_enable(false) + .compare_op(vk::CompareOp::ALWAYS) + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .mip_lod_bias(0.0) + .min_lod(0.0) + .max_lod(mip_count as f32); + sampler_info = if enable_anisotropy.0 { + sampler_info.anisotropy_enable(true) + .max_anisotropy(16.0) + } else { + sampler_info.anisotropy_enable(false) + .max_anisotropy(1.0) + }; + + let sampler = unsafe { device.create_sampler(&sampler_info, None) }?; + + Ok(sampler) +} + + #[allow(unsafe_code)] fn init_descriptor_pool(device: &Device, count: usize) -> Result @@ -1053,8 +1113,8 @@ fn init_concurrency(device: &Device, #[allow(unsafe_code)] -fn init_image_view(device: &Device, image: &vk::Image, format: vk::Format, - aspects: vk::ImageAspectFlags) +fn init_image_view(device: &Device, image: &vk::Image, mip_count: u32, + format: vk::Format, aspects: vk::ImageAspectFlags) -> Result { // Component mapping is only for color components (not, for example, depth @@ -1068,7 +1128,7 @@ fn init_image_view(device: &Device, image: &vk::Image, format: vk::Format, let subresource_range = vk::ImageSubresourceRange::builder() .aspect_mask(aspects) .base_mip_level(0) - .level_count(1) + .level_count(mip_count) .base_array_layer(0) .layer_count(1); @@ -1281,7 +1341,7 @@ fn copy_buffer(device: &Device, queue: &vk::Queue, #[allow(unsafe_code)] fn allocate_image(instance: &Instance, physical_device: &vk::PhysicalDevice, - device: &Device, width: u32, height: u32, + device: &Device, width: u32, height: u32, mip_count: u32, format: vk::Format, tiling: vk::ImageTiling, usage: vk::ImageUsageFlags, memory_flags: vk::MemoryPropertyFlags) @@ -1290,7 +1350,7 @@ fn allocate_image(instance: &Instance, physical_device: &vk::PhysicalDevice, let image_info = vk::ImageCreateInfo::builder() .image_type(vk::ImageType::_2D) .extent(vk::Extent3D { width, height, depth: 1 }) - .mip_levels(1) + .mip_levels(mip_count) .array_layers(1) .format(format) .tiling(tiling) @@ -1356,8 +1416,8 @@ fn copy_buffer_to_image(device: &Device, queue: &vk::Queue, #[allow(unsafe_code)] fn change_image_layout(device: &Device, queue: &vk::Queue, command_pool: &vk::CommandPool, image: &vk::Image, - format: vk::Format, old: vk::ImageLayout, - new: vk::ImageLayout) + mip_count: u32, format: vk::Format, + old: vk::ImageLayout, new: vk::ImageLayout) -> Result<()> { let command_buffer = begin_transient_commands(device, command_pool)?; @@ -1365,7 +1425,7 @@ fn change_image_layout(device: &Device, queue: &vk::Queue, let subresource_range = vk::ImageSubresourceRange::builder() .aspect_mask(vk::ImageAspectFlags::COLOR) .base_mip_level(0) - .level_count(1) + .level_count(mip_count) .base_array_layer(0) .layer_count(1); @@ -1419,6 +1479,162 @@ fn change_image_layout(device: &Device, queue: &vk::Queue, } +// An Image can store multiple mip levels within it, as one of several kinds +// of subresource it has. We deal with this by +#[allow(unsafe_code)] +fn fill_mip_levels(device: &Device, queue: &vk::Queue, + command_pool: &vk::CommandPool, image: &vk::Image, + original_width: u32, original_height: u32, + mip_count: u32) + -> Result<()> +{ + let command_buffer = begin_transient_commands(device, command_pool)?; + + // We'll be mutating these two builders as we loop through the mip levels, + // because we need to construct a lot of similar things. Remember, the + // builder methods don't mutate in-place, they return a new builder; to + // avoid confusion we always assign that result back to the same variable. + let mut barrier_subresource_range = vk::ImageSubresourceRange::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .level_count(1) + .base_array_layer(0) + .layer_count(1); + + let mut blit_barrier_info = vk::ImageMemoryBarrier::builder() + .image(*image) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED); + + // Now we loop through the mip levels from largest (low numbers) to + // smallest (high numbers). Conceptually, the only thing we're doing is a + // blit that copies each mip level from the one immediately before. Recall + // though that we don't just want to fill in the pixels, we also care about + // pixel format and memory sharing. There are additional operations to deal + // with that. These are best done together, as detailed below. + // + // This loop has a lot of code in it, so we make the "paragraphs" a little + // more dense than usual to make sure the logical grouping is clear. + let mut source_width = original_width; + let mut source_height = original_height; + for destination_mip_level in 1 .. mip_count { + let source_mip_level = destination_mip_level - 1; + let destination_width = (source_width / 2).max(1); + let destination_height = (source_height / 2).max(1); + + // So. The name pipeline_barrier is a little misleading; it does indeed + // mean "barrier" in the concurrency sense, but it isn't just initiating + // a wait, it's also performing any needed mutation. We do one of them + // here, acting on this iteration's source level, to set it up for + // reading. + barrier_subresource_range = barrier_subresource_range + .base_mip_level(source_mip_level as u32); + blit_barrier_info = blit_barrier_info + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .dst_access_mask(vk::AccessFlags::TRANSFER_READ) + .subresource_range(barrier_subresource_range); + unsafe { + device.cmd_pipeline_barrier(command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::TRANSFER, + vk::DependencyFlags::empty(), + &[] as &[vk::MemoryBarrier], + &[] as &[vk::BufferMemoryBarrier], + &[blit_barrier_info]) + }; + + // Now we do the actual blit. Nice and easy, though specifying the + // coordinates is a bit verbose. + let blit_source_layer_info = vk::ImageSubresourceLayers::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(source_mip_level as u32) + .base_array_layer(0) + .layer_count(1); + let blit_destination_layer_info = vk::ImageSubresourceLayers::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(destination_mip_level as u32) + .base_array_layer(0) + .layer_count(1); + let blit_info = vk::ImageBlit::builder() + .src_offsets([vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: source_width as i32, + y: source_height as i32, + z: 1 + }]) + .src_subresource(blit_source_layer_info) + .dst_offsets([vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: destination_width as i32, + y: destination_height as i32, + z: 1 + }]) + .dst_subresource(blit_destination_layer_info); + unsafe { + device.cmd_blit_image(command_buffer, + *image, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + *image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[blit_info], + vk::Filter::LINEAR) + }; + + // Now we do another pipeline_barrier. We're still acting on this + // iteration's source level, not on the destination. We'll never need to + // use it again except from the shader, so we set it appropriately for + // that. + blit_barrier_info = blit_barrier_info + .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .src_access_mask(vk::AccessFlags::TRANSFER_READ) + .dst_access_mask(vk::AccessFlags::SHADER_READ); + unsafe { + device.cmd_pipeline_barrier(command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::DependencyFlags::empty(), + &[] as &[vk::MemoryBarrier], + &[] as &[vk::BufferMemoryBarrier], + &[blit_barrier_info]) + }; + + source_width = destination_width; + source_height = destination_height; + } + + let final_mip_level = mip_count - 1; + + // We need to do one final pipeline_barrier, because the loop didn't do it + // to the smallest (last) mip level. We change it to have the same settings + // the loop left the rest of them in. The barrier source properties for this + // barrier are different from the others because this level was never useds + // as a blit source, only as a blit destination. The barrier destination + // properties are the same as the rest, so after this all the subresourcess + // will be in their fully-ready state. + barrier_subresource_range = barrier_subresource_range + .base_mip_level(final_mip_level as u32); + blit_barrier_info = blit_barrier_info + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .dst_access_mask(vk::AccessFlags::SHADER_READ) + .subresource_range(barrier_subresource_range); + unsafe { + device.cmd_pipeline_barrier(command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::DependencyFlags::empty(), + &[] as &[vk::MemoryBarrier], + &[] as &[vk::BufferMemoryBarrier], + &[blit_barrier_info]) + }; + + end_transient_commands(command_buffer, device, queue, command_pool)?; + + Ok(()) +} + + #[allow(unsafe_code)] fn begin_transient_commands(device: &Device, command_pool: &vk::CommandPool) -> Result -- cgit 1.4.1