new workspace

This commit is contained in:
galister
2025-06-18 01:14:04 +09:00
parent 95f2ae4296
commit f05d3a8251
252 changed files with 24618 additions and 184 deletions

View File

@@ -0,0 +1,221 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use cosmic_text::Buffer;
use glam::{Mat4, Vec2, Vec3};
use crate::{
drawing,
gfx::{WGfx, cmd::GfxCommandBuffer},
};
use super::{
rect::{RectPipeline, RectRenderer},
text::{
DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE, TextArea, TextBounds,
text_atlas::{TextAtlas, TextPipeline},
text_renderer::TextRenderer,
},
viewport::Viewport,
};
struct RendererPass<'a> {
submitted: bool,
text_areas: Vec<TextArea<'a>>,
text_renderer: TextRenderer,
rect_renderer: RectRenderer,
}
impl RendererPass<'_> {
fn new(text_atlas: &mut TextAtlas, rect_pipeline: RectPipeline) -> anyhow::Result<Self> {
let text_renderer = TextRenderer::new(text_atlas)?;
let rect_renderer = RectRenderer::new(rect_pipeline)?;
Ok(Self {
submitted: false,
text_renderer,
rect_renderer,
text_areas: Vec::new(),
})
}
fn submit(
&mut self,
gfx: &Arc<WGfx>,
viewport: &mut Viewport,
cmd_buf: &mut GfxCommandBuffer,
text_atlas: &mut TextAtlas,
) -> anyhow::Result<()> {
if self.submitted {
return Ok(());
}
self.submitted = true;
self.rect_renderer.render(gfx, viewport, cmd_buf)?;
{
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut swash_cache = SWASH_CACHE.lock().unwrap();
self.text_renderer.prepare(
&mut font_system,
text_atlas,
viewport,
std::mem::take(&mut self.text_areas),
&mut swash_cache,
)?;
}
self.text_renderer.render(text_atlas, viewport, cmd_buf)?;
Ok(())
}
}
pub struct Context {
viewport: Viewport,
text_atlas: TextAtlas,
rect_pipeline: RectPipeline,
text_pipeline: TextPipeline,
pixel_scale: f32,
pub dirty: bool,
empty_text: Rc<RefCell<Buffer>>,
}
impl Context {
pub fn new(
gfx: Arc<WGfx>,
native_format: vulkano::format::Format,
pixel_scale: f32,
) -> anyhow::Result<Self> {
let rect_pipeline = RectPipeline::new(gfx.clone(), native_format)?;
let text_pipeline = TextPipeline::new(gfx.clone(), native_format)?;
let viewport = Viewport::new(gfx.clone())?;
let text_atlas = TextAtlas::new(text_pipeline.clone())?;
Ok(Self {
viewport,
text_atlas,
rect_pipeline,
text_pipeline,
pixel_scale,
dirty: true,
empty_text: Rc::new(RefCell::new(Buffer::new_empty(DEFAULT_METRICS))),
})
}
pub fn regen(&mut self) -> anyhow::Result<()> {
self.text_atlas = TextAtlas::new(self.text_pipeline.clone())?;
self.dirty = true;
Ok(())
}
pub fn update_viewport(&mut self, resolution: [u32; 2], pixel_scale: f32) -> anyhow::Result<()> {
if self.pixel_scale != pixel_scale {
self.pixel_scale = pixel_scale;
self.regen()?;
}
if self.viewport.resolution() != resolution {
self.dirty = true;
}
let size = Vec2::new(
resolution[0] as f32 / pixel_scale,
resolution[1] as f32 / pixel_scale,
);
let fov = 0.4;
let aspect_ratio = size.x / size.y;
let projection = Mat4::perspective_rh(fov, aspect_ratio, 1.0, 100000.0);
let b = size.y / 2.0;
let angle_half = fov / 2.0;
let distance = (std::f32::consts::PI / 2.0 - angle_half).tan() * b;
let view = Mat4::look_at_rh(
Vec3::new(size.x / 2.0, size.y / 2.0, distance),
Vec3::new(size.x / 2.0, size.y / 2.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
);
let fin = projection * view;
self.viewport.update(resolution, &fin, pixel_scale)?;
Ok(())
}
fn new_pass(&mut self, passes: &mut Vec<RendererPass>) -> anyhow::Result<()> {
passes.push(RendererPass::new(
&mut self.text_atlas,
self.rect_pipeline.clone(),
)?);
Ok(())
}
fn submit_pass(
&mut self,
gfx: &Arc<WGfx>,
cmd_buf: &mut GfxCommandBuffer,
pass: &mut RendererPass,
) -> anyhow::Result<()> {
pass.submit(gfx, &mut self.viewport, cmd_buf, &mut self.text_atlas)?;
Ok(())
}
pub fn draw(
&mut self,
gfx: &Arc<WGfx>,
cmd_buf: &mut GfxCommandBuffer,
primitives: &[drawing::RenderPrimitive],
) -> anyhow::Result<()> {
self.dirty = false;
let mut passes = Vec::<RendererPass>::new();
self.new_pass(&mut passes)?;
for primitive in primitives.iter() {
let pass = passes.last_mut().unwrap(); // always safe
match &primitive.payload {
drawing::PrimitivePayload::Rectangle(rectangle) => {
pass.rect_renderer.add_rect(
primitive.boundary,
*rectangle,
&primitive.transform,
primitive.depth,
);
}
drawing::PrimitivePayload::Text(text) => {
pass.text_areas.push(TextArea {
buffer: text.clone(),
left: primitive.boundary.pos.x * self.pixel_scale,
top: primitive.boundary.pos.y * self.pixel_scale,
bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work
scale: self.pixel_scale,
default_color: cosmic_text::Color::rgb(0, 0, 0),
custom_glyphs: &[],
depth: primitive.depth,
transform: primitive.transform,
});
}
drawing::PrimitivePayload::Sprite(sprites) => {
pass.text_areas.push(TextArea {
buffer: self.empty_text.clone(),
left: primitive.boundary.pos.x * self.pixel_scale,
top: primitive.boundary.pos.y * self.pixel_scale,
bounds: TextBounds::default(),
scale: self.pixel_scale,
custom_glyphs: sprites.as_slice(),
default_color: cosmic_text::Color::rgb(255, 0, 255),
depth: primitive.depth,
transform: primitive.transform,
});
}
}
}
let pass = passes.last_mut().unwrap();
self.submit_pass(gfx, cmd_buf, pass)?;
Ok(())
}
}

View File

@@ -0,0 +1,6 @@
pub mod context;
pub mod model_buffer;
pub mod rect;
pub mod text;
pub mod util;
pub mod viewport;

View File

@@ -0,0 +1,124 @@
use std::sync::Arc;
use glam::{Mat4, Vec3};
use vulkano::{
buffer::{BufferUsage, Subbuffer},
descriptor_set::DescriptorSet,
};
use crate::{
gfx,
renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline},
};
pub struct ModelBuffer {
idx: u32,
models: Vec<glam::Mat4>,
buffer: Subbuffer<[f32]>, //4x4 floats = 1 mat4
buffer_capacity_f32: u32,
rect_descriptor: Option<Arc<DescriptorSet>>,
}
impl ModelBuffer {
pub fn new(gfx: &Arc<gfx::WGfx>) -> anyhow::Result<Self> {
const INITIAL_CAPACITY_MAT4: u32 = 16;
const INITIAL_CAPACITY_F32: u32 = INITIAL_CAPACITY_MAT4 * (4 * 4);
let buffer = gfx.empty_buffer::<f32>(
BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_DST,
INITIAL_CAPACITY_F32 as _,
)?;
let mut models = Vec::<glam::Mat4>::new();
models.resize(INITIAL_CAPACITY_MAT4 as _, Default::default());
Ok(Self {
models,
idx: 0,
buffer,
buffer_capacity_f32: INITIAL_CAPACITY_F32,
rect_descriptor: None,
})
}
pub fn clear(&mut self) {
self.models.clear(); // note: capacity is being preserved here
self.idx = 0;
}
pub fn upload(&mut self, gfx: &Arc<gfx::WGfx>) -> anyhow::Result<()> {
// resize buffer if it's too small
let required_capacity_f32 = (self.models.len() * (4 * 4)) as u32;
if self.buffer_capacity_f32 < required_capacity_f32 {
self.buffer_capacity_f32 = required_capacity_f32;
self.buffer = gfx.empty_buffer::<f32>(
BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_DST,
required_capacity_f32 as _,
)?;
//log::info!("resized to {}", required_capacity_f32);
}
//safe
let floats = unsafe {
std::slice::from_raw_parts(
self.models.as_slice().as_ptr() as *const f32,
required_capacity_f32 as usize,
)
};
self.buffer.write()?.copy_from_slice(floats);
Ok(())
}
// Returns model matrix ID from the model
pub fn register(&mut self, model: &glam::Mat4) -> u32 {
/*for (idx, iter_model) in self.models.iter().enumerate() {
if iter_model == model {
return idx as u32;
}
}*/
if self.idx == self.models.len() as u32 {
self
.models
.resize(self.models.len() * 2, Default::default());
//log::info!("ModelBuffer: resized to {}", self.models.len());
}
// insert new
self.models[self.idx as usize] = *model;
let ret = self.idx;
self.idx += 1;
ret
}
pub fn register_pos_size(
&mut self,
pos: &glam::Vec2,
size: &glam::Vec2,
transform: &Mat4,
) -> u32 {
let mut model = glam::Mat4::from_translation(Vec3::new(pos.x, pos.y, 0.0));
model *= *transform;
model *= glam::Mat4::from_scale(Vec3::new(size.x, size.y, 1.0));
self.register(&model)
}
pub fn get_rect_descriptor(&mut self, pipeline: &RectPipeline) -> Arc<DescriptorSet> {
self
.rect_descriptor
.get_or_insert_with(|| pipeline.color_rect.buffer(1, self.buffer.clone()).unwrap())
.clone()
}
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
self
.rect_descriptor
.get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap())
.clone()
}
}

View File

@@ -0,0 +1,171 @@
use std::sync::Arc;
use glam::Mat4;
use vulkano::{
buffer::{BufferContents, BufferUsage, Subbuffer},
format::Format,
pipeline::graphics::{input_assembly::PrimitiveTopology, vertex_input::Vertex},
};
use crate::{
drawing::{Boundary, Rectangle},
gfx::{BLEND_ALPHA, WGfx, cmd::GfxCommandBuffer, pipeline::WGfxPipeline},
renderer_vk::model_buffer::ModelBuffer,
};
use super::viewport::Viewport;
#[repr(C)]
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
pub struct RectVertex {
#[format(R32_UINT)]
pub in_model_idx: u32,
#[format(R32_UINT)]
pub in_rect_dim: [u16; 2],
#[format(R32_UINT)]
pub in_color: u32,
#[format(R32_UINT)]
pub in_color2: u32,
#[format(R32_UINT)]
pub in_border_color: u32,
#[format(R32_UINT)]
pub round_border_gradient: [u8; 4],
#[format(R32_SFLOAT)]
pub depth: f32,
}
/// Cloneable pipeline & shaders to be shared between RectRenderer instances.
#[derive(Clone)]
pub struct RectPipeline {
gfx: Arc<WGfx>,
pub(super) color_rect: Arc<WGfxPipeline<RectVertex>>,
}
impl RectPipeline {
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
let vert = vert_rect::load(gfx.device.clone())?;
let frag = frag_rect::load(gfx.device.clone())?;
let color_rect = gfx.create_pipeline::<RectVertex>(
vert,
frag,
format,
Some(BLEND_ALPHA),
PrimitiveTopology::TriangleStrip,
true,
)?;
Ok(Self { gfx, color_rect })
}
}
pub struct RectRenderer {
pipeline: RectPipeline,
rect_vertices: Vec<RectVertex>,
vert_buffer: Subbuffer<[RectVertex]>,
vert_buffer_size: usize,
model_buffer: ModelBuffer,
}
impl RectRenderer {
pub fn new(pipeline: RectPipeline) -> anyhow::Result<Self> {
const BUFFER_SIZE: usize = 128;
let vert_buffer = pipeline.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
BUFFER_SIZE as _,
)?;
Ok(Self {
model_buffer: ModelBuffer::new(&pipeline.gfx)?,
pipeline,
rect_vertices: vec![],
vert_buffer,
vert_buffer_size: BUFFER_SIZE,
})
}
pub fn add_rect(
&mut self,
boundary: Boundary,
rectangle: Rectangle,
transform: &Mat4,
depth: f32,
) {
let in_model_idx =
self
.model_buffer
.register_pos_size(&boundary.pos, &boundary.size, transform);
self.rect_vertices.push(RectVertex {
in_model_idx,
in_rect_dim: [boundary.size.x as u16, boundary.size.y as u16],
in_color: cosmic_text::Color::from(rectangle.color).0,
in_color2: cosmic_text::Color::from(rectangle.color2).0,
in_border_color: cosmic_text::Color::from(rectangle.border_color).0,
round_border_gradient: [
rectangle.round_units,
(rectangle.border) as u8,
rectangle.gradient as u8,
0, // unused
],
depth,
});
}
fn upload_verts(&mut self) -> anyhow::Result<()> {
if self.vert_buffer_size < self.rect_vertices.len() {
let new_size = self.vert_buffer_size * 2;
self.vert_buffer = self.pipeline.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
new_size as _,
)?;
self.vert_buffer_size = new_size;
}
self.vert_buffer.write()?[0..self.rect_vertices.len()].clone_from_slice(&self.rect_vertices);
Ok(())
}
pub fn render(
&mut self,
gfx: &Arc<WGfx>,
viewport: &mut Viewport,
cmd_buf: &mut GfxCommandBuffer,
) -> anyhow::Result<()> {
let vp = viewport.resolution();
self.model_buffer.upload(gfx)?;
self.upload_verts()?;
let set0 = viewport.get_rect_descriptor(&self.pipeline);
let set1 = self.model_buffer.get_rect_descriptor(&self.pipeline);
let pass = self.pipeline.color_rect.create_pass(
[vp[0] as _, vp[1] as _],
self.vert_buffer.clone(),
0..4,
0..self.rect_vertices.len() as _,
vec![set0, set1],
)?;
self.rect_vertices.clear();
cmd_buf.run_ref(&pass)
}
}
pub mod vert_rect {
vulkano_shaders::shader! {
ty: "vertex",
path: "src/renderer_vk/shaders/rect.vert",
}
}
pub mod frag_rect {
vulkano_shaders::shader! {
ty: "fragment",
path: "src/renderer_vk/shaders/rect.frag",
}
}

View File

@@ -0,0 +1,5 @@
layout(std140, set = MODEL_BUFFER_SET,
binding = 0) readonly buffer ModelBuffer {
mat4 models[];
}
model_buffer;

View File

@@ -0,0 +1,54 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec4 in_color2;
layout(location = 2) in vec2 in_uv;
layout(location = 3) in vec4 in_border_color;
layout(location = 4) in float in_border_size; // in units
layout(location = 5) in float in_radius; // in units
layout(location = 6) in vec2 in_rect_size;
layout(location = 0) out vec4 out_color;
#define UNIFORM_PARAMS_SET 0
#include "uniform.glsl"
void main() {
float rect_aspect = in_rect_size.x / in_rect_size.y;
vec2 center = in_rect_size / 2.0;
vec2 coords = in_uv * in_rect_size;
float radius = in_radius;
vec2 sdf_rect_dim = center - vec2(radius);
float sdf = length(max(abs(coords - center), sdf_rect_dim) - sdf_rect_dim) -
in_radius;
vec4 color =
mix(in_color, in_color2, min(length((in_uv - vec2(0.5)) * 2.0), 1.0));
float pixel_size = 1.0 / uniforms.pixel_scale;
if (in_border_size < in_radius) {
// rounded border
float f = in_border_size > 0.0 ? smoothstep(in_border_size + pixel_size,
in_border_size, -sdf) *
in_border_color.a
: 0.0;
out_color = mix(color, in_border_color, f);
} else {
// square border
vec2 a = abs(coords - center);
float aa = center.x - in_border_size;
float bb = center.y - in_border_size;
out_color = (a.x > aa || a.y > bb) ? in_border_color : color;
}
if (in_radius > 0.0) {
// rounding cutout alpha
out_color.a *= 1.0 - smoothstep(-pixel_size, 0.0, sdf);
}
}

View File

@@ -0,0 +1,93 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in uint in_model_idx;
layout(location = 1) in uint in_rect_dim;
layout(location = 2) in uint in_color;
layout(location = 3) in uint in_color2;
layout(location = 4) in uint in_border_color;
layout(location = 5) in uint round_border_gradient;
layout(location = 6) in float depth;
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec4 out_color2;
layout(location = 2) out vec2 out_uv;
layout(location = 3) out vec4 out_border_color;
layout(location = 4) out float out_border_size;
layout(location = 5) out float out_radius;
layout(location = 6) out vec2 out_rect_size;
#define UNIFORM_PARAMS_SET 0
#define MODEL_BUFFER_SET 1
#include "model_buffer.glsl"
#include "uniform.glsl"
void main() {
uint v = uint(gl_VertexIndex); // 0-3
uint rect_width = in_rect_dim & 0xffffu;
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
vec2 rect_size = vec2(float(rect_width), float(rect_height));
float rect_aspect = rect_size.x / rect_size.y;
// 0.0 - 1.0 normalized
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
vec2 corner_pos = vec2(corner_pos_u);
out_uv = corner_pos;
mat4 model_matrix = model_buffer.models[in_model_idx];
out_rect_size = rect_size;
gl_Position =
uniforms.projection * model_matrix * vec4(corner_pos, depth, 1.0);
out_border_color =
vec4(float((in_border_color & 0x00ff0000u) >> 16u) / 255.0,
float((in_border_color & 0x0000ff00u) >> 8u) / 255.0,
float(in_border_color & 0x000000ffu) / 255.0,
float((in_border_color & 0xff000000u) >> 24u) / 255.0);
float radius = float(round_border_gradient & 0xffu);
out_radius = radius;
float border_size = float((round_border_gradient & 0xff00u) >> 8);
out_border_size = border_size;
uint gradient_mode = (round_border_gradient & 0x00ff0000u) >> 16;
uint color;
uint color2;
switch (gradient_mode) {
case 1:
// horizontal
color = corner_pos_u.x > 0u ? in_color2 : in_color;
color2 = color;
break;
case 2:
// vertical
color = corner_pos_u.y > 0u ? in_color2 : in_color;
color2 = color;
break;
case 3:
// radial
color = in_color;
color2 = in_color2;
break;
default: // none
color = in_color;
color2 = in_color;
break;
}
out_color = vec4(float((color & 0x00ff0000u) >> 16u) / 255.0,
float((color & 0x0000ff00u) >> 8u) / 255.0,
float(color & 0x000000ffu) / 255.0,
float((color & 0xff000000u) >> 24u) / 255.0);
out_color2 = vec4(float((color2 & 0x00ff0000u) >> 16u) / 255.0,
float((color2 & 0x0000ff00u) >> 8u) / 255.0,
float(color2 & 0x000000ffu) / 255.0,
float((color2 & 0xff000000u) >> 24u) / 255.0);
}

View File

@@ -0,0 +1,22 @@
struct StylesData {
float radius;
vec4 color0;
vec4 color1;
uint gradient_style;
vec2 gradient_curve;
vec4 border_size_tlbr;
vec4 border_color0;
vec4 border_color1;
vec4 border_color2;
vec4 border_color3;
uint border_gradient_style;
vec2 border_gradient_curve;
}
layout(std140, set = STYLES_BUFFER_SET,
binding = 0) readonly buffer StylesBuffer {
StylesData styles[];
}
styles_buffer;

View File

@@ -0,0 +1,22 @@
#version 310 es
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec2 in_uv;
layout(location = 2) flat in uint in_content_type;
layout(location = 0) out vec4 out_color;
layout(set = 0, binding = 0) uniform sampler2D color_atlas;
layout(set = 1, binding = 0) uniform sampler2D mask_atlas;
void main() {
if (in_content_type == 0u) {
out_color = texture(color_atlas, in_uv);
} else {
out_color.rgb = in_color.rgb;
out_color.a = in_color.a * texture(mask_atlas, in_uv).r;
}
}

View File

@@ -0,0 +1,61 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in uint in_model_idx;
layout(location = 1) in uint in_rect_dim;
layout(location = 2) in uint in_uv;
layout(location = 3) in uint in_color;
layout(location = 4) in uint in_content_type;
layout(location = 5) in float depth;
layout(location = 7) in float scale;
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec2 out_uv;
layout(location = 2) flat out uint out_content_type;
layout(set = 0, binding = 0) uniform sampler2D color_atlas;
layout(set = 1, binding = 0) uniform sampler2D mask_atlas;
#define UNIFORM_PARAMS_SET 2
#define MODEL_BUFFER_SET 3
#include "model_buffer.glsl"
#include "uniform.glsl"
void main() {
uint v = uint(gl_VertexIndex); // 0-3
uint rect_width = in_rect_dim & 0xffffu;
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
vec2 rect_size = vec2(float(rect_width), float(rect_height));
float rect_aspect = rect_size.x / rect_size.y;
uvec2 uv = uvec2(in_uv & 0xffffu, (in_uv & 0xffff0000u) >> 16u);
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
vec2 corner_pos = vec2(corner_pos_u);
uvec2 corner_offset = uvec2(rect_width, rect_height) * corner_pos_u;
uv = uv + corner_offset;
mat4 model_matrix = model_buffer.models[in_model_idx];
gl_Position =
uniforms.projection * model_matrix * vec4(corner_pos * scale, depth, 1.0);
out_content_type = in_content_type & 0xffffu;
out_color = vec4(float((in_color & 0x00ff0000u) >> 16u) / 255.0,
float((in_color & 0x0000ff00u) >> 8u) / 255.0,
float(in_color & 0x000000ffu) / 255.0,
float((in_color & 0xff000000u) >> 24u) / 255.0);
uvec2 dim = uvec2(0, 0);
if (in_content_type == 0u) {
dim = uvec2(textureSize(color_atlas, 0));
} else {
dim = uvec2(textureSize(mask_atlas, 0));
}
out_uv = vec2(uv) / vec2(dim);
}

View File

@@ -0,0 +1,8 @@
// Viewport
layout(std140, set = UNIFORM_PARAMS_SET, binding = 0) uniform UniformParams {
uniform uvec2 screen_resolution;
uniform float pixel_scale;
uniform mat4 projection;
}
uniforms;

View File

@@ -0,0 +1,259 @@
use std::{
f32,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use cosmic_text::SubpixelBin;
use image::RgbaImage;
use resvg::usvg::{Options, Tree};
use crate::assets::AssetProvider;
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone)]
pub enum CustomGlyphContent {
Svg(Box<Tree>),
Image(RgbaImage),
}
impl CustomGlyphContent {
pub fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> {
let tree = Tree::from_data(data, &Options::default())?;
Ok(CustomGlyphContent::Svg(Box::new(tree)))
}
pub fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> {
let image = image::load_from_memory(data)?.into_rgba8();
Ok(CustomGlyphContent::Image(image))
}
pub fn from_assets(provider: &mut Box<dyn AssetProvider>, path: &str) -> anyhow::Result<Self> {
let data = provider.load_from_path(path)?;
if path.ends_with(".svg") || path.ends_with(".svgz") {
Ok(CustomGlyphContent::from_bin_svg(&data)?)
} else {
Ok(CustomGlyphContent::from_bin_raster(&data)?)
}
}
pub fn from_file(path: &str) -> anyhow::Result<Self> {
let data = std::fs::read(path)?;
if path.ends_with(".svg") || path.ends_with(".svgz") {
Ok(CustomGlyphContent::from_bin_svg(&data)?)
} else {
Ok(CustomGlyphContent::from_bin_raster(&data)?)
}
}
}
#[derive(Debug, Clone)]
pub struct CustomGlyphData {
pub(super) id: usize,
pub(super) content: Arc<CustomGlyphContent>,
}
impl CustomGlyphData {
pub fn new(content: CustomGlyphContent) -> Self {
Self {
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
content: Arc::new(content),
}
}
pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) {
const MAX_RASTER_DIM: u16 = 256;
match self.content.as_ref() {
CustomGlyphContent::Svg(..) => (
width.next_power_of_two().min(MAX_RASTER_DIM),
height.next_power_of_two().min(MAX_RASTER_DIM),
),
CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _),
}
}
}
impl PartialEq for CustomGlyphData {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
/// A custom glyph to render
#[derive(Debug, Clone, PartialEq)]
pub struct CustomGlyph {
/// The unique identifier for this glyph
pub data: CustomGlyphData,
/// The position of the left edge of the glyph
pub left: f32,
/// The position of the top edge of the glyph
pub top: f32,
/// The width of the glyph
pub width: f32,
/// The height of the glyph
pub height: f32,
/// The color of this glyph (only relevant if the glyph is rendered with the
/// type [`ContentType::Mask`])
///
/// Set to `None` to use [`crate::TextArea::default_color`].
pub color: Option<cosmic_text::Color>,
/// If `true`, then this glyph will be snapped to the nearest whole physical
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
/// be `Zero` (useful for images and other large glyphs).
pub snap_to_physical_pixel: bool,
}
impl CustomGlyph {
pub fn new(data: CustomGlyphData) -> Self {
Self {
data,
left: 0.0,
top: 0.0,
width: 0.0,
height: 0.0,
color: None,
snap_to_physical_pixel: true,
}
}
}
/// A request to rasterize a custom glyph
#[derive(Debug, Clone, PartialEq)]
pub struct RasterizeCustomGlyphRequest {
/// The unique identifier of the glyph
pub data: CustomGlyphData,
/// The width of the glyph in physical pixels
pub width: u16,
/// The height of the glyph in physical pixels
pub height: u16,
/// Binning of fractional X offset
///
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
/// will always be `Zero`.
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
///
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
/// will always be `Zero`.
pub y_bin: SubpixelBin,
/// The scaling factor applied to the text area (Note that `width` and
/// `height` are already scaled by this factor.)
pub scale: f32,
}
/// A rasterized custom glyph
#[derive(Debug, Clone)]
pub struct RasterizedCustomGlyph {
/// The raw image data
pub data: Vec<u8>,
/// The type of image data contained in `data`
pub content_type: ContentType,
pub width: u16,
pub height: u16,
}
impl RasterizedCustomGlyph {
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph> {
match input.data.content.as_ref() {
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
CustomGlyphContent::Image(data) => rasterize_image(data),
}
}
pub(super) fn validate(
&self,
input: &RasterizeCustomGlyphRequest,
expected_type: Option<ContentType>,
) {
if let Some(expected_type) = expected_type {
assert_eq!(
self.content_type, expected_type,
"Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}",
expected_type, self.content_type, input
);
}
assert_eq!(
self.data.len(),
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
self.data.len(),
input,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CustomGlyphCacheKey {
/// Font ID
pub glyph_id: usize,
/// Glyph width
pub width: u16,
/// Glyph height
pub height: u16,
/// Binning of fractional X offset
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
pub y_bin: SubpixelBin,
}
/// The type of image data contained in a rasterized glyph
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ContentType {
/// Each pixel contains 32 bits of rgba data
Color,
/// Each pixel contains a single 8 bit channel
Mask,
}
impl ContentType {
/// The number of bytes per pixel for this content type
pub fn bytes_per_pixel(&self) -> usize {
match self {
Self::Color => 4,
Self::Mask => 1,
}
}
}
fn rasterize_svg(
tree: &Tree,
input: &RasterizeCustomGlyphRequest,
) -> Option<RasterizedCustomGlyph> {
// Calculate the scale based on the "glyph size".
let svg_size = tree.size();
let scale_x = input.width as f32 / svg_size.width();
let scale_y = input.height as f32 / svg_size.height();
let mut pixmap = resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32)?;
let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y);
// Offset the glyph by the subpixel amount.
let offset_x = input.x_bin.as_float();
let offset_y = input.y_bin.as_float();
if offset_x != 0.0 || offset_y != 0.0 {
transform = transform.post_translate(offset_x, offset_y);
}
resvg::render(tree, transform, &mut pixmap.as_mut());
Some(RasterizedCustomGlyph {
data: pixmap.data().to_vec(),
content_type: ContentType::Color,
width: input.width,
height: input.height,
})
}
fn rasterize_image(image: &RgbaImage) -> Option<RasterizedCustomGlyph> {
Some(RasterizedCustomGlyph {
data: image.to_vec(),
content_type: ContentType::Color,
width: image.width() as _,
height: image.height() as _,
})
}

View File

@@ -0,0 +1,225 @@
pub mod custom_glyph;
mod shaders;
pub mod text_atlas;
pub mod text_renderer;
use std::{
cell::RefCell,
rc::Rc,
sync::{LazyLock, Mutex},
};
use cosmic_text::{
Align, Attrs, Buffer, Color, FontSystem, Metrics, Style, SwashCache, Weight, Wrap,
};
use custom_glyph::{ContentType, CustomGlyph};
use etagere::AllocId;
use glam::Mat4;
use crate::drawing::{self};
pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> =
LazyLock::new(|| Mutex::new(FontSystem::new()));
pub static SWASH_CACHE: LazyLock<Mutex<SwashCache>> =
LazyLock::new(|| Mutex::new(SwashCache::new()));
/// Used in case no font_size is defined
const DEFAULT_FONT_SIZE: f32 = 14.;
/// In case no line_height is defined, use font_size * DEFAULT_LINE_HEIGHT_RATIO
const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.43;
pub(crate) const DEFAULT_METRICS: Metrics = Metrics::new(
DEFAULT_FONT_SIZE,
DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO,
);
#[derive(Default, Clone)]
pub struct TextStyle {
pub size: Option<f32>,
pub line_height: Option<f32>,
pub color: Option<drawing::Color>, // TODO: should this be hex?
pub style: Option<FontStyle>,
pub weight: Option<FontWeight>,
pub align: Option<HorizontalAlign>,
pub wrap: bool,
}
impl From<&TextStyle> for Attrs<'_> {
fn from(style: &TextStyle) -> Self {
Attrs::new()
.color(style.color.unwrap_or_default().into())
.style(style.style.unwrap_or_default().into())
.weight(style.weight.unwrap_or_default().into())
}
}
impl From<&TextStyle> for Metrics {
fn from(style: &TextStyle) -> Self {
let font_size = style.size.unwrap_or(DEFAULT_FONT_SIZE);
Metrics {
font_size,
line_height: style
.size
.unwrap_or_else(|| (font_size * DEFAULT_LINE_HEIGHT_RATIO).round()),
}
}
}
impl From<&TextStyle> for Wrap {
fn from(value: &TextStyle) -> Self {
if value.wrap {
Wrap::WordOrGlyph
} else {
Wrap::None
}
}
}
// helper structs for serde
#[derive(Default, Debug, Clone, Copy)]
pub enum FontStyle {
#[default]
Normal,
Italic,
}
impl From<FontStyle> for Style {
fn from(value: FontStyle) -> Style {
match value {
FontStyle::Normal => Style::Normal,
FontStyle::Italic => Style::Italic,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum FontWeight {
#[default]
Normal,
Bold,
}
impl From<FontWeight> for Weight {
fn from(value: FontWeight) -> Weight {
match value {
FontWeight::Normal => Weight::NORMAL,
FontWeight::Bold => Weight::BOLD,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum HorizontalAlign {
#[default]
Left,
Right,
Center,
Justified,
End,
}
impl From<HorizontalAlign> for Align {
fn from(value: HorizontalAlign) -> Align {
match value {
HorizontalAlign::Left => Align::Left,
HorizontalAlign::Right => Align::Right,
HorizontalAlign::Center => Align::Center,
HorizontalAlign::Justified => Align::Justified,
HorizontalAlign::End => Align::End,
}
}
}
impl From<drawing::Color> for cosmic_text::Color {
fn from(value: drawing::Color) -> cosmic_text::Color {
cosmic_text::Color::rgba(
(value.r * 255.999) as _,
(value.g * 255.999) as _,
(value.b * 255.999) as _,
(value.a * 255.999) as _,
)
}
}
impl From<cosmic_text::Color> for drawing::Color {
fn from(value: cosmic_text::Color) -> drawing::Color {
drawing::Color::new(
value.r() as f32 / 255.999,
value.g() as f32 / 255.999,
value.b() as f32 / 255.999,
value.a() as f32 / 255.999,
)
}
}
// glyphon types below
pub(super) enum GpuCacheStatus {
InAtlas {
x: u16,
y: u16,
content_type: ContentType,
},
SkipRasterization,
}
pub(super) struct GlyphDetails {
width: u16,
height: u16,
gpu_cache: GpuCacheStatus,
atlas_id: Option<AllocId>,
top: i16,
left: i16,
}
/// Controls the visible area of the text. Any text outside of the visible area will be clipped.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextBounds {
/// The position of the left edge of the visible area.
pub left: i32,
/// The position of the top edge of the visible area.
pub top: i32,
/// The position of the right edge of the visible area.
pub right: i32,
/// The position of the bottom edge of the visible area.
pub bottom: i32,
}
/// The default visible area doesn't clip any text.
impl Default for TextBounds {
fn default() -> Self {
Self {
left: i32::MIN,
top: i32::MIN,
right: i32::MAX,
bottom: i32::MAX,
}
}
}
/// A text area containing text to be rendered along with its overflow behavior.
#[derive(Clone)]
pub struct TextArea<'a> {
/// The buffer containing the text to be rendered.
pub buffer: Rc<RefCell<Buffer>>,
/// The left edge of the buffer.
pub left: f32,
/// The top edge of the buffer.
pub top: f32,
/// The scaling to apply to the buffer.
pub scale: f32,
/// The visible bounds of the text area. This is used to clip the text and doesn't have to
/// match the `left` and `top` values.
pub bounds: TextBounds,
/// The default color of the text area.
pub default_color: Color,
/// Additional custom glyphs to render.
pub custom_glyphs: &'a [CustomGlyph],
/// Distance from camera, 0.0..=1.0
pub depth: f32,
/// Text transformation
pub transform: Mat4,
}

View File

@@ -0,0 +1,13 @@
pub mod vert_atlas {
vulkano_shaders::shader! {
ty: "vertex",
path: "src/renderer_vk/shaders/text.vert",
}
}
pub mod frag_atlas {
vulkano_shaders::shader! {
ty: "fragment",
path: "src/renderer_vk/shaders/text.frag",
}
}

View File

@@ -0,0 +1,335 @@
use cosmic_text::{FontSystem, SwashCache};
use etagere::{Allocation, BucketedAtlasAllocator, size2};
use lru::LruCache;
use rustc_hash::FxHasher;
use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc};
use vulkano::{
buffer::BufferContents,
command_buffer::CommandBufferUsage,
descriptor_set::DescriptorSet,
format::Format,
image::{Image, ImageCreateInfo, ImageType, ImageUsage, view::ImageView},
memory::allocator::AllocationCreateInfo,
pipeline::graphics::{input_assembly::PrimitiveTopology, vertex_input::Vertex},
};
use super::{
GlyphDetails, GpuCacheStatus,
custom_glyph::ContentType,
shaders::{frag_atlas, vert_atlas},
text_renderer::GlyphonCacheKey,
};
use crate::gfx::{BLEND_ALPHA, WGfx, pipeline::WGfxPipeline};
/// Pipeline & shaders to be reused between TextRenderer instances
#[derive(Clone)]
pub struct TextPipeline {
pub(super) gfx: Arc<WGfx>,
pub(in super::super) inner: Arc<WGfxPipeline<GlyphVertex>>,
}
impl TextPipeline {
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
let vert = vert_atlas::load(gfx.device.clone())?;
let frag = frag_atlas::load(gfx.device.clone())?;
let pipeline = gfx.create_pipeline::<GlyphVertex>(
vert,
frag,
format,
Some(BLEND_ALPHA),
PrimitiveTopology::TriangleStrip,
true,
)?;
Ok(Self {
gfx,
inner: pipeline,
})
}
}
#[repr(C)]
#[derive(BufferContents, Vertex, Copy, Clone, Debug, Default)]
pub struct GlyphVertex {
#[format(R32_UINT)]
pub in_model_idx: u32,
#[format(R32_UINT)]
pub in_rect_dim: [u16; 2],
#[format(R32_UINT)]
pub in_uv: [u16; 2],
#[format(R32_UINT)]
pub in_color: u32,
#[format(R32_UINT)]
pub in_content_type: [u16; 2], // 2 bytes unused! TODO
#[format(R32_SFLOAT)]
pub depth: f32,
#[format(R32_SFLOAT)]
pub scale: f32,
}
type Hasher = BuildHasherDefault<FxHasher>;
pub(super) struct InnerAtlas {
pub kind: Kind,
pub image_view: Arc<ImageView>,
pub image_descriptor: Arc<DescriptorSet>,
pub packer: BucketedAtlasAllocator,
pub size: u32,
pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
pub max_texture_dimension_2d: u32,
common: TextPipeline,
}
impl InnerAtlas {
const INITIAL_SIZE: u32 = 256;
fn new(common: TextPipeline, kind: Kind) -> anyhow::Result<Self> {
let max_texture_dimension_2d = common
.gfx
.device
.physical_device()
.properties()
.max_image_dimension2_d;
let size = Self::INITIAL_SIZE.min(max_texture_dimension_2d);
let packer = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
// Create a texture to use for our atlas
let image = Image::new(
common.gfx.memory_allocator.clone(),
ImageCreateInfo {
image_type: ImageType::Dim2d,
format: kind.texture_format(),
extent: [size, size, 1],
usage: ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
..Default::default()
},
AllocationCreateInfo::default(),
)?;
let image_view = ImageView::new_default(image).unwrap();
let image_descriptor = common.inner.uniform_sampler(
Self::descriptor_set(kind),
image_view.clone(),
common.gfx.texture_filter,
)?;
let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default());
let glyphs_in_use = HashSet::with_hasher(Hasher::default());
Ok(Self {
kind,
image_view,
image_descriptor,
packer,
size,
glyph_cache,
glyphs_in_use,
max_texture_dimension_2d,
common,
})
}
fn descriptor_set(kind: Kind) -> usize {
match kind {
Kind::Color => 0,
Kind::Mask => 1,
}
}
pub(super) fn try_allocate(&mut self, width: usize, height: usize) -> Option<Allocation> {
let size = size2(width as i32, height as i32);
loop {
let allocation = self.packer.allocate(size);
if allocation.is_some() {
return allocation;
}
// Try to free least recently used allocation
let (mut key, mut value) = self.glyph_cache.peek_lru()?;
// Find a glyph with an actual size
while value.atlas_id.is_none() {
// All sized glyphs are in use, cache is full
if self.glyphs_in_use.contains(key) {
return None;
}
let _ = self.glyph_cache.pop_lru();
(key, value) = self.glyph_cache.peek_lru()?;
}
// All sized glyphs are in use, cache is full
if self.glyphs_in_use.contains(key) {
return None;
}
let (_, value) = self.glyph_cache.pop_lru().unwrap();
self.packer.deallocate(value.atlas_id.unwrap());
}
}
#[allow(dead_code)]
pub fn num_channels(&self) -> usize {
self.kind.num_channels()
}
pub(super) fn grow(
&mut self,
font_system: &mut FontSystem,
cache: &mut SwashCache,
) -> anyhow::Result<bool> {
if self.size >= self.max_texture_dimension_2d {
return Ok(false);
}
// Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
// factor of `Vec`.`
const GROWTH_FACTOR: u32 = 2;
let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
log::info!("Grow {:?} atlas {} → {new_size}", self.kind, self.size);
self.packer.grow(size2(new_size as i32, new_size as i32));
let old_image = self.image_view.image().clone();
let image = self.common.gfx.new_image(
new_size,
new_size,
old_image.format(),
ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
)?;
self.image_view = ImageView::new_default(image.clone()).unwrap();
let mut cmd_buf = self
.common
.gfx
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
// Re-upload glyphs
for (&cache_key, glyph) in &self.glyph_cache {
let (x, y) = match glyph.gpu_cache {
GpuCacheStatus::InAtlas { x, y, .. } => (x, y),
GpuCacheStatus::SkipRasterization => continue,
};
let (width, height) = match cache_key {
GlyphonCacheKey::Text(cache_key) => {
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
let width = image.placement.width as usize;
let height = image.placement.height as usize;
(width, height)
}
GlyphonCacheKey::Custom(cache_key) => (cache_key.width as usize, cache_key.height as usize),
};
let offset = [x as _, y as _, 0];
cmd_buf.copy_image(
old_image.clone(),
offset,
image.clone(),
offset,
Some([width as _, height as _, 1]),
)?;
}
cmd_buf.build_and_execute_now()?;
self.size = new_size;
Ok(true)
}
fn trim(&mut self) {
self.glyphs_in_use.clear();
}
fn rebind_descriptor(&mut self) -> anyhow::Result<()> {
self.image_descriptor = self.common.inner.uniform_sampler(
Self::descriptor_set(self.kind),
self.image_view.clone(),
self.common.gfx.texture_filter,
)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Kind {
Mask,
Color,
}
impl Kind {
fn num_channels(self) -> usize {
match self {
Kind::Mask => 1,
Kind::Color => 4,
}
}
fn texture_format(self) -> Format {
match self {
Kind::Mask => Format::R8_UNORM,
Kind::Color => Format::R8G8B8A8_UNORM,
}
}
}
/// An atlas containing a cache of rasterized glyphs that can be rendered.
pub struct TextAtlas {
pub(super) common: TextPipeline,
pub(super) color_atlas: InnerAtlas,
pub(super) mask_atlas: InnerAtlas,
}
impl TextAtlas {
/// Creates a new [`TextAtlas`].
pub fn new(common: TextPipeline) -> anyhow::Result<Self> {
let color_atlas = InnerAtlas::new(common.clone(), Kind::Color)?;
let mask_atlas = InnerAtlas::new(common.clone(), Kind::Mask)?;
Ok(Self {
common,
color_atlas,
mask_atlas,
})
}
pub fn trim(&mut self) {
self.mask_atlas.trim();
self.color_atlas.trim();
}
pub(super) fn grow(
&mut self,
font_system: &mut FontSystem,
cache: &mut SwashCache,
content_type: ContentType,
) -> anyhow::Result<bool> {
let did_grow = match content_type {
ContentType::Mask => self.mask_atlas.grow(font_system, cache)?,
ContentType::Color => self.color_atlas.grow(font_system, cache)?,
};
if did_grow {
self.color_atlas.rebind_descriptor()?;
self.mask_atlas.rebind_descriptor()?;
}
Ok(did_grow)
}
pub(super) fn inner_for_content_mut(&mut self, content_type: ContentType) -> &mut InnerAtlas {
match content_type {
ContentType::Color => &mut self.color_atlas,
ContentType::Mask => &mut self.mask_atlas,
}
}
}

View File

@@ -0,0 +1,484 @@
use crate::{
gfx::cmd::GfxCommandBuffer,
renderer_vk::{model_buffer::ModelBuffer, viewport::Viewport},
};
use super::{
ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea,
custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
text_atlas::{GlyphVertex, TextAtlas, TextPipeline},
};
use cosmic_text::{Color, SubpixelBin, SwashContent};
use glam::{Mat4, Vec2, Vec3};
use vulkano::{
buffer::{BufferUsage, Subbuffer},
command_buffer::CommandBufferUsage,
};
/// A text renderer that uses cached glyphs to render text into an existing render pass.
pub struct TextRenderer {
pipeline: TextPipeline,
vertex_buffer: Subbuffer<[GlyphVertex]>,
vertex_buffer_capacity: usize,
glyph_vertices: Vec<GlyphVertex>,
model_buffer: ModelBuffer,
}
impl TextRenderer {
/// Creates a new `TextRenderer`.
pub fn new(atlas: &mut TextAtlas) -> anyhow::Result<Self> {
// A buffer element is a single quad with a glyph on it
const INITIAL_CAPACITY: usize = 256;
let vertex_buffer = atlas.common.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
INITIAL_CAPACITY as _,
)?;
Ok(Self {
model_buffer: ModelBuffer::new(&atlas.common.gfx)?,
pipeline: atlas.common.clone(),
vertex_buffer,
vertex_buffer_capacity: INITIAL_CAPACITY,
glyph_vertices: Vec::new(),
})
}
/// Prepares all of the provided text areas for rendering.
pub fn prepare<'a>(
&mut self,
font_system: &mut FontSystem,
atlas: &mut TextAtlas,
viewport: &Viewport,
text_areas: impl IntoIterator<Item = TextArea<'a>>,
cache: &mut SwashCache,
) -> anyhow::Result<()> {
self.glyph_vertices.clear();
let resolution = viewport.resolution();
for text_area in text_areas {
let bounds_min_x = text_area.bounds.left.max(0);
let bounds_min_y = text_area.bounds.top.max(0);
let bounds_max_x = text_area.bounds.right.min(resolution[0] as i32);
let bounds_max_y = text_area.bounds.bottom.min(resolution[1] as i32);
for glyph in text_area.custom_glyphs.iter() {
let x = text_area.left + (glyph.left * text_area.scale);
let y = text_area.top + (glyph.top * text_area.scale);
let width = (glyph.width * text_area.scale).round() as u16;
let height = (glyph.height * text_area.scale).round() as u16;
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
(
x.round() as i32,
y.round() as i32,
SubpixelBin::Zero,
SubpixelBin::Zero,
)
} else {
let (x, x_bin) = SubpixelBin::new(x);
let (y, y_bin) = SubpixelBin::new(y);
(x, y, x_bin, y_bin)
};
let (cached_width, cached_height) = glyph.data.dim_for_cache_key(width, height);
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
glyph_id: glyph.data.id,
width: cached_width,
height: cached_height,
x_bin,
y_bin,
});
let color = glyph.color.unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph(
PrepareGlyphParams {
label_pos: Vec2::new(text_area.left, text_area.top),
x,
y,
line_y: 0.0,
color,
cache_key,
atlas,
cache,
font_system,
model_buffer: &mut self.model_buffer,
scale_factor: text_area.scale,
glyph_scale: width as f32 / cached_width as f32,
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
depth: text_area.depth,
transform: &text_area.transform,
},
|_cache, _font_system| -> Option<GetGlyphImageResult> {
if cached_width == 0 || cached_height == 0 {
return None;
}
let input = RasterizeCustomGlyphRequest {
data: glyph.data.clone(),
width: cached_width,
height: cached_height,
x_bin,
y_bin,
scale: text_area.scale,
};
let output = RasterizedCustomGlyph::try_from(&input)?;
output.validate(&input, None);
Some(GetGlyphImageResult {
content_type: output.content_type,
top: 0,
left: 0,
width: output.width,
height: output.height,
data: output.data,
})
},
)? {
self.glyph_vertices.push(glyph_to_render);
}
}
let is_run_visible = |run: &cosmic_text::LayoutRun| {
let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32;
let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32;
start_y_physical <= text_area.bounds.bottom && text_area.bounds.top <= end_y_physical
};
let buffer = text_area.buffer.borrow();
let layout_runs = buffer
.layout_runs()
.skip_while(|run| !is_run_visible(run))
.take_while(is_run_visible);
for run in layout_runs {
for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale);
let color = match glyph.color_opt {
Some(some) => some,
None => text_area.default_color,
};
if let Some(glyph_to_render) = prepare_glyph(
PrepareGlyphParams {
label_pos: Vec2::new(text_area.left, text_area.top),
x: physical_glyph.x,
y: physical_glyph.y,
line_y: run.line_y,
color,
cache_key: GlyphonCacheKey::Text(physical_glyph.cache_key),
atlas,
cache,
font_system,
model_buffer: &mut self.model_buffer,
glyph_scale: 1.0,
scale_factor: text_area.scale,
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
depth: text_area.depth,
transform: &text_area.transform,
},
|cache, font_system| -> Option<GetGlyphImageResult> {
let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
let content_type = match image.content {
SwashContent::Color => ContentType::Color,
SwashContent::Mask => ContentType::Mask,
SwashContent::SubpixelMask => {
// Not implemented yet, but don't panic if this happens.
ContentType::Mask
}
};
Some(GetGlyphImageResult {
content_type,
top: image.placement.top as i16,
left: image.placement.left as i16,
width: image.placement.width as u16,
height: image.placement.height as u16,
data: image.data,
})
},
)? {
self.glyph_vertices.push(glyph_to_render);
}
}
}
}
let will_render = !self.glyph_vertices.is_empty();
if !will_render {
return Ok(());
}
let vertices = self.glyph_vertices.as_slice();
while self.vertex_buffer_capacity < vertices.len() {
let new_capacity = self.vertex_buffer_capacity * 2;
self.vertex_buffer = self.pipeline.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
new_capacity as _,
)?;
self.vertex_buffer_capacity = new_capacity;
}
self.vertex_buffer.write()?[..vertices.len()].clone_from_slice(vertices);
Ok(())
}
/// Renders all layouts that were previously provided to `prepare`.
pub fn render(
&mut self,
atlas: &TextAtlas,
viewport: &mut Viewport,
cmd_buf: &mut GfxCommandBuffer,
) -> anyhow::Result<()> {
if self.glyph_vertices.is_empty() {
return Ok(());
}
self.model_buffer.upload(&atlas.common.gfx)?;
let descriptor_sets = vec![
atlas.color_atlas.image_descriptor.clone(),
atlas.mask_atlas.image_descriptor.clone(),
viewport.get_text_descriptor(&self.pipeline),
self.model_buffer.get_text_descriptor(&self.pipeline),
];
let res = viewport.resolution();
let pass = self.pipeline.inner.create_pass(
[res[0] as _, res[1] as _],
self.vertex_buffer.clone(),
0..4,
0..self.glyph_vertices.len() as u32,
descriptor_sets,
)?;
cmd_buf.run_ref(&pass)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) enum GlyphonCacheKey {
Text(cosmic_text::CacheKey),
Custom(CustomGlyphCacheKey),
}
struct GetGlyphImageResult {
content_type: ContentType,
top: i16,
left: i16,
width: u16,
height: u16,
data: Vec<u8>,
}
struct PrepareGlyphParams<'a> {
label_pos: Vec2,
x: i32,
y: i32,
line_y: f32,
color: Color,
cache_key: GlyphonCacheKey,
atlas: &'a mut TextAtlas,
cache: &'a mut SwashCache,
font_system: &'a mut FontSystem,
model_buffer: &'a mut ModelBuffer,
transform: &'a Mat4,
scale_factor: f32,
glyph_scale: f32,
bounds_min_x: i32,
bounds_min_y: i32,
bounds_max_x: i32,
bounds_max_y: i32,
depth: f32,
}
#[allow(clippy::too_many_arguments)]
fn prepare_glyph(
par: PrepareGlyphParams,
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,
) -> anyhow::Result<Option<GlyphVertex>> {
let gfx = par.atlas.common.gfx.clone();
let details = if let Some(details) = par.atlas.mask_atlas.glyph_cache.get(&par.cache_key) {
par.atlas.mask_atlas.glyphs_in_use.insert(par.cache_key);
details
} else if let Some(details) = par.atlas.color_atlas.glyph_cache.get(&par.cache_key) {
par.atlas.color_atlas.glyphs_in_use.insert(par.cache_key);
details
} else {
let Some(image) = (get_glyph_image)(par.cache, par.font_system) else {
return Ok(None);
};
let should_rasterize = image.width > 0 && image.height > 0;
let (gpu_cache, atlas_id, inner) = if should_rasterize {
let mut inner = par.atlas.inner_for_content_mut(image.content_type);
// Find a position in the packer
let allocation = loop {
match inner.try_allocate(image.width as usize, image.height as usize) {
Some(a) => break a,
None => {
if !par
.atlas
.grow(par.font_system, par.cache, image.content_type)?
{
anyhow::bail!(
"Atlas full. atlas: {:?} cache_key: {:?}",
image.content_type,
par.cache_key
);
}
inner = par.atlas.inner_for_content_mut(image.content_type);
}
}
};
let atlas_min = allocation.rectangle.min;
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buf.update_image(
inner.image_view.image().clone(),
&image.data,
[atlas_min.x as _, atlas_min.y as _, 0],
Some([image.width as _, image.height as _, 1]),
)?;
cmd_buf.build_and_execute_now()?; //TODO: do not wait for fence here
(
GpuCacheStatus::InAtlas {
x: atlas_min.x as u16,
y: atlas_min.y as u16,
content_type: image.content_type,
},
Some(allocation.id),
inner,
)
} else {
let inner = &mut par.atlas.color_atlas;
(GpuCacheStatus::SkipRasterization, None, inner)
};
inner.glyphs_in_use.insert(par.cache_key);
// Insert the glyph into the cache and return the details reference
inner
.glyph_cache
.get_or_insert(par.cache_key, || GlyphDetails {
width: image.width,
height: image.height,
gpu_cache,
atlas_id,
top: image.top,
left: image.left,
})
};
let mut x = par.x + details.left as i32;
let mut y = (par.line_y * par.scale_factor).round() as i32 + par.y - details.top as i32;
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
GpuCacheStatus::SkipRasterization => return Ok(None),
};
let mut glyph_width = details.width as i32;
let mut glyph_height = details.height as i32;
// Starts beyond right edge or ends beyond left edge
let max_x = x + glyph_width;
if x > par.bounds_max_x || max_x < par.bounds_min_x {
return Ok(None);
}
// Starts beyond bottom edge or ends beyond top edge
let max_y = y + glyph_height;
if y > par.bounds_max_y || max_y < par.bounds_min_y {
return Ok(None);
}
// Clip left ege
if x < par.bounds_min_x {
let right_shift = par.bounds_min_x - x;
x = par.bounds_min_x;
glyph_width = max_x - par.bounds_min_x;
atlas_x += right_shift as u16;
}
// Clip right edge
if x + glyph_width > par.bounds_max_x {
glyph_width = par.bounds_max_x - x;
}
// Clip top edge
if y < par.bounds_min_y {
let bottom_shift = par.bounds_min_y - y;
y = par.bounds_min_y;
glyph_height = max_y - par.bounds_min_y;
atlas_y += bottom_shift as u16;
}
// Clip bottom edge
if y + glyph_height > par.bounds_max_y {
glyph_height = par.bounds_max_y - y;
}
let mut model = Mat4::IDENTITY;
// top-left text transform
model *= Mat4::from_translation(Vec3::new(
par.label_pos.x / par.scale_factor,
par.label_pos.y / par.scale_factor,
0.0,
));
model *= *par.transform;
// per-character transform
model *= Mat4::from_translation(Vec3::new(
((x as f32) - par.label_pos.x) / par.scale_factor,
((y as f32) - par.label_pos.y) / par.scale_factor,
0.0,
));
model *= glam::Mat4::from_scale(Vec3::new(
glyph_width as f32 / par.scale_factor,
glyph_height as f32 / par.scale_factor,
0.0,
));
let in_model_idx = par.model_buffer.register(&model);
Ok(Some(GlyphVertex {
in_model_idx,
in_rect_dim: [glyph_width as u16, glyph_height as u16],
in_uv: [atlas_x, atlas_y],
in_color: par.color.0,
in_content_type: [
content_type as u16,
0, // unused (TODO!)
],
depth: par.depth,
scale: par.glyph_scale,
}))
}

View File

@@ -0,0 +1,18 @@
use vulkano::buffer::BufferContents;
// binary compatible mat4 which could be transparently used by vulkano BufferContents
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, BufferContents)]
pub struct WMat4(pub [f32; 16]);
impl WMat4 {
pub fn from_glam(mat: &glam::Mat4) -> WMat4 {
WMat4(*mat.as_ref())
}
}
impl Default for WMat4 {
fn default() -> Self {
Self(*glam::Mat4::IDENTITY.as_ref())
}
}

View File

@@ -0,0 +1,103 @@
use std::sync::Arc;
use vulkano::{
buffer::{BufferContents, BufferUsage, Subbuffer},
descriptor_set::DescriptorSet,
};
use crate::{gfx::WGfx, renderer_vk::util::WMat4};
use super::{rect::RectPipeline, text::text_atlas::TextPipeline};
/// Controls the visible area of all text for a given renderer. Any text outside of the visible
/// area will be clipped.
pub struct Viewport {
params: Params,
params_buffer: Subbuffer<[Params]>,
text_descriptor: Option<Arc<DescriptorSet>>,
rect_descriptor: Option<Arc<DescriptorSet>>,
}
impl Viewport {
/// Creates a new `Viewport` with the given `device` and `cache`.
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
let params = Params {
screen_resolution: [0, 0],
pixel_scale: 1.0,
padding1: [0.0],
projection: WMat4::default(),
};
let params_buffer = gfx.new_buffer(
BufferUsage::UNIFORM_BUFFER | BufferUsage::TRANSFER_DST,
[params].iter(),
)?;
Ok(Self {
params,
params_buffer,
text_descriptor: None,
rect_descriptor: None,
})
}
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
self
.text_descriptor
.get_or_insert_with(|| {
pipeline
.inner
.buffer(2, self.params_buffer.clone())
.unwrap() // safe unwrap
})
.clone()
}
pub fn get_rect_descriptor(&mut self, pipeline: &RectPipeline) -> Arc<DescriptorSet> {
self
.rect_descriptor
.get_or_insert_with(|| {
pipeline
.color_rect
.buffer(0, self.params_buffer.clone())
.unwrap() // safe unwrap
})
.clone()
}
/// Updates the `Viewport` with the given `resolution` and `projection`.
pub fn update(
&mut self,
resolution: [u32; 2],
projection: &glam::Mat4,
pixel_scale: f32,
) -> anyhow::Result<()> {
if self.params.screen_resolution == resolution
&& self.params.projection.0 == *projection.as_ref()
&& self.params.pixel_scale == pixel_scale
{
return Ok(());
}
self.params.screen_resolution = resolution;
self.params.projection = WMat4::from_glam(projection);
self.params.pixel_scale = pixel_scale;
self.params_buffer.write()?.copy_from_slice(&[self.params]);
Ok(())
}
/// Returns the current resolution of the `Viewport`.
pub fn resolution(&self) -> [u32; 2] {
self.params.screen_resolution
}
}
#[repr(C)]
#[derive(BufferContents, Clone, Copy, Debug, PartialEq)]
pub(crate) struct Params {
pub screen_resolution: [u32; 2],
pub pixel_scale: f32,
pub padding1: [f32; 1], // always zero
pub projection: WMat4,
}