use std::ops::Range;
use std::sync::Arc;
use aetna_core::icons::msdf_atlas::{
DEFAULT_PX_PER_UNIT, DEFAULT_SPREAD, IconMsdfAtlas, IconMsdfPage, IconMsdfSlot, IconRect,
};
use aetna_core::icons::svg::{IconSource, SvgIconPaintMode};
use aetna_core::paint::{IconRun, IconRunKind, PhysicalScissor, rgba_f32};
use aetna_core::shader::stock_wgsl;
use aetna_core::tree::{Color, Rect};
use aetna_core::vector::{
IconMaterial, VectorAsset, VectorMeshOptions, VectorMeshVertex, VectorRenderMode,
append_vector_asset_mesh,
};
use bytemuck::{Pod, Zeroable};
use smallvec::smallvec;
use vulkano::{
buffer::{
Buffer, BufferCreateInfo, BufferUsage, Subbuffer,
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
},
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},
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, multisample_state};
const TESS_VERTEX_ARENA_SIZE: u64 = 256 * 1024;
const MSDF_INSTANCE_ARENA_SIZE: u64 = 64 * 1024;
#[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_alloc: SubbufferAllocator,
tess_vertex_buf: Option<Subbuffer<[VectorMeshVertex]>>,
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_alloc: SubbufferAllocator,
msdf_instance_buf: Option<Subbuffer<[MsdfIconInstance]>>,
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,
sample_count: u32,
) -> Self {
let flat_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
sample_count,
"stock::vector",
stock_wgsl::VECTOR,
);
let relief_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
sample_count,
"stock::vector_relief",
stock_wgsl::VECTOR_RELIEF,
);
let glass_pipeline = build_tess_pipeline(
device.clone(),
subpass.clone(),
sample_count,
"stock::vector_glass",
stock_wgsl::VECTOR_GLASS,
);
let tess_vertex_alloc = SubbufferAllocator::new(
memory_alloc.clone(),
SubbufferAllocatorCreateInfo {
arena_size: TESS_VERTEX_ARENA_SIZE,
buffer_usage: BufferUsage::VERTEX_BUFFER,
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
);
let msdf_pipeline = build_msdf_pipeline(device.clone(), subpass, sample_count);
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_alloc = SubbufferAllocator::new(
memory_alloc.clone(),
SubbufferAllocatorCreateInfo {
arena_size: MSDF_INSTANCE_ARENA_SIZE,
buffer_usage: BufferUsage::VERTEX_BUFFER,
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
);
Self {
tess_vertices: Vec::new(),
tess_vertex_alloc,
tess_vertex_buf: None,
flat_pipeline,
relief_pipeline,
glass_pipeline,
msdf_atlas: IconMsdfAtlas::new(DEFAULT_PX_PER_UNIT, DEFAULT_SPREAD),
msdf_pages: Vec::new(),
msdf_instances: Vec::new(),
msdf_instance_alloc,
msdf_instance_buf: None,
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>,
source: &IconSource,
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();
let use_msdf = matches!(self.material, IconMaterial::Flat)
&& matches!(source.paint_mode(), SvgIconPaintMode::CurrentColorMask);
if use_msdf {
if let Some(slot) = self.msdf_atlas.ensure(source, 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,
});
}
} else {
let material = self.material;
let asset = source.vector_asset();
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()
}
pub(crate) fn record_vector(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
asset: &VectorAsset,
render_mode: VectorRenderMode,
) -> 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 render_mode {
VectorRenderMode::Mask { color } => {
if let Some(slot) = self.msdf_atlas.ensure_vector_asset(asset) {
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,
});
}
}
VectorRenderMode::Painted => {
let first = self.tess_vertices.len() as u32;
let mesh_run = append_vector_asset_mesh(
asset,
VectorMeshOptions::icon(rect, Color::rgb(255, 255, 255), 1.0),
&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: IconMaterial::Flat,
});
}
}
}
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.is_empty() {
self.tess_vertex_buf = None;
} else {
let buf = self
.tess_vertex_alloc
.allocate_slice::<VectorMeshVertex>(self.tess_vertices.len() as u64)
.expect("aetna-vulkano: icon tess vertex suballocate");
buf.write()
.expect("aetna-vulkano: icon tess vertex suballocation write")
.copy_from_slice(&self.tess_vertices);
self.tess_vertex_buf = Some(buf);
}
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.is_empty() {
self.msdf_instance_buf = None;
} else {
let buf = self
.msdf_instance_alloc
.allocate_slice::<MsdfIconInstance>(self.msdf_instances.len() as u64)
.expect("aetna-vulkano: icon msdf instance suballocate");
buf.write()
.expect("aetna-vulkano: icon msdf instance suballocation write")
.copy_from_slice(&self.msdf_instances);
self.msdf_instance_buf = Some(buf);
}
}
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
.as_ref()
.expect("aetna-vulkano: icon tess_vertex_buf accessed with no draws")
}
pub(crate) fn msdf_pipeline(&self) -> &Arc<GraphicsPipeline> {
&self.msdf_pipeline
}
pub(crate) fn msdf_instance_buf(&self) -> &Subbuffer<[MsdfIconInstance]> {
self.msdf_instance_buf
.as_ref()
.expect("aetna-vulkano: icon msdf_instance_buf accessed with no draws")
}
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 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))
}
fn build_tess_pipeline(
device: Arc<Device>,
subpass: Subpass,
sample_count: u32,
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(multisample_state(sample_count)),
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,
sample_count: u32,
) -> 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(multisample_state(sample_count)),
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")
}