better font atlas implementation

c3
Alessandro Mauri 1 week ago
parent 5a89e9ec7d
commit 6d8300f9d9
  1. 3
      src/main.c3
  2. 85
      src/ugui_atlas.c3
  3. 54
      src/ugui_cmd.c3
  4. 39
      src/ugui_data.c3
  5. 95
      src/ugui_font.c3
  6. 9
      src/ugui_impl.c3
  7. 28
      src/ugui_text.c3

@ -27,7 +27,7 @@ fn int main(String[] args)
{ {
ugui::Ctx ui; ugui::Ctx ui;
ui.init()!!; ui.init()!!;
ui.font.load("/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!; ui.load_font("font1", "/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!;
short width = 800; short width = 800;
short height = 450; short height = 450;
@ -195,7 +195,6 @@ fn int main(String[] args)
rl::close_window(); rl::close_window();
ui.font.free();
ui.free(); ui.free();
return 0; return 0;
} }

@ -0,0 +1,85 @@
module ugui;
import std::io;
fault UgAtlasError {
CANNOT_PLACE,
INVALID_TYPE,
}
enum AtlasType {
ATLAS_GRAYSCALE,
}
// black and white atlas
struct Atlas {
AtlasType type;
Id id;
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
// bytes per pixel
macro usz AtlasType.bpp(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return 1;
}
}
fn void! Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height)
{
atlas.id = id;
atlas.type = type;
atlas.width = width;
atlas.height = height;
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
}
fn void Atlas.free(&atlas)
{
free(atlas.buffer);
}
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
fn Point! Atlas.place(&atlas, char[] pixels, ushort w, ushort h)
{
Point p;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return UgAtlasError.CANNOT_PLACE?;
}
}
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x) ..];
char[] pix = pixels[y*w + x ..];
usz bpp = atlas.type.bpp();
buf[0..bpp-1] = pix[0..bpp-1];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}

@ -2,6 +2,45 @@ module ugui;
import std::ascii; import std::ascii;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
// TODO:
// 1. Add atlases as a data type
// 2. Each atlas has an id
struct CmdSprite {
Rect rect;
Rect texture_rect;
}
// command structure
struct Cmd {
CmdType type;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
}
}
// FIXME: is this really the best solution? // FIXME: is this really the best solution?
// "rect" is the bounding box of the element, which includes the border and the padding (so not just the content) // "rect" is the bounding box of the element, which includes the border and the padding (so not just the content)
fn void! Ctx.push_rect(&ctx, Rect rect, Color color, bool do_border = false, bool do_padding = false, bool do_radius = false) fn void! Ctx.push_rect(&ctx, Rect rect, Color color, bool do_border = false, bool do_padding = false, bool do_radius = false)
@ -97,3 +136,18 @@ fn void! Ctx.push_string(&ctx, Rect bounds, String text)
} }
} }
} }
fn void! Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
ctx.cmd_queue.enqueue(&up)!;
}

@ -128,45 +128,6 @@ const uint MAX_ELEMS = 128;
const uint MAX_CMDS = 256; const uint MAX_CMDS = 256;
const uint ROOT_ID = 1; const uint ROOT_ID = 1;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
}
// command to draw a rect
// TODO: implement radius
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE
struct CmdUpdateAtlas {
char* raw_buffer;
short width, height;
}
// TODO:
// 1. Add atlases as a data type
// 2. Each atlas has an id
struct CmdSprite {
Rect rect;
Rect texture_rect;
}
// command structure
struct Cmd {
CmdType type;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
}
}
enum Layout { enum Layout {
ROW, ROW,
COLUMN, COLUMN,

@ -34,7 +34,6 @@ struct Glyph {
ushort u, v; ushort u, v;
ushort w, h; ushort w, h;
short adv, ox, oy; short adv, ox, oy;
short idx; // atlas index
} }
const uint FONT_CACHED = 512; const uint FONT_CACHED = 512;
@ -47,79 +46,23 @@ fault UgFontError {
RENDER_ERROR, RENDER_ERROR,
} }
fault UgAtlasError {
CANNOT_PLACE,
}
// black and white atlas
struct AtlasBW {
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
struct Font { struct Font {
schrift::Sft sft; schrift::Sft sft;
String path; String path;
Id id; // font id, same as atlas id
GlyphTable table; GlyphTable table;
float size; float size;
float ascender, descender, linegap; // Line Metrics float ascender, descender, linegap; // Line Metrics
AtlasBW[] atlas; Atlas atlas;
} bool should_update; // should send update_atlas command, resets at frame_end()
fn void! AtlasBW.new(&atlas, ushort width, ushort height)
{
atlas.width = width;
atlas.height = height;
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height);
} }
fn void AtlasBW.free(&atlas) fn void! Font.load(&font, String name, String path, uint height, float scale)
{
free(atlas.buffer);
}
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
fn Point! AtlasBW.place(&atlas, char[] pixels, ushort w, ushort h)
{
Point p;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return UgAtlasError.CANNOT_PLACE?;
}
}
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}
fn void! Font.load(&font, String path, uint height, float scale = 1)
{ {
font.table.new_init(capacity: FONT_CACHED); font.table.new_init(capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale; font.size = height*scale;
font.sft = schrift::Sft{ font.sft = schrift::Sft{
@ -142,27 +85,25 @@ fn void! Font.load(&font, String path, uint height, float scale = 1)
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
// like the letter 'A' // like the letter 'A'
font.atlas = mem::new_array(AtlasBW, 1);
ushort size = (ushort)font.size*256; ushort size = (ushort)font.size*256;
font.atlas[0].new(size, size)!; font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// preallocate the ASCII range // preallocate the ASCII range
// for (char c = ' '; c < '~'; c++) { // for (char c = ' '; c < '~'; c++) {
// font.get_glyph((Codepoint)c)!; // font.get_glyph((Codepoint)c)!;
// } // }
} }
fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) fn Glyph*! Font.get_glyph(&font, Codepoint code)
{ {
Glyph*! gp; Glyph*! gp;
gp = font.table.get_ref(code); gp = font.table.get_ref(code);
if (catch excuse = gp) { if (catch excuse = gp) {
if (excuse != SearchResult.MISSING) { if (excuse != SearchResult.MISSING) {
return excuse?; return excuse?;
} }
} else { } else {
if (is_new) { *is_new = false; }
return gp; return gp;
} }
@ -171,7 +112,7 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
schrift::SftGlyph gid; schrift::SftGlyph gid;
schrift::SftGMetrics gmtx; schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, code, &gid) < 0) { if (schrift::lookup(&font.sft, code, &gid) < 0) {
return UgFontError.MISSING_GLYPH?; return UgFontError.MISSING_GLYPH?;
} }
@ -199,8 +140,7 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d", //io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv); // glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
Point uv = font.atlas[0].place(pixels, glyph.w, glyph.h)!; Point uv = font.atlas.place(pixels, glyph.w, glyph.h)!;
glyph.idx = 0;
glyph.u = uv.x; glyph.u = uv.x;
glyph.v = uv.y; glyph.v = uv.y;
@ -208,14 +148,17 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
font.table.set(code, glyph); font.table.set(code, glyph);
if (is_new) { *is_new = true; } font.should_update = true;
return font.table.get_ref(code); return font.table.get_ref(code);
} }
fn void Font.free(&font) fn void Font.free(&font)
{ {
foreach (atlas: font.atlas) { font.atlas.free();
atlas.free();
}
schrift::freefont(font.sft.font); schrift::freefont(font.sft.font);
} }
fn void! Ctx.load_font(&ctx, String name, String path, uint height, float scale = 1.0)
{
return ctx.font.load(name, path, height, scale);
}

@ -67,6 +67,7 @@ fn void Ctx.free(&ctx)
(void)ctx.tree.free(); (void)ctx.tree.free();
(void)ctx.cache.free(); (void)ctx.cache.free();
(void)ctx.cmd_queue.free(); (void)ctx.cmd_queue.free();
(void)ctx.font.free();
} }
fn void! Ctx.frame_begin(&ctx) fn void! Ctx.frame_begin(&ctx)
@ -127,8 +128,14 @@ fn void! Ctx.frame_end(&ctx)
ctx.input.events = (InputEvents)0; ctx.input.events = (InputEvents)0;
ctx.input.events.force_update = f; ctx.input.events.force_update = f;
// draw mouse position // send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
ctx.font.should_update = false;
}
$if 1: $if 1:
// draw mouse position
Cmd cmd = { Cmd cmd = {
.type = CMD_RECT, .type = CMD_RECT,
.rect.rect = { .rect.rect = {

@ -18,7 +18,7 @@ fn Codepoint str_to_codepoint(char[] str, usz* off)
return cp; return cp;
} }
fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas) fn Rect! Ctx.get_text_bounds(&ctx, String text)
{ {
Rect text_bounds; Rect text_bounds;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
@ -34,9 +34,8 @@ fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas)
off += x; off += x;
bool n; bool n;
if (!ascii::is_cntrl((char)cp)) { if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp, &n)!; gp = ctx.font.get_glyph(cp)!;
line_len += gp.adv; line_len += gp.adv;
if (n) { *update_atlas = true; }
} else if (cp == '\n'){ } else if (cp == '\n'){
text_bounds.h += line_height + line_gap; text_bounds.h += line_height + line_gap;
line_len = 0; line_len = 0;
@ -67,10 +66,9 @@ fn void! Ctx.text_unbounded(&ctx, String label, String text)
short baseline = (short)ctx.font.ascender; short baseline = (short)ctx.font.ascender;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap; short line_gap = (short)ctx.font.linegap;
bool update_atlas;
// if the element is new or the parent was updated then redo layout // if the element is new or the parent was updated then redo layout
if (c_elem.flags.is_new || parent.flags.updated) { if (c_elem.flags.is_new || parent.flags.updated) {
Rect text_size = ctx.get_text_bounds(text, &update_atlas)!; Rect text_size = ctx.get_text_bounds(text)!;
// 2. Layout // 2. Layout
c_elem.bounds = ctx.position_element(parent, text_size, true); c_elem.bounds = ctx.position_element(parent, text_size, true);
@ -79,25 +77,5 @@ fn void! Ctx.text_unbounded(&ctx, String label, String text)
c_elem.text.str = text; c_elem.text.str = text;
} }
if (update_atlas) {
// FIXME: atlas here is hardcoded, look at the todo in ugui_data
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.raw_buffer = ctx.font.atlas[0].buffer,
.width = ctx.font.atlas[0].width,
.height = ctx.font.atlas[0].height,
},
};
ctx.cmd_queue.enqueue(&up)!;
}
Cmd bounds = {
.type = CMD_RECT,
.rect.rect = c_elem.bounds,
.rect.color = uint_to_rgba(0x000000ff),
};
ctx.cmd_queue.enqueue(&bounds)!;
ctx.push_string(c_elem.bounds, text)!; ctx.push_string(c_elem.bounds, text)!;
} }

Loading…
Cancel
Save