diff options
| author | Irene Knapp <ireneista@irenes.space> | 2026-08-09 17:10:03 -0700 |
|---|---|---|
| committer | Irene Knapp <ireneista@irenes.space> | 2026-08-09 17:10:03 -0700 |
| commit | 4158fed109ae789cb3ca7ff53c2790797a3b4087 (patch) | |
| tree | 4e264225e211faaacc8259997dff207378c50e39 /src/graphics/texture.rs | |
| parent | 4b877079872248d3a4c17312b18599c6084e1e2d (diff) | |
refactor Texture into its own thing
as part of this, the command pools are moved from window dressing to permanent note also how mip count is a value computed by loading the texture, and used when creating the sampler, which is part of the window dressing. the assumption about how it's computed will have to change when we support multiple textures, but for now we settle for just pushing the assumption to the top level. Force-Push: yes Change-Id: I545c53feedc99628fbe943120798fe66fe9e065b
Diffstat (limited to 'src/graphics/texture.rs')
| -rw-r--r-- | src/graphics/texture.rs | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/src/graphics/texture.rs b/src/graphics/texture.rs new file mode 100644 index 0000000..cd4dd7a --- /dev/null +++ b/src/graphics/texture.rs @@ -0,0 +1,376 @@ +#![deny(unsafe_code)] +use crate::error::*; +use crate::graphics::permanent::PermanentGraphicsState; +use crate::graphics::util::{ + stage_in_buffer, allocate_image, init_image_view, + begin_transient_commands, end_transient_commands +}; + +use std::io::Cursor; + +use png::Decoder; +use vulkanalia::{ Device, Instance }; +use vulkanalia::vk::{ self, HasBuilder, InstanceV1_0, DeviceV1_0 }; + + +#[derive(Debug)] +pub struct Texture { + image: vk::Image, + image_memory: vk::DeviceMemory, + pub image_view: vk::ImageView, +} + + +impl Texture { + pub fn new(permanent: &PermanentGraphicsState) -> Result<(Self, u32)> { + let graphics_queue = &permanent.graphics_queue; + let instance = &permanent.instance; + let device = &permanent.device; + let transient_command_pool = &permanent.transient_command_pool; + + let (image, image_memory, image_view, mip_count) + = init_texture(instance, device, graphics_queue, + &transient_command_pool)?; + + Ok((Texture { + image, + image_memory, + image_view, + }, mip_count)) + } + + // This relies on its caller to have already waited for the device to be + // idle. + #[allow(unsafe_code)] + pub fn destroy(self, device: &Device) { + unsafe { device.destroy_image(self.image, None) }; + unsafe { device.free_memory(self.image_memory, None) }; + unsafe { device.destroy_image_view(self.image_view, None) }; + } +} + + +#[allow(unsafe_code)] +fn init_texture(instance: &Instance, device: &Device, queue: &vk::Queue, + command_pool: &vk::CommandPool) + -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView, u32)> +{ + let physical_device = device.physical_device(); + + let png = include_bytes!("../../textures/forest_leaves_04_diff.png"); + + let decoder = Decoder::new(Cursor::new(png)); + let mut reader = decoder.read_info()?; + + 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)?; + + let (staging_buffer, staging_memory, _byte_size) + = stage_in_buffer(instance, device, &pixels)?; + + let (image, image_memory) + = allocate_image(instance, device, + width, height, mip_count, vk::SampleCountFlags::_1, + 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, mip_count, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL)?; + + copy_buffer_to_image(device, queue, command_pool, &staging_buffer, &image, + width, height)?; + + // This will also change the layout to SHADER_READ_ONLY_OPTIMAL. + + 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, mip_count)) +} + + +#[allow(unsafe_code)] +fn change_image_layout(device: &Device, queue: &vk::Queue, + command_pool: &vk::CommandPool, image: &vk::Image, + mip_count: u32, old: vk::ImageLayout, + new: vk::ImageLayout) + -> Result<()> +{ + let command_buffer = begin_transient_commands(device, command_pool)?; + + let subresource_range = vk::ImageSubresourceRange::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(mip_count) + .base_array_layer(0) + .layer_count(1); + + // Notionally this is a property that our caller is in a better position + // to know than we are, but in practice the nature of the transition + // strongly implies a particular phase of the image's lifecycle, so we just + // compute it here. + let (source_access, source_stage, destination_access, destination_stage) + = match (old, new) + { + (vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL) + => (vk::AccessFlags::empty(), + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TRANSFER), + (vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + => (vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TRANSFER, + vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::FRAGMENT_SHADER), + _ => return Err(Error { + message: + format!("Don't know how to change from image layout {:?} to {:?}", + old, new) + }) + }; + + let barrier_info = vk::ImageMemoryBarrier::builder() + .image(*image) + .subresource_range(subresource_range) + .old_layout(old) + .new_layout(new) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .src_access_mask(source_access) + .dst_access_mask(destination_access); + + unsafe { + device.cmd_pipeline_barrier(command_buffer, + source_stage, destination_stage, + vk::DependencyFlags::empty(), + &[] as &[vk::MemoryBarrier], + &[] as &[vk::BufferMemoryBarrier], + &[barrier_info]) + }; + + end_transient_commands(command_buffer, device, queue, command_pool)?; + + Ok(()) +} + + + +#[allow(unsafe_code)] +fn copy_buffer_to_image(device: &Device, queue: &vk::Queue, + command_pool: &vk::CommandPool, source: &vk::Buffer, + destination: &vk::Image, width: u32, height: u32) + -> Result<()> +{ + let command_buffer = begin_transient_commands(device, command_pool)?; + + let subresource_layers = vk::ImageSubresourceLayers::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(0) + .base_array_layer(0) + .layer_count(1); + + let copy_info = vk::BufferImageCopy::builder() + .buffer_offset(0) + .buffer_row_length(0) + .buffer_image_height(0) + .image_subresource(subresource_layers) + .image_offset(vk::Offset3D { x: 0, y: 0, z: 0 }) + .image_extent(vk::Extent3D { width, height, depth: 1 }); + + unsafe { + device.cmd_copy_buffer_to_image(command_buffer, *source, *destination, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[copy_info]) + }; + + end_transient_commands(command_buffer, device, queue, command_pool)?; + + Ok(()) +} + + +// 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(()) +} + |