wallet: for labels, create a texture atlas so quads can be batched as a single draw call

This commit is contained in:
darkfi
2024-06-23 13:41:22 +02:00
parent 061f50c2a0
commit cbff8febe1
6 changed files with 259 additions and 39 deletions

View File

@@ -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();

View File

@@ -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. }
}

View File

@@ -22,6 +22,7 @@ mod expr;
//mod gfx;
mod gfx2;
mod keysym;
mod mesh;
mod net;
//mod plugin;
mod prop;

View File

@@ -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<Vertex>,
indices: Vec<u16>,
}
impl MeshBuilder {
pub fn new() -> Self {
Self { verts: vec![], indices: vec![] }
}
pub fn append(&mut self, mut verts: Vec<Vertex>, indices: Vec<u16>) {
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))
}
}

View File

@@ -49,56 +49,157 @@ pub async fn make_texture_atlas(
font_size: f32,
glyphs: &Vec<Glyph>,
) -> Result<RenderedAtlas> {
// 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<u32> = vec![];
let mut uv_rects: Vec<Rectangle> = 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<u8>, 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<Glyph>,
current_x: f32,
current_y: f32,
i: usize,
}
impl<'a> GlyphPositionIter<'a> {
pub fn new(font_size: f32, glyphs: &'a Vec<Glyph>, 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<Self::Item> {
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<FtFaces>,
cache: Mutex<TextShaperCache>,
@@ -434,16 +535,16 @@ struct CacheKey {
face_idx: usize,
}
type SpritePtr = Arc<Sprite>;
pub type SpritePtr = Arc<Sprite>;
struct Sprite {
pub struct Sprite {
bmp: Vec<u8>,
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 {

View File

@@ -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<smol::Task<()>>,
glyphs: Mutex<(Vec<Glyph>, RenderedAtlas)>,
glyph_sprites: Mutex<Vec<SpritePtr>>,
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<Self>| {
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.));