Ver código fonte

app: add missing string_atlas.rs file, and actually pass enum values for make_layout2 instead of using strings

darkfi 3 semanas atrás
pai
commit
7dd1791c9c

+ 42 - 20
bin/app/src/text/mod.rs

@@ -16,6 +16,44 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+//! GPU text rendering pipeline.
+//!
+//! Text is drawn as textured quads on the GPU; no platform text API is
+//! involved. The pipeline has three layers:
+//!
+//! * Layout (`make_layout`): parley shapes a string into glyph runs
+//!   using the font stack (IBM Plex Mono, Noto Color Emoji, DarkIRC
+//!   Emoji) registered in `GLOBAL_FONT_CTX`. Coordinates are physical
+//!   pixels (window scale is baked in by parley); `TextLayout` remembers
+//!   the scale, and consumers divide by it exactly once to get the
+//!   virtual units the renderer expects. Rasterization still happens at
+//!   physical resolution so text stays crisp while the renderer
+//!   re-applies the scale via `SetScale`.
+//!
+//! * Rendering (`render`): two passes over a layout. First every glyph
+//!   of every run is rasterized with swash and packed into an `Atlas`:
+//!   one RGBA8 texture per call, color glyphs stored as RGBA and mask
+//!   glyphs as alpha. Then each run becomes a `DrawMesh` of quads whose
+//!   UVs reference that texture, so a whole run draws in one call. This
+//!   atlas is transient (one per layout): the right trade-off for
+//!   arbitrary dynamic text such as chat messages and labels, where
+//!   layouts are short-lived and each draws from its own texture.
+//!
+//! * Batching for fixed sets (`string_atlas`): the opposite trade-off.
+//!   `make_string_atlas` lays out many fixed strings up front and packs
+//!   all their glyphs into a single shared atlas: one texture and one
+//!   raster per glyph for the entire set, returning per-string quad
+//!   geometry to the caller. Used by the emoji picker, where rendering
+//!   each icon through the dynamic path allocated one texture plus a
+//!   vertex/index buffer pair per emoji (~574 icons, ~2s of generation)
+//!   for glyphs that only ever needed to reference a shared sheet.
+//!
+//! The packer itself lives in `atlas`: sprites are separated by a 2px
+//! gap on all sides to prevent UV bleed, and can optionally wrap into
+//! rows capped at `MAX_TEXTURE_DIMENSION` so large fixed sets stay
+//! within GPU texture size limits (GLES3/WebGL2 only guarantee 2048),
+//! failing loudly at build time instead of corrupting at draw time.
+
 use parley::fontique::{Collection, CollectionOptions, SourceCache, SourceCacheOptions};
 use std::{
     cell::RefCell,
@@ -135,8 +173,8 @@ pub fn make_layout(
         width,
         underlines,
         &[],
-        "start",
-        "normal",
+        parley::Alignment::Start,
+        parley::OverflowWrap::Normal,
     )
 }
 
@@ -149,26 +187,10 @@ pub fn make_layout2(
     width: Option<f32>,
     underlines: &[Range<usize>],
     foreground_colors: &[(Range<usize>, Color)],
-    text_align: &str,
-    overflow_wrap: &str,
+    text_align: parley::Alignment,
+    overflow_wrap: parley::OverflowWrap,
 ) -> TextLayout {
     THREAD_LAYOUT_CTX.with(|layout_ctx| {
-        let text_align = match text_align {
-            "start" => parley::Alignment::Start,
-            "end" => parley::Alignment::End,
-            "left" => parley::Alignment::Left,
-            "center" => parley::Alignment::Center,
-            "right" => parley::Alignment::Right,
-            "justify" => parley::Alignment::Justify,
-            _ => parley::Alignment::Start,
-        };
-        let overflow_wrap = match overflow_wrap {
-            "normal" => parley::OverflowWrap::Normal,
-            "anywhere" => parley::OverflowWrap::Anywhere,
-            "break-word" => parley::OverflowWrap::BreakWord,
-            _ => parley::OverflowWrap::Normal,
-        };
-
         let mut layout_ctx = layout_ctx.borrow_mut();
         let mut font_ctx = GLOBAL_FONT_CTX.clone();
 

+ 158 - 0
bin/app/src/text/string_atlas.rs

@@ -0,0 +1,158 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{
+    gfx::{DebugTag, Rectangle, RectangleUnion, Renderer},
+    mesh::COLOR_WHITE,
+};
+
+use super::{
+    atlas::{Atlas, RenderedAtlas},
+    make_layout,
+    render::push_glyphs,
+};
+
+/// One rasterized glyph of a string, placed relative to the string's
+/// layout origin and mapped into the shared atlas texture.
+pub struct StringAtlasGlyph {
+    /// Glyph rect in virtual units relative to the string layout origin
+    pub rect: Rectangle,
+    /// UV rect within the shared atlas texture
+    pub uv_rect: Rectangle,
+}
+
+/// Atlas-backed geometry for one input string.
+pub struct StringAtlasEntry {
+    pub glyphs: Vec<StringAtlasGlyph>,
+    /// Union of the glyph rects in virtual units relative to the string
+    /// layout origin
+    pub ink_bounds: Rectangle,
+}
+
+/// A single shared atlas texture covering a fixed list of strings.
+pub struct StringAtlas {
+    /// The rendered atlas holding every glyph of every input string
+    pub rendered: RenderedAtlas,
+    /// One entry per input string, index-aligned with the input
+    pub entries: Vec<StringAtlasEntry>,
+}
+
+/// Rasterize every glyph of every string once and pack them into a
+/// single shared atlas texture. Returns per-string glyph geometry so
+/// callers can draw each string as textured quads referencing the
+/// shared texture instead of allocating per-string GPU resources.
+pub fn make_string_atlas(
+    strings: &[&str],
+    font_size: f32,
+    window_scale: f32,
+    max_row_width: usize,
+    renderer: &Renderer,
+    tag: DebugTag,
+) -> StringAtlas {
+    let layouts: Vec<_> = strings
+        .iter()
+        .map(|string| make_layout(string, COLOR_WHITE, font_size, 1., window_scale, None, &[]))
+        .collect();
+
+    let mut atlas = Atlas::with_max_row_width(renderer, tag, max_row_width);
+    let mut scale_ctx = swash::scale::ScaleContext::new();
+    let mut run_idx = 0;
+
+    for layout in &layouts {
+        for line in layout.lines() {
+            for item in line.items() {
+                let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue };
+                push_glyphs(&mut atlas, &glyph_run, run_idx, &mut scale_ctx);
+                run_idx += 1;
+            }
+        }
+    }
+
+    let rendered = atlas.make();
+
+    let mut entries = Vec::with_capacity(layouts.len());
+    let mut run_idx = 0;
+    for layout in &layouts {
+        let mut glyphs = vec![];
+        let mut bounds = RectangleUnion::new();
+
+        for line in layout.lines() {
+            for item in line.items() {
+                let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue };
+                let mut run_x = glyph_run.offset();
+                let run_y = glyph_run.baseline();
+
+                for glyph in glyph_run.glyphs() {
+                    let glyph_inf =
+                        rendered.fetch_uv(glyph.id as u16, run_idx).expect("missing glyph UV rect");
+
+                    let glyph_x = run_x + glyph.x;
+                    let glyph_y = run_y - glyph.y;
+                    run_x += glyph.advance;
+
+                    let rect = Rectangle::new(
+                        (glyph_x + glyph_inf.place.left as f32) / layout.scale(),
+                        (glyph_y - glyph_inf.place.top as f32) / layout.scale(),
+                        glyph_inf.place.width as f32 / layout.scale(),
+                        glyph_inf.place.height as f32 / layout.scale(),
+                    );
+                    bounds.add(rect);
+
+                    let uv_rect = glyph_inf.uv_rect;
+                    glyphs.push(StringAtlasGlyph { rect, uv_rect });
+                }
+
+                run_idx += 1;
+            }
+        }
+
+        let ink_bounds = bounds.get().unwrap_or(Rectangle::zero());
+        entries.push(StringAtlasEntry { glyphs, ink_bounds });
+    }
+
+    StringAtlas { rendered, entries }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_make_string_atlas() {
+        let (method_send, _method_recv) = async_channel::unbounded();
+        let renderer = Renderer::new(method_send);
+
+        let strings = ["a", ":", "\u{1F600}", "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"];
+        let atlas = make_string_atlas(&strings, 40., 1., 4096, &renderer, None);
+
+        assert_eq!(atlas.entries.len(), 4);
+        for entry in &atlas.entries {
+            assert!(!entry.glyphs.is_empty());
+            assert!(entry.ink_bounds.w > 0.);
+            assert!(entry.ink_bounds.h > 0.);
+            for glyph in &entry.glyphs {
+                assert!(glyph.rect.w > 0.);
+                assert!(glyph.rect.h > 0.);
+                assert!((0. ..=1.).contains(&glyph.uv_rect.x));
+                assert!((0. ..=1.).contains(&glyph.uv_rect.y));
+                assert!((0. ..=1.).contains(&(glyph.uv_rect.x + glyph.uv_rect.w)));
+                assert!((0. ..=1.).contains(&(glyph.uv_rect.y + glyph.uv_rect.h)));
+            }
+        }
+    }
+}

+ 4 - 4
bin/app/src/ui/chatview/page.rs

@@ -209,8 +209,8 @@ impl PrivMessage {
                 Some(clip.w - timestamp_width),
                 &[],
                 &url_ranges,
-                "start",
-                "normal",
+                parley::Alignment::Start,
+                parley::OverflowWrap::Normal,
             )
         } else {
             let body_color = if self.is_action {
@@ -235,8 +235,8 @@ impl PrivMessage {
                 Some(clip.w - timestamp_width),
                 &[],
                 &foreground_colors,
-                "start",
-                "normal",
+                parley::Alignment::Start,
+                parley::OverflowWrap::Normal,
             )
         };
         self.txt_layout = Some(txt_layout);

+ 15 - 4
bin/app/src/ui/text.rs

@@ -128,8 +128,19 @@ impl Text {
         let text_color = self.text_color.get();
         let window_scale = self.window_scale.get();
         let width = self.rect.get_width();
-        let text_align = self.text_align.get();
-        let overflow_wrap = self.overflow_wrap.get();
+        let text_align = match self.text_align.get().as_str() {
+            "end" => parley::Alignment::End,
+            "left" => parley::Alignment::Left,
+            "center" => parley::Alignment::Center,
+            "right" => parley::Alignment::Right,
+            "justify" => parley::Alignment::Justify,
+            _ => parley::Alignment::Start,
+        };
+        let overflow_wrap = match self.overflow_wrap.get().as_str() {
+            "anywhere" => parley::OverflowWrap::Anywhere,
+            "break-word" => parley::OverflowWrap::BreakWord,
+            _ => parley::OverflowWrap::Normal,
+        };
 
         let text = if self.use_i18n.get() {
             if let Some(trans) = self.i18n_fish.tr(&text) {
@@ -151,8 +162,8 @@ impl Text {
             Some(width),
             &[],
             &[],
-            &text_align,
-            &overflow_wrap,
+            text_align,
+            overflow_wrap,
         )
     }
 

+ 15 - 4
bin/app/src/ui/text_scramble.rs

@@ -224,8 +224,19 @@ impl TextScramble {
         let scramble_color = self.scramble_color.get();
         let window_scale = self.window_scale.get();
         let width = self.rect.get_width();
-        let text_align = self.text_align.get();
-        let overflow_wrap = self.overflow_wrap.get();
+        let text_align = match self.text_align.get().as_str() {
+            "end" => parley::Alignment::End,
+            "left" => parley::Alignment::Left,
+            "center" => parley::Alignment::Center,
+            "right" => parley::Alignment::Right,
+            "justify" => parley::Alignment::Justify,
+            _ => parley::Alignment::Start,
+        };
+        let overflow_wrap = match self.overflow_wrap.get().as_str() {
+            "anywhere" => parley::OverflowWrap::Anywhere,
+            "break-word" => parley::OverflowWrap::BreakWord,
+            _ => parley::OverflowWrap::Normal,
+        };
 
         let scramble_colors =
             scramble_ranges.into_iter().map(|range| (range, scramble_color)).collect::<Vec<_>>();
@@ -239,8 +250,8 @@ impl TextScramble {
             Some(width),
             &[],
             &scramble_colors,
-            &text_align,
-            &overflow_wrap,
+            text_align,
+            overflow_wrap,
         )
     }