From 4b78e39af727b70b74c229ad3a60bfcb16c4b36e Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:29:31 +0900 Subject: [PATCH] glyph upload fixes (#531) * remove unused pass cache, cache imageviews instead * glyph upload batching * image upload batching (wip) --- .../src/tab/settings/tab_features.rs | 2 +- wgui/src/drawing.rs | 1 + wgui/src/renderer_vk/context.rs | 13 +- wgui/src/renderer_vk/image.rs | 202 +++++--- wgui/src/renderer_vk/rect.rs | 44 +- wgui/src/renderer_vk/text/custom_glyph.rs | 1 + wgui/src/renderer_vk/text/text_renderer.rs | 485 +++++++++++------- wgui/src/widget/image.rs | 5 + 8 files changed, 462 insertions(+), 291 deletions(-) diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index 96d40fc3..a69113ba 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -1,6 +1,6 @@ use crate::tab::settings::{ 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 {} diff --git a/wgui/src/drawing.rs b/wgui/src/drawing.rs index 7acc52e9..c0c59ea1 100644 --- a/wgui/src/drawing.rs +++ b/wgui/src/drawing.rs @@ -289,6 +289,7 @@ pub struct Rectangle { pub struct ImagePrimitive { pub content: CustomGlyphData, pub content_key: usize, + pub skip_cache: bool, pub border: f32, // width in pixels pub border_color: Color, diff --git a/wgui/src/renderer_vk/context.rs b/wgui/src/renderer_vk/context.rs index 4dd552dc..74be225b 100644 --- a/wgui/src/renderer_vk/context.rs +++ b/wgui/src/renderer_vk/context.rs @@ -9,7 +9,7 @@ use crate::{ drawing::{self}, font_config, gfx::{WGfx, cmd::GfxCommandBuffer}, - renderer_vk::image::{ImagePipeline, ImageRenderer}, + renderer_vk::image::{ImagePipeline, ImageRenderer, ImageViewCache}, }; use super::{ @@ -62,6 +62,7 @@ impl RendererPass<'_> { viewport: &mut Viewport, cmd_buf: &mut GfxCommandBuffer, text_atlas: &mut TextAtlas, + image_view_cache: &mut ImageViewCache, ) -> anyhow::Result<()> { if self.submitted { return Ok(()); @@ -95,7 +96,9 @@ impl RendererPass<'_> { self.submitted = true; 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(); @@ -169,6 +172,7 @@ pub struct Context { pub dirty: bool, pixel_scale: f32, empty_text: Rc>, + image_cache: ImageViewCache, } pub struct ContextDrawResult { @@ -187,6 +191,7 @@ impl Context { pixel_scale, dirty: true, 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 cur_scissor: Option = None; + // drop unreferenced image views to avoid vram leaks + self.image_cache.retain(|_, v| v.content.strong_count() > 0); + for primitive in primitives { if needs_new_pass { passes.push(RendererPass::new( @@ -339,6 +347,7 @@ impl Context { &mut self.viewport, cmd_buf, &mut atlas.text_atlas, + &mut self.image_cache, )?; } diff --git a/wgui/src/renderer_vk/image.rs b/wgui/src/renderer_vk/image.rs index bf2aacca..541ede09 100644 --- a/wgui/src/renderer_vk/image.rs +++ b/wgui/src/renderer_vk/image.rs @@ -1,10 +1,13 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, +}; use cosmic_text::SubpixelBin; use glam::Mat4; use smallvec::smallvec; use vulkano::{ - buffer::{BufferContents, BufferUsage, Subbuffer}, + buffer::{BufferContents, BufferUsage}, command_buffer::CommandBufferUsage, format::Format, image::view::ImageView, @@ -16,12 +19,11 @@ use crate::{ gfx::{ BLEND_ALPHA, WGfx, cmd::GfxCommandBuffer, - pass::WGfxPass, pipeline::{WGfxPipeline, WPipelineCreateInfo}, }, renderer_vk::{ 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; + +pub struct CachedImageView { + pub(super) content: Weak, + view: Arc, + res: [u32; 2], +} + struct ImageVertexWithContent { vert: ImageVertex, content: CustomGlyphData, - content_key: usize, // identifies an image tag. + skip_cache: bool, } -struct CachedPass { +struct PendingImageUpload { content_id: usize, - vert_buffer: Subbuffer<[ImageVertex]>, - inner: WGfxPass, - res: [u32; 2], + content: Weak, + raster: RasterizedCustomGlyph, +} + +enum ImageViewSource { + Ready(Arc), + PendingUpload(usize), + Missing, } pub struct ImageRenderer { pipeline: ImagePipeline, image_verts: Vec, model_buffer: ModelBuffer, - cached_passes: HashMap, } impl ImageRenderer { @@ -91,15 +105,9 @@ impl ImageRenderer { model_buffer: ModelBuffer::new(&pipeline.gfx)?, pipeline, 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) { let in_model_idx = self .model_buffer @@ -118,15 +126,11 @@ impl ImageRenderer { ], }, content: image.content, - content_key: image.content_key, + skip_cache: image.skip_cache, }); } - fn upload_image( - gfx: &Arc, - res: [u32; 2], - img: &ImageVertexWithContent, - ) -> anyhow::Result>> { + fn rasterize_image(res: [u32; 2], img: &ImageVertexWithContent) -> Option { let Some(raster) = RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest { data: img.content.clone(), width: res[0] as _, @@ -136,21 +140,10 @@ impl ImageRenderer { scale: 1.0, // unused }) else { 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( - 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)) + Some(raster) } pub fn render( @@ -159,65 +152,122 @@ impl ImageRenderer { viewport: &mut Viewport, vk_scissor: &graphics::viewport::Scissor, cmd_buf: &mut GfxCommandBuffer, + image_view_cache: &mut ImageViewCache, ) -> anyhow::Result<()> { let res = viewport.resolution(); self.model_buffer.upload(gfx)?; + let mut pending_upload_by_key = HashMap::::new(); + let mut pending_uploads = Vec::::new(); + let mut image_sources = Vec::::with_capacity(self.image_verts.len()); + + // decide which images need to be rasterized and uploaded for img in &self.image_verts { - let pass = if let Some(x) = self.cached_passes.get_mut(&img.content_key) { - if x.content_id != img.content.id || x.res != res { - // image changed - let Some(image_view) = Self::upload_image(&self.pipeline.gfx, res, img)? else { - continue; - }; + if let Some(upload_idx) = pending_upload_by_key.get(&img.content.id) { + image_sources.push(ImageViewSource::PendingUpload(*upload_idx)); + continue; + } - x.inner - .update_sampler(2, image_view, self.pipeline.gfx.texture_filter)?; - } + if let Some(cached) = image_view_cache.get(&img.content.id) + && !img.skip_cache + && cached.res == res + { + image_sources.push(ImageViewSource::Ready(cached.view.clone())); + continue; + } - x - } else { - let vert_buffer = self.pipeline.gfx.empty_buffer( - BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST, - (std::mem::size_of::()) as _, + let Some(raster) = Self::rasterize_image(res, img) else { + image_sources.push(ImageViewSource::Missing); + continue; + }; + + 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; }; - 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, - )?; - - self.cached_passes.insert( - img.content_key, - CachedPass { - content_id: img.content.id, - vert_buffer, - inner: pass, + image_view_cache.insert( + upload.content_id, + CachedImageView { + content: upload.content.clone(), + view: image_view.clone(), 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::()) 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(()) diff --git a/wgui/src/renderer_vk/rect.rs b/wgui/src/renderer_vk/rect.rs index 4047d65d..c197e734 100644 --- a/wgui/src/renderer_vk/rect.rs +++ b/wgui/src/renderer_vk/rect.rs @@ -12,7 +12,6 @@ use crate::{ gfx::{ BLEND_ALPHA, WGfx, cmd::GfxCommandBuffer, - pass::WGfxPass, pipeline::{WGfxPipeline, WPipelineCreateInfo}, }, renderer_vk::model_buffer::ModelBuffer, @@ -59,18 +58,12 @@ impl RectPipeline { } } -struct CachedPass { - pass: WGfxPass, - res: [u32; 2], -} - pub struct RectRenderer { pipeline: RectPipeline, rect_vertices: Vec, vert_buffer: Subbuffer<[RectVertex]>, vert_buffer_len: usize, model_buffer: ModelBuffer, - pass: Option, } impl RectRenderer { @@ -88,15 +81,9 @@ impl RectRenderer { rect_vertices: vec![], vert_buffer, 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) { let in_model_idx = self .model_buffer @@ -144,27 +131,20 @@ impl RectRenderer { self.model_buffer.upload(gfx)?; self.upload_verts()?; - let cache = match self.pass.take() { - Some(p) if p.res == res => p, - _ => { - 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( - [res[0] as _, res[1] as _], - [0.0, 0.0], - self.vert_buffer.clone(), - 0..4, - 0..self.rect_vertices.len() as _, - vec![set0, set1], - vk_scissor, - )?; - CachedPass { pass, res } - } - }; + 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( + [res[0] as _, res[1] as _], + [0.0, 0.0], + self.vert_buffer.clone(), + 0..4, + 0..self.rect_vertices.len() as _, + vec![set0, set1], + vk_scissor, + )?; self.rect_vertices.clear(); - cmd_buf.run_ref(&cache.pass)?; - self.pass = Some(cache); + cmd_buf.run_ref(&pass)?; Ok(()) } } diff --git a/wgui/src/renderer_vk/text/custom_glyph.rs b/wgui/src/renderer_vk/text/custom_glyph.rs index 7bcdcd7a..10237969 100644 --- a/wgui/src/renderer_vk/text/custom_glyph.rs +++ b/wgui/src/renderer_vk/text/custom_glyph.rs @@ -163,6 +163,7 @@ impl CustomGlyphData { Ok(data) => Ok(data), Err(hashed_asset) => { 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); Ok(data) } diff --git a/wgui/src/renderer_vk/text/text_renderer.rs b/wgui/src/renderer_vk/text/text_renderer.rs index a02d7e49..258c0be9 100644 --- a/wgui/src/renderer_vk/text/text_renderer.rs +++ b/wgui/src/renderer_vk/text/text_renderer.rs @@ -1,5 +1,5 @@ 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}, }; @@ -9,19 +9,16 @@ use super::{ text_atlas::{GlyphVertex, TextAtlas, TextPipeline}, }; use cosmic_text::{Color, SubpixelBin, SwashContent}; -use etagere::size2; +use etagere::{AllocId, size2}; use glam::{Mat4, Vec2, Vec3}; +use std::collections::HashSet; + use vulkano::{ buffer::{BufferUsage, Subbuffer}, command_buffer::CommandBufferUsage, pipeline::graphics, }; -struct CachedPass { - pass: WGfxPass, - res: [u32; 2], -} - /// A text renderer that uses cached glyphs to render text into an existing render pass. pub struct TextRenderer { pipeline: TextPipeline, @@ -29,7 +26,6 @@ pub struct TextRenderer { vertex_buffer_capacity: usize, glyph_vertices: Vec, model_buffer: ModelBuffer, - pass: Option, } impl TextRenderer { @@ -49,7 +45,6 @@ impl TextRenderer { vertex_buffer, vertex_buffer_capacity: INITIAL_CAPACITY, glyph_vertices: Vec::new(), - pass: None, }) } @@ -65,6 +60,10 @@ impl TextRenderer { self.glyph_vertices.clear(); 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 { let bounds_min_x = text_area.bounds.left.max(0); @@ -101,26 +100,14 @@ impl TextRenderer { .or(glyph.color) .unwrap_or(text_area.default_color); - if let Some(glyph_to_render) = prepare_glyph( - &mut 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: f32::from(width) / f32::from(cached_width), - bounds_min_x, - bounds_min_y, - bounds_max_x, - bounds_max_y, - transform: &text_area.transform, - }, + if queue_missing_glyph_upload( + atlas, + font_system, + cache, + cache_key, + &mut missing_glyphs, + &mut unavailable_glyphs, + &mut pending_glyph_uploads, |_cache, _font_system| -> Option { if cached_width == 0 || cached_height == 0 { return None; @@ -148,8 +135,22 @@ impl TextRenderer { 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) .unwrap_or(text_area.default_color); - if let Some(glyph_to_render) = prepare_glyph( - &mut 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, - transform: &text_area.transform, - }, + let cache_key = GlyphonCacheKey::Text(physical_glyph.cache_key); + + if queue_missing_glyph_upload( + atlas, + font_system, + cache, + cache_key, + &mut missing_glyphs, + &mut unavailable_glyphs, + &mut pending_glyph_uploads, |cache, font_system| -> Option { let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?; @@ -217,13 +208,39 @@ impl TextRenderer { 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(); if !will_render { return Ok(()); @@ -259,31 +276,24 @@ impl TextRenderer { let res = viewport.resolution(); self.model_buffer.upload(&atlas.common.gfx)?; - let cache = match self.pass.take() { - Some(p) if p.res == res => p, - _ => { - 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 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 pass = self.pipeline.inner.create_pass( - [res[0] as _, res[1] as _], - [0.0, 0.0], - self.vertex_buffer.clone(), - 0..4, - 0..self.glyph_vertices.len() as u32, - descriptor_sets, - vk_scissor, - )?; - CachedPass { pass, res } - } - }; + let pass = self.pipeline.inner.create_pass( + [res[0] as _, res[1] as _], + [0.0, 0.0], + self.vertex_buffer.clone(), + 0..4, + 0..self.glyph_vertices.len() as u32, + descriptor_sets, + vk_scissor, + )?; - cmd_buf.run_ref(&cache.pass)?; - self.pass = Some(cache); + cmd_buf.run_ref(&pass)?; Ok(()) } } @@ -303,18 +313,28 @@ struct GetGlyphImageResult { data: Vec, } -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, 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, + transform: Mat4, scale_factor: f32, glyph_scale: f32, bounds_min_x: i32, @@ -323,102 +343,207 @@ struct PrepareGlyphParams<'a> { bounds_max_y: i32, } -fn prepare_glyph( - par: &mut PrepareGlyphParams, +struct PrepareGlyphParams<'a> { + 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, + unavailable_glyphs: &mut HashSet, + pending_glyph_uploads: &mut Vec, get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option, -) -> anyhow::Result> { - 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); +) -> bool { + if mark_glyph_in_use_if_cached(atlas, cache_key) { + return true; + } + + if unavailable_glyphs.contains(&cache_key) { + return false; + } + + if missing_glyphs.insert(cache_key) { + 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 { - let mut inner = par.atlas.inner_for_content_mut(image.content_type); + true +} - // Find a position in the packer - let allocation = loop { - if let Some(a) = inner.try_allocate(image.width as usize, image.height as usize) { - break a; - } - 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 - ); - } +fn mark_glyph_in_use_if_cached(atlas: &mut TextAtlas, cache_key: GlyphonCacheKey) -> bool { + if atlas.mask_atlas.glyph_cache.get(&cache_key).is_some() { + atlas.mask_atlas.glyphs_in_use.insert(cache_key); + true + } else if atlas.color_atlas.glyph_cache.get(&cache_key).is_some() { + atlas.color_atlas.glyphs_in_use.insert(cache_key); + true + } else { + false + } +} - 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, +) -> anyhow::Result<()> { + if pending_glyph_uploads.is_empty() { + return Ok(()); + } - 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); + let gfx = atlas.common.gfx.clone(); + let mut rasterized_uploads = Vec::new(); + let mut skipped_uploads = Vec::new(); - 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 - // TODO: use `vkCmdClearColorImage` with an image subresource (or xywh region?) to omit unnecessary allocation - let zero_bytes_data: Vec = vec![0x00; size_with_island.area() as usize * 4 /* RGBX */]; + let mut atlas_uploads = Vec::new(); + + 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 = vec![0x00; upload.size_with_island_area * 4 /* RGBX */]; cmd_buf.update_image( inner.image_view.image(), &zero_bytes_data, - [atlas_with_island_min.x as u32, atlas_with_island_min.y as u32, 0], - Some([size_with_island.width as u32, size_with_island.height as u32, 1]), + [upload.atlas_with_island_min[0], upload.atlas_with_island_min[1], 0], + Some([upload.size_with_island[0], upload.size_with_island[1], 1]), )?; - // Upload glyph itself + // Upload glyph itself. cmd_buf.update_image( inner.image_view.image(), - &image.data, - [atlas_glyph_min.x as u32, atlas_glyph_min.y as u32, 0], - Some([image.width.into(), image.height.into(), 1]), + &rasterized.image.data, + [upload.atlas_glyph_min[0], upload.atlas_glyph_min[1], 0], + 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()?; + } - ( - GpuCacheStatus::InAtlas { - x: atlas_glyph_min.x as u16, - 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) - }; + for upload in atlas_uploads { + let rasterized = &rasterized_uploads[upload.upload_index]; + let inner = atlas.inner_for_content_mut(rasterized.image.content_type); - 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, - }) + inner.glyphs_in_use.insert(rasterized.cache_key); + let _ = inner.glyph_cache.get_or_insert(rasterized.cache_key, || GlyphDetails { + width: rasterized.image.width, + height: rasterized.image.height, + gpu_cache: GpuCacheStatus::InAtlas { + x: upload.atlas_glyph_min[0] as u16, + y: upload.atlas_glyph_min[1] as u16, + content_type: rasterized.image.content_type, + }, + 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 { + 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 y = (par.line_y * par.scale_factor).round() as i32 + par.y - i32::from(details.top); + let mut x = glyph.x + i32::from(details.left); + 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 { 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); @@ -426,79 +551,79 @@ fn prepare_glyph( // 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); + if x > glyph.bounds_max_x || max_x < glyph.bounds_min_x { + return 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); + if y > glyph.bounds_max_y || max_y < glyph.bounds_min_y { + return None; } // Clip left edge - if x < par.bounds_min_x { - let right_shift = par.bounds_min_x - x; + if x < glyph.bounds_min_x { + let right_shift = glyph.bounds_min_x - x; - x = par.bounds_min_x; - glyph_width = max_x - par.bounds_min_x; + x = glyph.bounds_min_x; + glyph_width = max_x - glyph.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; + if x + glyph_width > glyph.bounds_max_x { + glyph_width = glyph.bounds_max_x - x; } // Clip top edge - if y < par.bounds_min_y { - let bottom_shift = par.bounds_min_y - y; + if y < glyph.bounds_min_y { + let bottom_shift = glyph.bounds_min_y - y; - y = par.bounds_min_y; - glyph_height = max_y - par.bounds_min_y; + y = glyph.bounds_min_y; + glyph_height = max_y - glyph.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; + if y + glyph_height > glyph.bounds_max_y { + glyph_height = glyph.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, + glyph.label_pos.x / glyph.scale_factor, + glyph.label_pos.y / glyph.scale_factor, 0.0, )); - model *= *par.transform; + model *= glyph.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, + ((x as f32) - glyph.label_pos.x) / glyph.scale_factor, + ((y as f32) - glyph.label_pos.y) / glyph.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, + glyph_width as f32 / glyph.scale_factor, + glyph_height as f32 / glyph.scale_factor, 0.0, )); let in_model_idx = par.model_buffer.register(&model); - Ok(Some(GlyphVertex { + 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_color: glyph.color.0, in_content_type: [ content_type as u16, 0, // unused (TODO!) ], - scale: par.glyph_scale, - })) + scale: glyph.glyph_scale, + }) } diff --git a/wgui/src/widget/image.rs b/wgui/src/widget/image.rs index 0a63ebac..7f6525d6 100644 --- a/wgui/src/widget/image.rs +++ b/wgui/src/widget/image.rs @@ -30,6 +30,7 @@ pub struct WidgetImage { params: WidgetImageParams, id: WidgetID, content_key: usize, + dirty: bool, } impl WidgetImage { @@ -40,6 +41,7 @@ impl WidgetImage { params, id: WidgetID::null(), content_key: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed), + dirty: true, }), ) } @@ -50,6 +52,7 @@ impl WidgetImage { } self.params.glyph_data = content; + self.dirty = true; alterables.mark_dirty_and_redraw(self.id); } @@ -79,11 +82,13 @@ impl WidgetObj for WidgetImage { ImagePrimitive { content, content_key: self.content_key, + skip_cache: self.dirty, border: self.params.border, border_color: self.params.border_color, round_units, }, )); + self.dirty = false; } fn measure(