use std::ops::Range;
use std::sync::Arc;
use aetna_core::icon_msdf_atlas::{
DEFAULT_PX_PER_UNIT, DEFAULT_SPREAD, IconMsdfAtlas, IconMsdfKey, IconMsdfPage, IconMsdfSlot,
IconRect,
};
use aetna_core::icons::icon_vector_asset;
use aetna_core::paint::{IconRun, IconRunKind, PhysicalScissor, rgba_f32};
use aetna_core::shader::stock_wgsl;
use aetna_core::tree::{Color, IconName, Rect};
use aetna_core::vector::{
IconMaterial, VectorMeshOptions, VectorMeshVertex, append_vector_asset_mesh,
};
use bytemuck::{Pod, Zeroable};
use smallvec::smallvec;
use vulkano::{
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
command_buffer::{
AutoCommandBufferBuilder, BufferImageCopy, CommandBufferUsage, CopyBufferToImageInfo,
allocator::StandardCommandBufferAllocator,
},
descriptor_set::{
DescriptorSet, WriteDescriptorSet, allocator::StandardDescriptorSetAllocator,
},
device::{Device, Queue},
format::Format,
image::{
Image, ImageAspects, ImageCreateInfo, ImageSubresourceLayers, ImageType, ImageUsage,
sampler::{Filter, Sampler, SamplerAddressMode, SamplerCreateInfo, SamplerMipmapMode},
view::ImageView,
},
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator},
pipeline::{
DynamicState, GraphicsPipeline, Pipeline, PipelineShaderStageCreateInfo,
graphics::{
GraphicsPipelineCreateInfo,
color_blend::{
AttachmentBlend, BlendFactor, BlendOp, ColorBlendAttachmentState, ColorBlendState,
},
input_assembly::{InputAssemblyState, PrimitiveTopology},
multisample::MultisampleState,
rasterization::RasterizationState,
subpass::PipelineSubpassType,
vertex_input::{
VertexInputAttributeDescription, VertexInputBindingDescription, VertexInputRate,
VertexInputState,
},
viewport::ViewportState,
},
},
render_pass::Subpass,
shader::{ShaderModule, ShaderModuleCreateInfo},
sync::{self, GpuFuture},
};
use crate::naga_compile::wgsl_to_spirv;
use crate::pipeline::build_shared_pipeline_layout;
const INITIAL_VERTEX_CAPACITY: u64 = 1024;
const INITIAL_INSTANCE_CAPACITY: u64 = 256;
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable, Debug)]
pub(crate) struct MsdfIconInstance {
pub rect: [f32; 4],
pub uv: [f32; 4],
pub color: [f32; 4],
pub params: [f32; 4],
}
struct MsdfPageGpu {
image: Arc<Image>,
descriptor_set: Arc<DescriptorSet>,
}
pub(crate) struct IconPaint {
tess_vertices: Vec<VectorMeshVertex>,
tess_vertex_buf: Subbuffer<[VectorMeshVertex]>,
tess_vertex_capacity: u64,
flat_pipeline: Arc<GraphicsPipeline>,
relief_pipeline: Arc<GraphicsPipeline>,
glass_pipeline: Arc<GraphicsPipeline>,
msdf_atlas: IconMsdfAtlas,
msdf_pages: Vec<MsdfPageGpu>,
msdf_instances: Vec<MsdfIconInstance>,
msdf_instance_buf: Subbuffer<[MsdfIconInstance]>,
msdf_instance_capacity: u64,
msdf_pipeline: Arc<GraphicsPipeline>,
msdf_sampler: Arc<Sampler>,
runs: Vec<IconRun>,
material: IconMaterial,
memory_alloc: Arc<StandardMemoryAllocator>,
descriptor_alloc: Arc<StandardDescriptorSetAllocator>,
cmd_alloc: Arc<StandardCommandBufferAllocator>,
queue: Arc<Queue>,
}
impl IconPaint {
pub(crate) fn new(
device: Arc<Device>,
queue: Arc<Queue>,
memory_alloc: Arc<StandardMemoryAllocator>,
descriptor_alloc: Arc<StandardDescriptorSetAllocator>,
cmd_alloc: Arc<StandardCommandBufferAllocator>,
subpass: Subpass,
) -> Self {
let flat_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
"stock::vector",
stock_wgsl::VECTOR,
);
let relief_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
"stock::vector_relief",
stock_wgsl::VECTOR_RELIEF,
);
let glass_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
"stock::vector_glass",
stock_wgsl::VECTOR_GLASS,
);
let tess_vertex_buf = create_vector_vertex_buffer(&memory_alloc, INITIAL_VERTEX_CAPACITY);
let msdf_pipeline = build_msdf_pipeline(device.clone(), subpass);
let msdf_sampler = Sampler::new(
device,
SamplerCreateInfo {
mag_filter: Filter::Linear,
min_filter: Filter::Linear,
mipmap_mode: SamplerMipmapMode::Nearest,
address_mode: [SamplerAddressMode::ClampToEdge; 3],
..Default::default()
},
)
.expect("aetna-vulkano: icon msdf sampler");
let msdf_instance_buf =
create_msdf_instance_buffer(&memory_alloc, INITIAL_INSTANCE_CAPACITY);
Self {
tess_vertices: Vec::with_capacity(INITIAL_VERTEX_CAPACITY as usize),
tess_vertex_buf,
tess_vertex_capacity: INITIAL_VERTEX_CAPACITY,
flat_pipeline,
relief_pipeline,
glass_pipeline,
msdf_atlas: IconMsdfAtlas::new(DEFAULT_PX_PER_UNIT, DEFAULT_SPREAD),
msdf_pages: Vec::new(),
msdf_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY as usize),
msdf_instance_buf,
msdf_instance_capacity: INITIAL_INSTANCE_CAPACITY,
msdf_pipeline,
msdf_sampler,
runs: Vec::new(),
material: IconMaterial::Flat,
memory_alloc,
descriptor_alloc,
cmd_alloc,
queue,
}
}
pub(crate) fn set_material(&mut self, material: IconMaterial) {
self.material = material;
}
pub(crate) fn material(&self) -> IconMaterial {
self.material
}
pub(crate) fn frame_begin(&mut self) {
self.tess_vertices.clear();
self.msdf_instances.clear();
self.runs.clear();
}
pub(crate) fn record(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
name: IconName,
color: Color,
stroke_width: f32,
) -> Range<usize> {
if rect.w <= 0.0 || rect.h <= 0.0 {
let start = self.runs.len();
return start..start;
}
let start = self.runs.len();
match self.material {
IconMaterial::Flat => {
if let Some(slot) = self.msdf_atlas.ensure(IconMsdfKey::new(name, stroke_width)) {
let (page_w, page_h) = self.msdf_page_dims(slot.page);
let instance = msdf_instance_for_icon(rect, color, &slot, page_w, page_h);
let first = self.msdf_instances.len() as u32;
self.msdf_instances.push(instance);
self.runs.push(IconRun {
kind: IconRunKind::Msdf,
scissor,
first,
count: 1,
page: slot.page,
material: IconMaterial::Flat,
});
}
}
material => {
let asset = icon_vector_asset(name);
let first = self.tess_vertices.len() as u32;
let mesh_run = append_vector_asset_mesh(
asset,
VectorMeshOptions::icon(rect, color, stroke_width),
&mut self.tess_vertices,
);
if mesh_run.count > 0 {
self.runs.push(IconRun {
kind: IconRunKind::Tess,
scissor,
first,
count: mesh_run.count,
page: 0,
material,
});
}
}
}
start..self.runs.len()
}
fn msdf_page_dims(&self, page_idx: u32) -> (u32, u32) {
let page = self
.msdf_atlas
.page(page_idx)
.expect("freshly-ensured slot references a missing atlas page");
(page.width, page.height)
}
pub(crate) fn flush(&mut self) {
if (self.tess_vertices.len() as u64) > self.tess_vertex_capacity {
let new_cap = (self.tess_vertices.len() as u64).next_power_of_two();
self.tess_vertex_buf = create_vector_vertex_buffer(&self.memory_alloc, new_cap);
self.tess_vertex_capacity = new_cap;
}
if !self.tess_vertices.is_empty() {
let mut write = self
.tess_vertex_buf
.write()
.expect("aetna-vulkano: icon tess vertex buf write");
write[..self.tess_vertices.len()].copy_from_slice(&self.tess_vertices);
}
while self.msdf_pages.len() < self.msdf_atlas.pages().len() {
let i = self.msdf_pages.len();
let page = &self.msdf_atlas.pages()[i];
let new_page = self.create_msdf_page(page.width, page.height);
self.msdf_pages.push(new_page);
}
let dirty = self.msdf_atlas.take_dirty();
if !dirty.is_empty() {
let mut builder = AutoCommandBufferBuilder::primary(
self.cmd_alloc.clone(),
self.queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.expect("aetna-vulkano: icon msdf upload cmd builder");
for (page_idx, rect) in &dirty {
if rect.w == 0 || rect.h == 0 {
continue;
}
let page = &self.msdf_atlas.pages()[*page_idx];
let bytes = pack_rect_bytes(page, *rect);
let staging = Buffer::from_iter(
self.memory_alloc.clone(),
BufferCreateInfo {
usage: BufferUsage::TRANSFER_SRC,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
bytes,
)
.expect("aetna-vulkano: icon msdf staging buf");
let copy_info = CopyBufferToImageInfo {
regions: smallvec![BufferImageCopy {
buffer_offset: 0,
buffer_row_length: 0,
buffer_image_height: 0,
image_subresource: ImageSubresourceLayers {
aspects: ImageAspects::COLOR,
mip_level: 0,
array_layers: 0..1,
},
image_offset: [rect.x, rect.y, 0],
image_extent: [rect.w, rect.h, 1],
..Default::default()
}],
..CopyBufferToImageInfo::buffer_image(
staging,
self.msdf_pages[*page_idx].image.clone(),
)
};
builder
.copy_buffer_to_image(copy_info)
.expect("aetna-vulkano: icon msdf copy_buffer_to_image");
}
let cb = builder
.build()
.expect("aetna-vulkano: icon msdf upload cmd build");
let future = sync::now(self.queue.device().clone())
.then_execute(self.queue.clone(), cb)
.expect("aetna-vulkano: icon msdf upload then_execute")
.then_signal_fence_and_flush()
.expect("aetna-vulkano: icon msdf upload flush");
future
.wait(None)
.expect("aetna-vulkano: icon msdf upload fence wait");
}
if (self.msdf_instances.len() as u64) > self.msdf_instance_capacity {
let new_cap = (self.msdf_instances.len() as u64).next_power_of_two();
self.msdf_instance_buf = create_msdf_instance_buffer(&self.memory_alloc, new_cap);
self.msdf_instance_capacity = new_cap;
}
if !self.msdf_instances.is_empty() {
let mut write = self
.msdf_instance_buf
.write()
.expect("aetna-vulkano: icon msdf instance buf write");
write[..self.msdf_instances.len()].copy_from_slice(&self.msdf_instances);
}
}
fn create_msdf_page(&self, width: u32, height: u32) -> MsdfPageGpu {
let image = Image::new(
self.memory_alloc.clone(),
ImageCreateInfo {
image_type: ImageType::Dim2d,
format: Format::R8G8B8A8_UNORM,
extent: [width, height, 1],
usage: ImageUsage::TRANSFER_DST | ImageUsage::SAMPLED,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
..Default::default()
},
)
.expect("aetna-vulkano: icon msdf atlas page image");
let view =
ImageView::new_default(image.clone()).expect("aetna-vulkano: icon msdf page view");
let descriptor_set = DescriptorSet::new(
self.descriptor_alloc.clone(),
self.msdf_pipeline.layout().set_layouts()[1].clone(),
[
WriteDescriptorSet::image_view(0, view),
WriteDescriptorSet::sampler(1, self.msdf_sampler.clone()),
],
[],
)
.expect("aetna-vulkano: icon msdf page descriptor set");
MsdfPageGpu {
image,
descriptor_set,
}
}
pub(crate) fn run(&self, index: usize) -> IconRun {
self.runs[index]
}
pub(crate) fn tess_pipeline(&self, material: IconMaterial) -> &Arc<GraphicsPipeline> {
match material {
IconMaterial::Flat => &self.flat_pipeline,
IconMaterial::Relief => &self.relief_pipeline,
IconMaterial::Glass => &self.glass_pipeline,
}
}
pub(crate) fn tess_vertex_buf(&self) -> &Subbuffer<[VectorMeshVertex]> {
&self.tess_vertex_buf
}
pub(crate) fn msdf_pipeline(&self) -> &Arc<GraphicsPipeline> {
&self.msdf_pipeline
}
pub(crate) fn msdf_instance_buf(&self) -> &Subbuffer<[MsdfIconInstance]> {
&self.msdf_instance_buf
}
pub(crate) fn msdf_page_descriptor(&self, page: u32) -> &Arc<DescriptorSet> {
&self.msdf_pages[page as usize].descriptor_set
}
}
fn msdf_instance_for_icon(
rect: Rect,
color: Color,
slot: &IconMsdfSlot,
page_w: u32,
page_h: u32,
) -> MsdfIconInstance {
let [_, _, vw, vh] = slot.view_box;
let logical_per_unit_x = rect.w / vw.max(0.001);
let logical_per_unit_y = rect.h / vh.max(0.001);
let spread_x = slot.spread * logical_per_unit_x / slot.px_per_unit.max(0.001);
let spread_y = slot.spread * logical_per_unit_y / slot.px_per_unit.max(0.001);
let bx = rect.x - spread_x;
let by = rect.y - spread_y;
let bw = rect.w + 2.0 * spread_x;
let bh = rect.h + 2.0 * spread_y;
let pw = page_w as f32;
let ph = page_h as f32;
let uv = [
slot.rect.x as f32 / pw,
slot.rect.y as f32 / ph,
slot.rect.w as f32 / pw,
slot.rect.h as f32 / ph,
];
MsdfIconInstance {
rect: [bx, by, bw, bh],
uv,
color: rgba_f32(color),
params: [slot.spread, 0.0, 0.0, 0.0],
}
}
fn pack_rect_bytes(page: &IconMsdfPage, rect: IconRect) -> Vec<u8> {
const BPP: usize = 4;
let row_bytes = rect.w as usize * BPP;
let mut bytes = Vec::with_capacity(row_bytes * rect.h as usize);
for row in 0..rect.h {
let y = rect.y + row;
let start = (y as usize * page.width as usize + rect.x as usize) * BPP;
let end = start + row_bytes;
bytes.extend_from_slice(&page.pixels[start..end]);
}
bytes
}
fn create_vector_vertex_buffer(
allocator: &Arc<StandardMemoryAllocator>,
capacity: u64,
) -> Subbuffer<[VectorMeshVertex]> {
Buffer::new_slice::<VectorMeshVertex>(
allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
capacity,
)
.expect("aetna-vulkano: icon tess vertex buffer alloc")
}
fn create_msdf_instance_buffer(
allocator: &Arc<StandardMemoryAllocator>,
capacity: u64,
) -> Subbuffer<[MsdfIconInstance]> {
Buffer::new_slice::<MsdfIconInstance>(
allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
capacity,
)
.expect("aetna-vulkano: icon msdf instance buffer alloc")
}
fn tess_vertex_input_state() -> VertexInputState {
let bind_vertex = VertexInputBindingDescription {
stride: std::mem::size_of::<VectorMeshVertex>() as u32,
input_rate: VertexInputRate::Vertex,
..Default::default()
};
let attr = |offset: u32, format: Format| VertexInputAttributeDescription {
binding: 0,
offset,
format,
..Default::default()
};
VertexInputState::new()
.binding(0, bind_vertex)
.attribute(0, attr(0, Format::R32G32_SFLOAT))
.attribute(1, attr(8, Format::R32G32_SFLOAT))
.attribute(2, attr(16, Format::R32G32B32A32_SFLOAT))
.attribute(3, attr(32, Format::R32G32B32A32_SFLOAT))
.attribute(4, attr(48, Format::R32G32_SFLOAT))
}
fn build_tess_pipeline(
device: Arc<Device>,
subpass: Subpass,
name: &str,
wgsl: &str,
) -> Arc<GraphicsPipeline> {
let words = wgsl_to_spirv(name, wgsl)
.unwrap_or_else(|e| panic!("aetna-vulkano: icon WGSL compile for `{name}`: {e}"));
let module = unsafe {
ShaderModule::new(device.clone(), ShaderModuleCreateInfo::new(&words))
.unwrap_or_else(|e| panic!("aetna-vulkano: icon ShaderModule::new for `{name}`: {e}"))
};
let vs = module
.entry_point("vs_main")
.unwrap_or_else(|| panic!("{name}: missing vs_main"));
let fs = module
.entry_point("fs_main")
.unwrap_or_else(|| panic!("{name}: missing fs_main"));
let stages = [
PipelineShaderStageCreateInfo::new(vs),
PipelineShaderStageCreateInfo::new(fs),
];
let layout = build_shared_pipeline_layout(device.clone(), &stages);
GraphicsPipeline::new(
device,
None,
GraphicsPipelineCreateInfo {
stages: stages.into_iter().collect(),
vertex_input_state: Some(tess_vertex_input_state()),
input_assembly_state: Some(InputAssemblyState {
topology: PrimitiveTopology::TriangleList,
..Default::default()
}),
viewport_state: Some(ViewportState::default()),
rasterization_state: Some(RasterizationState::default()),
multisample_state: Some(MultisampleState::default()),
color_blend_state: Some(ColorBlendState::with_attachment_states(
subpass.num_color_attachments(),
ColorBlendAttachmentState {
blend: Some(AttachmentBlend::alpha()),
..Default::default()
},
)),
dynamic_state: [DynamicState::Viewport, DynamicState::Scissor]
.into_iter()
.collect(),
subpass: Some(PipelineSubpassType::BeginRenderPass(subpass)),
..GraphicsPipelineCreateInfo::layout(layout)
},
)
.unwrap_or_else(|e| panic!("aetna-vulkano: icon GraphicsPipeline::new for `{name}`: {e:?}"))
}
fn build_msdf_pipeline(device: Arc<Device>, subpass: Subpass) -> Arc<GraphicsPipeline> {
let words = wgsl_to_spirv("stock::text_msdf (icon)", stock_wgsl::TEXT_MSDF)
.expect("aetna-vulkano: icon msdf WGSL compile");
let module = unsafe {
ShaderModule::new(device.clone(), ShaderModuleCreateInfo::new(&words))
.expect("aetna-vulkano: icon msdf ShaderModule::new")
};
let vs = module
.entry_point("vs_main")
.expect("text_msdf.wgsl: missing vs_main");
let fs = module
.entry_point("fs_main")
.expect("text_msdf.wgsl: missing fs_main");
let stages = [
PipelineShaderStageCreateInfo::new(vs),
PipelineShaderStageCreateInfo::new(fs),
];
let layout = build_shared_pipeline_layout(device.clone(), &stages);
let bind_vertex = VertexInputBindingDescription {
stride: (2 * std::mem::size_of::<f32>()) as u32,
input_rate: VertexInputRate::Vertex,
..Default::default()
};
let bind_instance = VertexInputBindingDescription {
stride: std::mem::size_of::<MsdfIconInstance>() as u32,
input_rate: VertexInputRate::Instance { divisor: 1 },
..Default::default()
};
let attr = |binding: u32, offset: u32, format: Format| VertexInputAttributeDescription {
binding,
offset,
format,
..Default::default()
};
let vertex_input_state = VertexInputState::new()
.binding(0, bind_vertex)
.binding(1, bind_instance)
.attribute(0, attr(0, 0, Format::R32G32_SFLOAT))
.attribute(1, attr(1, 0, Format::R32G32B32A32_SFLOAT))
.attribute(2, attr(1, 16, Format::R32G32B32A32_SFLOAT))
.attribute(3, attr(1, 32, Format::R32G32B32A32_SFLOAT))
.attribute(4, attr(1, 48, Format::R32G32B32A32_SFLOAT));
let premultiplied = AttachmentBlend {
src_color_blend_factor: BlendFactor::One,
dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
alpha_blend_op: BlendOp::Add,
};
GraphicsPipeline::new(
device,
None,
GraphicsPipelineCreateInfo {
stages: stages.into_iter().collect(),
vertex_input_state: Some(vertex_input_state),
input_assembly_state: Some(InputAssemblyState {
topology: PrimitiveTopology::TriangleStrip,
..Default::default()
}),
viewport_state: Some(ViewportState::default()),
rasterization_state: Some(RasterizationState::default()),
multisample_state: Some(MultisampleState::default()),
color_blend_state: Some(ColorBlendState::with_attachment_states(
subpass.num_color_attachments(),
ColorBlendAttachmentState {
blend: Some(premultiplied),
..Default::default()
},
)),
dynamic_state: [DynamicState::Viewport, DynamicState::Scissor]
.into_iter()
.collect(),
subpass: Some(PipelineSubpassType::BeginRenderPass(subpass)),
..GraphicsPipelineCreateInfo::layout(layout)
},
)
.expect("aetna-vulkano: icon msdf GraphicsPipeline::new")
}