glyph upload fixes (#531)

* remove unused pass cache, cache imageviews instead

* glyph upload batching

* image upload batching (wip)
This commit is contained in:
galister 2026-06-03 17:29:31 +09:00 committed by GitHub
parent 7f97dc884f
commit 4b78e39af7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 462 additions and 291 deletions

View File

@ -1,6 +1,6 @@
use crate::tab::settings::{ use crate::tab::settings::{
SettingType, SettingsMountParams, SettingsTab, SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_range_f32}, macros::{options_category, options_checkbox, options_range_f32, options_slider_f32},
}; };
pub struct State {} pub struct State {}

View File

@ -289,6 +289,7 @@ pub struct Rectangle {
pub struct ImagePrimitive { pub struct ImagePrimitive {
pub content: CustomGlyphData, pub content: CustomGlyphData,
pub content_key: usize, pub content_key: usize,
pub skip_cache: bool,
pub border: f32, // width in pixels pub border: f32, // width in pixels
pub border_color: Color, pub border_color: Color,

View File

@ -9,7 +9,7 @@ use crate::{
drawing::{self}, drawing::{self},
font_config, font_config,
gfx::{WGfx, cmd::GfxCommandBuffer}, gfx::{WGfx, cmd::GfxCommandBuffer},
renderer_vk::image::{ImagePipeline, ImageRenderer}, renderer_vk::image::{ImagePipeline, ImageRenderer, ImageViewCache},
}; };
use super::{ use super::{
@ -62,6 +62,7 @@ impl RendererPass<'_> {
viewport: &mut Viewport, viewport: &mut Viewport,
cmd_buf: &mut GfxCommandBuffer, cmd_buf: &mut GfxCommandBuffer,
text_atlas: &mut TextAtlas, text_atlas: &mut TextAtlas,
image_view_cache: &mut ImageViewCache,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if self.submitted { if self.submitted {
return Ok(()); return Ok(());
@ -95,7 +96,9 @@ impl RendererPass<'_> {
self.submitted = true; self.submitted = true;
self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?; self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?;
self.image_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?; self
.image_renderer
.render(gfx, viewport, &vk_scissor, cmd_buf, image_view_cache)?;
{ {
let mut font_system = font_system.system.lock(); let mut font_system = font_system.system.lock();
@ -169,6 +172,7 @@ pub struct Context {
pub dirty: bool, pub dirty: bool,
pixel_scale: f32, pixel_scale: f32,
empty_text: Rc<RefCell<Buffer>>, empty_text: Rc<RefCell<Buffer>>,
image_cache: ImageViewCache,
} }
pub struct ContextDrawResult { pub struct ContextDrawResult {
@ -187,6 +191,7 @@ impl Context {
pixel_scale, pixel_scale,
dirty: true, dirty: true,
empty_text: Rc::new(RefCell::new(Buffer::new_empty(DEFAULT_METRICS))), empty_text: Rc::new(RefCell::new(Buffer::new_empty(DEFAULT_METRICS))),
image_cache: ImageViewCache::new(),
}) })
} }
@ -242,6 +247,9 @@ impl Context {
let mut needs_new_pass = true; let mut needs_new_pass = true;
let mut cur_scissor: Option<drawing::Boundary> = None; let mut cur_scissor: Option<drawing::Boundary> = None;
// drop unreferenced image views to avoid vram leaks
self.image_cache.retain(|_, v| v.content.strong_count() > 0);
for primitive in primitives { for primitive in primitives {
if needs_new_pass { if needs_new_pass {
passes.push(RendererPass::new( passes.push(RendererPass::new(
@ -339,6 +347,7 @@ impl Context {
&mut self.viewport, &mut self.viewport,
cmd_buf, cmd_buf,
&mut atlas.text_atlas, &mut atlas.text_atlas,
&mut self.image_cache,
)?; )?;
} }

View File

@ -1,10 +1,13 @@
use std::{collections::HashMap, sync::Arc}; use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use cosmic_text::SubpixelBin; use cosmic_text::SubpixelBin;
use glam::Mat4; use glam::Mat4;
use smallvec::smallvec; use smallvec::smallvec;
use vulkano::{ use vulkano::{
buffer::{BufferContents, BufferUsage, Subbuffer}, buffer::{BufferContents, BufferUsage},
command_buffer::CommandBufferUsage, command_buffer::CommandBufferUsage,
format::Format, format::Format,
image::view::ImageView, image::view::ImageView,
@ -16,12 +19,11 @@ use crate::{
gfx::{ gfx::{
BLEND_ALPHA, WGfx, BLEND_ALPHA, WGfx,
cmd::GfxCommandBuffer, cmd::GfxCommandBuffer,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo}, pipeline::{WGfxPipeline, WPipelineCreateInfo},
}, },
renderer_vk::{ renderer_vk::{
model_buffer::ModelBuffer, model_buffer::ModelBuffer,
text::custom_glyph::{CustomGlyphData, RasterizeCustomGlyphRequest, RasterizedCustomGlyph}, text::custom_glyph::{CustomGlyphContent, CustomGlyphData, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
}, },
}; };
@ -65,24 +67,36 @@ impl ImagePipeline {
} }
} }
pub type ImageViewCache = HashMap<usize, CachedImageView>;
pub struct CachedImageView {
pub(super) content: Weak<CustomGlyphContent>,
view: Arc<ImageView>,
res: [u32; 2],
}
struct ImageVertexWithContent { struct ImageVertexWithContent {
vert: ImageVertex, vert: ImageVertex,
content: CustomGlyphData, content: CustomGlyphData,
content_key: usize, // identifies an image tag. skip_cache: bool,
} }
struct CachedPass { struct PendingImageUpload {
content_id: usize, content_id: usize,
vert_buffer: Subbuffer<[ImageVertex]>, content: Weak<CustomGlyphContent>,
inner: WGfxPass<ImageVertex>, raster: RasterizedCustomGlyph,
res: [u32; 2], }
enum ImageViewSource {
Ready(Arc<ImageView>),
PendingUpload(usize),
Missing,
} }
pub struct ImageRenderer { pub struct ImageRenderer {
pipeline: ImagePipeline, pipeline: ImagePipeline,
image_verts: Vec<ImageVertexWithContent>, image_verts: Vec<ImageVertexWithContent>,
model_buffer: ModelBuffer, model_buffer: ModelBuffer,
cached_passes: HashMap<usize, CachedPass>,
} }
impl ImageRenderer { impl ImageRenderer {
@ -91,15 +105,9 @@ impl ImageRenderer {
model_buffer: ModelBuffer::new(&pipeline.gfx)?, model_buffer: ModelBuffer::new(&pipeline.gfx)?,
pipeline, pipeline,
image_verts: vec![], image_verts: vec![],
cached_passes: HashMap::new(),
}) })
} }
pub fn begin(&mut self) {
self.image_verts.clear();
self.model_buffer.begin();
}
pub fn add_image(&mut self, boundary: Boundary, image: ImagePrimitive, transform: &Mat4) { pub fn add_image(&mut self, boundary: Boundary, image: ImagePrimitive, transform: &Mat4) {
let in_model_idx = self let in_model_idx = self
.model_buffer .model_buffer
@ -118,15 +126,11 @@ impl ImageRenderer {
], ],
}, },
content: image.content, content: image.content,
content_key: image.content_key, skip_cache: image.skip_cache,
}); });
} }
fn upload_image( fn rasterize_image(res: [u32; 2], img: &ImageVertexWithContent) -> Option<RasterizedCustomGlyph> {
gfx: &Arc<WGfx>,
res: [u32; 2],
img: &ImageVertexWithContent,
) -> anyhow::Result<Option<Arc<ImageView>>> {
let Some(raster) = RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest { let Some(raster) = RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest {
data: img.content.clone(), data: img.content.clone(),
width: res[0] as _, width: res[0] as _,
@ -136,21 +140,10 @@ impl ImageRenderer {
scale: 1.0, // unused scale: 1.0, // unused
}) else { }) else {
log::error!("Unable to rasterize custom image"); log::error!("Unable to rasterize custom image");
return Ok(None); return None;
}; };
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let image = cmd_buf.upload_image( Some(raster)
raster.width.into(),
raster.height.into(),
Format::R8G8B8A8_UNORM,
&raster.data,
)?;
let image_view = ImageView::new_default(image)?;
cmd_buf.build_and_execute_now()?;
Ok(Some(image_view))
} }
pub fn render( pub fn render(
@ -159,65 +152,122 @@ impl ImageRenderer {
viewport: &mut Viewport, viewport: &mut Viewport,
vk_scissor: &graphics::viewport::Scissor, vk_scissor: &graphics::viewport::Scissor,
cmd_buf: &mut GfxCommandBuffer, cmd_buf: &mut GfxCommandBuffer,
image_view_cache: &mut ImageViewCache,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let res = viewport.resolution(); let res = viewport.resolution();
self.model_buffer.upload(gfx)?; self.model_buffer.upload(gfx)?;
let mut pending_upload_by_key = HashMap::<usize, usize>::new();
let mut pending_uploads = Vec::<PendingImageUpload>::new();
let mut image_sources = Vec::<ImageViewSource>::with_capacity(self.image_verts.len());
// decide which images need to be rasterized and uploaded
for img in &self.image_verts { for img in &self.image_verts {
let pass = if let Some(x) = self.cached_passes.get_mut(&img.content_key) { if let Some(upload_idx) = pending_upload_by_key.get(&img.content.id) {
if x.content_id != img.content.id || x.res != res { image_sources.push(ImageViewSource::PendingUpload(*upload_idx));
// image changed continue;
let Some(image_view) = Self::upload_image(&self.pipeline.gfx, res, img)? else { }
continue;
};
x.inner if let Some(cached) = image_view_cache.get(&img.content.id)
.update_sampler(2, image_view, self.pipeline.gfx.texture_filter)?; && !img.skip_cache
} && cached.res == res
{
image_sources.push(ImageViewSource::Ready(cached.view.clone()));
continue;
}
x let Some(raster) = Self::rasterize_image(res, img) else {
} else { image_sources.push(ImageViewSource::Missing);
let vert_buffer = self.pipeline.gfx.empty_buffer( continue;
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST, };
(std::mem::size_of::<ImageVertex>()) as _,
let upload_idx = pending_uploads.len();
pending_uploads.push(PendingImageUpload {
content: Arc::downgrade(&img.content.content),
content_id: img.content.id,
raster,
});
pending_upload_by_key.insert(img.content.id, upload_idx);
image_sources.push(ImageViewSource::PendingUpload(upload_idx));
}
// upload every missing/stale image using one transfer command buffer
let mut uploaded_image_views = vec![None; pending_uploads.len()];
if !pending_uploads.is_empty() {
let mut xfer_cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
for (upload_idx, upload) in pending_uploads.iter().enumerate() {
log::trace!("Uploading image {}", upload.content_id);
let image = xfer_cmd_buf.upload_image(
upload.raster.width.into(),
upload.raster.height.into(),
Format::R8G8B8A8_UNORM,
&upload.raster.data,
)?; )?;
uploaded_image_views[upload_idx] = Some(ImageView::new_default(image)?);
}
let Some(image_view) = Self::upload_image(&self.pipeline.gfx, res, img)? else { xfer_cmd_buf.build_and_execute_now()?;
for (upload_idx, upload) in pending_uploads.iter().enumerate() {
let Some(image_view) = uploaded_image_views[upload_idx].as_ref() else {
continue; continue;
}; };
let set0 = viewport.get_image_descriptor(&self.pipeline); image_view_cache.insert(
let set1 = self.model_buffer.get_image_descriptor(&self.pipeline); upload.content_id,
let set2 = self CachedImageView {
.pipeline content: upload.content.clone(),
.inner view: image_view.clone(),
.uniform_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
let pass = self.pipeline.inner.create_pass(
[res[0] as _, res[1] as _],
[0.0, 0.0],
vert_buffer.clone(),
0..4,
0..1,
vec![set0, set1, set2],
vk_scissor,
)?;
self.cached_passes.insert(
img.content_key,
CachedPass {
content_id: img.content.id,
vert_buffer,
inner: pass,
res, res,
}, },
); );
self.cached_passes.get_mut(&img.content_key).unwrap() }
}
// run the rendering work
for (img, image_source) in self.image_verts.iter().zip(image_sources.iter()) {
let image_view = match image_source {
ImageViewSource::Ready(image_view) => image_view.clone(),
ImageViewSource::PendingUpload(upload_idx, ..) => {
let Some(image_view) = uploaded_image_views
.get(*upload_idx)
.and_then(|image_view| image_view.as_ref())
else {
continue;
};
image_view.clone()
}
ImageViewSource::Missing => continue,
}; };
pass.vert_buffer.write()?[0..1].clone_from_slice(&[img.vert]); let vert_buffer = self.pipeline.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
(std::mem::size_of::<ImageVertex>()) as _,
)?;
cmd_buf.run_ref(&pass.inner)?; let set0 = viewport.get_image_descriptor(&self.pipeline);
let set1 = self.model_buffer.get_image_descriptor(&self.pipeline);
let set2 = self
.pipeline
.inner
.uniform_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
let pass = self.pipeline.inner.create_pass(
[res[0] as _, res[1] as _],
[0.0, 0.0],
vert_buffer.clone(),
0..4,
0..1,
vec![set0, set1, set2],
vk_scissor,
)?;
vert_buffer.write()?[0..1].clone_from_slice(&[img.vert]);
cmd_buf.run_ref(&pass)?;
} }
Ok(()) Ok(())

View File

@ -12,7 +12,6 @@ use crate::{
gfx::{ gfx::{
BLEND_ALPHA, WGfx, BLEND_ALPHA, WGfx,
cmd::GfxCommandBuffer, cmd::GfxCommandBuffer,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo}, pipeline::{WGfxPipeline, WPipelineCreateInfo},
}, },
renderer_vk::model_buffer::ModelBuffer, renderer_vk::model_buffer::ModelBuffer,
@ -59,18 +58,12 @@ impl RectPipeline {
} }
} }
struct CachedPass {
pass: WGfxPass<RectVertex>,
res: [u32; 2],
}
pub struct RectRenderer { pub struct RectRenderer {
pipeline: RectPipeline, pipeline: RectPipeline,
rect_vertices: Vec<RectVertex>, rect_vertices: Vec<RectVertex>,
vert_buffer: Subbuffer<[RectVertex]>, vert_buffer: Subbuffer<[RectVertex]>,
vert_buffer_len: usize, vert_buffer_len: usize,
model_buffer: ModelBuffer, model_buffer: ModelBuffer,
pass: Option<CachedPass>,
} }
impl RectRenderer { impl RectRenderer {
@ -88,15 +81,9 @@ impl RectRenderer {
rect_vertices: vec![], rect_vertices: vec![],
vert_buffer, vert_buffer,
vert_buffer_len: BUFFER_SIZE, vert_buffer_len: BUFFER_SIZE,
pass: None,
}) })
} }
pub fn begin(&mut self) {
self.rect_vertices.clear();
self.model_buffer.begin();
}
pub fn add_rect(&mut self, boundary: Boundary, rectangle: Rectangle, transform: &Mat4) { pub fn add_rect(&mut self, boundary: Boundary, rectangle: Rectangle, transform: &Mat4) {
let in_model_idx = self let in_model_idx = self
.model_buffer .model_buffer
@ -144,27 +131,20 @@ impl RectRenderer {
self.model_buffer.upload(gfx)?; self.model_buffer.upload(gfx)?;
self.upload_verts()?; self.upload_verts()?;
let cache = match self.pass.take() { let set0 = viewport.get_rect_descriptor(&self.pipeline);
Some(p) if p.res == res => p, let set1 = self.model_buffer.get_rect_descriptor(&self.pipeline);
_ => { let pass = self.pipeline.color_rect.create_pass(
let set0 = viewport.get_rect_descriptor(&self.pipeline); [res[0] as _, res[1] as _],
let set1 = self.model_buffer.get_rect_descriptor(&self.pipeline); [0.0, 0.0],
let pass = self.pipeline.color_rect.create_pass( self.vert_buffer.clone(),
[res[0] as _, res[1] as _], 0..4,
[0.0, 0.0], 0..self.rect_vertices.len() as _,
self.vert_buffer.clone(), vec![set0, set1],
0..4, vk_scissor,
0..self.rect_vertices.len() as _, )?;
vec![set0, set1],
vk_scissor,
)?;
CachedPass { pass, res }
}
};
self.rect_vertices.clear(); self.rect_vertices.clear();
cmd_buf.run_ref(&cache.pass)?; cmd_buf.run_ref(&pass)?;
self.pass = Some(cache);
Ok(()) Ok(())
} }
} }

View File

@ -163,6 +163,7 @@ impl CustomGlyphData {
Ok(data) => Ok(data), Ok(data) => Ok(data),
Err(hashed_asset) => { Err(hashed_asset) => {
let data = Self::new(CustomGlyphContent::from_bin_raster(data)?); let data = Self::new(CustomGlyphContent::from_bin_raster(data)?);
log::trace!("Caching {path} with content_id {}", data.id);
globals_borrow.custom_glyph_cache.insert(hashed_asset, &data); globals_borrow.custom_glyph_cache.insert(hashed_asset, &data);
Ok(data) Ok(data)
} }

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
gfx::{cmd::GfxCommandBuffer, pass::WGfxPass}, gfx::cmd::GfxCommandBuffer,
renderer_vk::{model_buffer::ModelBuffer, text::text_atlas::TEXT_ATLAS_ISLAND_PADDING_PX, viewport::Viewport}, renderer_vk::{model_buffer::ModelBuffer, text::text_atlas::TEXT_ATLAS_ISLAND_PADDING_PX, viewport::Viewport},
}; };
@ -9,19 +9,16 @@ use super::{
text_atlas::{GlyphVertex, TextAtlas, TextPipeline}, text_atlas::{GlyphVertex, TextAtlas, TextPipeline},
}; };
use cosmic_text::{Color, SubpixelBin, SwashContent}; use cosmic_text::{Color, SubpixelBin, SwashContent};
use etagere::size2; use etagere::{AllocId, size2};
use glam::{Mat4, Vec2, Vec3}; use glam::{Mat4, Vec2, Vec3};
use std::collections::HashSet;
use vulkano::{ use vulkano::{
buffer::{BufferUsage, Subbuffer}, buffer::{BufferUsage, Subbuffer},
command_buffer::CommandBufferUsage, command_buffer::CommandBufferUsage,
pipeline::graphics, pipeline::graphics,
}; };
struct CachedPass {
pass: WGfxPass<GlyphVertex>,
res: [u32; 2],
}
/// A text renderer that uses cached glyphs to render text into an existing render pass. /// A text renderer that uses cached glyphs to render text into an existing render pass.
pub struct TextRenderer { pub struct TextRenderer {
pipeline: TextPipeline, pipeline: TextPipeline,
@ -29,7 +26,6 @@ pub struct TextRenderer {
vertex_buffer_capacity: usize, vertex_buffer_capacity: usize,
glyph_vertices: Vec<GlyphVertex>, glyph_vertices: Vec<GlyphVertex>,
model_buffer: ModelBuffer, model_buffer: ModelBuffer,
pass: Option<CachedPass>,
} }
impl TextRenderer { impl TextRenderer {
@ -49,7 +45,6 @@ impl TextRenderer {
vertex_buffer, vertex_buffer,
vertex_buffer_capacity: INITIAL_CAPACITY, vertex_buffer_capacity: INITIAL_CAPACITY,
glyph_vertices: Vec::new(), glyph_vertices: Vec::new(),
pass: None,
}) })
} }
@ -65,6 +60,10 @@ impl TextRenderer {
self.glyph_vertices.clear(); self.glyph_vertices.clear();
let resolution = viewport.resolution(); let resolution = viewport.resolution();
let mut glyphs_to_render = Vec::new();
let mut pending_glyph_uploads = Vec::new();
let mut missing_glyphs = HashSet::new();
let mut unavailable_glyphs = HashSet::new();
for text_area in text_areas { for text_area in text_areas {
let bounds_min_x = text_area.bounds.left.max(0); let bounds_min_x = text_area.bounds.left.max(0);
@ -101,26 +100,14 @@ impl TextRenderer {
.or(glyph.color) .or(glyph.color)
.unwrap_or(text_area.default_color); .unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph( if queue_missing_glyph_upload(
&mut PrepareGlyphParams { atlas,
label_pos: Vec2::new(text_area.left, text_area.top), font_system,
x, cache,
y, cache_key,
line_y: 0.0, &mut missing_glyphs,
color, &mut unavailable_glyphs,
cache_key, &mut pending_glyph_uploads,
atlas,
cache,
font_system,
model_buffer: &mut self.model_buffer,
scale_factor: text_area.scale,
glyph_scale: f32::from(width) / f32::from(cached_width),
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
transform: &text_area.transform,
},
|_cache, _font_system| -> Option<GetGlyphImageResult> { |_cache, _font_system| -> Option<GetGlyphImageResult> {
if cached_width == 0 || cached_height == 0 { if cached_width == 0 || cached_height == 0 {
return None; return None;
@ -148,8 +135,22 @@ impl TextRenderer {
data: output.data, data: output.data,
}) })
}, },
)? { ) {
self.glyph_vertices.push(glyph_to_render); glyphs_to_render.push(QueuedGlyph {
label_pos: Vec2::new(text_area.left, text_area.top),
x,
y,
line_y: 0.0,
color,
cache_key,
transform: text_area.transform,
scale_factor: text_area.scale,
glyph_scale: f32::from(width) / f32::from(cached_width),
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
});
} }
} }
@ -176,26 +177,16 @@ impl TextRenderer {
.or(glyph.color_opt) .or(glyph.color_opt)
.unwrap_or(text_area.default_color); .unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph( let cache_key = GlyphonCacheKey::Text(physical_glyph.cache_key);
&mut PrepareGlyphParams {
label_pos: Vec2::new(text_area.left, text_area.top), if queue_missing_glyph_upload(
x: physical_glyph.x, atlas,
y: physical_glyph.y, font_system,
line_y: run.line_y, cache,
color, cache_key,
cache_key: GlyphonCacheKey::Text(physical_glyph.cache_key), &mut missing_glyphs,
atlas, &mut unavailable_glyphs,
cache, &mut pending_glyph_uploads,
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,
transform: &text_area.transform,
},
|cache, font_system| -> Option<GetGlyphImageResult> { |cache, font_system| -> Option<GetGlyphImageResult> {
let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?; let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
@ -217,13 +208,39 @@ impl TextRenderer {
data: image.data, data: image.data,
}) })
}, },
)? { ) {
self.glyph_vertices.push(glyph_to_render); glyphs_to_render.push(QueuedGlyph {
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,
transform: text_area.transform,
glyph_scale: 1.0,
scale_factor: text_area.scale,
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
});
} }
} }
} }
} }
upload_missing_glyphs(atlas, font_system, cache, pending_glyph_uploads)?;
for glyph in &glyphs_to_render {
if let Some(glyph_to_render) = prepare_glyph(&mut PrepareGlyphParams {
glyph,
atlas,
model_buffer: &mut self.model_buffer,
}) {
self.glyph_vertices.push(glyph_to_render);
}
}
let will_render = !self.glyph_vertices.is_empty(); let will_render = !self.glyph_vertices.is_empty();
if !will_render { if !will_render {
return Ok(()); return Ok(());
@ -259,31 +276,24 @@ impl TextRenderer {
let res = viewport.resolution(); let res = viewport.resolution();
self.model_buffer.upload(&atlas.common.gfx)?; self.model_buffer.upload(&atlas.common.gfx)?;
let cache = match self.pass.take() { let descriptor_sets = vec![
Some(p) if p.res == res => p, atlas.color_atlas.image_descriptor.clone(),
_ => { atlas.mask_atlas.image_descriptor.clone(),
let descriptor_sets = vec![ viewport.get_text_descriptor(&self.pipeline),
atlas.color_atlas.image_descriptor.clone(), self.model_buffer.get_text_descriptor(&self.pipeline),
atlas.mask_atlas.image_descriptor.clone(), ];
viewport.get_text_descriptor(&self.pipeline),
self.model_buffer.get_text_descriptor(&self.pipeline),
];
let pass = self.pipeline.inner.create_pass( let pass = self.pipeline.inner.create_pass(
[res[0] as _, res[1] as _], [res[0] as _, res[1] as _],
[0.0, 0.0], [0.0, 0.0],
self.vertex_buffer.clone(), self.vertex_buffer.clone(),
0..4, 0..4,
0..self.glyph_vertices.len() as u32, 0..self.glyph_vertices.len() as u32,
descriptor_sets, descriptor_sets,
vk_scissor, vk_scissor,
)?; )?;
CachedPass { pass, res }
}
};
cmd_buf.run_ref(&cache.pass)?; cmd_buf.run_ref(&pass)?;
self.pass = Some(cache);
Ok(()) Ok(())
} }
} }
@ -303,18 +313,28 @@ struct GetGlyphImageResult {
data: Vec<u8>, data: Vec<u8>,
} }
struct PrepareGlyphParams<'a> { struct PendingGlyphUpload {
cache_key: GlyphonCacheKey,
image: GetGlyphImageResult,
}
struct AtlasGlyphUpload {
upload_index: usize,
atlas_id: AllocId,
atlas_with_island_min: [u32; 2],
size_with_island: [u32; 2],
size_with_island_area: usize,
atlas_glyph_min: [u32; 2],
}
struct QueuedGlyph {
label_pos: Vec2, label_pos: Vec2,
x: i32, x: i32,
y: i32, y: i32,
line_y: f32, line_y: f32,
color: Color, color: Color,
cache_key: GlyphonCacheKey, cache_key: GlyphonCacheKey,
atlas: &'a mut TextAtlas, transform: Mat4,
cache: &'a mut SwashCache,
font_system: &'a mut FontSystem,
model_buffer: &'a mut ModelBuffer,
transform: &'a Mat4,
scale_factor: f32, scale_factor: f32,
glyph_scale: f32, glyph_scale: f32,
bounds_min_x: i32, bounds_min_x: i32,
@ -323,102 +343,207 @@ struct PrepareGlyphParams<'a> {
bounds_max_y: i32, bounds_max_y: i32,
} }
fn prepare_glyph( struct PrepareGlyphParams<'a> {
par: &mut PrepareGlyphParams, glyph: &'a QueuedGlyph,
atlas: &'a mut TextAtlas,
model_buffer: &'a mut ModelBuffer,
}
fn queue_missing_glyph_upload(
atlas: &mut TextAtlas,
font_system: &mut FontSystem,
cache: &mut SwashCache,
cache_key: GlyphonCacheKey,
missing_glyphs: &mut HashSet<GlyphonCacheKey>,
unavailable_glyphs: &mut HashSet<GlyphonCacheKey>,
pending_glyph_uploads: &mut Vec<PendingGlyphUpload>,
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>, get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,
) -> anyhow::Result<Option<GlyphVertex>> { ) -> bool {
let gfx = par.atlas.common.gfx.clone(); if mark_glyph_in_use_if_cached(atlas, cache_key) {
let details = if let Some(details) = par.atlas.mask_atlas.glyph_cache.get(&par.cache_key) { return true;
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) { if unavailable_glyphs.contains(&cache_key) {
par.atlas.color_atlas.glyphs_in_use.insert(par.cache_key); return false;
details }
} else {
let Some(image) = (get_glyph_image)(par.cache, par.font_system) else { if missing_glyphs.insert(cache_key) {
return Ok(None); let Some(image) = get_glyph_image(cache, font_system) else {
unavailable_glyphs.insert(cache_key);
return false;
}; };
let should_rasterize = image.width > 0 && image.height > 0; pending_glyph_uploads.push(PendingGlyphUpload { cache_key, image });
}
let (gpu_cache, atlas_id, inner) = if should_rasterize { true
let mut inner = par.atlas.inner_for_content_mut(image.content_type); }
// Find a position in the packer fn mark_glyph_in_use_if_cached(atlas: &mut TextAtlas, cache_key: GlyphonCacheKey) -> bool {
let allocation = loop { if atlas.mask_atlas.glyph_cache.get(&cache_key).is_some() {
if let Some(a) = inner.try_allocate(image.width as usize, image.height as usize) { atlas.mask_atlas.glyphs_in_use.insert(cache_key);
break a; true
} } else if atlas.color_atlas.glyph_cache.get(&cache_key).is_some() {
if !par.atlas.grow(par.font_system, par.cache, image.content_type)? { atlas.color_atlas.glyphs_in_use.insert(cache_key);
anyhow::bail!( true
"Atlas full. atlas: {:?} cache_key: {:?}", } else {
image.content_type, false
par.cache_key }
); }
}
inner = par.atlas.inner_for_content_mut(image.content_type); fn upload_missing_glyphs(
}; atlas: &mut TextAtlas,
font_system: &mut FontSystem,
cache: &mut SwashCache,
pending_glyph_uploads: Vec<PendingGlyphUpload>,
) -> anyhow::Result<()> {
if pending_glyph_uploads.is_empty() {
return Ok(());
}
let atlas_with_island_min = allocation.rectangle.min; let gfx = atlas.common.gfx.clone();
let size_with_island = allocation.rectangle.size(); let mut rasterized_uploads = Vec::new();
let atlas_glyph_min = let mut skipped_uploads = Vec::new();
allocation.rectangle.min + size2(TEXT_ATLAS_ISLAND_PADDING_PX as i32, TEXT_ATLAS_ISLAND_PADDING_PX as i32);
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?; for upload in pending_glyph_uploads {
if upload.image.width > 0 && upload.image.height > 0 {
rasterized_uploads.push(upload);
} else {
skipped_uploads.push(upload);
}
}
// Set data to zeros for the whole glyph island let mut atlas_uploads = Vec::new();
// TODO: use `vkCmdClearColorImage` with an image subresource (or xywh region?) to omit unnecessary allocation
let zero_bytes_data: Vec<u8> = vec![0x00; size_with_island.area() as usize * 4 /* RGBX */]; if !rasterized_uploads.is_empty() {
'allocate_all: loop {
atlas_uploads.clear();
for (upload_index, upload) in rasterized_uploads.iter().enumerate() {
let content_type = upload.image.content_type;
let allocation = loop {
if let Some(allocation) = {
let inner = atlas.inner_for_content_mut(content_type);
inner.try_allocate(upload.image.width as usize, upload.image.height as usize)
} {
break allocation;
}
if !atlas.grow(font_system, cache, content_type)? {
anyhow::bail!(
"Atlas full. atlas: {:?} cache_key: {:?}",
content_type,
upload.cache_key
);
}
// `grow` can rebuild the atlas allocator and move existing glyphs. Any
// allocations made for this batch before the grow are provisional, so
// discard them and recompute the batch offsets against the new atlas.
continue 'allocate_all;
};
let atlas_with_island_min = allocation.rectangle.min;
let size_with_island = allocation.rectangle.size();
let atlas_glyph_min =
allocation.rectangle.min + size2(TEXT_ATLAS_ISLAND_PADDING_PX as i32, TEXT_ATLAS_ISLAND_PADDING_PX as i32);
atlas_uploads.push(AtlasGlyphUpload {
upload_index,
atlas_id: allocation.id,
atlas_with_island_min: [atlas_with_island_min.x as u32, atlas_with_island_min.y as u32],
size_with_island: [size_with_island.width as u32, size_with_island.height as u32],
size_with_island_area: size_with_island.area() as usize,
atlas_glyph_min: [atlas_glyph_min.x as u32, atlas_glyph_min.y as u32],
});
}
break;
}
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
for upload in &atlas_uploads {
let rasterized = &rasterized_uploads[upload.upload_index];
let inner = atlas.inner_for_content_mut(rasterized.image.content_type);
// Set data to zeros for the whole glyph island.
// TODO: use `vkCmdClearColorImage` with an image subresource (or xywh region?) to omit unnecessary allocation.
let zero_bytes_data: Vec<u8> = vec![0x00; upload.size_with_island_area * 4 /* RGBX */];
cmd_buf.update_image( cmd_buf.update_image(
inner.image_view.image(), inner.image_view.image(),
&zero_bytes_data, &zero_bytes_data,
[atlas_with_island_min.x as u32, atlas_with_island_min.y as u32, 0], [upload.atlas_with_island_min[0], upload.atlas_with_island_min[1], 0],
Some([size_with_island.width as u32, size_with_island.height as u32, 1]), Some([upload.size_with_island[0], upload.size_with_island[1], 1]),
)?; )?;
// Upload glyph itself // Upload glyph itself.
cmd_buf.update_image( cmd_buf.update_image(
inner.image_view.image(), inner.image_view.image(),
&image.data, &rasterized.image.data,
[atlas_glyph_min.x as u32, atlas_glyph_min.y as u32, 0], [upload.atlas_glyph_min[0], upload.atlas_glyph_min[1], 0],
Some([image.width.into(), image.height.into(), 1]), Some([rasterized.image.width.into(), rasterized.image.height.into(), 1]),
)?; )?;
}
cmd_buf.build_and_execute_now()?; //TODO: do not wait for fence here cmd_buf.build_and_execute_now()?;
}
( for upload in atlas_uploads {
GpuCacheStatus::InAtlas { let rasterized = &rasterized_uploads[upload.upload_index];
x: atlas_glyph_min.x as u16, let inner = atlas.inner_for_content_mut(rasterized.image.content_type);
y: atlas_glyph_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); inner.glyphs_in_use.insert(rasterized.cache_key);
// Insert the glyph into the cache and return the details reference let _ = inner.glyph_cache.get_or_insert(rasterized.cache_key, || GlyphDetails {
inner.glyph_cache.get_or_insert(par.cache_key, || GlyphDetails { width: rasterized.image.width,
width: image.width, height: rasterized.image.height,
height: image.height, gpu_cache: GpuCacheStatus::InAtlas {
gpu_cache, x: upload.atlas_glyph_min[0] as u16,
atlas_id, y: upload.atlas_glyph_min[1] as u16,
top: image.top, content_type: rasterized.image.content_type,
left: image.left, },
}) atlas_id: Some(upload.atlas_id),
top: rasterized.image.top,
left: rasterized.image.left,
});
}
for upload in skipped_uploads {
let inner = &mut atlas.color_atlas;
inner.glyphs_in_use.insert(upload.cache_key);
let _ = inner.glyph_cache.get_or_insert(upload.cache_key, || GlyphDetails {
width: upload.image.width,
height: upload.image.height,
gpu_cache: GpuCacheStatus::SkipRasterization,
atlas_id: None,
top: upload.image.top,
left: upload.image.left,
});
}
Ok(())
}
fn prepare_glyph(par: &mut PrepareGlyphParams) -> Option<GlyphVertex> {
let glyph = par.glyph;
let details = if let Some(details) = par.atlas.mask_atlas.glyph_cache.get(&glyph.cache_key) {
par.atlas.mask_atlas.glyphs_in_use.insert(glyph.cache_key);
details
} else if let Some(details) = par.atlas.color_atlas.glyph_cache.get(&glyph.cache_key) {
par.atlas.color_atlas.glyphs_in_use.insert(glyph.cache_key);
details
} else {
return None;
}; };
let mut x = par.x + i32::from(details.left); let mut x = glyph.x + i32::from(details.left);
let mut y = (par.line_y * par.scale_factor).round() as i32 + par.y - i32::from(details.top); let mut y = (glyph.line_y * glyph.scale_factor).round() as i32 + glyph.y - i32::from(details.top);
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
GpuCacheStatus::SkipRasterization => return Ok(None), GpuCacheStatus::SkipRasterization => return None,
}; };
let mut glyph_width = i32::from(details.width); let mut glyph_width = i32::from(details.width);
@ -426,79 +551,79 @@ fn prepare_glyph(
// Starts beyond right edge or ends beyond left edge // Starts beyond right edge or ends beyond left edge
let max_x = x + glyph_width; let max_x = x + glyph_width;
if x > par.bounds_max_x || max_x < par.bounds_min_x { if x > glyph.bounds_max_x || max_x < glyph.bounds_min_x {
return Ok(None); return None;
} }
// Starts beyond bottom edge or ends beyond top edge // Starts beyond bottom edge or ends beyond top edge
let max_y = y + glyph_height; let max_y = y + glyph_height;
if y > par.bounds_max_y || max_y < par.bounds_min_y { if y > glyph.bounds_max_y || max_y < glyph.bounds_min_y {
return Ok(None); return None;
} }
// Clip left edge // Clip left edge
if x < par.bounds_min_x { if x < glyph.bounds_min_x {
let right_shift = par.bounds_min_x - x; let right_shift = glyph.bounds_min_x - x;
x = par.bounds_min_x; x = glyph.bounds_min_x;
glyph_width = max_x - par.bounds_min_x; glyph_width = max_x - glyph.bounds_min_x;
atlas_x += right_shift as u16; atlas_x += right_shift as u16;
} }
// Clip right edge // Clip right edge
if x + glyph_width > par.bounds_max_x { if x + glyph_width > glyph.bounds_max_x {
glyph_width = par.bounds_max_x - x; glyph_width = glyph.bounds_max_x - x;
} }
// Clip top edge // Clip top edge
if y < par.bounds_min_y { if y < glyph.bounds_min_y {
let bottom_shift = par.bounds_min_y - y; let bottom_shift = glyph.bounds_min_y - y;
y = par.bounds_min_y; y = glyph.bounds_min_y;
glyph_height = max_y - par.bounds_min_y; glyph_height = max_y - glyph.bounds_min_y;
atlas_y += bottom_shift as u16; atlas_y += bottom_shift as u16;
} }
// Clip bottom edge // Clip bottom edge
if y + glyph_height > par.bounds_max_y { if y + glyph_height > glyph.bounds_max_y {
glyph_height = par.bounds_max_y - y; glyph_height = glyph.bounds_max_y - y;
} }
let mut model = Mat4::IDENTITY; let mut model = Mat4::IDENTITY;
// top-left text transform // top-left text transform
model *= Mat4::from_translation(Vec3::new( model *= Mat4::from_translation(Vec3::new(
par.label_pos.x / par.scale_factor, glyph.label_pos.x / glyph.scale_factor,
par.label_pos.y / par.scale_factor, glyph.label_pos.y / glyph.scale_factor,
0.0, 0.0,
)); ));
model *= *par.transform; model *= glyph.transform;
// per-character transform // per-character transform
model *= Mat4::from_translation(Vec3::new( model *= Mat4::from_translation(Vec3::new(
((x as f32) - par.label_pos.x) / par.scale_factor, ((x as f32) - glyph.label_pos.x) / glyph.scale_factor,
((y as f32) - par.label_pos.y) / par.scale_factor, ((y as f32) - glyph.label_pos.y) / glyph.scale_factor,
0.0, 0.0,
)); ));
model *= glam::Mat4::from_scale(Vec3::new( model *= glam::Mat4::from_scale(Vec3::new(
glyph_width as f32 / par.scale_factor, glyph_width as f32 / glyph.scale_factor,
glyph_height as f32 / par.scale_factor, glyph_height as f32 / glyph.scale_factor,
0.0, 0.0,
)); ));
let in_model_idx = par.model_buffer.register(&model); let in_model_idx = par.model_buffer.register(&model);
Ok(Some(GlyphVertex { Some(GlyphVertex {
in_model_idx, in_model_idx,
in_rect_dim: [glyph_width as u16, glyph_height as u16], in_rect_dim: [glyph_width as u16, glyph_height as u16],
in_uv: [atlas_x, atlas_y], in_uv: [atlas_x, atlas_y],
in_color: par.color.0, in_color: glyph.color.0,
in_content_type: [ in_content_type: [
content_type as u16, content_type as u16,
0, // unused (TODO!) 0, // unused (TODO!)
], ],
scale: par.glyph_scale, scale: glyph.glyph_scale,
})) })
} }

View File

@ -30,6 +30,7 @@ pub struct WidgetImage {
params: WidgetImageParams, params: WidgetImageParams,
id: WidgetID, id: WidgetID,
content_key: usize, content_key: usize,
dirty: bool,
} }
impl WidgetImage { impl WidgetImage {
@ -40,6 +41,7 @@ impl WidgetImage {
params, params,
id: WidgetID::null(), id: WidgetID::null(),
content_key: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed), content_key: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
dirty: true,
}), }),
) )
} }
@ -50,6 +52,7 @@ impl WidgetImage {
} }
self.params.glyph_data = content; self.params.glyph_data = content;
self.dirty = true;
alterables.mark_dirty_and_redraw(self.id); alterables.mark_dirty_and_redraw(self.id);
} }
@ -79,11 +82,13 @@ impl WidgetObj for WidgetImage {
ImagePrimitive { ImagePrimitive {
content, content,
content_key: self.content_key, content_key: self.content_key,
skip_cache: self.dirty,
border: self.params.border, border: self.params.border,
border_color: self.params.border_color, border_color: self.params.border_color,
round_units, round_units,
}, },
)); ));
self.dirty = false;
} }
fn measure( fn measure(