From cbff8febe15f505bb150c6d031648d79e076c614 Mon Sep 17 00:00:00 2001 From: darkfi Date: Sun, 23 Jun 2024 13:41:22 +0200 Subject: [PATCH] wallet: for labels, create a texture atlas so quads can be batched as a single draw call --- bin/darkwallet/src/app.rs | 2 +- bin/darkwallet/src/gfx2.rs | 18 ++++ bin/darkwallet/src/main.rs | 1 + bin/darkwallet/src/mesh.rs | 84 ++++++++++++++++++ bin/darkwallet/src/text2.rs | 155 ++++++++++++++++++++++++++++------ bin/darkwallet/src/ui/text.rs | 38 ++++++--- 6 files changed, 259 insertions(+), 39 deletions(-) create mode 100644 bin/darkwallet/src/mesh.rs diff --git a/bin/darkwallet/src/app.rs b/bin/darkwallet/src/app.rs index 1337e0bb9..38cbc9423 100644 --- a/bin/darkwallet/src/app.rs +++ b/bin/darkwallet/src/app.rs @@ -294,7 +294,7 @@ impl App { prop.set_f32(2, 800.).unwrap(); prop.set_f32(3, 200.).unwrap(); node.set_property_f32("baseline", 40.).unwrap(); - node.set_property_f32("font_size", 20.).unwrap(); + node.set_property_f32("font_size", 100.).unwrap(); //node.set_property_str("text", "anon1🍆").unwrap(); node.set_property_str("text", "anon1").unwrap(); let prop = node.get_property("color").unwrap(); diff --git a/bin/darkwallet/src/gfx2.rs b/bin/darkwallet/src/gfx2.rs index 36724c466..4f3eb9e74 100644 --- a/bin/darkwallet/src/gfx2.rs +++ b/bin/darkwallet/src/gfx2.rs @@ -36,6 +36,20 @@ pub struct Point { pub y: f32, } +impl Point { + pub fn unpack(&self) -> (f32, f32) { + (self.x, self.y) + } + + pub fn offset(&self, off_x: f32, off_y: f32) -> Self { + Self { x: self.x + off_x, y: self.y + off_y } + } + + pub fn to_rect(&self, w: f32, h: f32) -> Rectangle { + Rectangle { x: self.x, y: self.y, w, h } + } +} + #[derive(Debug, Clone)] pub struct Rectangle { pub x: f32, @@ -45,6 +59,10 @@ pub struct Rectangle { } impl Rectangle { + pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + Self { x, y, w, h } + } + pub fn zero() -> Self { Self { x: 0., y: 0., w: 0., h: 0. } } diff --git a/bin/darkwallet/src/main.rs b/bin/darkwallet/src/main.rs index 0691565ce..1faead5f0 100644 --- a/bin/darkwallet/src/main.rs +++ b/bin/darkwallet/src/main.rs @@ -22,6 +22,7 @@ mod expr; //mod gfx; mod gfx2; mod keysym; +mod mesh; mod net; //mod plugin; mod prop; diff --git a/bin/darkwallet/src/mesh.rs b/bin/darkwallet/src/mesh.rs new file mode 100644 index 000000000..2f4c3faf9 --- /dev/null +++ b/bin/darkwallet/src/mesh.rs @@ -0,0 +1,84 @@ +use crate::{ + error::Result, + gfx2::{Point, Rectangle, RenderApi, Vertex}, +}; +use miniquad::BufferId; + +pub type Color = [f32; 4]; + +pub const COLOR_RED: Color = [1., 0., 0., 1.]; +pub const COLOR_DARKGREY: Color = [0.2, 0.2, 0.2, 1.]; +pub const COLOR_GREEN: Color = [0., 1., 0., 1.]; +pub const COLOR_BLUE: Color = [0., 0., 1., 1.]; +pub const COLOR_WHITE: Color = [1., 1., 1., 1.]; + +pub struct MeshBuilder { + verts: Vec, + indices: Vec, +} + +impl MeshBuilder { + pub fn new() -> Self { + Self { verts: vec![], indices: vec![] } + } + + pub fn append(&mut self, mut verts: Vec, indices: Vec) { + let mut indices = indices.into_iter().map(|i| i + self.verts.len() as u16).collect(); + self.verts.append(&mut verts); + self.indices.append(&mut indices); + } + + pub fn draw_box(&mut self, obj: &Rectangle, color: Color, uv: &Rectangle) { + let (x1, y1) = obj.top_left().unpack(); + let (x2, y2) = obj.bottom_right().unpack(); + + let (u1, v1) = uv.top_left().unpack(); + let (u2, v2) = uv.bottom_right().unpack(); + + let verts = vec![ + // top left + Vertex { pos: [x1, y1], color, uv: [u1, v1] }, + // top right + Vertex { pos: [x2, y1], color, uv: [u2, v1] }, + // bottom left + Vertex { pos: [x1, y2], color, uv: [u1, v2] }, + // bottom right + Vertex { pos: [x2, y2], color, uv: [u2, v2] }, + ]; + let indices = vec![0, 2, 1, 1, 2, 3]; + + self.append(verts, indices); + } + + pub fn draw_outline(&mut self, obj: &Rectangle, color: Color, thickness: f32) { + let uv = Rectangle { x: 0., y: 0., w: 0., h: 0. }; + + let (x1, y1) = obj.top_left().unpack(); + let (dist_x, dist_y) = (obj.w, obj.h); + let (x2, y2) = obj.bottom_right().unpack(); + + // top + self.draw_box(&Rectangle::new(x1, y1, dist_x, thickness), color, &uv); + // left + self.draw_box(&Rectangle::new(x1, y1, thickness, dist_y), color, &uv); + // right + self.draw_box(&Rectangle::new(x2 - thickness, y1, thickness, dist_y), color, &uv); + // bottom + self.draw_box(&Rectangle::new(x1, y2 - thickness, dist_x, thickness), color, &uv); + } + + // Needed by OpenGL + pub fn num_elements(&self) -> i32 { + self.indices.len() as i32 + } + + pub async fn alloc(self, render_api: &RenderApi) -> Result<(BufferId, BufferId)> { + debug!(target: "mesh", "allocating {} verts:", self.verts.len()); + for vert in &self.verts { + debug!(target: "mesh", " {:?}", vert); + } + let vertex_buffer = render_api.new_vertex_buffer(self.verts).await?; + let index_buffer = render_api.new_index_buffer(self.indices).await?; + Ok((vertex_buffer, index_buffer)) + } +} diff --git a/bin/darkwallet/src/text2.rs b/bin/darkwallet/src/text2.rs index 6be901a7d..0b7cb8d4a 100644 --- a/bin/darkwallet/src/text2.rs +++ b/bin/darkwallet/src/text2.rs @@ -49,56 +49,157 @@ pub async fn make_texture_atlas( font_size: f32, glyphs: &Vec, ) -> Result { + // First compute total size of the atlas let mut total_width = ATLAS_GAP; let mut total_height = ATLAS_GAP; - for glyph in glyphs { + + // Glyph IDs already rendered so we don't do it twice + let mut rendered = vec![]; + + for (idx, glyph) in glyphs.iter().enumerate() { let sprite = &glyph.sprite; assert_eq!(sprite.bmp.len(), 4 * sprite.bmp_width * sprite.bmp_height); + // Already done this one so skip + if rendered.contains(&glyph.glyph_id) { + continue + } + rendered.push(glyph.glyph_id); + total_width += sprite.bmp_width + ATLAS_GAP; total_height = std::cmp::max(total_height, sprite.bmp_height); } total_width += ATLAS_GAP; total_height += 2 * ATLAS_GAP; + // Allocate the big texture now let mut atlas_bmp = vec![0; 4 * total_width * total_height]; + // For debug lines we want a single white pixel. + atlas_bmp[0] = 255; + atlas_bmp[1] = 255; + atlas_bmp[2] = 255; + atlas_bmp[3] = 255; // Calculate dimensions of final product first let mut current_x = ATLAS_GAP; - let mut uv_rects = vec![]; + let mut rendered_glyphs: Vec = vec![]; + let mut uv_rects: Vec = vec![]; - for glyph in glyphs { + for (idx, glyph) in glyphs.iter().enumerate() { let sprite = &glyph.sprite; - for i in 0..sprite.bmp_height { - for j in 0..sprite.bmp_width { - let off_dest = 4 * ((i + ATLAS_GAP) * total_width + j + current_x + ATLAS_GAP); - let off_src = 4 * (i * sprite.bmp_width + j); - atlas_bmp[off_dest] = sprite.bmp[off_src]; - atlas_bmp[off_dest + 1] = sprite.bmp[off_src + 1]; - atlas_bmp[off_dest + 2] = sprite.bmp[off_src + 2]; - atlas_bmp[off_dest + 3] = sprite.bmp[off_src + 3]; + // Did we already rendered this glyph? + // If so just copy the UV rect from before. + let mut uv_rect = None; + for (rendered_glyph_id, rendered_uv_rect) in rendered_glyphs.iter().zip(uv_rects.iter()) { + if *rendered_glyph_id == glyph.glyph_id { + uv_rect = Some(rendered_uv_rect.clone()); } } - // Compute UV coords - let uv_rect = Rectangle { - x: current_x as f32 / total_width as f32, - y: 0., - w: sprite.bmp_width as f32 / total_width as f32, - h: sprite.bmp_height as f32 / total_height as f32, - }; - uv_rects.push(uv_rect); + let uv_rect = match uv_rect { + Some(uv_rect) => uv_rect, + // Allocating a new glyph sprite in the atlas + None => { + copy_image(sprite, &mut atlas_bmp, total_width, current_x); - current_x += sprite.bmp_width + ATLAS_GAP; + // Compute UV coords + let uv_rect = Rectangle { + x: current_x as f32 / total_width as f32, + y: 0., + w: sprite.bmp_width as f32 / total_width as f32, + h: sprite.bmp_height as f32 / total_height as f32, + }; + + current_x += sprite.bmp_width + ATLAS_GAP; + + uv_rect + } + }; + + rendered_glyphs.push(glyph.glyph_id); + uv_rects.push(uv_rect); } + // Finally allocate the texture let texture_id = render_api.new_texture(total_width as u16, total_height as u16, atlas_bmp).await?; Ok(RenderedAtlas { uv_rects, texture_id }) } +fn copy_image(sprite: &Sprite, atlas_bmp: &mut Vec, total_width: usize, current_x: usize) { + for i in 0..sprite.bmp_height { + for j in 0..sprite.bmp_width { + let off_dest = 4 * ((i + ATLAS_GAP) * total_width + j + current_x + ATLAS_GAP); + let off_src = 4 * (i * sprite.bmp_width + j); + atlas_bmp[off_dest] = sprite.bmp[off_src]; + atlas_bmp[off_dest + 1] = sprite.bmp[off_src + 1]; + atlas_bmp[off_dest + 2] = sprite.bmp[off_src + 2]; + atlas_bmp[off_dest + 3] = sprite.bmp[off_src + 3]; + } + } +} + +pub struct GlyphPositionIter<'a> { + font_size: f32, + glyphs: &'a Vec, + current_x: f32, + current_y: f32, + i: usize, +} + +impl<'a> GlyphPositionIter<'a> { + pub fn new(font_size: f32, glyphs: &'a Vec, baseline_y: f32) -> Self { + Self { font_size, glyphs, current_x: 0., current_y: baseline_y, i: 0 } + } +} + +impl<'a> Iterator for GlyphPositionIter<'a> { + type Item = Rectangle; + + fn next(&mut self) -> Option { + assert!(self.i <= self.glyphs.len()); + if self.i == self.glyphs.len() { + return None; + } + + let glyph = &self.glyphs[self.i]; + let sprite = &glyph.sprite; + + let rect = if sprite.has_fixed_sizes { + // Downscale by height + let w = (sprite.bmp_width as f32 * self.font_size) / sprite.bmp_height as f32; + let h = self.font_size; + + let x = self.current_x; + let y = self.current_y - h; + + self.current_x += w; + + Rectangle { x, y, w, h } + } else { + let (w, h) = (sprite.bmp_width as f32, sprite.bmp_height as f32); + + let off_x = glyph.x_offset as f32 / 64.; + let off_y = glyph.y_offset as f32 / 64.; + + let x = self.current_x + off_x + sprite.bearing_x; + let y = self.current_y - off_y - sprite.bearing_y; + + let x_advance = glyph.x_advance; + let y_advance = glyph.y_advance; + self.current_x += x_advance; + self.current_y += y_advance; + + Rectangle { x, y, w, h } + }; + + self.i += 1; + Some(rect) + } +} + pub struct TextShaper { font_faces: Mutex, cache: Mutex, @@ -434,16 +535,16 @@ struct CacheKey { face_idx: usize, } -type SpritePtr = Arc; +pub type SpritePtr = Arc; -struct Sprite { +pub struct Sprite { bmp: Vec, - bmp_width: usize, - bmp_height: usize, + pub bmp_width: usize, + pub bmp_height: usize, - bearing_x: f32, - bearing_y: f32, - has_fixed_sizes: bool, + pub bearing_x: f32, + pub bearing_y: f32, + pub has_fixed_sizes: bool, } pub struct Glyph { diff --git a/bin/darkwallet/src/ui/text.rs b/bin/darkwallet/src/ui/text.rs index d9b0bdca1..6db5fde39 100644 --- a/bin/darkwallet/src/ui/text.rs +++ b/bin/darkwallet/src/ui/text.rs @@ -1,12 +1,14 @@ use async_lock::Mutex; +use miniquad::{BufferId, TextureId}; use rand::{rngs::OsRng, Rng}; use std::sync::{Arc, Weak}; use crate::{ gfx2::{DrawCall, DrawInstruction, DrawMesh, Rectangle, RenderApiPtr, Vertex}, + mesh::{MeshBuilder, COLOR_BLUE, COLOR_WHITE}, prop::{PropertyPtr, PropertyUint32}, scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId}, - text2::{self, Glyph, RenderedAtlas, TextShaperPtr}, + text2::{self, Glyph, GlyphPositionIter, RenderedAtlas, SpritePtr, TextShaperPtr}, }; use super::{eval_rect, get_parent_rect, read_rect, DrawUpdate, OnModify, Stoppable}; @@ -19,10 +21,11 @@ pub struct Text { text_shaper: TextShaperPtr, tasks: Vec>, - glyphs: Mutex<(Vec, RenderedAtlas)>, + glyph_sprites: Mutex>, - vertex_buffer: miniquad::BufferId, - index_buffer: miniquad::BufferId, + texture_id: TextureId, + vertex_buffer: BufferId, + index_buffer: BufferId, num_elements: i32, dc_key: u64, @@ -72,10 +75,21 @@ impl Text { Vertex { pos: [x2, y2], color: [1., 1., 0., 1.], uv: [1., 1.] }, ]; let indices = vec![0, 2, 1, 1, 2, 3]; + assert_eq!(atlas.uv_rects.len(), glyphs.len()); - let num_elements = indices.len() as i32; - let vertex_buffer = render_api.new_vertex_buffer(verts).await.unwrap(); - let index_buffer = render_api.new_index_buffer(indices).await.unwrap(); + let baseline_y = baseline.get_f32(0).unwrap(); + + let mut mesh = MeshBuilder::new(); + let mut glyph_pos_iter = GlyphPositionIter::new(font_size_val, &glyphs, baseline_y); + for (uv_rect, glyph_rect) in atlas.uv_rects.into_iter().zip(glyph_pos_iter) { + mesh.draw_box(&glyph_rect, COLOR_WHITE, &uv_rect); + //mesh.draw_outline(&rect, COLOR_BLUE, 2.); + } + + let num_elements = mesh.num_elements(); + let (vertex_buffer, index_buffer) = mesh.alloc(&render_api).await.unwrap(); + + let sprites = glyphs.into_iter().map(|glyph| glyph.sprite).collect(); let self_ = Arc::new_cyclic(|me: &Weak| { let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone()); @@ -92,7 +106,9 @@ impl Text { render_api, text_shaper, tasks: on_modify.tasks, - glyphs: Mutex::new((glyphs, atlas)), + //glyphs: Mutex::new((glyphs, atlas)), + glyph_sprites: Mutex::new(sprites), + texture_id: atlas.texture_id, vertex_buffer, index_buffer, num_elements, @@ -130,7 +146,7 @@ impl Text { let mesh = DrawMesh { vertex_buffer: self.vertex_buffer, index_buffer: self.index_buffer, - texture: Some(self.glyphs.lock().await.1.texture_id), + texture: Some(self.texture_id), num_elements: self.num_elements, }; @@ -147,8 +163,8 @@ impl Text { let off_x = rect.x / parent_rect.w; let off_y = rect.y / parent_rect.h; - let scale_x = rect.w / parent_rect.w; - let scale_y = rect.h / parent_rect.h; + let scale_x = 1. / parent_rect.w; + let scale_y = 1. / parent_rect.h; let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) * glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.));