Explorar el Código

app/emoji: fix the emoji mesh bg loader from blocking app quit during startup

darkfi hace 3 semanas
padre
commit
ff5f7c4fe7

+ 1 - 8
bin/app/src/app/schema/mod.rs

@@ -437,14 +437,7 @@ pub async fn make(
 
     let emoji_meshes = emoji_picker::EmojiMeshes::new(app.renderer.clone(), EMOJI_PICKER_ICON_SIZE);
 
-    let emoji_meshes2 = emoji_meshes.clone();
-    let _ = std::thread::spawn(move || {
-        for i in (0..500).step_by(20) {
-            for j in i..(i + 20) {
-                emoji_meshes2.lock().get(j);
-            }
-        }
-    });
+    emoji_meshes.clone().start_make();
 
     // Initialize default channels if the table is empty
     if app_db.channels().await.expect("cannot read channels").is_empty() {

+ 183 - 43
bin/app/src/text/atlas.rs

@@ -25,6 +25,12 @@ use crate::gfx::{DebugTag, ManagedTexturePtr, Rectangle, RenderApi, Renderer};
 /// due to UV coord calcs. Adding a gap perfectly fixes this.
 const ATLAS_GAP: usize = 2;
 
+/// Conservative upper bound for atlas texture dimensions. Row wrapping
+/// is capped to this and capped atlases assert their final size against
+/// it so oversized builds fail loudly at build time instead of rendering
+/// garbage at draw time.
+pub const MAX_TEXTURE_DIMENSION: usize = 4096;
+
 /*
 /// Convenience wrapper fn. Use if rendering a single line of glyphs.
 pub fn make_texture_atlas(renderer: &Renderer, glyphs: &Vec<Glyph>) -> RenderedAtlas {
@@ -37,6 +43,84 @@ pub fn make_texture_atlas(renderer: &Renderer, glyphs: &Vec<Glyph>) -> RenderedA
 pub(super) type RunIdx = usize;
 type GlyphKey = (swash::GlyphId, RunIdx);
 
+/// Pure packing state for the atlas: decides where each sprite goes and
+/// how large the texture must be. Sprites are packed left to right into
+/// rows no wider than `max_row_width`, wrapping to a new row as needed.
+/// Every sprite keeps an `ATLAS_GAP` margin on all sides to avoid
+/// bleeding. `usize::MAX` keeps the original single-row strip behavior.
+struct AtlasLayout {
+    /// (x, y) position of each sprite
+    positions: Vec<(usize, usize)>,
+    /// Height of the row currently being packed
+    row_height: usize,
+    /// LHS x pos for the next sprite
+    x: usize,
+    /// Top y pos of the row currently being packed
+    y: usize,
+
+    width: usize,
+    height: usize,
+
+    /// Row width cap. GPUs have a hard limit on texture dimensions
+    /// (`GL_MAX_TEXTURE_SIZE`, only guaranteed 2048 on GLES3/WebGL2,
+    /// typically 16384 on desktop), so a single-row strip cannot hold
+    /// large icon sets (e.g. ~574 emoji at 120px would be ~70,000px
+    /// wide). Sprites wrap into rows so the atlas grows as a roughly
+    /// square texture that fits within GPU limits. `usize::MAX` keeps
+    /// the original single-row strip behavior.
+    max_row_width: usize,
+}
+
+impl AtlasLayout {
+    fn new(max_row_width: usize) -> Self {
+        Self {
+            positions: vec![],
+            row_height: 0,
+            x: ATLAS_GAP,
+            y: ATLAS_GAP,
+
+            width: ATLAS_GAP,
+            // Not really important to set a value here since it will
+            // get overwritten.
+            // FYI glyphs have a gap on all sides (top and bottom here).
+            height: 2 * ATLAS_GAP,
+
+            max_row_width,
+        }
+    }
+
+    fn push(&mut self, glyph_width: usize, glyph_height: usize) {
+        let row_end = self.x + glyph_width + ATLAS_GAP;
+        if self.x > ATLAS_GAP && row_end > self.max_row_width {
+            // Wrap to a new row, leaving a gap below the previous one
+            self.y += self.row_height + ATLAS_GAP;
+            self.x = ATLAS_GAP;
+            self.row_height = 0;
+        }
+
+        self.positions.push((self.x, self.y));
+
+        self.row_height = std::cmp::max(glyph_height, self.row_height);
+        self.x += glyph_width + ATLAS_GAP;
+        self.width = std::cmp::max(self.width, self.x);
+
+        let height = self.y + glyph_height + ATLAS_GAP;
+        self.height = std::cmp::max(height, self.height);
+    }
+
+    /// UV rect for sprite `i` in the range [0, 1]
+    fn uv_rect(&self, i: usize, sprite_w: usize, sprite_h: usize) -> Rectangle {
+        let (x, y) = self.positions[i];
+        let (self_w, self_h) = (self.width as f32, self.height as f32);
+        Rectangle {
+            x: x as f32 / self_w,
+            y: y as f32 / self_h,
+            w: sprite_w as f32 / self_w,
+            h: sprite_h as f32 / self_h,
+        }
+    }
+}
+
 /// Responsible for aggregating glyphs, and then producing a single software
 /// blitted texture usable in a single draw call.
 /// This makes OpenGL batch precomputation of meshes efficient.
@@ -51,11 +135,7 @@ type GlyphKey = (swash::GlyphId, RunIdx);
 pub struct Atlas<'a> {
     glyph_keys: Vec<GlyphKey>,
     sprites: Vec<swash::scale::image::Image>,
-    // LHS x pos of glyph
-    x_pos: Vec<usize>,
-
-    width: usize,
-    height: usize,
+    layout: AtlasLayout,
 
     renderer: &'a Renderer,
     tag: DebugTag,
@@ -63,16 +143,16 @@ pub struct Atlas<'a> {
 
 impl<'a> Atlas<'a> {
     pub fn new(renderer: &'a Renderer, tag: DebugTag) -> Self {
+        Self::with_max_row_width(renderer, tag, usize::MAX)
+    }
+
+    /// Create an atlas whose sprites wrap into rows no wider than
+    /// `max_row_width`, keeping the texture within GPU limits.
+    pub fn with_max_row_width(renderer: &'a Renderer, tag: DebugTag, max_row_width: usize) -> Self {
         Self {
             glyph_keys: vec![],
             sprites: vec![],
-            x_pos: vec![],
-
-            width: ATLAS_GAP,
-            // Not really important to set a value here since it will
-            // get overwritten.
-            // FYI glyphs have a gap on all sides (top and bottom here).
-            height: 2 * ATLAS_GAP,
+            layout: AtlasLayout::new(max_row_width),
 
             renderer,
             tag,
@@ -110,18 +190,11 @@ impl<'a> Atlas<'a> {
 
         self.sprites.push(rendered_glyph);
 
-        self.x_pos.push(self.width);
-
-        // Gap on the top and bottom
-        let height = ATLAS_GAP + glyph_height + ATLAS_GAP;
-        self.height = std::cmp::max(height, self.height);
-
-        // Gap between glyphs and on both sides
-        self.width += glyph_width + ATLAS_GAP;
+        self.layout.push(glyph_width, glyph_height);
     }
 
     fn render(&self) -> Vec<u8> {
-        let mut atlas = vec![255, 255, 255, 0].repeat(self.width * self.height);
+        let mut atlas = vec![255, 255, 255, 0].repeat(self.layout.width * self.layout.height);
         // For drawing debug lines we want a single white pixel.
         // This is very useful to have in our texture for debugging.
         atlas[0] = 255;
@@ -129,11 +202,10 @@ impl<'a> Atlas<'a> {
         atlas[2] = 255;
         atlas[3] = 255;
 
-        let y = ATLAS_GAP;
         // Copy all the sprites to our atlas.
         // They should have ATLAS_GAP spacing on all sides to avoid bleeding.
-        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
-            copy_image(sprite, *x, y, &mut atlas, self.width);
+        for (sprite, (x, y)) in self.sprites.iter().zip(self.layout.positions.iter()) {
+            copy_image(sprite, *x, *y, &mut atlas, self.layout.width);
         }
 
         atlas
@@ -143,21 +215,13 @@ impl<'a> Atlas<'a> {
         // UV coords are in the range [0, 1]
         let mut uvs = Vec::with_capacity(self.sprites.len());
 
-        let (self_w, self_h) = (self.width as f32, self.height as f32);
-        let y = ATLAS_GAP as f32;
-
-        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
-            let x = *x as f32;
-            let sprite_w = sprite.placement.width as f32;
-            let sprite_h = sprite.placement.height as f32;
-
-            let uv = Rectangle {
-                x: x / self_w,
-                y: y / self_h,
-                w: sprite_w / self_w,
-                h: sprite_h / self_h,
-            };
-            uvs.push(uv);
+        for (i, sprite) in self.sprites.iter().enumerate() {
+            let uv_rect = self.layout.uv_rect(
+                i,
+                sprite.placement.width as usize,
+                sprite.placement.height as usize,
+            );
+            uvs.push(uv_rect);
         }
 
         uvs
@@ -167,7 +231,9 @@ impl<'a> Atlas<'a> {
     #[allow(dead_code)]
     pub fn dump(&self, output_path: &str) {
         let atlas = self.render();
-        let img = image::RgbaImage::from_raw(self.width as u32, self.height as u32, atlas).unwrap();
+        let img =
+            image::RgbaImage::from_raw(self.layout.width as u32, self.layout.height as u32, atlas)
+                .unwrap();
         img.save(output_path).unwrap();
     }
 
@@ -181,12 +247,27 @@ impl<'a> Atlas<'a> {
         //}
 
         assert_eq!(self.glyph_keys.len(), self.sprites.len());
-        assert_eq!(self.glyph_keys.len(), self.x_pos.len());
+        assert_eq!(self.glyph_keys.len(), self.layout.positions.len());
+
+        if self.layout.max_row_width != usize::MAX {
+            assert!(
+                self.layout.width <= MAX_TEXTURE_DIMENSION,
+                "atlas width {} exceeds MAX_TEXTURE_DIMENSION {}",
+                self.layout.width,
+                MAX_TEXTURE_DIMENSION
+            );
+            assert!(
+                self.layout.height <= MAX_TEXTURE_DIMENSION,
+                "atlas height {} exceeds MAX_TEXTURE_DIMENSION {}",
+                self.layout.height,
+                MAX_TEXTURE_DIMENSION
+            );
+        }
 
         let atlas = self.render();
         let texture = self.renderer.new_texture(
-            self.width as u16,
-            self.height as u16,
+            self.layout.width as u16,
+            self.layout.height as u16,
             atlas,
             TextureFormat::RGBA8,
             self.tag,
@@ -295,3 +376,62 @@ impl RenderedAtlas {
         None
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_single_row_without_wrap() {
+        let mut layout = AtlasLayout::new(usize::MAX);
+        layout.push(10, 20);
+        layout.push(5, 8);
+
+        assert_eq!(layout.positions[0], (ATLAS_GAP, ATLAS_GAP));
+        assert_eq!(layout.positions[1], (ATLAS_GAP + 12, ATLAS_GAP));
+        assert_eq!(layout.width, ATLAS_GAP + 12 + 7);
+        assert_eq!(layout.height, ATLAS_GAP + 20 + ATLAS_GAP);
+    }
+
+    #[test]
+    fn test_sprites_wrap_into_rows() {
+        let mut layout = AtlasLayout::new(50);
+        for _ in 0..5 {
+            layout.push(10, 10);
+        }
+
+        // Each sprite consumes 10 + ATLAS_GAP = 12 units of row width
+        // starting at ATLAS_GAP, so four fit in a 50-wide row and the
+        // fifth wraps onto a new one.
+        assert_eq!(layout.positions.len(), 5);
+        for i in 0..4 {
+            assert_eq!(layout.positions[i].1, ATLAS_GAP);
+            assert!(layout.positions[i].0 + 10 + ATLAS_GAP <= 50);
+        }
+        assert_eq!(layout.positions[4], (ATLAS_GAP, ATLAS_GAP + 10 + ATLAS_GAP));
+        assert_eq!(layout.width, 50);
+        assert_eq!(layout.height, layout.positions[4].1 + 10 + ATLAS_GAP);
+    }
+
+    #[test]
+    fn test_uv_rects_match_placements() {
+        let mut layout = AtlasLayout::new(50);
+        for _ in 0..5 {
+            layout.push(10, 10);
+        }
+
+        for i in 0..layout.positions.len() {
+            let uv = layout.uv_rect(i, 10, 10);
+            let (x, y) = layout.positions[i];
+
+            assert!((0. ..=1.).contains(&uv.x));
+            assert!((0. ..=1.).contains(&uv.y));
+            assert!((0. ..=1.).contains(&(uv.x + uv.w)));
+            assert!((0. ..=1.).contains(&(uv.y + uv.h)));
+            assert_eq!(uv.x, x as f32 / layout.width as f32);
+            assert_eq!(uv.y, y as f32 / layout.height as f32);
+            assert_eq!(uv.w, 10. / layout.width as f32);
+            assert_eq!(uv.h, 10. / layout.height as f32);
+        }
+    }
+}

+ 3 - 4
bin/app/src/text/mod.rs

@@ -31,10 +31,9 @@ pub use editor::Editor;
 mod render;
 #[cfg(not(target_os = "android"))]
 pub use render::render_raw_layout;
-pub use render::{
-    render_backgrounds, render_layout, render_layout_with_bounds, render_layout_with_opts,
-    DebugRenderOptions,
-};
+pub use render::{render_backgrounds, render_layout, render_layout_with_opts, DebugRenderOptions};
+mod string_atlas;
+pub use string_atlas::make_string_atlas;
 
 pub static GLOBAL_FONT_CTX: LazyLock<parley::FontContext> = LazyLock::new(|| {
     let mut font_ctx = parley::FontContext {

+ 1 - 12
bin/app/src/text/render.rs

@@ -138,17 +138,6 @@ pub fn render_layout_with_opts(
     render_raw_layout_impl(layout, layout.scale(), opts, renderer, tag).0
 }
 
-/// Render a layout and also return the union of the glyph ink bounds in
-/// virtual units, relative to the layout origin. Used by callers that
-/// position the mesh by its ink, e.g. centering emoji icons in grid cells.
-pub fn render_layout_with_bounds(
-    layout: &TextLayout,
-    renderer: &Renderer,
-    tag: DebugTag,
-) -> (Vec<DrawInstruction>, Rectangle) {
-    render_raw_layout_impl(layout, layout.scale(), DebugRenderOptions::OFF, renderer, tag)
-}
-
 /// Layout coordinates are physical (scale is baked in by parley) while
 /// meshes are consumed in virtual units and scaled up again by the
 /// renderer's `SetScale`. So every emitted coordinate is divided by
@@ -201,7 +190,7 @@ fn render_raw_layout_impl(
     (instrs, bounds.get().unwrap_or(Rectangle::zero()))
 }
 
-fn push_glyphs(
+pub(super) fn push_glyphs(
     atlas: &mut Atlas,
     glyph_run: &parley::GlyphRun<'_, Color>,
     run_idx: RunIdx,

+ 112 - 48
bin/app/src/ui/emoji_picker/emoji.rs

@@ -16,82 +16,146 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use std::{
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+    time::Instant,
+};
+
+use atomic_float::AtomicF32;
 use parking_lot::Mutex as SyncMutex;
-use std::sync::Arc;
 
 use crate::{
-    gfx::{gfxtag, DrawInstruction, DrawMesh, EpochTracker, Rectangle, Renderer},
-    mesh::COLOR_WHITE,
+    gfx::{gfxtag, DrawMesh, EpochTracker, Rectangle, Renderer},
+    mesh::{MeshBuilder, COLOR_WHITE},
     text,
+    text::atlas::MAX_TEXTURE_DIMENSION,
+    util::spawn_thread,
 };
 
 use super::default::DEFAULT_EMOJI_LIST;
 
-pub type EmojiMeshesPtr = Arc<SyncMutex<EmojiMeshes>>;
+macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui:emoji_picker", $($arg)*); } }
+
+/// The fully-built atlas cache: one prebuilt quad mesh per emoji,
+/// index-aligned with `DEFAULT_EMOJI_LIST`, all referencing a single
+/// shared atlas texture.
+pub struct EmojiAtlasData {
+    meshes: Vec<(DrawMesh, Rectangle)>,
+}
 
 pub struct EmojiMeshes {
     renderer: Renderer,
-    emoji_size: f32,
-    epoch_tracker: EpochTracker,
-    meshes: Vec<(DrawMesh, Rectangle)>,
+    emoji_size: AtomicF32,
+    epoch_tracker: SyncMutex<EpochTracker>,
+    inner: SyncMutex<Option<EmojiAtlasData>>,
+    building: AtomicBool,
 }
 
+pub type EmojiMeshesPtr = Arc<EmojiMeshes>;
+
 impl EmojiMeshes {
     pub fn new(renderer: Renderer, emoji_size: f32) -> EmojiMeshesPtr {
         let epoch_tracker = EpochTracker::new(&renderer);
-        Arc::new(SyncMutex::new(Self { renderer, emoji_size, epoch_tracker, meshes: vec![] }))
+        Arc::new(Self {
+            renderer,
+            emoji_size: AtomicF32::new(emoji_size),
+            epoch_tracker: SyncMutex::new(epoch_tracker),
+            inner: SyncMutex::new(None),
+            building: AtomicBool::new(false),
+        })
     }
 
-    pub fn set_size(&mut self, emoji_size: f32) {
-        self.emoji_size = emoji_size;
-        self.meshes.clear();
+    /// Build the atlas at the current emoji size and swap it in. The
+    /// whole build (layouts, rasters, single texture upload, quad
+    /// meshes) runs outside the mutex, so it never blocks teardown or
+    /// the draw pass. A build whose epoch went stale mid-flight is
+    /// discarded.
+    pub fn make(&self) {
+        let now = Instant::now();
+        let epoch_before = self.renderer.epoch.load(Ordering::Relaxed);
+        let emoji_size = self.emoji_size.load(Ordering::Relaxed);
+
+        let strings: Vec<&str> = DEFAULT_EMOJI_LIST.to_vec();
+        let string_atlas = text::make_string_atlas(
+            &strings,
+            emoji_size,
+            1.,
+            MAX_TEXTURE_DIMENSION,
+            &self.renderer,
+            gfxtag!("emoji_atlas"),
+        );
+
+        let mut meshes = Vec::with_capacity(string_atlas.entries.len());
+        for entry in &string_atlas.entries {
+            let mut mesh = MeshBuilder::new(gfxtag!("emoji_atlas"));
+            for glyph in &entry.glyphs {
+                mesh.draw_box(&glyph.rect, COLOR_WHITE, &glyph.uv_rect);
+            }
+            let draw_mesh = mesh
+                .alloc(&self.renderer)
+                .draw_with_textures(vec![string_atlas.rendered.texture.clone()]);
+            meshes.push((draw_mesh, entry.ink_bounds));
+        }
+
+        let data = EmojiAtlasData { meshes };
+
+        let epoch_after = self.renderer.epoch.load(Ordering::Relaxed);
+        if epoch_before != epoch_after {
+            d!("Discarding emoji atlas built for a stale epoch");
+            return
+        }
+
+        *self.inner.lock() = Some(data);
+        d!("Built emoji atlas ({} emoji) in {:?}", DEFAULT_EMOJI_LIST.len(), now.elapsed());
     }
 
-    pub fn clear(&mut self) {
-        self.meshes.clear();
+    /// Start building the atlas if it is not available yet. Returns
+    /// `true` only when the atlas is available right now; `false`
+    /// means a build was started (or is already in flight) and the
+    /// caller should retry later.
+    pub fn start_make(self: Arc<Self>) -> bool {
+        if self.inner.lock().is_some() {
+            return true
+        }
+        if self.building.swap(true, Ordering::SeqCst) {
+            return false
+        }
+
+        spawn_thread("emoji-atlas", move || {
+            self.make();
+            self.building.store(false, Ordering::SeqCst);
+        });
+
+        false
     }
 
-    pub fn get(&mut self, i: usize) -> (DrawMesh, Rectangle) {
+    /// Prebuilt quad mesh and ink bounds for emoji `i`, or `None` while
+    /// the atlas is unbuilt (or was dropped by a size change, epoch
+    /// bump, or teardown).
+    pub fn get(&self, i: usize) -> Option<(DrawMesh, Rectangle)> {
         assert!(i < DEFAULT_EMOJI_LIST.len());
 
-        // Meshes hold epoch-scoped buffers; after a UI restart they are
-        // dead, so drop them and rebuild lazily.
-        if self.epoch_tracker.changed() {
-            self.meshes.clear();
+        if self.epoch_tracker.lock().changed() {
+            *self.inner.lock() = None;
         }
 
-        self.meshes.reserve_exact(DEFAULT_EMOJI_LIST.len());
-
-        if i >= self.meshes.len() {
-            //d!("EmojiMeshes loading new glyphs");
-            for j in self.meshes.len()..=i {
-                let emoji = DEFAULT_EMOJI_LIST[j];
-                let mesh = self.gen_emoji_mesh(emoji);
-                self.meshes.push(mesh);
-            }
-        }
+        let guard = self.inner.lock();
+        let data = guard.as_ref()?;
+        data.meshes.get(i).cloned()
+    }
 
-        self.meshes[i].clone()
+    /// Drop the atlas; it is rebuilt lazily via `start_make`.
+    pub fn clear(&self) {
+        *self.inner.lock() = None;
     }
 
-    /// Make the mesh for this emoji, plus its ink bounds relative to the
-    /// mesh origin (the text baseline), so callers can center the visible
-    /// glyph inside a cell.
-    fn gen_emoji_mesh(&self, emoji: &str) -> (DrawMesh, Rectangle) {
-        //d!("rendering emoji: '{emoji}'");
-        // The params here don't actually matter since we're talking about BMP fixed sizes
-        let layout = text::make_layout(emoji, COLOR_WHITE, self.emoji_size, 1., 1., None, &[]);
-
-        let (instrs, bounds) =
-            text::render_layout_with_bounds(&layout, &self.renderer, gfxtag!("emoji_mesh"));
-
-        // Extract the mesh from the draw instructions
-        // For a single emoji, we should get exactly one Draw instruction with a mesh
-        let mesh = match instrs.first() {
-            Some(DrawInstruction::Draw(mesh)) => mesh.clone(),
-            _ => panic!("Expected Draw instruction for emoji"),
-        };
-
-        (mesh, bounds)
+    /// Change the emoji size and drop the atlas so the next build uses
+    /// the new size.
+    pub fn set_size(&self, emoji_size: f32) {
+        self.emoji_size.store(emoji_size, Ordering::Relaxed);
+        self.clear();
     }
 }

+ 8 - 3
bin/app/src/ui/emoji_picker/mod.rs

@@ -236,6 +236,11 @@ impl EmojiPicker {
         if rect_changed {
             self.draw_cache.clear();
         }
+        if !self.emoji_meshes.clone().start_make() {
+            // Skip the draw while the atlas is unbuilt so an empty grid
+            // never lands in the cache; the pass retries once built.
+            return None
+        }
         let instrs = self.draw_cache.get_or_insert_with(|| {
             let mut instrs = vec![DrawInstruction::ApplyView(rect)];
 
@@ -253,7 +258,7 @@ impl EmojiPicker {
                     break
                 }
 
-                let (mesh, ink) = self.emoji_meshes.lock().get(i);
+                let Some((mesh, ink)) = self.emoji_meshes.get(i) else { break };
                 // Center the emoji's ink inside its cell so the margin pads
                 // it evenly on all sides. The ink origin sits above the
                 // mesh origin (text baseline), hence the -ink.x/-ink.y.
@@ -302,7 +307,7 @@ impl UIObject for EmojiPicker {
         });
         on_modify.when_change_external(self.emoji_size.prop(), |self_, _| async move {
             let emoji_size = self_.emoji_size.get();
-            self_.emoji_meshes.lock().set_size(emoji_size);
+            self_.emoji_meshes.set_size(emoji_size);
             self_.draw_cache.clear();
             self_.redraw.trigger();
         });
@@ -317,7 +322,7 @@ impl UIObject for EmojiPicker {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.draw_cache.clear();
-        self.emoji_meshes.lock().clear();
+        self.emoji_meshes.clear();
     }
 
     #[instrument(target = "ui::emoji_picker")]