use ash::vk::{self, Handle};
use imgui::internal::RawWrapper;
use vk_mem::Alloc;
use self::util::RaiiWrapper;
mod fonts;
mod init;
mod util;
pub const MIN_DESCRIPTOR_POOL_SIZE: u32 = 1;
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RendererCreateInfo {
pub queue_family: u32,
pub queue: vk::Queue,
pub render_pass: vk::RenderPass,
pub subpass: u32,
pub image_count: u32,
pub msaa_samples: Option<vk::SampleCountFlags>,
pub pipeline_cache: vk::PipelineCache,
pub surface_color_fmt: SurfaceColorFormat,
pub descriptor_pool_size: Option<u32>,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum SurfaceColorFormat {
#[default]
Linear,
Srgb,
}
impl RendererCreateInfo {
pub fn queue_family(mut self, queue_family: u32) -> Self {
self.queue_family = queue_family;
self
}
pub fn queue(mut self, queue: vk::Queue) -> Self {
self.queue = queue;
self
}
pub fn render_pass(mut self, render_pass: vk::RenderPass) -> Self {
self.render_pass = render_pass;
self
}
pub fn subpass(mut self, subpass: u32) -> Self {
self.subpass = subpass;
self
}
pub fn image_count(mut self, image_count: u32) -> Self {
self.image_count = image_count;
self
}
pub fn msaa_samples(mut self, msaa_samples: Option<vk::SampleCountFlags>) -> Self {
self.msaa_samples = msaa_samples;
self
}
pub fn pipeline_cache(mut self, pipeline_cache: vk::PipelineCache) -> Self {
self.pipeline_cache = pipeline_cache;
self
}
pub fn surface_color_fmt(mut self, surface_color_fmt: SurfaceColorFormat) -> Self {
self.surface_color_fmt = surface_color_fmt;
self
}
pub fn descriptor_pool_size(mut self, descriptor_pool_size: Option<u32>) -> Self {
self.descriptor_pool_size = descriptor_pool_size;
self
}
}
pub struct Renderer<A>
where
A: vk_mem::Alloc,
{
_instance: ash::Instance,
device: ash::Device,
create_info: RendererCreateInfo,
device_objects: DeviceObjects,
fonts_texture: Option<Texture>,
render_index: usize,
render_buffers: Vec<RenderBuffer>,
allocator: A,
}
struct Texture {
memory: vk_mem::Allocation,
image: vk::Image,
image_view: vk::ImageView,
descriptor_set: vk::DescriptorSet,
}
struct GpuBuffer {
buffer: vk::Buffer,
alloc: vk_mem::Allocation,
size: usize,
}
#[derive(Default)]
struct RenderBuffer {
vertex: Option<GpuBuffer>,
index: Option<GpuBuffer>,
}
struct DeviceObjects {
tex_sampler: vk::Sampler,
descriptor_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
pipeline_layout: vk::PipelineLayout,
pipeline: vk::Pipeline,
vert_shader: vk::ShaderModule,
frag_shader: vk::ShaderModule,
tex_command_pool: vk::CommandPool,
tex_command_buffer: vk::CommandBuffer,
wait_fence: vk::Fence,
}
impl<A> Renderer<A>
where
A: vk_mem::Alloc,
{
pub unsafe fn new(
instance: &ash::Instance,
device: &ash::Device,
allocator: A,
create_info: &RendererCreateInfo,
context: &mut imgui::Context,
) -> Result<Self, RendererCreateError> {
debug_assert!(
!instance.handle().is_null(),
"must pass in a valid instance (is null)"
);
debug_assert!(
!device.handle().is_null(),
"must pass in a valid device (is null)"
);
debug_assert!(
!create_info.queue.is_null(),
"must pass in a valid queue (is null)"
);
debug_assert!(
!create_info.render_pass.is_null(),
"must pass in a valid render pass (is null)"
);
debug_assert!(
create_info.image_count >= 2,
"must at least have two images, currently are {}",
create_info.image_count
);
debug_assert!(
create_info
.descriptor_pool_size
.is_none_or(|s| s >= MIN_DESCRIPTOR_POOL_SIZE),
"if descriptor pool size is set explicitly it needs to be bigger than MIN_DESCRIPTOR_POOL_SIZE"
);
let device_objects = unsafe { Self::create_device_objects(instance, device, create_info)? };
context
.io_mut()
.backend_flags
.insert(imgui::BackendFlags::RENDERER_HAS_VTX_OFFSET);
Ok(Self {
_instance: instance.clone(),
device: device.clone(),
allocator,
create_info: *create_info,
device_objects: device_objects.finalise(),
render_index: 0,
fonts_texture: None,
render_buffers: Vec::new(),
})
}
pub fn new_frame(&mut self, context: &mut imgui::Context) -> Result<(), FontsCreateError> {
if self.fonts_texture.is_some() {
return Ok(());
}
unsafe { self.create_fonts_texture(context) }
}
pub fn add_texture(
&self,
sampler: vk::Sampler,
view: vk::ImageView,
layout: vk::ImageLayout,
) -> Result<vk::DescriptorSet, TextureError> {
let layouts = [self.device_objects.descriptor_layout];
let descriptor_set_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(self.device_objects.descriptor_pool)
.set_layouts(&layouts);
let descriptor_set = RaiiWrapper::new(
unsafe { self.device.allocate_descriptor_sets(&descriptor_set_info) }
.map_err(TextureError::DescriptorSetCreateError)?[0],
|ds| unsafe {
self.device
.free_descriptor_sets(self.device_objects.descriptor_pool, &[ds])
.expect("cannot fail, see spec");
},
);
let descriptor_image_info = [vk::DescriptorImageInfo::default()
.sampler(sampler)
.image_view(view)
.image_layout(layout)];
let write_desc = vk::WriteDescriptorSet::default()
.dst_set(*descriptor_set)
.descriptor_count(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&descriptor_image_info);
unsafe {
self.device.update_descriptor_sets(&[write_desc], &[]);
}
Ok(descriptor_set.finalise())
}
pub unsafe fn remove_texture(&self, descriptor_set: vk::DescriptorSet) {
unsafe {
self.device
.free_descriptor_sets(self.device_objects.descriptor_pool, &[descriptor_set])
.expect("cannot fail according to spec")
}
}
pub fn render(
&mut self,
draw_data: &imgui::DrawData,
command_buffer: vk::CommandBuffer,
) -> Result<(), RendererError> {
let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0];
let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1];
if fb_width <= 0.0 || fb_height <= 0.0 {
return Ok(());
}
if self.render_buffers.is_empty() {
self.render_buffers
.resize_with(self.create_info.image_count as _, Default::default);
}
assert_eq!(
self.create_info.image_count,
self.render_buffers.len() as _,
"if image count has changed, call set_image_count"
);
let index = (self.render_index + 1) % self.render_buffers.len();
self.render_index = index;
if draw_data.total_vtx_count > 0 {
let vertex_size =
draw_data.total_vtx_count as usize * core::mem::size_of::<imgui::DrawVert>();
let idx_size =
draw_data.total_idx_count as usize * core::mem::size_of::<imgui::DrawIdx>();
create_or_update_buffer(
self.allocator.allocator(),
&mut self.render_buffers[index].vertex,
vertex_size,
vk::BufferUsageFlags::VERTEX_BUFFER,
)
.unwrap();
create_or_update_buffer(
self.allocator.allocator(),
&mut self.render_buffers[index].index,
idx_size,
vk::BufferUsageFlags::INDEX_BUFFER,
)
.unwrap();
{
let mut vtx_dst = unsafe {
self.allocator
.allocator()
.map_memory(&mut self.render_buffers[index].vertex.as_mut().unwrap().alloc)
}
.map_err(RendererError::MmapError)? as *mut _;
let mut idx_dst = unsafe {
self.allocator
.allocator()
.map_memory(&mut self.render_buffers[index].index.as_mut().unwrap().alloc)
}
.map_err(RendererError::MmapError)? as *mut _;
for list in draw_data.draw_lists() {
unsafe {
core::ptr::copy_nonoverlapping(
list.vtx_buffer().as_ptr(),
vtx_dst,
list.vtx_buffer().len(),
);
core::ptr::copy_nonoverlapping(
list.idx_buffer().as_ptr(),
idx_dst,
list.idx_buffer().len(),
);
vtx_dst = vtx_dst.add(list.vtx_buffer().len());
idx_dst = idx_dst.add(list.idx_buffer().len());
}
}
unsafe {
self.allocator.allocator().unmap_memory(
&mut self.render_buffers[index].vertex.as_mut().unwrap().alloc,
);
self.allocator.allocator().unmap_memory(
&mut self.render_buffers[index].index.as_mut().unwrap().alloc,
);
self.allocator
.allocator()
.flush_allocations(
[
&self.render_buffers[index].vertex.as_ref().unwrap().alloc,
&self.render_buffers[index].index.as_ref().unwrap().alloc,
],
None,
None,
)
.map_err(RendererError::FlushError)?;
}
}
}
unsafe {
self.setup_render_state(
draw_data,
self.device_objects.pipeline,
command_buffer,
&self.render_buffers[index],
fb_width,
fb_height,
)
};
let clip_off = draw_data.display_pos;
let clip_scale = draw_data.framebuffer_scale;
let mut global_vtx_offset = 0;
let mut global_idx_offset = 0;
if draw_data.draw_lists_count() > 0 {
for draw_list in draw_data.draw_lists() {
for draw_cmd in draw_list.commands() {
match draw_cmd {
imgui::DrawCmd::Elements { count, cmd_params } => {
let mut clip_min = [
(cmd_params.clip_rect[0] - clip_off[0]) * clip_scale[0],
(cmd_params.clip_rect[1] - clip_off[1]) * clip_scale[1],
];
let mut clip_max = [
(cmd_params.clip_rect[2] - clip_off[0]) * clip_scale[0],
(cmd_params.clip_rect[3] - clip_off[1]) * clip_scale[1],
];
if clip_min[0] < 0.0 {
clip_min[0] = 0.0;
}
if clip_min[1] < 0.0 {
clip_min[1] = 0.0;
}
if clip_max[0] > fb_width {
clip_max[0] = fb_width;
}
if clip_max[1] > fb_height {
clip_max[1] = fb_height;
}
if clip_max[0] <= clip_min[0] || clip_max[1] <= clip_min[1] {
continue;
}
let scissor = vk::Rect2D::default()
.offset(
vk::Offset2D::default()
.x(clip_min[0] as _)
.y(clip_min[1] as _),
)
.extent(
vk::Extent2D::default()
.width((clip_max[0] - clip_min[0]) as _)
.height((clip_max[1] - clip_min[1]) as _),
);
let desc_set =
vk::DescriptorSet::from_raw(cmd_params.texture_id.id() as _);
unsafe {
self.device.cmd_set_scissor(command_buffer, 0, &[scissor]);
self.device.cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.device_objects.pipeline_layout,
0,
&[desc_set],
&[],
);
self.device.cmd_draw_indexed(
command_buffer,
count as _,
1,
(cmd_params.idx_offset + global_idx_offset) as _,
(cmd_params.vtx_offset + global_vtx_offset) as _,
0,
);
}
}
imgui::DrawCmd::ResetRenderState => unsafe {
self.setup_render_state(
draw_data,
self.device_objects.pipeline,
command_buffer,
&self.render_buffers[index],
fb_width,
fb_height,
);
},
imgui::DrawCmd::RawCallback { callback, raw_cmd } => unsafe {
callback(draw_list.raw(), raw_cmd);
},
}
}
global_vtx_offset += draw_list.vtx_buffer().len();
global_idx_offset += draw_list.idx_buffer().len();
}
}
let scissor = vk::Rect2D::default()
.offset(vk::Offset2D::default().x(0).y(0))
.extent(
vk::Extent2D::default()
.width(fb_width as u32)
.height(fb_height as u32),
);
unsafe {
self.device.cmd_set_scissor(command_buffer, 0, &[scissor]);
}
Ok(())
}
unsafe fn setup_render_state(
&self,
draw_data: &imgui::DrawData,
pipeline: vk::Pipeline,
command_buffer: vk::CommandBuffer,
_render_buffer: &RenderBuffer,
width: f32,
height: f32,
) {
let device = &self.device;
unsafe {
device.cmd_bind_pipeline(command_buffer, vk::PipelineBindPoint::GRAPHICS, pipeline);
}
if draw_data.total_vtx_count > 0 {
unsafe {
device.cmd_bind_vertex_buffers(
command_buffer,
0,
&[self.render_buffers[self.render_index]
.vertex
.as_ref()
.unwrap()
.buffer],
&[0],
);
device.cmd_bind_index_buffer(
command_buffer,
self.render_buffers[self.render_index]
.index
.as_ref()
.unwrap()
.buffer,
0,
if core::mem::size_of::<imgui::DrawIdx>() == 2 {
vk::IndexType::UINT16
} else {
vk::IndexType::UINT32
},
);
}
}
{
let viewport = vk::Viewport::default()
.x(0.0)
.y(0.0)
.width(width)
.height(height)
.min_depth(0.0)
.max_depth(1.0);
unsafe {
device.cmd_set_viewport(command_buffer, 0, &[viewport]);
}
}
{
let scale = [
2.0 / draw_data.display_size[0],
2.0 / draw_data.display_size[1],
];
let scale_bytes = bytemuck::cast_slice(&scale);
let translate = [
-1.0 - draw_data.display_pos[0] * scale[0],
-1.0 - draw_data.display_pos[1] * scale[1],
];
let translate_bytes = bytemuck::cast_slice(&translate);
let is_srgb = [match self.create_info.surface_color_fmt {
SurfaceColorFormat::Srgb => vk::TRUE,
_ => vk::FALSE,
}];
unsafe {
#[allow(clippy::erasing_op)]
device.cmd_push_constants(
command_buffer,
self.device_objects.pipeline_layout,
vk::ShaderStageFlags::VERTEX,
core::mem::size_of::<f32>() as u32 * 0,
scale_bytes,
);
device.cmd_push_constants(
command_buffer,
self.device_objects.pipeline_layout,
vk::ShaderStageFlags::VERTEX,
core::mem::size_of::<f32>() as u32 * 2,
translate_bytes,
);
device.cmd_push_constants(
command_buffer,
self.device_objects.pipeline_layout,
vk::ShaderStageFlags::VERTEX,
core::mem::size_of::<f32>() as u32 * 4,
bytemuck::cast_slice(&is_srgb),
);
}
}
}
unsafe fn destroy_device_objects(device: &ash::Device, device_objects: &DeviceObjects) {
unsafe {
device.destroy_command_pool(device_objects.tex_command_pool, None);
device.destroy_descriptor_pool(device_objects.descriptor_pool, None);
device.destroy_pipeline(device_objects.pipeline, None);
device.destroy_shader_module(device_objects.vert_shader, None);
device.destroy_shader_module(device_objects.frag_shader, None);
device.destroy_pipeline_layout(device_objects.pipeline_layout, None);
device.destroy_descriptor_set_layout(device_objects.descriptor_layout, None);
device.destroy_sampler(device_objects.tex_sampler, None);
device.destroy_fence(device_objects.wait_fence, None);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RendererCreateError {
#[error("failed to create texture sampler")]
SamplerCreateError(#[source] vk::Result),
#[error("failed to create descriptor set layout for sampling texture")]
DescriptorSetCreateError(#[source] vk::Result),
#[error("failed to create descriptor pool for sampling textures")]
DescriptorPoolCreateError(#[source] vk::Result),
#[error("failed to create pipeline layout")]
PipelineLayoutCreateError(#[source] vk::Result),
#[error("failed to create shader modules")]
ShaderModuleCreateError(#[source] vk::Result),
#[error("failed to create graphics pipeline")]
PipelineCreateError(#[source] vk::Result),
#[error("failed to create command pool for texture transfer")]
TexCommandPoolCreateError(#[source] vk::Result),
#[error("failed to create command buffer for texture transfer")]
TexCommandBufferAllocError(#[source] vk::Result),
#[error("failed to allocate synchronisation objects")]
SyncobjCreateError(#[source] vk::Result),
}
#[derive(thiserror::Error, Debug)]
pub enum FontsCreateError {
#[error("failed to reset command pool for texture transfer")]
CommandPoolResetError(#[source] vk::Result),
#[error("failed to begin command buffer for texture transfer")]
CommandBeginError(#[source] vk::Result),
#[error("failed to allocate image for font atlas texture")]
ImageAllocError(#[source] vk::Result),
#[error("failed to create image view for font atlas texture")]
ImageViewCreateError(#[source] vk::Result),
#[error("failed to create texture for font atlas")]
TextureCreateError(
#[source]
#[from]
TextureError,
),
#[error("failed to create image transfer buffer for copying atlas into image")]
TransferbufferCreateError(#[source] vk::Result),
#[error("failed to map memory for copy operation")]
MmapError(#[source] vk::Result),
#[error("failed to flush allocated region after copy")]
FlushError(#[source] vk::Result),
#[error("failed to end command buffer recording for transfer operation")]
CommandEndError(#[source] vk::Result),
#[error("failed to submit copy operation to queue")]
SubmitError(#[source] vk::Result),
}
#[derive(thiserror::Error, Debug)]
pub enum TextureError {
#[error("failed to create descriptor set for texture")]
DescriptorSetCreateError(#[source] vk::Result),
}
#[derive(thiserror::Error, Debug)]
pub enum RendererError {
#[error("failed to map memory for vertex or index copy")]
MmapError(#[source] vk::Result),
#[error("flushing allocations for copied vertex data failed")]
FlushError(#[source] vk::Result),
}
fn create_or_update_buffer(
allocator: &vk_mem::Allocator,
buffer: &mut Option<GpuBuffer>,
new_size: usize,
usage: vk::BufferUsageFlags,
) -> ash::prelude::VkResult<()> {
if let Some(buf) = buffer {
if buf.size >= new_size {
return Ok(());
}
unsafe { allocator.destroy_buffer(buf.buffer, &mut buf.alloc) };
}
*buffer = None;
let buffer_info = vk::BufferCreateInfo::default()
.size(new_size as _)
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let alloc_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::AutoPreferDevice,
flags: vk_mem::AllocationCreateFlags::HOST_ACCESS_SEQUENTIAL_WRITE,
..Default::default()
};
let (buf, alloc) = unsafe { allocator.create_buffer(&buffer_info, &alloc_info) }?;
*buffer = Some(GpuBuffer {
buffer: buf,
alloc,
size: new_size,
});
Ok(())
}
impl<A> Drop for Renderer<A>
where
A: vk_mem::Alloc,
{
fn drop(&mut self) {
unsafe {
for buffer in self.render_buffers.drain(..) {
if let Some(GpuBuffer {
buffer, mut alloc, ..
}) = buffer.vertex
{
self.allocator
.allocator()
.destroy_buffer(buffer, &mut alloc);
}
if let Some(GpuBuffer {
buffer, mut alloc, ..
}) = buffer.index
{
self.allocator
.allocator()
.destroy_buffer(buffer, &mut alloc);
}
}
self.destroy_fonts_texture(None);
Self::destroy_device_objects(&self.device, &self.device_objects);
}
}
}