use crate::device::VulkanContext;
use ash::vk;
use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme};
use gpu_allocator::MemoryLocation;
use impeller_hal::{Error, Extent2D, PixelFormat, Result, TextureDescriptor, TextureUsage};
pub struct VulkanTexture {
pub(crate) image: vk::Image,
pub(crate) memory: TextureMemory,
pub(crate) extent: Extent2D,
pub(crate) format: PixelFormat,
pub(crate) layout: std::cell::Cell<vk::ImageLayout>,
pub(crate) usage: TextureUsage,
pub(crate) mip_levels: u32,
}
pub(crate) enum TextureMemory {
Pooled(Allocation),
Dedicated(vk::DeviceMemory),
Borrowed,
}
impl VulkanTexture {
pub fn wrap_image(
image: vk::Image,
extent: Extent2D,
format: PixelFormat,
layout: vk::ImageLayout,
) -> VulkanTexture {
VulkanTexture {
image,
memory: TextureMemory::Borrowed,
extent,
format,
layout: std::cell::Cell::new(layout),
mip_levels: 1,
usage: TextureUsage {
render_target: true,
sampled: false,
..TextureUsage::default()
},
}
}
pub fn extent(&self) -> Extent2D {
self.extent
}
pub fn format(&self) -> PixelFormat {
self.format
}
pub fn raw_image(&self) -> vk::Image {
self.image
}
pub(crate) fn layout(&self) -> vk::ImageLayout {
self.layout.get()
}
pub(crate) fn set_layout(&self, layout: vk::ImageLayout) {
self.layout.set(layout);
}
pub fn byte_size(&self) -> u64 {
self.extent.area() * self.format.bytes_per_pixel() as u64
}
}
pub(crate) fn vk_format(format: PixelFormat) -> vk::Format {
match format {
PixelFormat::Rgba8Unorm => vk::Format::R8G8B8A8_UNORM,
PixelFormat::Rgba8UnormSrgb => vk::Format::R8G8B8A8_SRGB,
PixelFormat::Bgra8Unorm => vk::Format::B8G8R8A8_UNORM,
PixelFormat::Bgra8UnormSrgb => vk::Format::B8G8R8A8_SRGB,
PixelFormat::Rgb10A2Unorm => vk::Format::A2B10G10R10_UNORM_PACK32,
PixelFormat::Rgba16Float => vk::Format::R16G16B16A16_SFLOAT,
PixelFormat::R8Unorm => vk::Format::R8_UNORM,
}
}
impl VulkanContext {
pub fn create_texture(&mut self, desc: &TextureDescriptor) -> Result<VulkanTexture> {
if desc.is_external() {
return Err(Error::Unsupported("external image import"));
}
self.capabilities().check_texture(desc)?;
if !self.capabilities().can_allocate(desc.extent) {
return Err(Error::LimitExceeded {
what: "texture dimension",
requested: desc.extent.width.max(desc.extent.height) as u64,
limit: self.capabilities().max_texture_size as u64,
});
}
if desc.extent.is_empty() {
return Err(Error::Unsupported("zero-sized texture"));
}
let mut usage = vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST;
if desc.usage.sampled {
usage |= vk::ImageUsageFlags::SAMPLED;
}
if desc.usage.render_target {
usage |= vk::ImageUsageFlags::COLOR_ATTACHMENT;
}
let info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk_format(desc.format))
.extent(vk::Extent3D {
width: desc.extent.width,
height: desc.extent.height,
depth: 1,
})
.mip_levels(desc.mip_levels.max(1))
.array_layers(1)
.samples(sample_count_flags(desc.sample_count))
.tiling(vk::ImageTiling::OPTIMAL)
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED);
let device = self.raw_device().clone();
let image = unsafe { device.create_image(&info, None) }
.map_err(|e| backend_err("create_image", e))?;
let requirements = unsafe { device.get_image_memory_requirements(image) };
let allocation = self
.allocator_mut()
.allocate(&AllocationCreateDesc {
name: "texture",
requirements,
location: MemoryLocation::GpuOnly,
linear: false,
allocation_scheme: AllocationScheme::GpuAllocatorManaged,
})
.map_err(|e| {
unsafe { device.destroy_image(image, None) };
Error::OutOfMemory {
what: "texture memory",
}
.with_detail(e)
})?;
unsafe { device.bind_image_memory(image, allocation.memory(), allocation.offset()) }
.map_err(|e| backend_err("bind_image_memory", e))?;
Ok(VulkanTexture {
image,
memory: TextureMemory::Pooled(allocation),
extent: desc.extent,
format: desc.format,
layout: std::cell::Cell::new(vk::ImageLayout::UNDEFINED),
usage: desc.usage,
mip_levels: desc.mip_levels.max(1),
})
}
pub fn destroy_texture(&mut self, texture: VulkanTexture) {
match texture.memory {
TextureMemory::Pooled(allocation) => {
let _ = self.allocator_mut().free(allocation);
}
TextureMemory::Dedicated(memory) => {
unsafe { self.raw_device().free_memory(memory, None) };
}
TextureMemory::Borrowed => return,
}
unsafe { self.raw_device().destroy_image(texture.image, None) };
}
pub fn transition_for_present(&mut self, texture: &VulkanTexture) -> Result<()> {
if texture.layout() == vk::ImageLayout::PRESENT_SRC_KHR {
return Ok(());
}
let device = self.raw_device().clone();
let cmd = self.begin_one_shot()?;
transition(
&device,
cmd,
texture.raw_image(),
texture.layout(),
vk::ImageLayout::PRESENT_SRC_KHR,
);
self.submit_one_shot(cmd)?;
texture.set_layout(vk::ImageLayout::PRESENT_SRC_KHR);
Ok(())
}
pub fn clear_texture(&mut self, texture: &mut VulkanTexture, color: [f32; 4]) -> Result<()> {
let device = self.raw_device().clone();
let cmd = self.begin_one_shot()?;
transition(
&device,
cmd,
texture.image,
texture.layout.get(),
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
);
let clear = vk::ClearColorValue { float32: color };
let range = vk::ImageSubresourceRange::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.level_count(vk::REMAINING_MIP_LEVELS)
.layer_count(1);
unsafe {
device.cmd_clear_color_image(
cmd,
texture.image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&clear,
&[range],
);
}
texture.layout.set(vk::ImageLayout::TRANSFER_DST_OPTIMAL);
self.submit_one_shot(cmd)
}
pub fn write_texture(&mut self, texture: &mut VulkanTexture, pixels: &[u8]) -> Result<()> {
let size = texture.byte_size();
if pixels.len() as u64 != size {
return Err(Error::Backend {
backend: "vulkan",
detail: format!("texture wants {size} bytes, {} supplied", pixels.len()),
});
}
let device = self.raw_device().clone();
let buffer_info = vk::BufferCreateInfo::default()
.size(size)
.usage(vk::BufferUsageFlags::TRANSFER_SRC)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.create_buffer(&buffer_info, None) }
.map_err(|e| backend_err("create_buffer", e))?;
let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
let mut allocation = self
.allocator_mut()
.allocate(&AllocationCreateDesc {
name: "upload",
requirements,
location: MemoryLocation::CpuToGpu,
linear: true,
allocation_scheme: AllocationScheme::GpuAllocatorManaged,
})
.map_err(|e| {
unsafe { device.destroy_buffer(buffer, None) };
Error::OutOfMemory {
what: "upload buffer",
}
.with_detail(e)
})?;
if let Err(e) =
unsafe { device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) }
{
let _ = self.allocator_mut().free(allocation);
unsafe { device.destroy_buffer(buffer, None) };
return Err(backend_err("bind_buffer_memory", e));
}
let staged = match allocation.mapped_slice_mut() {
Some(slice) => {
slice[..pixels.len()].copy_from_slice(pixels);
Ok(())
}
None => Err(Error::Backend {
backend: "vulkan",
detail: "upload allocation was not host-visible".into(),
}),
};
let outcome = staged.and_then(|()| {
let cmd = self.begin_one_shot()?;
transition(
&device,
cmd,
texture.image,
texture.layout.get(),
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
);
let region = vk::BufferImageCopy::default()
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.layer_count(1),
)
.image_extent(vk::Extent3D {
width: texture.extent.width,
height: texture.extent.height,
depth: 1,
});
unsafe {
device.cmd_copy_buffer_to_image(
cmd,
buffer,
texture.image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[region],
);
}
texture.layout.set(vk::ImageLayout::TRANSFER_DST_OPTIMAL);
generate_mipmaps(&device, cmd, texture);
self.submit_one_shot(cmd)
});
let _ = self.allocator_mut().free(allocation);
unsafe { device.destroy_buffer(buffer, None) };
outcome
}
pub fn read_texture(&mut self, texture: &mut VulkanTexture) -> Result<Vec<u8>> {
let size = texture.byte_size();
let device = self.raw_device().clone();
let buffer_info = vk::BufferCreateInfo::default()
.size(size)
.usage(vk::BufferUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.create_buffer(&buffer_info, None) }
.map_err(|e| backend_err("create_buffer", e))?;
let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
let allocation = self
.allocator_mut()
.allocate(&AllocationCreateDesc {
name: "readback",
requirements,
location: MemoryLocation::GpuToCpu,
linear: true,
allocation_scheme: AllocationScheme::GpuAllocatorManaged,
})
.map_err(|e| {
unsafe { device.destroy_buffer(buffer, None) };
Error::OutOfMemory {
what: "readback buffer",
}
.with_detail(e)
})?;
unsafe { device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) }
.map_err(|e| backend_err("bind_buffer_memory", e))?;
let cmd = self.begin_one_shot()?;
transition(
&device,
cmd,
texture.image,
texture.layout.get(),
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
);
let region = vk::BufferImageCopy::default()
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.layer_count(1),
)
.image_extent(vk::Extent3D {
width: texture.extent.width,
height: texture.extent.height,
depth: 1,
});
unsafe {
device.cmd_copy_image_to_buffer(
cmd,
texture.image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
buffer,
&[region],
);
}
texture.layout.set(vk::ImageLayout::TRANSFER_SRC_OPTIMAL);
let pixels = self.submit_one_shot(cmd).and_then(|()| {
allocation
.mapped_slice()
.map(|s| s[..size as usize].to_vec())
.ok_or(Error::Backend {
backend: "vulkan",
detail: "readback allocation was not host-visible".into(),
})
});
let _ = self.allocator_mut().free(allocation);
unsafe { device.destroy_buffer(buffer, None) };
pixels
}
}
fn generate_mipmaps(device: &ash::Device, cmd: vk::CommandBuffer, texture: &VulkanTexture) {
if texture.mip_levels <= 1 {
return;
}
let level_range = |level: u32| {
vk::ImageSubresourceRange::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.base_mip_level(level)
.level_count(1)
.layer_count(1)
};
let barrier = |level: u32, from: vk::ImageLayout, to: vk::ImageLayout| {
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(from)
.new_layout(to)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(texture.image)
.subresource_range(level_range(level))
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::TRANSFER_READ);
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
}
};
let mut width = texture.extent.width.max(1) as i32;
let mut height = texture.extent.height.max(1) as i32;
for level in 1..texture.mip_levels {
barrier(
level - 1,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
);
let next_width = (width / 2).max(1);
let next_height = (height / 2).max(1);
let blit = vk::ImageBlit::default()
.src_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.mip_level(level - 1)
.layer_count(1),
)
.src_offsets([
vk::Offset3D { x: 0, y: 0, z: 0 },
vk::Offset3D {
x: width,
y: height,
z: 1,
},
])
.dst_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.mip_level(level)
.layer_count(1),
)
.dst_offsets([
vk::Offset3D { x: 0, y: 0, z: 0 },
vk::Offset3D {
x: next_width,
y: next_height,
z: 1,
},
]);
unsafe {
device.cmd_blit_image(
cmd,
texture.image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
texture.image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[blit],
vk::Filter::LINEAR,
);
}
width = next_width;
height = next_height;
}
for level in 0..texture.mip_levels - 1 {
barrier(
level,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
);
}
}
pub(crate) fn transition(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
from: vk::ImageLayout,
to: vk::ImageLayout,
) {
let (src_stage, src_access) = source_scope(from);
let (dst_stage, dst_access) = destination_scope(to);
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(from)
.new_layout(to)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(
vk::ImageSubresourceRange::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.level_count(vk::REMAINING_MIP_LEVELS)
.layer_count(1),
)
.src_access_mask(src_access)
.dst_access_mask(dst_access);
unsafe {
device.cmd_pipeline_barrier(
cmd,
src_stage,
dst_stage,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
}
}
fn source_scope(layout: vk::ImageLayout) -> (vk::PipelineStageFlags, vk::AccessFlags) {
match layout {
vk::ImageLayout::UNDEFINED | vk::ImageLayout::PREINITIALIZED => (
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::AccessFlags::empty(),
),
vk::ImageLayout::TRANSFER_DST_OPTIMAL => (
vk::PipelineStageFlags::TRANSFER,
vk::AccessFlags::TRANSFER_WRITE,
),
vk::ImageLayout::TRANSFER_SRC_OPTIMAL => (
vk::PipelineStageFlags::TRANSFER,
vk::AccessFlags::TRANSFER_READ,
),
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => (
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
),
vk::ImageLayout::PRESENT_SRC_KHR => (
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::AccessFlags::empty(),
),
_ => (
vk::PipelineStageFlags::ALL_COMMANDS,
vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
),
}
}
fn destination_scope(layout: vk::ImageLayout) -> (vk::PipelineStageFlags, vk::AccessFlags) {
match layout {
vk::ImageLayout::TRANSFER_DST_OPTIMAL => (
vk::PipelineStageFlags::TRANSFER,
vk::AccessFlags::TRANSFER_WRITE,
),
vk::ImageLayout::TRANSFER_SRC_OPTIMAL => (
vk::PipelineStageFlags::TRANSFER,
vk::AccessFlags::TRANSFER_READ,
),
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::COLOR_ATTACHMENT_READ,
),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => (
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
),
vk::ImageLayout::PRESENT_SRC_KHR => (
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::AccessFlags::empty(),
),
_ => (
vk::PipelineStageFlags::ALL_COMMANDS,
vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
),
}
}
fn sample_count_flags(count: u32) -> vk::SampleCountFlags {
match count {
2 => vk::SampleCountFlags::TYPE_2,
4 => vk::SampleCountFlags::TYPE_4,
8 => vk::SampleCountFlags::TYPE_8,
16 => vk::SampleCountFlags::TYPE_16,
_ => vk::SampleCountFlags::TYPE_1,
}
}
pub(crate) fn backend_err(what: &str, e: vk::Result) -> Error {
Error::Backend {
backend: "vulkan",
detail: format!("{what}: {e:?}"),
}
}
pub(crate) trait WithDetail {
fn with_detail<E: std::fmt::Display>(self, e: E) -> Error;
}
impl WithDetail for Error {
fn with_detail<E: std::fmt::Display>(self, e: E) -> Error {
Error::Backend {
backend: "vulkan",
detail: format!("{self}: {e}"),
}
}
}
#[cfg(test)]
mod barrier_tests {
use super::*;
const USED: &[vk::ImageLayout] = &[
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::ImageLayout::PRESENT_SRC_KHR,
];
#[test]
fn a_known_layout_names_less_than_everything() {
for layout in USED {
let (src_stage, _) = source_scope(*layout);
let (dst_stage, _) = destination_scope(*layout);
assert_ne!(
src_stage,
vk::PipelineStageFlags::ALL_COMMANDS,
"{layout:?} falls back to a full drain as a source"
);
assert_ne!(
dst_stage,
vk::PipelineStageFlags::ALL_COMMANDS,
"{layout:?} falls back to a full drain as a destination"
);
}
}
#[test]
fn an_unknown_layout_names_everything() {
let (stage, access) = source_scope(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
assert_eq!(stage, vk::PipelineStageFlags::ALL_COMMANDS);
assert!(access.contains(vk::AccessFlags::MEMORY_WRITE));
}
#[test]
fn leaving_undefined_waits_for_nothing() {
let (stage, access) = source_scope(vk::ImageLayout::UNDEFINED);
assert_eq!(stage, vk::PipelineStageFlags::TOP_OF_PIPE);
assert!(access.is_empty());
}
}