use std::collections::HashMap;
use std::io::Cursor;
use ash::vk;
use fontdue::{Font, FontSettings};
use super::{Gpu, Target};
const VERT_SPV: &[u8] = include_bytes!("../../shaders/text.vert.spv");
const FRAG_SPV: &[u8] = include_bytes!("../../shaders/text.frag.spv");
const FONT_TTF: &[u8] = include_bytes!("../../assets/LiberationSans-Regular.ttf");
const PX: f32 = 56.0;
const ATLAS_W: usize = 1024;
pub const THUMB_W: u32 = 480;
pub const THUMB_H: u32 = 270;
const THUMB_COLS: u32 = 8;
const THUMB_ROWS: u32 = 8;
pub const THUMB_SLOTS: usize = (THUMB_COLS * THUMB_ROWS) as usize;
const THUMB_ATLAS_W: u32 = THUMB_W * THUMB_COLS;
const THUMB_ATLAS_H: u32 = THUMB_H * THUMB_ROWS;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Vertex {
pos: [f32; 2],
uv: [f32; 2],
color: [f32; 4],
tex: f32, }
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Thumb {
pub uv: [f32; 4], pub w: f32,
pub h: f32,
}
impl Thumb {
fn new(slot: usize, w: u32, h: u32) -> Thumb {
let (x, y) = slot_origin(slot);
let (aw, ah) = (THUMB_ATLAS_W as f32, THUMB_ATLAS_H as f32);
Thumb {
uv: [x as f32 / aw, y as f32 / ah, (x + w) as f32 / aw, (y + h) as f32 / ah],
w: w as f32,
h: h as f32,
}
}
pub fn cover(&self, w: f32, h: f32) -> [f32; 4] {
let [u0, v0, u1, v1] = self.uv;
if self.w <= 0.0 || self.h <= 0.0 || w <= 0.0 || h <= 0.0 {
return self.uv;
}
let (box_aspect, img_aspect) = (w / h, self.w / self.h);
if img_aspect > box_aspect {
let keep = (u1 - u0) * box_aspect / img_aspect;
let u = u0 + (u1 - u0 - keep) / 2.0;
[u, v0, u + keep, v1]
} else {
let keep = (v1 - v0) * img_aspect / box_aspect;
let v = v0 + (v1 - v0 - keep) / 2.0;
[u0, v, u1, v + keep]
}
}
}
fn slot_origin(slot: usize) -> (u32, u32) {
let slot = slot as u32 % (THUMB_COLS * THUMB_ROWS);
((slot % THUMB_COLS) * THUMB_W, (slot / THUMB_COLS) * THUMB_H)
}
#[derive(Clone, Copy)]
struct Glyph {
uv: [f32; 4], size: [f32; 2], offset: [f32; 2], advance: f32,
}
struct Atlas {
glyphs: HashMap<char, Glyph>,
pixels: Vec<u8>,
height: usize,
solid_uv: [f32; 2],
}
fn glyph(glyphs: &HashMap<char, Glyph>, c: char) -> &Glyph {
glyphs.get(&c).unwrap_or_else(|| &glyphs[&'?'])
}
pub struct Text {
glyphs: HashMap<char, Glyph>,
solid_uv: [f32; 2], line_height: f32,
ascent: f32,
image: vk::Image,
mem: vk::DeviceMemory,
view: vk::ImageView,
sampler: vk::Sampler,
set_layout: vk::DescriptorSetLayout,
desc_pool: vk::DescriptorPool,
set: vk::DescriptorSet,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
pipeline: vk::Pipeline,
vert: vk::ShaderModule,
frag: vk::ShaderModule,
vbuf: vk::Buffer,
vmem: vk::DeviceMemory,
vcap: usize,
vptr: *mut Vertex,
thumbs: ThumbGpu,
}
struct ThumbGpu {
image: vk::Image,
mem: vk::DeviceMemory,
view: vk::ImageView,
pool: vk::CommandPool,
cmd: vk::CommandBuffer,
fence: vk::Fence,
staging: vk::Buffer,
smem: vk::DeviceMemory,
sptr: *mut u8,
}
#[derive(Default)]
pub struct Frame {
verts: Vec<Vertex>,
}
impl Frame {
pub fn clear(&mut self) {
self.verts.clear();
}
pub fn rect(&mut self, text: &Text, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) {
self.quad(x, y, w, h, text.solid_uv, text.solid_uv, color, 0.0);
}
pub fn image(&mut self, uv: [f32; 4], x: f32, y: f32, w: f32, h: f32, tint: [f32; 4]) {
self.quad(x, y, w, h, [uv[0], uv[1]], [uv[2], uv[3]], tint, 1.0);
}
pub fn text(&mut self, text: &Text, x: f32, y: f32, px: f32, s: &str, color: [f32; 4]) -> f32 {
let scale = px / PX;
let baseline = y + text.ascent * scale;
let mut pen = x;
for c in s.chars() {
let g = glyph(&text.glyphs, c);
if g.size[0] > 0.0 && g.size[1] > 0.0 {
let gx = pen + g.offset[0] * scale;
let gy = baseline - (g.offset[1] + g.size[1]) * scale;
self.quad(
gx,
gy,
g.size[0] * scale,
g.size[1] * scale,
[g.uv[0], g.uv[1]],
[g.uv[2], g.uv[3]],
color,
0.0,
);
}
pen += g.advance * scale;
}
pen - x
}
pub fn measure(&self, text: &Text, px: f32, s: &str) -> f32 {
let scale = px / PX;
s.chars().map(|c| glyph(&text.glyphs, c).advance * scale).sum()
}
#[allow(clippy::too_many_arguments)]
fn quad(&mut self, x: f32, y: f32, w: f32, h: f32, uv0: [f32; 2], uv1: [f32; 2], c: [f32; 4], tex: f32) {
let (x1, y1) = (x + w, y + h);
let tl = Vertex { pos: [x, y], uv: uv0, color: c, tex };
let tr = Vertex { pos: [x1, y], uv: [uv1[0], uv0[1]], color: c, tex };
let br = Vertex { pos: [x1, y1], uv: uv1, color: c, tex };
let bl = Vertex { pos: [x, y1], uv: [uv0[0], uv1[1]], color: c, tex };
self.verts.extend_from_slice(&[tl, tr, br, tl, br, bl]);
}
}
fn rasterize_atlas(font: &Font) -> Atlas {
let mut pixels = vec![0u8; ATLAS_W * 16];
let mut height = 16usize;
let (mut cx, mut cy, mut row_h) = (3usize, 1usize, 2usize);
let mut glyphs = HashMap::new();
for y in 0..2 {
for x in 0..2 {
pixels[y * ATLAS_W + x] = 255;
}
}
let mut chars: Vec<char> = font.chars().keys().copied().collect();
chars.sort_unstable();
for c in chars {
let (m, bitmap) = font.rasterize(c, PX);
if cx + m.width + 1 > ATLAS_W {
cx = 1;
cy += row_h + 1;
row_h = 0;
}
while cy + m.height + 1 > height {
height *= 2;
pixels.resize(ATLAS_W * height, 0);
}
let (gx, gy) = (cx, cy);
cx += m.width + 1;
row_h = row_h.max(m.height);
for row in 0..m.height {
let dst = (gy + row) * ATLAS_W + gx;
pixels[dst..dst + m.width].copy_from_slice(&bitmap[row * m.width..(row + 1) * m.width]);
}
glyphs.insert(
c,
Glyph {
uv: [gx as f32, gy as f32, (gx + m.width) as f32, (gy + m.height) as f32],
size: [m.width as f32, m.height as f32],
offset: [m.xmin as f32, m.ymin as f32],
advance: m.advance_width,
},
);
}
let (aw, ah) = (ATLAS_W as f32, height as f32);
for g in glyphs.values_mut() {
g.uv = [g.uv[0] / aw, g.uv[1] / ah, g.uv[2] / aw, g.uv[3] / ah];
}
Atlas { glyphs, pixels, height, solid_uv: [1.0 / aw, 1.0 / ah] }
}
impl Gpu {
pub fn create_text(&self) -> Result<Text, String> {
let font = Font::from_bytes(FONT_TTF, FontSettings { scale: PX, ..Default::default() })
.map_err(|e| format!("font: {e}"))?;
let Atlas { glyphs, pixels, height, solid_uv } = rasterize_atlas(&font);
let line_metrics = font.horizontal_line_metrics(PX).ok_or("no line metrics")?;
let text_gpu = self.upload_atlas(&pixels, ATLAS_W as u32, height as u32)?;
let thumbs = self.create_thumb_atlas()?;
self.finish_text(text_gpu, thumbs, glyphs, solid_uv, line_metrics.new_line_size, line_metrics.ascent)
}
}
struct AtlasGpu {
image: vk::Image,
mem: vk::DeviceMemory,
view: vk::ImageView,
sampler: vk::Sampler,
}
const RANGE: vk::ImageSubresourceRange = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
impl Gpu {
fn upload_atlas(&self, pixels: &[u8], w: u32, h: u32) -> Result<AtlasGpu, String> {
let d = &self.device;
let image = unsafe {
d.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::R8_UNORM)
.extent(vk::Extent3D { width: w, height: h, depth: 1 })
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)
}
.map_err(|e| format!("atlas image: {e}"))?;
let req = unsafe { d.get_image_memory_requirements(image) };
let mt = self
.find_memory_type_with(req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)
.ok_or("no device-local memory")?;
let mem = unsafe {
d.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None)
}
.map_err(|e| format!("atlas mem: {e}"))?;
unsafe { d.bind_image_memory(image, mem, 0) }.map_err(|e| format!("bind atlas: {e}"))?;
let (staging, smem) = self.host_buffer(pixels)?;
let pool = unsafe {
d.create_command_pool(&vk::CommandPoolCreateInfo::default().queue_family_index(self.queue_family), None)
}
.map_err(|e| format!("pool: {e}"))?;
let cmd = unsafe {
d.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default().command_pool(pool).command_buffer_count(1),
)
}
.map_err(|e| format!("cmd: {e}"))?[0];
unsafe {
d.begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)).map_err(|e| format!("begin: {e}"))?;
barrier(d, cmd, image, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::AccessFlags::empty(), vk::AccessFlags::TRANSFER_WRITE, vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER);
let region = vk::BufferImageCopy::default()
.image_subresource(vk::ImageSubresourceLayers::default().aspect_mask(vk::ImageAspectFlags::COLOR).layer_count(1))
.image_extent(vk::Extent3D { width: w, height: h, depth: 1 });
d.cmd_copy_buffer_to_image(cmd, staging, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[region]);
barrier(d, cmd, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::AccessFlags::TRANSFER_WRITE, vk::AccessFlags::SHADER_READ, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER);
d.end_command_buffer(cmd).map_err(|e| format!("end: {e}"))?;
let fence = d.create_fence(&vk::FenceCreateInfo::default(), None).map_err(|e| format!("fence: {e}"))?;
let cmds = [cmd];
d.queue_submit(self.queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], fence).map_err(|e| format!("submit: {e}"))?;
d.wait_for_fences(&[fence], true, u64::MAX).map_err(|e| format!("wait: {e}"))?;
d.destroy_fence(fence, None);
d.destroy_command_pool(pool, None);
d.destroy_buffer(staging, None);
d.free_memory(smem, None);
}
let view = unsafe {
d.create_image_view(
&vk::ImageViewCreateInfo::default().image(image).view_type(vk::ImageViewType::TYPE_2D).format(vk::Format::R8_UNORM).subresource_range(RANGE),
None,
)
}
.map_err(|e| format!("atlas view: {e}"))?;
let sampler = unsafe {
d.create_sampler(
&vk::SamplerCreateInfo::default()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR)
.address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE),
None,
)
}
.map_err(|e| format!("sampler: {e}"))?;
Ok(AtlasGpu { image, mem, view, sampler })
}
fn create_thumb_atlas(&self) -> Result<ThumbGpu, String> {
let d = &self.device;
let format = vk::Format::R8G8B8A8_UNORM;
let image = unsafe {
d.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format)
.extent(vk::Extent3D { width: THUMB_ATLAS_W, height: THUMB_ATLAS_H, depth: 1 })
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)
}
.map_err(|e| format!("thumb image: {e}"))?;
let req = unsafe { d.get_image_memory_requirements(image) };
let mt = self
.find_memory_type_with(req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)
.ok_or("no device-local memory")?;
let mem = unsafe {
d.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None)
}
.map_err(|e| format!("thumb mem: {e}"))?;
unsafe { d.bind_image_memory(image, mem, 0) }.map_err(|e| format!("bind thumbs: {e}"))?;
let view = unsafe {
d.create_image_view(
&vk::ImageViewCreateInfo::default().image(image).view_type(vk::ImageViewType::TYPE_2D).format(format).subresource_range(RANGE),
None,
)
}
.map_err(|e| format!("thumb view: {e}"))?;
let pool = unsafe {
d.create_command_pool(&vk::CommandPoolCreateInfo::default().queue_family_index(self.queue_family), None)
}
.map_err(|e| format!("pool: {e}"))?;
let cmd = unsafe {
d.allocate_command_buffers(&vk::CommandBufferAllocateInfo::default().command_pool(pool).command_buffer_count(1))
}
.map_err(|e| format!("cmd: {e}"))?[0];
let fence = unsafe { d.create_fence(&vk::FenceCreateInfo::default(), None) }.map_err(|e| format!("fence: {e}"))?;
let size = (THUMB_W * THUMB_H * 4) as u64;
let (staging, smem) = self.empty_host_buffer(size, vk::BufferUsageFlags::TRANSFER_SRC)?;
let sptr = unsafe { d.map_memory(smem, 0, size, vk::MemoryMapFlags::empty()) }.map_err(|e| format!("map staging: {e}"))? as *mut u8;
let thumbs = ThumbGpu { image, mem, view, pool, cmd, fence, staging, smem, sptr };
thumbs.submit(self, |cmd| unsafe {
barrier(d, cmd, image, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::AccessFlags::empty(), vk::AccessFlags::TRANSFER_WRITE, vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER);
let color = vk::ClearColorValue { float32: [0.13, 0.15, 0.21, 1.0] };
d.cmd_clear_color_image(cmd, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &color, &[RANGE]);
barrier(d, cmd, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::AccessFlags::TRANSFER_WRITE, vk::AccessFlags::SHADER_READ, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER);
})?;
Ok(thumbs)
}
fn finish_text(&self, a: AtlasGpu, thumbs: ThumbGpu, glyphs: HashMap<char, Glyph>, solid_uv: [f32; 2], line_height: f32, ascent: f32) -> Result<Text, String> {
let d = &self.device;
let sampler_binding = |binding| {
vk::DescriptorSetLayoutBinding::default()
.binding(binding)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
};
let bindings = [sampler_binding(0), sampler_binding(1)];
let set_layout = unsafe {
d.create_descriptor_set_layout(&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), None)
}
.map_err(|e| format!("set layout: {e}"))?;
let sizes = [vk::DescriptorPoolSize::default().ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).descriptor_count(2)];
let desc_pool = unsafe {
d.create_descriptor_pool(&vk::DescriptorPoolCreateInfo::default().max_sets(1).pool_sizes(&sizes), None)
}
.map_err(|e| format!("desc pool: {e}"))?;
let layouts = [set_layout];
let set = unsafe {
d.allocate_descriptor_sets(&vk::DescriptorSetAllocateInfo::default().descriptor_pool(desc_pool).set_layouts(&layouts))
}
.map_err(|e| format!("alloc set: {e}"))?[0];
let img_info = [vk::DescriptorImageInfo::default().image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL).image_view(a.view).sampler(a.sampler)];
let thumb_info = [vk::DescriptorImageInfo::default().image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL).image_view(thumbs.view).sampler(a.sampler)];
unsafe {
d.update_descriptor_sets(&[
vk::WriteDescriptorSet::default().dst_set(set).dst_binding(0).descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).image_info(&img_info),
vk::WriteDescriptorSet::default().dst_set(set).dst_binding(1).descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).image_info(&thumb_info),
], &[]);
}
let render_pass = color_render_pass(d)?;
let vert = module(d, VERT_SPV)?;
let frag = module(d, FRAG_SPV)?;
let push = [vk::PushConstantRange::default().stage_flags(vk::ShaderStageFlags::VERTEX).offset(0).size(8)];
let set_layouts = [set_layout];
let layout = unsafe {
d.create_pipeline_layout(&vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts).push_constant_ranges(&push), None)
}
.map_err(|e| format!("layout: {e}"))?;
let pipeline = text_pipeline(d, render_pass, layout, vert, frag)?;
let vcap = 16384usize; let size = (vcap * std::mem::size_of::<Vertex>()) as u64;
let (vbuf, vmem) = self.empty_host_buffer(size, vk::BufferUsageFlags::VERTEX_BUFFER)?;
let vptr = unsafe { d.map_memory(vmem, 0, size, vk::MemoryMapFlags::empty()) }.map_err(|e| format!("map vbuf: {e}"))? as *mut Vertex;
Ok(Text {
glyphs, solid_uv, line_height, ascent,
image: a.image, mem: a.mem, view: a.view, sampler: a.sampler,
set_layout, desc_pool, set, render_pass, layout, pipeline, vert, frag,
vbuf, vmem, vcap, vptr, thumbs,
})
}
fn host_buffer(&self, bytes: &[u8]) -> Result<(vk::Buffer, vk::DeviceMemory), String> {
let (buf, mem) = self.empty_host_buffer(bytes.len() as u64, vk::BufferUsageFlags::TRANSFER_SRC)?;
unsafe {
let p = self.device.map_memory(mem, 0, bytes.len() as u64, vk::MemoryMapFlags::empty()).map_err(|e| format!("map: {e}"))? as *mut u8;
p.copy_from_nonoverlapping(bytes.as_ptr(), bytes.len());
self.device.unmap_memory(mem);
}
Ok((buf, mem))
}
fn empty_host_buffer(&self, size: u64, usage: vk::BufferUsageFlags) -> Result<(vk::Buffer, vk::DeviceMemory), String> {
let d = &self.device;
let buf = unsafe { d.create_buffer(&vk::BufferCreateInfo::default().size(size).usage(usage).sharing_mode(vk::SharingMode::EXCLUSIVE), None) }.map_err(|e| format!("buffer: {e}"))?;
let req = unsafe { d.get_buffer_memory_requirements(buf) };
let mt = self.find_memory_type_with(req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT).ok_or("no host memory")?;
let mem = unsafe { d.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None) }.map_err(|e| format!("mem: {e}"))?;
unsafe { d.bind_buffer_memory(buf, mem, 0) }.map_err(|e| format!("bind: {e}"))?;
Ok((buf, mem))
}
}
impl ThumbGpu {
fn submit(&self, gpu: &Gpu, f: impl FnOnce(vk::CommandBuffer)) -> Result<(), String> {
let d = &gpu.device;
unsafe {
d.reset_command_pool(self.pool, vk::CommandPoolResetFlags::empty()).map_err(|e| format!("reset: {e}"))?;
d.begin_command_buffer(self.cmd, &vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)).map_err(|e| format!("begin: {e}"))?;
f(self.cmd);
d.end_command_buffer(self.cmd).map_err(|e| format!("end: {e}"))?;
let cmds = [self.cmd];
d.queue_submit(gpu.queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], self.fence).map_err(|e| format!("submit: {e}"))?;
d.wait_for_fences(&[self.fence], true, u64::MAX).map_err(|e| format!("wait: {e}"))?;
d.reset_fences(&[self.fence]).map_err(|e| format!("reset fence: {e}"))?;
}
Ok(())
}
}
impl Text {
pub fn upload_thumb(&self, gpu: &Gpu, slot: usize, w: u32, h: u32, rgba: &[u8]) -> Result<Thumb, String> {
let (w, h) = (w.min(THUMB_W), h.min(THUMB_H));
let bytes = (w * h * 4) as usize;
if w == 0 || h == 0 || rgba.len() < bytes {
return Err(format!("thumb: {w}x{h} with {} bytes", rgba.len()));
}
let t = &self.thumbs;
unsafe { t.sptr.copy_from_nonoverlapping(rgba.as_ptr(), bytes) };
let (x, y) = slot_origin(slot);
let d = &gpu.device;
t.submit(gpu, |cmd| unsafe {
barrier(d, cmd, t.image, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::AccessFlags::SHADER_READ, vk::AccessFlags::TRANSFER_WRITE, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::PipelineStageFlags::TRANSFER);
let region = vk::BufferImageCopy::default()
.image_subresource(vk::ImageSubresourceLayers::default().aspect_mask(vk::ImageAspectFlags::COLOR).layer_count(1))
.image_offset(vk::Offset3D { x: x as i32, y: y as i32, z: 0 })
.image_extent(vk::Extent3D { width: w, height: h, depth: 1 });
d.cmd_copy_buffer_to_image(cmd, t.staging, t.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[region]);
barrier(d, cmd, t.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::AccessFlags::TRANSFER_WRITE, vk::AccessFlags::SHADER_READ, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER);
})?;
Ok(Thumb::new(slot, w, h))
}
#[allow(dead_code)] pub fn line_height(&self) -> f32 {
self.line_height
}
pub fn framebuffer(&self, gpu: &Gpu, target: &Target) -> Result<vk::Framebuffer, String> {
let views = [target.view];
unsafe {
gpu.device.create_framebuffer(
&vk::FramebufferCreateInfo::default()
.render_pass(self.render_pass)
.attachments(&views)
.width(target.width)
.height(target.height)
.layers(1),
None,
)
}
.map_err(|e| format!("framebuffer: {e}"))
}
pub fn render(&self, gpu: &Gpu, target: &Target, fb: vk::Framebuffer, frame: &Frame) -> Result<(), String> {
let n = frame.verts.len().min(self.vcap);
unsafe { self.vptr.copy_from_nonoverlapping(frame.verts.as_ptr(), n) };
let d = &gpu.device;
let cmd = target.cmd;
let extent = target.extent();
unsafe {
d.reset_command_pool(target.pool, vk::CommandPoolResetFlags::empty()).map_err(|e| format!("reset: {e}"))?;
d.begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)).map_err(|e| format!("begin: {e}"))?;
let clear = [vk::ClearValue { color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] } }];
d.cmd_begin_render_pass(cmd, &vk::RenderPassBeginInfo::default().render_pass(self.render_pass).framebuffer(fb).render_area(vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent }).clear_values(&clear), vk::SubpassContents::INLINE);
let vp = [vk::Viewport { x: 0.0, y: 0.0, width: extent.width as f32, height: extent.height as f32, min_depth: 0.0, max_depth: 1.0 }];
let sc = [vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent }];
d.cmd_set_viewport(cmd, 0, &vp);
d.cmd_set_scissor(cmd, 0, &sc);
d.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
d.cmd_bind_descriptor_sets(cmd, vk::PipelineBindPoint::GRAPHICS, self.layout, 0, &[self.set], &[]);
d.cmd_bind_vertex_buffers(cmd, 0, &[self.vbuf], &[0]);
let screen = [extent.width as f32, extent.height as f32];
d.cmd_push_constants(cmd, self.layout, vk::ShaderStageFlags::VERTEX, 0, bytemuck_f32x2(&screen));
d.cmd_draw(cmd, n as u32, 1, 0, 0);
d.cmd_end_render_pass(cmd);
d.end_command_buffer(cmd).map_err(|e| format!("end: {e}"))?;
let cmds = [cmd];
d.queue_submit(gpu.queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], target.fence).map_err(|e| format!("submit: {e}"))?;
d.wait_for_fences(&[target.fence], true, u64::MAX).map_err(|e| format!("wait: {e}"))?;
d.reset_fences(&[target.fence]).map_err(|e| format!("reset fence: {e}"))?;
}
Ok(())
}
pub fn destroy(self, gpu: &Gpu) {
let d = &gpu.device;
unsafe {
d.destroy_buffer(self.vbuf, None);
d.free_memory(self.vmem, None);
d.destroy_pipeline(self.pipeline, None);
d.destroy_pipeline_layout(self.layout, None);
d.destroy_shader_module(self.vert, None);
d.destroy_shader_module(self.frag, None);
d.destroy_render_pass(self.render_pass, None);
d.destroy_descriptor_pool(self.desc_pool, None);
d.destroy_descriptor_set_layout(self.set_layout, None);
d.destroy_sampler(self.sampler, None);
d.destroy_image_view(self.view, None);
d.destroy_image(self.image, None);
d.free_memory(self.mem, None);
let t = self.thumbs;
d.destroy_buffer(t.staging, None);
d.free_memory(t.smem, None);
d.destroy_fence(t.fence, None);
d.destroy_command_pool(t.pool, None);
d.destroy_image_view(t.view, None);
d.destroy_image(t.image, None);
d.free_memory(t.mem, None);
}
}
}
fn bytemuck_f32x2(v: &[f32; 2]) -> &[u8] {
unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, 8) }
}
fn barrier(d: &ash::Device, cmd: vk::CommandBuffer, image: vk::Image, old: vk::ImageLayout, new: vk::ImageLayout, src: vk::AccessFlags, dst: vk::AccessFlags, src_stage: vk::PipelineStageFlags, dst_stage: vk::PipelineStageFlags) {
let b = vk::ImageMemoryBarrier::default()
.old_layout(old).new_layout(new)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED).dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image).subresource_range(RANGE).src_access_mask(src).dst_access_mask(dst);
unsafe { d.cmd_pipeline_barrier(cmd, src_stage, dst_stage, vk::DependencyFlags::empty(), &[], &[], &[b]) };
}
fn module(d: &ash::Device, spv: &[u8]) -> Result<vk::ShaderModule, String> {
let code = ash::util::read_spv(&mut Cursor::new(spv)).map_err(|e| format!("spv: {e}"))?;
unsafe { d.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&code), None) }.map_err(|e| format!("module: {e}"))
}
fn color_render_pass(d: &ash::Device) -> Result<vk::RenderPass, String> {
let color = vk::AttachmentDescription::default()
.format(vk::Format::B8G8R8A8_UNORM).samples(vk::SampleCountFlags::TYPE_1)
.load_op(vk::AttachmentLoadOp::CLEAR).store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE).stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED).final_layout(vk::ImageLayout::GENERAL);
let cref = [vk::AttachmentReference::default().attachment(0).layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
let sub = [vk::SubpassDescription::default().pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS).color_attachments(&cref)];
let att = [color];
unsafe { d.create_render_pass(&vk::RenderPassCreateInfo::default().attachments(&att).subpasses(&sub), None) }.map_err(|e| format!("render pass: {e}"))
}
fn text_pipeline(d: &ash::Device, rp: vk::RenderPass, layout: vk::PipelineLayout, vert: vk::ShaderModule, frag: vk::ShaderModule) -> Result<vk::Pipeline, String> {
let entry = c"main";
let stages = [
vk::PipelineShaderStageCreateInfo::default().stage(vk::ShaderStageFlags::VERTEX).module(vert).name(entry),
vk::PipelineShaderStageCreateInfo::default().stage(vk::ShaderStageFlags::FRAGMENT).module(frag).name(entry),
];
let bind = [vk::VertexInputBindingDescription::default().binding(0).stride(std::mem::size_of::<Vertex>() as u32).input_rate(vk::VertexInputRate::VERTEX)];
let attrs = [
vk::VertexInputAttributeDescription::default().location(0).binding(0).format(vk::Format::R32G32_SFLOAT).offset(0),
vk::VertexInputAttributeDescription::default().location(1).binding(0).format(vk::Format::R32G32_SFLOAT).offset(8),
vk::VertexInputAttributeDescription::default().location(2).binding(0).format(vk::Format::R32G32B32A32_SFLOAT).offset(16),
vk::VertexInputAttributeDescription::default().location(3).binding(0).format(vk::Format::R32_SFLOAT).offset(32),
];
let vi = vk::PipelineVertexInputStateCreateInfo::default().vertex_binding_descriptions(&bind).vertex_attribute_descriptions(&attrs);
let ia = vk::PipelineInputAssemblyStateCreateInfo::default().topology(vk::PrimitiveTopology::TRIANGLE_LIST);
let vpst = vk::PipelineViewportStateCreateInfo::default().viewport_count(1).scissor_count(1);
let rs = vk::PipelineRasterizationStateCreateInfo::default().polygon_mode(vk::PolygonMode::FILL).cull_mode(vk::CullModeFlags::NONE).front_face(vk::FrontFace::COUNTER_CLOCKWISE).line_width(1.0);
let ms = vk::PipelineMultisampleStateCreateInfo::default().rasterization_samples(vk::SampleCountFlags::TYPE_1);
let ba = [vk::PipelineColorBlendAttachmentState::default()
.color_write_mask(vk::ColorComponentFlags::RGBA)
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::ONE)
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::ONE)
.dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.alpha_blend_op(vk::BlendOp::ADD)];
let cb = vk::PipelineColorBlendStateCreateInfo::default().attachments(&ba);
let dyns = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
let ds = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dyns);
let info = [vk::GraphicsPipelineCreateInfo::default().stages(&stages).vertex_input_state(&vi).input_assembly_state(&ia).viewport_state(&vpst).rasterization_state(&rs).multisample_state(&ms).color_blend_state(&cb).dynamic_state(&ds).layout(layout).render_pass(rp).subpass(0)];
unsafe { d.create_graphics_pipelines(vk::PipelineCache::null(), &info, None) }.map_err(|(_, e)| format!("pipeline: {e}")).map(|v| v[0])
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::OnceLock;
fn atlas() -> &'static (Font, Atlas) {
static ATLAS: OnceLock<(Font, Atlas)> = OnceLock::new();
ATLAS.get_or_init(|| {
let font = Font::from_bytes(
FONT_TTF,
FontSettings { scale: PX, ..Default::default() },
).unwrap();
let atlas = rasterize_atlas(&font);
(font, atlas)
})
}
#[test]
fn unicode_filename_glyphs_match_font_bitmaps() {
let (font, atlas) = atlas();
assert_eq!(atlas.glyphs.len(), font.chars().len());
assert!(font.chars().keys().all(|c| atlas.glyphs.contains_key(c)));
for c in "Shōgun éЖΩ".chars() {
let g = &atlas.glyphs[&c];
let (m, bitmap) = font.rasterize(c, PX);
assert_eq!(g.size, [m.width as f32, m.height as f32]);
assert_eq!(g.offset, [m.xmin as f32, m.ymin as f32]);
assert_eq!(g.advance, m.advance_width);
let x = (g.uv[0] * ATLAS_W as f32) as usize;
let y = (g.uv[1] * atlas.height as f32) as usize;
for row in 0..m.height {
let dst = (y + row) * ATLAS_W + x;
assert_eq!(
&atlas.pixels[dst..dst + m.width],
&bitmap[row * m.width..(row + 1) * m.width],
"{c}",
);
}
}
let macron = &atlas.glyphs[&'ō'];
assert!(macron.size[0] > 0.0 && macron.size[1] > 0.0 && macron.advance > 0.0);
}
#[test]
fn thumb_slots_tile_the_atlas_and_cover_crops_to_the_box() {
assert_eq!(slot_origin(0), (0, 0));
assert_eq!(slot_origin(9), (THUMB_W, THUMB_H));
assert_eq!(slot_origin(THUMB_SLOTS - 1), (THUMB_ATLAS_W - THUMB_W, THUMB_ATLAS_H - THUMB_H));
let t = Thumb::new(0, THUMB_W, THUMB_H);
assert_eq!(t.uv, [0.0, 0.0, 1.0 / THUMB_COLS as f32, 1.0 / THUMB_ROWS as f32]);
assert_eq!(t.cover(160.0, 90.0), t.uv); let [u0, v0, u1, v1] = t.cover(100.0, 100.0); assert!((v0, v1) == (t.uv[1], t.uv[3]));
assert!(((u1 - u0) / (t.uv[2] - t.uv[0]) - 9.0 / 16.0).abs() < 1e-5);
assert!(((u0 + u1) / 2.0 - t.uv[2] / 2.0).abs() < 1e-6);
let square = Thumb::new(1, 240, 240);
let [_, v0, _, v1] = square.cover(160.0, 90.0); assert!(((v1 - v0) / (square.uv[3] - square.uv[1]) - 9.0 / 16.0).abs() < 1e-5);
}
#[test]
fn missing_characters_use_visible_fallback() {
let (_, atlas) = atlas();
let missing = '\u{10ffff}';
assert!(!atlas.glyphs.contains_key(&missing));
let fallback = glyph(&atlas.glyphs, missing);
assert!(std::ptr::eq(fallback, &atlas.glyphs[&'?']));
assert!(fallback.size[0] > 0.0 && fallback.size[1] > 0.0 && fallback.advance > 0.0);
}
#[test]
fn atlas_uvs_and_solid_cell_survive_growth() {
let (_, atlas) = atlas();
assert!(atlas.height > 16);
assert!(atlas.height <= 4096, "atlas height: {}", atlas.height);
assert_eq!(atlas.pixels.len(), ATLAS_W * atlas.height);
assert_eq!(atlas.solid_uv, [1.0 / ATLAS_W as f32, 1.0 / atlas.height as f32]);
for y in 0..2 {
for x in 0..2 {
assert_eq!(atlas.pixels[y * ATLAS_W + x], 255);
}
assert_eq!(atlas.pixels[y * ATLAS_W + 2], 0);
}
for g in atlas.glyphs.values() {
assert!(g.uv.iter().all(|v| (0.0..=1.0).contains(v)));
assert_eq!((g.uv[2] - g.uv[0]) * ATLAS_W as f32, g.size[0]);
assert_eq!((g.uv[3] - g.uv[1]) * atlas.height as f32, g.size[1]);
}
}
}