diff --git a/fibs-scripts/libs.ts b/fibs-scripts/libs.ts index c2f42ebd..59c94548 100644 --- a/fibs-scripts/libs.ts +++ b/fibs-scripts/libs.ts @@ -143,6 +143,11 @@ export function addLibs(b: Builder) { t.addSources(['ozzutil.cc', 'ozzutil.h']); t.addDependencies(['sokol', 'ozzanimation']); }); + b.addTarget('slugutil', 'lib', (t) => { + t.setDir('libs/slugutil'); + t.addSources(['slugutil.c', 'slugutil.h']); + t.addDependencies(['sokol', 'stb']); + }); b.addTarget('ilbm', 'lib', (t) => { t.setDir('libs/ilbm'); t.addSources(['ilbm.c', 'ilbm.h']); diff --git a/fibs-scripts/sapp.ts b/fibs-scripts/sapp.ts index 7bbcadb4..1374e7f3 100644 --- a/fibs-scripts/sapp.ts +++ b/fibs-scripts/sapp.ts @@ -128,6 +128,12 @@ export const samples: SampleOptions[] = [ deps: ['fileutil'], jobs: [copy('data', ['DroidSansJapanese.ttf', 'DroidSerif-Bold.ttf', 'DroidSerif-Italic.ttf', 'DroidSerif-Regular.ttf'])], }, + { + name: 'slug', + shd: true, + deps: ['imgui', 'fileutil', 'slugutil'], + jobs: [copy('data', ['Cairo.ttf', 'lucide.ttf', 'twemoji.ttf'])], + }, { name: 'modplay', deps: ['libmodplug'], jobs: [embed('data', 'mods.h', ['disco_feva_baby.s3m'])] }, { name: 'restart', shd: true, deps: ['libmodplug', 'fileutil', 'stb'], jobs: [copy('data', ['baboon.png', 'comsi.s3m'])] }, { diff --git a/fibs-scripts/webpage.ts b/fibs-scripts/webpage.ts index a2d17699..47e6c3db 100644 --- a/fibs-scripts/webpage.ts +++ b/fibs-scripts/webpage.ts @@ -98,6 +98,9 @@ const assets = [ "venus.iff", "waterfall.iff", "yacht.iff", + "Cairo.ttf", + "lucide.ttf", + "twemoji.ttf", ]; export function addWebPageCommand(c: Configurer) { diff --git a/libs/slugutil/slugutil.c b/libs/slugutil/slugutil.c new file mode 100644 index 00000000..1405d0a2 --- /dev/null +++ b/libs/slugutil/slugutil.c @@ -0,0 +1,676 @@ +//------------------------------------------------------------------------------ +// Slug utility functions. +// NOTE: memory management during font loading is *really* unoptimized. +// +// PS: should the font processing actually be moved into an offline tool? +//------------------------------------------------------------------------------ + +#include "slugutil.h" +#include +#include +#include "stb_ds.h" + +typedef struct { uint16_t x, y; } u16vec2_t; + +typedef struct { + vec4_t* curve_pixels; // managed by stb_ds + int curve_height; + u16vec2_t* band_pixels; // managed by stb_ds + int band_height; +} pack_textures_t; + +static bool parse_colr_v0(slug_font_t* font, const slug_range_t* data); +static bool parse_cpal(slug_font_t* font, const slug_range_t* data); +static void init_build_glyph(const stbtt_fontinfo* info, int glyph_index, float scale, slug_glyph_build_t* out); +static void build_bands(slug_glyph_build_t* glyph); +static void free_build_glyph(slug_glyph_build_t* glyph); +static pack_textures_t pack_textures(slug_glyph_build_t* glyphs, int num_glyphs); + +const slug_glyph_t* slug_get_glyph(const slug_font_t* font, uint32_t codepoint) { + int idx = stbtt_FindGlyphIndex(&font->info, codepoint); + if ((idx >= 0) && (idx < arrlen(font->glyphs))) { + return &font->glyphs[idx]; + } else { + return 0; + } +} + +static int colr_base_cmp(const void* a, const void* b) { + const slug_colr_base_t* pa = (slug_colr_base_t*)a; + const slug_colr_base_t* pb = (slug_colr_base_t*)b; + if (pa->glyph_id < pb->glyph_id) { + return -1; + } else if (pa->glyph_id > pb->glyph_id) { + return 1; + } else { + return 0; + } +} + +const slug_colr_base_t* slug_find_colr_base(const slug_font_t* font, uint32_t codepoint) { + int idx = stbtt_FindGlyphIndex(&font->info, codepoint); + if (idx <= 0) { + return 0; + } + size_t num = arrlenu(font->colr_bases); + if (num == 0) { + return 0; + } + const slug_colr_base_t key = { .glyph_id = (uint16_t)idx }; + return (slug_colr_base_t*)bsearch(&key, font->colr_bases, num, sizeof(slug_colr_base_t), colr_base_cmp); +} + +bool slug_load_font(slug_font_t* font, const slug_range_t* data) { + assert(font); + assert(data && data->ptr && data->size > 0); + assert(!font->valid); + *font = (slug_font_t){0}; + + if (!stbtt_InitFont(&font->info, data->ptr, 0)) { + slug_unload_font(font); + return false; + } + float em_scale = stbtt_ScaleForMappingEmToPixels(&font->info, 1.0f); + + // colored emoji-fonts... + if (!parse_colr_v0(font, data)) { + slug_unload_font(font); + return false; + } + if (!parse_cpal(font, data)) { + slug_unload_font(font); + return false; + } + + slug_glyph_build_t* build_glyphs = 0; + arrsetlen(build_glyphs, font->info.numGlyphs); + for (int i = 0; i < arrlen(build_glyphs); i++) { + init_build_glyph(&font->info, i, em_scale, &build_glyphs[i]); + build_bands(&build_glyphs[i]); + } + + pack_textures_t res = pack_textures(build_glyphs, (int)arrlen(build_glyphs)); + font->curve.height = res.curve_height; + font->band.height = res.band_height; + + font->curve.img = sg_make_image(&(sg_image_desc){ + .width = SLUG_TEX_WIDTH, + .height = font->curve.height, + .pixel_format = SG_PIXELFORMAT_RGBA32F, + .data.mip_levels[0] = { + .ptr = res.curve_pixels, + .size = arrlen(res.curve_pixels) * sizeof(vec4_t), + }, + }); + font->curve.tex_view = sg_make_view(&(sg_view_desc){ .texture.image = font->curve.img }); + + font->band.img = sg_make_image(&(sg_image_desc){ + .width = SLUG_TEX_WIDTH, + .height = font->band.height, + .pixel_format = SG_PIXELFORMAT_RG16UI, + .data.mip_levels[0] = { + .ptr = res.band_pixels, + .size = arrlen(res.band_pixels) * sizeof(u16vec2_t), + }, + }); + font->band.tex_view = sg_make_view(&(sg_view_desc){ .texture.image = font->band.img }); + + int num_glyphs = (int)arrlen(build_glyphs); + arrsetlen(font->glyphs, num_glyphs); + for (int i = 0; i < num_glyphs; i++) { + const slug_glyph_build_t* bg = &build_glyphs[i]; + font->glyphs[i] = (slug_glyph_t){ + .bbox = bg->bbox, + .advance = bg->advance, + .lsb = bg->lsb, + .max_band_x = arrlen(bg->vertical_bands) - 1, + .max_band_y = arrlen(bg->horizontal_bands) - 1, + .band_scale = bg->band_scale, + .band_offset = bg->band_offset, + .glyph_loc = { + [0] = bg->glyph_loc[0], + [1] = bg->glyph_loc[1], + } + }; + } + + + arrfree(res.curve_pixels); + arrfree(res.band_pixels); + for (int i = 0; i < arrlen(build_glyphs); i++) { + free_build_glyph(&build_glyphs[i]); + } + arrfree(build_glyphs); + font->valid = true; + return true; +} + +void slug_unload_font(slug_font_t* font) { + sg_destroy_image(font->curve.img); + sg_destroy_view(font->curve.tex_view); + sg_destroy_image(font->band.img); + sg_destroy_view(font->band.tex_view); + arrfree(font->glyphs); + arrfree(font->cpal_colors); + arrfree(font->colr_bases); + arrfree(font->colr_layers); + *font = (slug_font_t){0}; +} + + +static uint32_t make_tag(char a, char b, char c, char d) { + return (a<<24) | (b<<16) | (c<<8) | d; +} + +static uint16_t read_u16be(const slug_range_t* data, size_t offset) { + assert((offset + sizeof(uint16_t)) <= data->size); + const uint8_t* p = (uint8_t*)data->ptr; + uint32_t b0 = p[offset]; + uint32_t b1 = p[offset + 1]; + return (b0<<8) | b1; +} + +static uint32_t read_u32be(const slug_range_t* data, size_t offset) { + assert((offset + sizeof(uint32_t)) <= data->size); + const uint8_t* p = (uint8_t*)data->ptr; + uint32_t b0 = p[offset]; + uint32_t b1 = p[offset + 1]; + uint32_t b2 = p[offset + 2]; + uint32_t b3 = p[offset + 3]; + return (b0<<24) | (b1<<16) | (b2<<8) | b3; +} + +static int find_otf_table(const slug_range_t* data, uint32_t tag) { + if (data->size < 12) { + return -1; + } + uint16_t num_tables = read_u16be(data, 4); + for (uint16_t i = 0; i < num_tables; i++) { + size_t record_offset = 12 + i * 16; + if ((record_offset + 16) > data->size) { + break; + } + uint32_t record_tag = read_u32be(data, record_offset); + if (record_tag == tag) { + return (int)read_u32be(data, record_offset + 8); + } + } + return -1; +} + +static bool parse_colr_v0(slug_font_t* font, const slug_range_t* data) { + int table_offset = find_otf_table(data, make_tag('C', 'O', 'L', 'R')); + if (table_offset < 0) { + // not a but, COLR table is optional + return true; + } + uint16_t version = read_u16be(data, table_offset); + // dont support COLRv1 + if (version != 0) { + return false; + } + //Table header (14 bytes from table start): + // Offset +0: u16 version (must be 0 for COLRv0) + // Offset +2: u16 numBaseGlyphRecords + // Offset +4: u32 offsetBaseGlyphRecord (from table start) + // Offset +8: u32 offsetLayerRecord (from table start) + // Offset +12: u16 numLayerRecords + int num_base_glyphs = (int)read_u16be(data, table_offset + 2); + int offset_base = table_offset + (int)read_u32be(data, table_offset + 4); + int offset_layer = table_offset + (int)read_u32be(data, table_offset + 8); + int num_layers = (int)read_u16be(data, table_offset + 12); + arrsetlen(font->colr_bases, num_base_glyphs); + for (int i = 0; i < num_base_glyphs; i++) { + slug_colr_base_t* ptr = &font->colr_bases[i]; + int offset = offset_base + i * 6; // each record is 6 bytes + //[glyphID: u16, firstLayerIndex: u16, numLayers: u16] + ptr->glyph_id = read_u16be(data, offset); + ptr->first_layer = read_u16be(data, offset + 2); + ptr->num_layers = read_u16be(data, offset + 4); + ptr->_pad = 0; + } + qsort(font->colr_bases, arrlen(font->colr_bases), sizeof(slug_colr_base_t), colr_base_cmp); + arrsetlen(font->colr_layers, num_layers); + for (int i = 0; i < num_layers; i++) { + slug_colr_layer_t* ptr = &font->colr_layers[i]; + int offset = offset_layer + i * 4; // each record is 4 bytes + ptr->glyph_id = read_u16be(data, offset); + ptr->palette_index = read_u16be(data, offset + 2); + } + return true; +} + +static int mini(int a, int b) { + return a < b ? a : b; +} + +static int clampi(int val, int minval, int maxval) { + if (val < minval) return minval; + else if (val > maxval) return maxval; + else return val; +} + +static float minf(float a, float b) { + return a < b ? a : b; +} + +static float maxf(float a, float b) { + return a > b ? a : b; +} + +static bool parse_cpal(slug_font_t* font, const slug_range_t* data) { + int table_offset = find_otf_table(data, make_tag('C', 'P', 'A', 'L')); + if (table_offset < 0) { + // not a bug, CPAL is optional + return true; + } + // Table header (12 bytes from table start): + // Offset +0: u16 version + // Offset +2: u16 numPaletteEntries (number of colors per palette) + // Offset +4: u16 numPalettes (usually 1) + // Offset +6: u16 numColorRecords (total colors across all palettes) + // Offset +8: u32 offsetFirstColorRecord (from table start) + int num_entries = (int)read_u16be(data, table_offset + 2); + int num_color_records = (int)read_u16be(data, table_offset + 6); + int color_offset = table_offset + (int)read_u32be(data, table_offset + 8); + int count = mini(num_entries, num_color_records); + arrsetlen(font->cpal_colors, count); + for (int i = 0; i < count; i++) { + const uint8_t* src = (uint8_t*)data->ptr + color_offset + i * 4; + vec4_t* dst = &font->cpal_colors[i]; + dst->z = ((float)*src++) / 255.0f; + dst->y = ((float)*src++) / 255.0f; + dst->x = ((float)*src++) / 255.0f; + dst->w = ((float)*src) / 255.0f; + } + return true; +} + +static void free_build_glyph(slug_glyph_build_t* glyph) { + arrfree(glyph->curves); + arrfree(glyph->contours); + for (int i = 0; i < arrlen(glyph->horizontal_bands); i++) { + arrfree(glyph->horizontal_bands[i]); + } + arrfree(glyph->horizontal_bands); + for (int i = 0; i < arrlen(glyph->vertical_bands); i++) { + arrfree(glyph->vertical_bands[i]); + } + arrfree(glyph->vertical_bands); +} + +static void init_build_glyph(const stbtt_fontinfo* info, int glyph_index, float em_scale, slug_glyph_build_t* out) { + slug_glyph_build_t* glyph = out; + memset(glyph, 0, sizeof(slug_glyph_build_t)); + int adv, lsb_raw; + stbtt_GetGlyphHMetrics(info, glyph_index, &adv, &lsb_raw); + glyph->advance = (float)adv * em_scale; + glyph->lsb = (float)lsb_raw * em_scale; + + int ix0, iy0, ix1, iy1; + if (stbtt_GetGlyphBox(info, glyph_index, &ix0, &iy0, &ix1, &iy1) == 0) { + return; + } + glyph->bbox = (slug_bbox_t){ + .x0 = (float)ix0 * em_scale, + .y0 = (float)iy0 * em_scale, + .x1 = (float)ix1 * em_scale, + .y1 = (float)iy1 * em_scale, + }; + + stbtt_vertex* verts; + int nv = stbtt_GetGlyphShape(info, glyph_index, &verts); + if (nv <= 0) { + return; + } + bool in_contour = false; + int contour_start = 0; + vec2_t previous = vec2(0.0f, 0.0f); + for (int i = 0; i < nv; i++) { + stbtt_vertex* vert = &verts[i]; + switch (vert->type) { + case 1: + // vmove + { + if (in_contour) { + int count = (int)arrlen(glyph->curves) - contour_start; + if (count > 0) { + arrput(glyph->contours, ((slug_contour_range_t){ .start = contour_start, .count = count })); + } + } + previous = vec2((float)vert->x * em_scale, (float)vert->y * em_scale); + contour_start = (int)arrlen(glyph->curves); + in_contour = true; + } + break; + case 2: + // vline + { + vec2_t current = vec2((float)vert->x * em_scale, (float)vert->y * em_scale); + vec2_t mid = vm_mul(vm_add(previous, current), 0.5f); + arrput(glyph->curves, ((slug_curve_t){ .p = { previous, mid, current } })); + previous = current; + } + break; + case 3: + // vcurve + { + vec2_t current = vec2((float)vert->x * em_scale, (float)vert->y * em_scale); + vec2_t control = vec2((float)vert->cx * em_scale, (float)vert->cy * em_scale); + arrput(glyph->curves, ((slug_curve_t){ .p = { previous, control, current }})); + previous = current; + } + break; + case 4: + // vcubic — approximate with three quadratic Beziers + // Split cubic P0,C1,C2,P3 at t=1/3 and t=2/3 via de Casteljau, + // then approximate each sub-cubic as a quadratic with ctrl=(c1+c2)/2. + { + vec2_t p3 = vec2((float)vert->x * em_scale, (float)vert->y * em_scale); + vec2_t c1 = vec2((float)vert->cx * em_scale, (float)vert->cy * em_scale); + vec2_t c2 = vec2((float)vert->cx1 * em_scale, (float)vert->cy1 * em_scale); + vec2_t p0 = previous; + // de Casteljau split at t=1/3 + const float t = 1.0f / 3.0f; + vec2_t ab = vm_add(p0, vm_mul(vm_sub(c1, p0), t)); + vec2_t bc = vm_add(c1, vm_mul(vm_sub(c2, c1), t)); + vec2_t cd = vm_add(c2, vm_mul(vm_sub(p3, c2), t)); + vec2_t abc = vm_add(ab, vm_mul(vm_sub(bc, ab), t)); + vec2_t bcd = vm_add(bc, vm_mul(vm_sub(cd, bc), t)); + vec2_t e1 = vm_add(abc, vm_mul(vm_sub(bcd, abc), t)); // point on curve at t=1/3 + // Sub-cubic 1: p0, ab, abc, e1 → quadratic ctrl = (ab + abc) * 0.5 + vec2_t q1 = vm_mul(vm_add(ab, abc), 0.5f); + // de Casteljau split remaining cubic (e1, bcd, cd, p3) at t=0.5 (= t=2/3 of original) + vec2_t ab2 = vm_add(e1, vm_mul(vm_sub(bcd, e1), 0.5f)); + vec2_t bc2 = vm_add(bcd, vm_mul(vm_sub(cd, bcd), 0.5f)); + vec2_t cd2 = vm_add(cd, vm_mul(vm_sub(p3, cd), 0.5f)); + vec2_t abc2 = vm_add(ab2, vm_mul(vm_sub(bc2, ab2), 0.5f)); + vec2_t bcd2 = vm_add(bc2, vm_mul(vm_sub(cd2, bc2), 0.5f)); + vec2_t e2 = vm_add(abc2, vm_mul(vm_sub(bcd2, abc2), 0.5f)); // point on curve at t=2/3 + // Sub-cubic 2: e1, ab2, abc2, e2 → quadratic ctrl = (ab2 + abc2) * 0.5 + vec2_t q2 = vm_mul(vm_add(ab2, abc2), 0.5f); + vec2_t q3 = vm_mul(vm_add(bcd2, cd2), 0.5f); + arrput(glyph->curves, ((slug_curve_t){ .p = { p0, q1, e1 } })); + arrput(glyph->curves, ((slug_curve_t){ .p = { e1, q2, e2 } })); + arrput(glyph->curves, ((slug_curve_t){ .p = { e2, q3, p3 } })); + previous = p3; + } + break; + } + } + if (in_contour) { + int count = (int)arrlen(glyph->curves) - contour_start; + if (count > 0) { + arrput(glyph->contours, ((slug_contour_range_t){ .start = contour_start, .count = count })); + } + } + stbtt_FreeShape(info, verts); +} + +static int band_cmp(const void* a, const void* b) { + const slug_band_entry_t* pa = (slug_band_entry_t*)a; + const slug_band_entry_t* pb = (slug_band_entry_t*)b; + // NOTE: inverted sort-order is not a bug + if (pa->sort_key > pb->sort_key) { + return -1; + } else if (pa->sort_key < pb->sort_key) { + return 1; + } else { + return 0; + } +} + +static void build_bands(slug_glyph_build_t* glyph) { + int num_curves = (int)arrlen(glyph->curves); + if (0 == num_curves) { + return; + } + + float band_width = maxf(glyph->bbox.x1 - glyph->bbox.x0, 1.0f); + float band_height = maxf(glyph->bbox.y1 - glyph->bbox.y0, 1.0f); + int number_of_bands_height = clampi(num_curves, 1, SLUG_MAX_BANDS); + int number_of_bands_width = clampi(num_curves, 1, SLUG_MAX_BANDS); + + arrsetlen(glyph->horizontal_bands, number_of_bands_height); + arrsetlen(glyph->vertical_bands, number_of_bands_width); + for (int i = 0; i < number_of_bands_height; i++) { + glyph->horizontal_bands[i] = 0; + } + for (int i = 0; i < number_of_bands_width; i++) { + glyph->vertical_bands[i] = 0; + } + glyph->band_scale = vec2((float)number_of_bands_width / band_width, (float)number_of_bands_height / band_height); + glyph->band_offset = vec2(-glyph->bbox.x0 * glyph->band_scale.x, -glyph->bbox.y0 * glyph->band_scale.y); + + float horizontal_band_height = band_height / (float)number_of_bands_height; + float vertical_band_width = band_width / (float)number_of_bands_width; + float horizontal_pad = horizontal_band_height * 0.5f; + float vertical_pad = vertical_band_width * 0.5f; + + int band_first, band_last; + for (int curve_index = 0; curve_index < arrlen(glyph->curves); curve_index++) { + slug_curve_t* curve = &glyph->curves[curve_index]; + float curve_y_min = minf(minf(curve->p[0].y, curve->p[1].y), curve->p[2].y); + float curve_y_max = maxf(maxf(curve->p[0].y, curve->p[1].y), curve->p[2].y); + float curve_x_min = minf(minf(curve->p[0].x, curve->p[1].x), curve->p[2].x); + float curve_x_max = maxf(maxf(curve->p[0].x, curve->p[1].x), curve->p[2].x); + + band_first = clampi( + (int)floorf((curve_y_min - horizontal_pad - glyph->bbox.y0) / horizontal_band_height), + 0, + number_of_bands_height - 1); + band_last = clampi( + (int)floorf((curve_y_max + horizontal_pad - glyph->bbox.y0) / horizontal_band_height), + 0, + number_of_bands_height - 1); + for (int i = band_first; i <= band_last; i++) { + arrput(glyph->horizontal_bands[i], ((slug_band_entry_t){ .curve_index = curve_index, .sort_key = curve_x_max })); + } + + band_first = clampi( + (int)floorf((curve_x_min - vertical_pad - glyph->bbox.x0) / vertical_band_width), + 0, + number_of_bands_width - 1); + band_last = clampi( + (int)floorf((curve_x_max + vertical_pad - glyph->bbox.x0) / vertical_band_width), + 0, + number_of_bands_width - 1); + for (int i = band_first; i <= band_last; i++) { + arrput(glyph->vertical_bands[i], ((slug_band_entry_t){ .curve_index = curve_index, .sort_key = curve_y_max })); + } + } + int num_horizontal_bands = (int)arrlen(glyph->horizontal_bands); + for (int i = 0; i < num_horizontal_bands; i++) { + if (glyph->horizontal_bands[i]) { + size_t num_band_entries = arrlen(glyph->horizontal_bands[i]); + qsort(glyph->horizontal_bands[i], num_band_entries, sizeof(slug_band_entry_t), band_cmp); + } + } + int num_vertical_bands = (int)arrlen(glyph->vertical_bands); + for (int i = 0; i < num_vertical_bands; i++) { + if (glyph->vertical_bands[i]) { + size_t num_band_entries = arrlen(glyph->vertical_bands[i]); + qsort(glyph->vertical_bands[i], num_band_entries, sizeof(slug_band_entry_t), band_cmp); + } + } +} + +static void pad_to_row_curve_pixels(pack_textures_t* res, int needed) { + int curlen = (int)arrlen(res->curve_pixels); + int column = curlen % SLUG_TEX_WIDTH; + if ((column + needed) > SLUG_TEX_WIDTH) { + arrsetlen(res->curve_pixels, curlen + SLUG_TEX_WIDTH - column); + int newlen = (int)arrlen(res->curve_pixels); + for (int i = curlen; i < newlen; i++) { + res->curve_pixels[i] = (vec4_t){0}; + } + } +} + +static void finalize_curve_pixels(pack_textures_t* res) { + int cur_size = (int)arrlen(res->curve_pixels); + int new_size = 0; + if (cur_size == 0) { + arrsetlen(res->curve_pixels, SLUG_TEX_WIDTH); + new_size = (int)arrlen(res->curve_pixels); + res->curve_height = 1; + } else { + res->curve_height = ((int)arrlen(res->curve_pixels) + SLUG_TEX_WIDTH - 1) / SLUG_TEX_WIDTH; + arrsetlen(res->curve_pixels, res->curve_height * SLUG_TEX_WIDTH); + new_size = (int)arrlen(res->curve_pixels); + } + for (int i = cur_size; i < new_size; i++) { + res->curve_pixels[i] = (vec4_t){0}; + } +} + +static void pad_to_row_band_pixels(pack_textures_t* res, int needed) { + int curlen = (int)arrlen(res->band_pixels); + int column = curlen % SLUG_TEX_WIDTH; + if ((column + needed) > SLUG_TEX_WIDTH) { + arrsetlen(res->band_pixels, curlen + SLUG_TEX_WIDTH - column); + int newlen = (int)arrlen(res->band_pixels); + for (int i = curlen; i < newlen; i++) { + res->band_pixels[i] = (u16vec2_t){0}; + } + } +} + +static void finalize_band_pixels(pack_textures_t* res) { + int cur_size = (int)arrlen(res->band_pixels); + int new_size = 0; + if (cur_size == 0) { + arrsetlen(res->band_pixels, SLUG_TEX_WIDTH); + new_size = (int)arrlen(res->band_pixels); + res->band_height = 1; + } else { + res->band_height = ((int)arrlen(res->band_pixels) + SLUG_TEX_WIDTH - 1) / SLUG_TEX_WIDTH; + arrsetlen(res->band_pixels, res->band_height * SLUG_TEX_WIDTH); + new_size = (int)arrlen(res->band_pixels); + } + for (int i = cur_size; i < new_size; i++) { + res->band_pixels[i] = (u16vec2_t){0}; + } +} + +static void write_band_set(slug_band_entry_t** bands, slug_curve_t* curves, u16vec2_t* pixels, int glyph_start, int header_offset, int* write_offset) { + // Write headers: each band stores (count, data_offset) where data_offset + // is relative to glyph_start, matching how the shader indexes into the texture. + int data_offset = *write_offset; + for (int band_index = 0; band_index < arrlen(bands); band_index++) { + slug_band_entry_t* band = bands[band_index]; + u16vec2_t pixel = { (uint16_t)arrlen(band), (uint16_t)data_offset }; + pixels[glyph_start + header_offset + band_index] = pixel; + data_offset += (int)arrlen(band); + } + // Write curve references at the offsets declared above + data_offset = *write_offset; + for (int band_index = 0; band_index < arrlen(bands); band_index++) { + slug_band_entry_t* band = bands[band_index]; + for (int entry_index = 0; entry_index < arrlen(band); entry_index++) { + slug_band_entry_t* entry = &band[entry_index]; + slug_curve_t* curve = &curves[entry->curve_index]; + u16vec2_t pixel = { curve->texture[0], curve->texture[1] }; + pixels[glyph_start + data_offset] = pixel; + data_offset += 1; + } + } + *write_offset = data_offset; +} + +static pack_textures_t pack_textures(slug_glyph_build_t* glyphs, int num_glyphs) { + pack_textures_t res = {0}; + + // Count how many texels we'll need so we can reserve upfront. + // This avoids repeated realloc+copy as the dynamic arrays grow. + int estimated_curve_size = 0; + int estimated_band_size = 0; + for (int glyph_index = 0; glyph_index < num_glyphs; glyph_index++) { + slug_glyph_build_t* glyph = &glyphs[glyph_index]; + // Each contour needs (count + 1) curve texels: + // count = number of bezier curves in this contour + // +1 for the shared endpoint texel + for (int contour_index = 0; contour_index < arrlen(glyph->contours); contour_index++) { + slug_contour_range_t* contour = &glyph->contours[contour_index]; + estimated_curve_size += contour->count + 1; + } + int num_h = (int)arrlen(glyph->horizontal_bands); + int num_v = (int)arrlen(glyph->vertical_bands); + if ((num_h == 0) && (num_v == 0)) { + continue; + } + // Band data per glyph: + // num_h + num_v = one header texel per band + // + sum of all curve references in each band + int band_size = num_h + num_v; + for (int i = 0; i < arrlen(glyph->horizontal_bands); i++) { + band_size += (int)arrlen(glyph->horizontal_bands[i]); + } + for (int i = 0; i < arrlen(glyph->vertical_bands); i++) { + band_size += (int)arrlen(glyph->vertical_bands[i]); + } + estimated_band_size += band_size; + } + arrsetcap(res.curve_pixels, (estimated_curve_size * 6) / 5); + arrsetcap(res.band_pixels, (estimated_band_size * 6) / 5); + + for (int glyph_index = 0; glyph_index < num_glyphs; glyph_index++) { + slug_glyph_build_t* glyph = &glyphs[glyph_index]; + // Pack curves into texture, recording each curve's texture coordinates + for (int contour_index = 0; contour_index < arrlen(glyph->contours); contour_index++) { + slug_contour_range_t* contour = &glyph->contours[contour_index]; + int entries_needed = contour->count + 1; + pad_to_row_curve_pixels(&res, entries_needed); + for (int i = 0; i < contour->count; i++) { + slug_curve_t* curve = &glyph->curves[contour->start + i]; + int pixel_index = (int)arrlen(res.curve_pixels); + arrput(res.curve_pixels, vec4(curve->p[0].x, curve->p[0].y, curve->p[1].x, curve->p[1].y)); + curve->texture[0] = (uint16_t)(pixel_index % SLUG_TEX_WIDTH); + curve->texture[1] = (uint16_t)(pixel_index / SLUG_TEX_WIDTH); + } + slug_curve_t* last_curve = &glyph->curves[contour->start + contour->count - 1]; + arrput(res.curve_pixels, vec4(last_curve->p[2].x, last_curve->p[2].y, 0.0f, 0.0f)); + } + + // Pack band lookup tables into texture, referencing the curve coords set above + int num_h_bands = (int)arrlen(glyph->horizontal_bands); + int num_v_bands = (int)arrlen(glyph->vertical_bands); + if ((num_h_bands == 0) && (num_v_bands == 0)) { + continue; + } + int header_size = num_h_bands + num_v_bands; + pad_to_row_band_pixels(&res, header_size); + + int glyph_start = (int)arrlen(res.band_pixels); + glyph->glyph_loc[0] = (int32_t)glyph_start % SLUG_TEX_WIDTH; + glyph->glyph_loc[1] = (int32_t)glyph_start / SLUG_TEX_WIDTH; + + int total_entries = header_size; + for (int i = 0; i < arrlen(glyph->horizontal_bands); i++) { + total_entries += (int)arrlen(glyph->horizontal_bands[i]); + } + for (int i = 0; i < arrlen(glyph->vertical_bands); i++) { + total_entries += (int)arrlen(glyph->vertical_bands[i]); + } + arrsetlen(res.band_pixels, glyph_start + total_entries); + + int write_offset = header_size; + write_band_set( + glyph->horizontal_bands, + glyph->curves, + res.band_pixels, + glyph_start, + 0, + &write_offset); + write_band_set( + glyph->vertical_bands, + glyph->curves, + res.band_pixels, + glyph_start, + num_h_bands, + &write_offset); + } + finalize_curve_pixels(&res); + finalize_band_pixels(&res); + return res; +} diff --git a/libs/slugutil/slugutil.h b/libs/slugutil/slugutil.h new file mode 100644 index 00000000..88ab5333 --- /dev/null +++ b/libs/slugutil/slugutil.h @@ -0,0 +1,92 @@ +#include "sokol_gfx.h" +#include "stb_truetype.h" +#include +#define VECMATH_GENERICS +#include "../vecmath/vecmath.h" + +#define SLUG_TEX_WIDTH (4096) +#define SLUG_MAX_BANDS (16) + +typedef struct { + vec2_t p[3]; + uint16_t texture[2]; +} slug_curve_t; + +typedef struct { + int start; + int count; +} slug_contour_range_t; + +typedef struct { + int curve_index; + float sort_key; +} slug_band_entry_t; + +typedef struct { + float x0, y0, x1, y1; +} slug_bbox_t; + +typedef struct { + slug_curve_t* curves; // managed via stb_ds + slug_contour_range_t* contours; // managed via stb_ds + slug_bbox_t bbox; + float advance; + float lsb; + slug_band_entry_t** horizontal_bands; // managed via stb_ds + slug_band_entry_t** vertical_bands; // managed via stb_ds + vec2_t band_scale; + vec2_t band_offset; + int32_t glyph_loc[2]; +} slug_glyph_build_t; + +typedef struct { + const void* ptr; + size_t size; +} slug_range_t; + +typedef struct { + slug_bbox_t bbox; + float advance; + float lsb; + float max_band_x; + float max_band_y; + vec2_t band_scale; + vec2_t band_offset; + int glyph_loc[2]; +} slug_glyph_t; + +typedef struct { + uint16_t glyph_id; + uint16_t palette_index; +} slug_colr_layer_t; + +typedef struct { + uint16_t glyph_id; // this also serves as hashmap key + uint16_t first_layer; + uint16_t num_layers; + uint16_t _pad; +} slug_colr_base_t; + +typedef struct { + bool valid; + slug_glyph_t* glyphs; // managed via stb_ds + stbtt_fontinfo info; + struct { + sg_image img; + sg_view tex_view; + int height; + } curve; + struct { + sg_image img; + sg_view tex_view; + int height; + } band; + vec4_t* cpal_colors; // managed via stb_ds + slug_colr_base_t* colr_bases; // managed via stb_ds + slug_colr_layer_t* colr_layers; // managed via stb_ds; +} slug_font_t; + +bool slug_load_font(slug_font_t* font, const slug_range_t* data); +void slug_unload_font(slug_font_t* font); +const slug_glyph_t* slug_get_glyph(const slug_font_t* font, uint32_t cp); +const slug_colr_base_t* slug_find_colr_base(const slug_font_t* font, uint32_t cp); diff --git a/sapp/data/Cairo.ttf b/sapp/data/Cairo.ttf new file mode 100644 index 00000000..5c44b4f1 Binary files /dev/null and b/sapp/data/Cairo.ttf differ diff --git a/sapp/data/lucide.ttf b/sapp/data/lucide.ttf new file mode 100644 index 00000000..2ebd6a52 Binary files /dev/null and b/sapp/data/lucide.ttf differ diff --git a/sapp/data/twemoji.ttf b/sapp/data/twemoji.ttf new file mode 100644 index 00000000..ea1dd90b Binary files /dev/null and b/sapp/data/twemoji.ttf differ diff --git a/sapp/imgui-usercallback-sapp.c b/sapp/imgui-usercallback-sapp.c index b26d4401..7ec2f90c 100644 --- a/sapp/imgui-usercallback-sapp.c +++ b/sapp/imgui-usercallback-sapp.c @@ -285,13 +285,13 @@ static void frame(void) { if (igBegin("Dear ImGui", 0, 0)) { if (igBeginChild("sokol-gfx", (ImVec2){360, 360}, true, ImGuiWindowFlags_None)) { ImDrawList* dl = igGetWindowDrawList(); - ImDrawList_AddCallback(dl, draw_scene_1, 0); + ImDrawList_AddCallback(dl, draw_scene_1); } igEndChild(); igSameLineEx(0, 10); if (igBeginChild("sokol-gl", (ImVec2){360, 360}, true, ImGuiWindowFlags_None)) { ImDrawList* dl = igGetWindowDrawList(); - ImDrawList_AddCallback(dl, draw_scene_2, 0); + ImDrawList_AddCallback(dl, draw_scene_2); } igEndChild(); } diff --git a/sapp/slug-sapp.c b/sapp/slug-sapp.c new file mode 100644 index 00000000..852b02ce --- /dev/null +++ b/sapp/slug-sapp.c @@ -0,0 +1,591 @@ +//------------------------------------------------------------------------------ +// slug-sapp.c +// +// Demonstrates Eric Lyengel's Slug text rendering algorithm. +// Ported from sokol-slug-odin: https://tangled.org/dosha.dev/sokol-slug-odin/ +// +// Also see: https://terathon.com/blog/decade-slug.html +// +// Main differences from above demo: +// - 4x reduced band texture size (RGBA32UI => RG16UI) +// - batched glyph rendering via hardware-instancing instead of one draw call +// per glyph where each glyph is a triangle-strip, with the corner vertices +// synthesized in the vertex shader +// - the pixel size multiplier isn't baked into the preprocessed +// Slug font data +// - add `@image_sample_type` and `@sampler_type` annotations to +// shader to silence validation layer warnings (and allow the sample to +// work with the WebGPU backend) +// +// Knows issues: +// - currently no kerning +// - TTF-to-Slug data preprocessing should be moved into an offline tool +// - more optimizations would be possible when switching to storage buffers +// (both for the curve+band data and the glyph 'vertices') +// - shader seems to be the Slug 'v1' shader, not the most recent one which +// has improvements in the vertex shader(?) +// - in general, also check against the BGFX slug sample, this does a couple +// of things differently: https://github.com/bkaradzic/bgfx/tree/master/examples/51-gpufont +//------------------------------------------------------------------------------ +#include "sokol_app.h" +#include "sokol_gfx.h" +#include "sokol_fetch.h" +#include "sokol_log.h" +#include "sokol_glue.h" +#include "cimgui.h" +#define SOKOL_IMGUI_IMPL +#include "sokol_imgui.h" +#define SOKOL_GFX_IMGUI_IMPL +#include "sokol_gfx_imgui.h" +#define SOKOL_APP_IMGUI_IMPL +#include "sokol_app_imgui.h" +#include "util/fileutil.h" +#include "slugutil/slugutil.h" +#define STB_TRUETYPE_IMPLEMENTATION +#include "stb_truetype.h" +#define STB_DS_IMPLEMENTATION +#include "stb_ds.h" +#include "slug-sapp.glsl.h" + +#define MAX_FONTS (3) +#define MAX_TTF_FILE_SIZE (2 * 1024 * 1024) +#define MAX_DRAWN_GLYPHS (16 * 1024) +#define MAX_DRAW_COMMANDS (128) +#define TOTAL_LINES (6) +#define FONT_SIZE (48.0f) +#define MIN_ZOOM (0.1f) +#define MAX_ZOOM (50.0f) + +uint32_t line[6][128]; + +static struct { + sg_pass_action pass_action; + sg_buffer buf; + sg_pipeline pip; + sg_sampler smp; + struct { + float zoom; + float pan_x; + float pan_y; + bool dragging; + } inp; + struct { + slug_font_t cairo; + slug_font_t lucide; + slug_font_t twemoji; + } fonts; + struct { + int start_glyph_vertex; + int cur_glyph_vertex; + int cur_draw_command; + const slug_font_t* cur_font; + } draw; + float font_size; +} state; + +// per-glyph data in glyph buffer, expanded 4x via hardware-instancing +typedef struct { + vec4_t draw_rect; + vec4_t glyph_bbox; + vec4_t band_transform; + int16_t glyph_params[4]; + uint32_t color; +} glyph_vertex_t; + +typedef struct { + int base_instance; + int num_instances; + sg_view curve_tex_view; + sg_view band_tex_view; +} draw_command_t; + +uint8_t file_buffers[MAX_FONTS][MAX_TTF_FILE_SIZE]; + +glyph_vertex_t glyph_vertices[MAX_DRAWN_GLYPHS]; +draw_command_t draw_commands[MAX_DRAW_COMMANDS]; + +static float measure_line(const slug_font_t* font, const uint32_t* text); +static void begin_push_glyphs(void); +static void push_centered_line(const slug_font_t* font, const uint32_t* text, int line_nr); +static void push_centered_line_emoji(const slug_font_t* font, const uint32_t* text, int line_nr); +static void push_line(const slug_font_t* font, const uint32_t* text, float x, float y); +static void push_line_emoji(const slug_font_t* font, const uint32_t* text, float x, float y); +static void push_emoji(const slug_font_t* font, const uint32_t codepoint, float x, float y); +static void push_glyph(const slug_font_t* font, const slug_glyph_t* glyph, float x, float y, vec4_t color); +static void push_draw_command(void); +static void end_push_glyphs(void); +static void cairo_fetch_callback(const sfetch_response_t* response); +static void lucide_fetch_callback(const sfetch_response_t* response); +static void twemoji_fetch_callback(const sfetch_response_t* response); +static void draw_ui(void); + +static void init(void) { + sg_setup(&(sg_desc){ + .environment = sglue_environment(), + .logger.func = slog_func, + }); + sfetch_setup(&(sfetch_desc_t){ + .num_channels = 1, + .num_lanes = 3, + .max_requests = 3, + .logger.func = slog_func, + }); + sgimgui_setup(&(sgimgui_desc_t){0}); + sappimgui_setup(); + simgui_setup(&(simgui_desc_t){ .logger.func = slog_func }); + + state.pass_action = (sg_pass_action){ + .colors[0] = { .load_action = SG_LOADACTION_CLEAR, .clear_value = { 0.1f, 0.1f, 0.1f, 1.0f } }, + }; + state.inp.zoom = 1.0f; + state.font_size = FONT_SIZE; + + // populate unicode lines, first the latin characters + for (uint32_t i = 0; i < 32; i++) { + line[0][i] = 0x40 + i; + if (i != 31) { + line[1][i] = 0x60 + i; + } + line[2][i] = 0x20 + i; + } + // tha arabic alphabet + for (uint32_t i = 0, cp = 0x0627; cp <= 0x064A; cp++) { + if ((cp < 0x063B) || (cp > 0x063F)) { + line[3][i++] = cp; + } + } + // icons from lucide font, note that the unicode assignment + // of icons is very messy, it has gaps and duplicates. The + // range picked here is somewhat 'orderly'. + for (uint32_t i = 0, cp = 0xE29A; cp <= 0xE2BA; cp++) { + line[4][i++] = cp; + } + // emojis + line[5][0] = 0x1F600; // grinning face + line[5][1] = 0x1F60D; // heart eyes + line[5][2] = 0x1F60E; // sunglasses + line[5][3] = 0x1F525; // fire + line[5][4] = 0x1F44D; // thumbs up + line[5][5] = 0x1F389; // party popper + line[5][6] = 0x1F680; // rocket + line[5][7] = 0x2764; // red heart + line[5][8] = 0x1F308; // rainbow + line[5][9] = 0x1F31F; // glowing star + line[5][10] = 0x1F3B5; // musical note + line[5][11] = 0x1F40D; // snake + line[5][12] = 0x1F436; // dog face + line[5][13] = 0x1F431; // cat face + line[5][14] = 0x1F34E; // red apple + line[5][15] = 0x1F370; // shortcake + + // A stream-update buffer which holds one glyph_vertex_t per glyph, this + // is expanded 4x via hardware instancing with the 4 corner vertex positions + // synthesized in the vertex shader. + // Another option (probably more efficient) might be to place this data + // in a storage buffer. + state.buf = sg_make_buffer(&(sg_buffer_desc){ + .usage.stream_update = true, + .size = MAX_DRAWN_GLYPHS * sizeof(glyph_vertex_t), + .label = "slug-glyph-buffer", + }); + + // the pipeline is configured with a single instance-stepped buffer which + // provides the per-glyph data, also note that rendering is non-indexed + // and each glyph is rendered as a 4-vertex triangle-strip + state.pip = sg_make_pipeline(&(sg_pipeline_desc){ + .shader = sg_make_shader(slug_shader_desc(sg_query_backend())), + .layout = { + .buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE, + .attrs = { + [ATTR_slug_draw_rect] = { .format = SG_VERTEXFORMAT_FLOAT4, .buffer_index = 0 }, + [ATTR_slug_glyph_bbox] = { .format = SG_VERTEXFORMAT_FLOAT4, .buffer_index = 0 }, + [ATTR_slug_in_band_transform] = { .format = SG_VERTEXFORMAT_FLOAT4, .buffer_index = 0 }, + [ATTR_slug_in_glyph_params] = { .format = SG_VERTEXFORMAT_SHORT4, .buffer_index = 0 }, + [ATTR_slug_in_text_color] = { .format = SG_VERTEXFORMAT_UBYTE4N, .buffer_index = 0 }, + }, + }, + .index_type = SG_INDEXTYPE_NONE, + .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, + .colors[0] = { + .blend = { + .enabled = true, + .src_factor_rgb = SG_BLENDFACTOR_ONE, + .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + .src_factor_alpha = SG_BLENDFACTOR_ONE, + .dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + } + }, + .label = "slug-pipeline" + }); + state.smp = sg_make_sampler(&(sg_sampler_desc){ + .min_filter = SG_FILTER_NEAREST, + .mag_filter = SG_FILTER_NEAREST, + .wrap_u = SG_WRAP_CLAMP_TO_EDGE, + .wrap_v = SG_WRAP_CLAMP_TO_EDGE, + .label = "slug-sampler", + }); + + // start loading fonts + char buf[512]; + sfetch_send(&(sfetch_request_t){ + .path = fileutil_get_path("Cairo.ttf", buf, sizeof(buf)), + .buffer = { .ptr = file_buffers[0], .size = sizeof(file_buffers[0]) }, + .callback = cairo_fetch_callback, + }); + sfetch_send(&(sfetch_request_t){ + .path = fileutil_get_path("lucide.ttf", buf, sizeof(buf)), + .buffer = { .ptr = file_buffers[1], .size = sizeof(file_buffers[1]) }, + .callback = lucide_fetch_callback, + }); + sfetch_send(&(sfetch_request_t){ + .path = fileutil_get_path("twemoji.ttf", buf, sizeof(buf)), + .buffer = { .ptr = file_buffers[2], .size = sizeof(file_buffers[2]) }, + .callback = twemoji_fetch_callback, + }); +} + +static void frame(void) { + sfetch_dowork(); + draw_ui(); + + float sx = 2.0f / ((sapp_widthf() / state.inp.zoom)); + float sy = 2.0f / ((sapp_heightf() / state.inp.zoom)); + float tx = -1.0f - state.inp.pan_x * sx; + float ty = -1.0f - state.inp.pan_y * sy; + const vs_params_t vs_params = { + .mvp = (mat44_t){ + .x = vec4(sx, 0.0f, 0.0f, 0.0f), + .y = vec4(0.0f, sy, 0.0f, 0.0f), + .z = vec4(0.0f, 0.0f, -1.0f, 0.0f), + .w = vec4(tx, ty, 0.0f, 1.0f), + }, + }; + + // record text into glyph buffer and draw commands + bool any_valid = state.fonts.cairo.valid || state.fonts.lucide.valid || state.fonts.twemoji.valid; + if (any_valid) { + begin_push_glyphs(); + if (state.fonts.cairo.valid) { + for (int i = 0; i < 4; i++) { + push_centered_line(&state.fonts.cairo, line[i], i); + } + } + if (state.fonts.lucide.valid) { + push_centered_line(&state.fonts.lucide, line[4], 4); + } + if (state.fonts.twemoji.valid) { + push_centered_line_emoji(&state.fonts.twemoji, line[5], 5); + } + end_push_glyphs(); + } + + // render the recorded draw commands + sg_begin_pass(&(sg_pass){ .action = state.pass_action, .swapchain = sglue_swapchain() }); + if (any_valid) { + sg_apply_pipeline(state.pip); + sg_apply_uniforms(UB_vs_params, &SG_RANGE(vs_params)); + for (int i = 0; i < state.draw.cur_draw_command; i++) { + const draw_command_t* cmd = &draw_commands[i]; + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = state.buf, + .vertex_buffer_offsets[0] = cmd->base_instance * sizeof(glyph_vertex_t), + .views = { + [VIEW_band_tex] = cmd->band_tex_view, + [VIEW_curve_tex] = cmd->curve_tex_view, + }, + .samplers[SMP_point_sampler] = state.smp, + }); + sg_draw(0, 6, cmd->num_instances); + } + } + simgui_render(); + sg_end_pass(); + sg_commit(); +} + +static void input(const sapp_event* ev) { + sappimgui_track_event(ev); + if (simgui_handle_event(ev)) { + return; + } + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_SCROLL: + { + float h = sapp_heightf(); + float mouse_world_x = state.inp.pan_x + ev->mouse_x / state.inp.zoom; + float mouse_world_y = state.inp.pan_y + (h - ev->mouse_y) / state.inp.zoom; + state.inp.zoom *= 1.0 + ev->scroll_y * 0.1f; + if (state.inp.zoom < MIN_ZOOM) { + state.inp.zoom = MIN_ZOOM; + } else if (state.inp.zoom > MAX_ZOOM) { + state.inp.zoom = MAX_ZOOM; + } + // Adjust pan so the world point under the mouse stays fixed + state.inp.pan_x = mouse_world_x - ev->mouse_x / state.inp.zoom; + state.inp.pan_y = mouse_world_y - (h - ev->mouse_y) / state.inp.zoom; + } + break; + case SAPP_EVENTTYPE_MOUSE_DOWN: + if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + state.inp.dragging = true; + } + break; + case SAPP_EVENTTYPE_MOUSE_UP: + if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + state.inp.dragging = false; + } + break; + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_UNFOCUSED: + case SAPP_EVENTTYPE_SUSPENDED: + case SAPP_EVENTTYPE_ICONIFIED: + state.inp.dragging = false; + break; + case SAPP_EVENTTYPE_MOUSE_MOVE: + if (state.inp.dragging) { + state.inp.pan_x -= ev->mouse_dx / state.inp.zoom; + state.inp.pan_y += ev->mouse_dy / state.inp.zoom; + } + break; + default: break; + } +} + +static void cleanup(void) { + slug_unload_font(&state.fonts.cairo); + slug_unload_font(&state.fonts.lucide); + slug_unload_font(&state.fonts.twemoji); + sfetch_shutdown(); + sappimgui_shutdown(); + sgimgui_shutdown(); + simgui_shutdown(); + sg_shutdown(); +} + +static void draw_ui(void) { + sappimgui_track_frame(); + simgui_new_frame(&(simgui_frame_desc_t){ + .width = sapp_width(), + .height = sapp_height(), + .delta_time = sapp_frame_duration(), + .dpi_scale = sapp_dpi_scale(), + }); + if (igBeginMainMenuBar()) { + sgimgui_draw_menu("sokol-gfx"); + sappimgui_draw_menu("sokol-app"); + igEndMainMenuBar(); + } + sgimgui_draw(); + sappimgui_draw(); + igSetNextWindowPos((ImVec2){ 30, 50 }, ImGuiCond_Once); + igSetNextWindowBgAlpha(0.75f); + if (igBegin("Controls", 0, ImGuiWindowFlags_NoDecoration|ImGuiWindowFlags_AlwaysAutoResize)) { + igText("Left mouse button and move mouse to pan."); + igText("Mouse wheel to zoom."); + igSeparator(); + igSliderFloat("Font Size", &state.font_size, 5.0f, 256.0f); + } + igEnd(); +} + +static void cairo_fetch_callback(const sfetch_response_t* response) { + if (response->fetched) { + slug_load_font(&state.fonts.cairo, &(slug_range_t){ + .ptr = response->data.ptr, + .size = response->data.size, + }); + } +} + +static void lucide_fetch_callback(const sfetch_response_t* response) { + if (response->fetched) { + slug_load_font(&state.fonts.lucide, &(slug_range_t){ + .ptr = response->data.ptr, + .size = response->data.size, + }); + } +} + +static void twemoji_fetch_callback(const sfetch_response_t* response) { + if (response->fetched) { + slug_load_font(&state.fonts.twemoji, &(slug_range_t){ + .ptr = response->data.ptr, + .size = response->data.size, + }); + } +} + +static float measure_line(const slug_font_t* font, const uint32_t* text) { + float total = 0.0f; + uint32_t ucp; + while ((ucp = *text++) != 0) { + const slug_glyph_t* glyph = slug_get_glyph(font, ucp); + if (glyph) { + total += glyph->advance * state.font_size; + } + } + return total; +} + +static void begin_push_glyphs(void) { + state.draw.start_glyph_vertex = 0; + state.draw.cur_glyph_vertex = 0; + state.draw.cur_draw_command = 0; + state.draw.cur_font = 0; +} + +static void end_push_glyphs(void) { + // push final draw command + push_draw_command(); + // update the glyph instance buffer + sg_update_buffer(state.buf, &(sg_range){ + .ptr = glyph_vertices, + .size = state.draw.cur_glyph_vertex * sizeof(glyph_vertex_t), + }); +} + +static void push_draw_command(void) { + if ((state.draw.cur_draw_command < MAX_DRAW_COMMANDS) && (state.draw.cur_glyph_vertex > state.draw.start_glyph_vertex)) { + assert(state.draw.cur_font); + draw_commands[state.draw.cur_draw_command++] = (draw_command_t){ + .base_instance = state.draw.start_glyph_vertex, + .num_instances = state.draw.cur_glyph_vertex - state.draw.start_glyph_vertex, + .curve_tex_view = state.draw.cur_font->curve.tex_view, + .band_tex_view = state.draw.cur_font->band.tex_view, + }; + state.draw.start_glyph_vertex = state.draw.cur_glyph_vertex; + } +} + +static void push_glyph_vertex(const glyph_vertex_t* v) { + if (state.draw.cur_glyph_vertex < MAX_DRAWN_GLYPHS) { + glyph_vertices[state.draw.cur_glyph_vertex++] = *v; + } +} + +static void push_centered_line(const slug_font_t* font, const uint32_t* text, int line_nr) { + const int total_lines = TOTAL_LINES; + const float line_height = state.font_size * 1.5f; + const float block_height = (float)total_lines * line_height; + float line_width = measure_line(font, text); + float base_x = (sapp_widthf() - line_width) * 0.5f; + float base_y = (sapp_heightf() + block_height) * 0.5f - (float)line_nr * line_height; + push_line(font, text, base_x, base_y); +} + +static void push_centered_line_emoji(const slug_font_t* font, const uint32_t* text, int line_nr) { + const int total_lines = TOTAL_LINES; + const float line_height = state.font_size * 1.5f; + const float block_height = (float)total_lines * line_height; + float line_width = measure_line(font, text); + float base_x = (sapp_widthf() - line_width) * 0.5f; + float base_y = (sapp_heightf() + block_height) * 0.5f - (float)line_nr * line_height; + push_line_emoji(font, text, base_x, base_y); +} + +static void push_line(const slug_font_t* font, const uint32_t* text, float x, float y) { + uint32_t cp = 0; + while ((cp = *text++) != 0) { + const slug_glyph_t* glyph = slug_get_glyph(font, cp); + if (glyph) { + push_glyph(font, glyph, x, y, vec4(1.0f, 1.0f, 1.0f, 1.0f)); + x += glyph->advance * state.font_size; + } + } +} + +static void push_line_emoji(const slug_font_t* font, const uint32_t* text, float x, float y) { + uint32_t cp = 0; + while ((cp = *text++) != 0) { + const slug_glyph_t* glyph = slug_get_glyph(font, cp); + if (glyph) { + push_emoji(font, cp, x, y); + x += glyph->advance * state.font_size; + } + } +} + +static void push_emoji(const slug_font_t* font, uint32_t codepoint, float x, float y) { + const slug_colr_base_t* colr_base = slug_find_colr_base(font, codepoint); + if (colr_base == 0) { + return; + } + // draw each layer as its own glyph + for (uint16_t i = 0; i < colr_base->num_layers; i++) { + slug_colr_layer_t* layer = &font->colr_layers[colr_base->first_layer + i]; + if (layer->glyph_id >= arrlen(font->glyphs)) { + continue; + } + slug_glyph_t* glyph = &font->glyphs[layer->glyph_id]; + vec4_t color = vec4(1.0f, 1.0f, 1.0f, 1.0f); + if (layer->palette_index < arrlen(font->cpal_colors)) { + color = font->cpal_colors[layer->palette_index]; + } + push_glyph(font, glyph, x, y, color); + } +} + +static uint32_t pack_color_u32(vec4_t color) { + uint32_t r = (uint32_t)(color.x * 255); + uint32_t g = (uint32_t)(color.y * 255); + uint32_t b = (uint32_t)(color.z * 255); + uint32_t a = (uint32_t)(color.w * 255); + return (a << 24) | (b << 16) | (g << 8) | r; +} + +static void push_glyph(const slug_font_t* font, const slug_glyph_t* glyph, float x, float y, vec4_t color) { + if ((glyph->max_band_x < 0.0f) || (glyph->max_band_y < 0.0f)) { + return; + } + if (font != state.draw.cur_font) { + if (state.draw.cur_font != 0) { + push_draw_command(); + } + state.draw.cur_font = font; + } + const glyph_vertex_t glyph_vertex = { + .draw_rect = { + x + (glyph->bbox.x0 * state.font_size), + y + (glyph->bbox.y0 * state.font_size), + (glyph->bbox.x1 - glyph->bbox.x0) * state.font_size, + (glyph->bbox.y1 - glyph->bbox.y0) * state.font_size, + }, + .glyph_bbox = { + glyph->bbox.x0, + glyph->bbox.y0, + glyph->bbox.x1, + glyph->bbox.y1, + }, + .band_transform = { + glyph->band_scale.x, + glyph->band_scale.y, + glyph->band_offset.x, + glyph->band_offset.y, + }, + .glyph_params = { + glyph->glyph_loc[0], + glyph->glyph_loc[1], + (int)glyph->max_band_x, + (int)glyph->max_band_y, + }, + .color = pack_color_u32(color), + }; + push_glyph_vertex(&glyph_vertex); +} + +sapp_desc sokol_main(int argc, char* argv[]) { + (void)argc; (void)argv; + return (sapp_desc){ + .init_cb = init, + .frame_cb = frame, + .event_cb = input, + .cleanup_cb = cleanup, + .width = 900, + .height = 500, + .sample_count = 1, + .high_dpi = true, + .window_title = "slug-sapp.c", + .icon.sokol_default = true, + .logger.func = slog_func, + }; +} diff --git a/sapp/slug-sapp.glsl b/sapp/slug-sapp.glsl new file mode 100644 index 00000000..3ccc687c --- /dev/null +++ b/sapp/slug-sapp.glsl @@ -0,0 +1,171 @@ +@ctype mat4 mat44_t +@ctype vec4 vec4_t + +@vs vs +layout(binding=0) uniform vs_params { + mat4 mvp; +}; + +in vec4 draw_rect; // xy = screen position (pixels), zw = screen size (pixels) +in vec4 glyph_bbox; // xy = min corner (glyph space), zw = max corner (glyph space) +in vec4 in_band_transform; // xy = band_scale, zw = band_offset +in ivec4 in_glyph_params; // x = glyph_loc_x, y = glyph_loc_y, z = max_band_x, w = max_band_y +in vec4 in_text_color; // RGBA +out vec2 glyph_pos; // fragment position in glyph space +flat out vec4 band_transform; +flat out ivec4 glyph_params; +flat out vec4 text_color; + +void main(){ + vec2 quad_pos = vec2(gl_VertexIndex & 1, (gl_VertexIndex>>1) & 1); + vec2 screen_pos = draw_rect.xy + quad_pos * draw_rect.zw; + gl_Position = mvp * vec4(screen_pos, 0.0, 1.0); + glyph_pos = mix(glyph_bbox.xy, glyph_bbox.zw, quad_pos); + band_transform = in_band_transform; + glyph_params = in_glyph_params; + text_color = in_text_color; +} +@end + +@fs fs +@image_sample_type curve_tex unfilterable_float +layout(binding=0) uniform texture2D curve_tex; +@image_sample_type band_tex uint +layout(binding=1) uniform utexture2D band_tex; +@sampler_type point_sampler nonfiltering +layout(binding=0) uniform sampler point_sampler; + +in vec2 glyph_pos; +flat in vec4 band_transform; +flat in ivec4 glyph_params; +flat in vec4 text_color; +out vec4 frag_color; + +ivec2 bandLoc(ivec2 base, int offset) { + ivec2 pos = ivec2(base.x + offset, base.y); + pos.y += pos.x >> 12; + pos.x &= 4095; + return pos; +} +uint calcRootCode(float y1, float y2, float y3) { + uint s1 = floatBitsToUint(y1) >> 31u; + uint s2 = floatBitsToUint(y2) >> 30u; + uint s3 = floatBitsToUint(y3) >> 29u; + uint combined = (s2 & 2u) | (s1 & ~2u); + combined = (s3 & 4u) | (combined & ~4u); + return (0x2E74u >> combined) & 0x0101u; +} +vec2 solveHoriz(vec4 points_01, vec2 point_2) { + vec2 a = points_01.xy - points_01.zw * 2.0 + point_2; + vec2 b = points_01.xy - points_01.zw; + float inv_a = 1.0 / a.y; + float half_inv_b = 0.5 / b.y; + float discriminant = sqrt(max(b.y * b.y - a.y * points_01.y, 0.0)); + float t1 = (b.y - discriminant) * inv_a; + float t2 = (b.y + discriminant) * inv_a; + if (abs(a.y) < 1.0 / 65536.0) { t1 = points_01.y * half_inv_b; t2 = t1; } + return vec2( + (a.x * t1 - b.x * 2.0) * t1 + points_01.x, + (a.x * t2 - b.x * 2.0) * t2 + points_01.x + ); +} +vec2 solveVert(vec4 points_01, vec2 point_2) { + vec2 a = points_01.xy - points_01.zw * 2.0 + point_2; + vec2 b = points_01.xy - points_01.zw; + float inv_a = 1.0 / a.x; + float half_inv_b = 0.5 / b.x; + float discriminant = sqrt(max(b.x * b.x - a.x * points_01.x, 0.0)); + float t1 = (b.x - discriminant) * inv_a; + float t2 = (b.x + discriminant) * inv_a; + if (abs(a.x) < 1.0 / 65536.0) { t1 = points_01.x * half_inv_b; t2 = t1; } + return vec2( + (a.y * t1 - b.y * 2.0) * t1 + points_01.y, + (a.y * t2 - b.y * 2.0) * t2 + points_01.y + ); +} +void main() { + ivec2 glyph_loc = glyph_params.xy; + int max_band_x = glyph_params.z; + int max_band_y = glyph_params.w; + vec2 glyph_units_per_pixel = 1.0 / fwidth(glyph_pos); + ivec2 band_index = clamp( + ivec2(glyph_pos * band_transform.xy + band_transform.zw), + ivec2(0, 0), + ivec2(max_band_x, max_band_y) + ); + // Horizontal bands (ray in +X) + float h_winding = 0.0; + float h_edge_weight = 0.0; + { + uvec2 band_header = texelFetch(usampler2D(band_tex, point_sampler), + ivec2(glyph_loc.x + band_index.y, glyph_loc.y), 0).xy; + int curve_count = int(band_header.x); + ivec2 entry_list_start = bandLoc(glyph_loc, int(band_header.y)); + for (int i = 0; i < curve_count; i++) { + ivec2 entry_coord = bandLoc(entry_list_start, i); + uvec2 band_entry = texelFetch(usampler2D(band_tex, point_sampler), entry_coord, 0).xy; + ivec2 curve_tex_coord = ivec2(band_entry.x, band_entry.y); + vec4 points_01 = texelFetch(sampler2D(curve_tex, point_sampler), curve_tex_coord, 0) + - vec4(glyph_pos, glyph_pos); + ivec2 next_tex_coord = bandLoc(curve_tex_coord, 1); + vec2 point_2 = texelFetch(sampler2D(curve_tex, point_sampler), next_tex_coord, 0).xy + - glyph_pos; + if (max(max(points_01.x, points_01.z), point_2.x) * glyph_units_per_pixel.x < -0.5) break; + uint root_mask = calcRootCode(points_01.y, points_01.w, point_2.y); + if (root_mask != 0u) { + vec2 crossings = solveHoriz(points_01, point_2) * glyph_units_per_pixel.x; + if ((root_mask & 1u) != 0u) { + h_winding += clamp(crossings.x + 0.5, 0.0, 1.0); + h_edge_weight = max(h_edge_weight, clamp(1.0 - abs(crossings.x) * 2.0, 0.0, 1.0)); + } + if (root_mask > 1u) { + h_winding -= clamp(crossings.y + 0.5, 0.0, 1.0); + h_edge_weight = max(h_edge_weight, clamp(1.0 - abs(crossings.y) * 2.0, 0.0, 1.0)); + } + } + } + } + // Vertical bands (ray in +Y) + float v_winding = 0.0; + float v_edge_weight = 0.0; + { + uvec2 band_header = texelFetch(usampler2D(band_tex, point_sampler), + ivec2(glyph_loc.x + max_band_y + 1 + band_index.x, glyph_loc.y), 0).xy; + int curve_count = int(band_header.x); + ivec2 entry_list_start = bandLoc(glyph_loc, int(band_header.y)); + for (int i = 0; i < curve_count; i++) { + ivec2 entry_coord = bandLoc(entry_list_start, i); + uvec2 band_entry = texelFetch(usampler2D(band_tex, point_sampler), entry_coord, 0).xy; + ivec2 curve_tex_coord = ivec2(band_entry.x, band_entry.y); + vec4 points_01 = texelFetch(sampler2D(curve_tex, point_sampler), curve_tex_coord, 0) + - vec4(glyph_pos, glyph_pos); + ivec2 next_tex_coord = bandLoc(curve_tex_coord, 1); + vec2 point_2 = texelFetch(sampler2D(curve_tex, point_sampler), next_tex_coord, 0).xy + - glyph_pos; + if (max(max(points_01.y, points_01.w), point_2.y) * glyph_units_per_pixel.y < -0.5) break; + uint root_mask = calcRootCode(points_01.x, points_01.z, point_2.x); + if (root_mask != 0u) { + vec2 crossings = solveVert(points_01, point_2) * glyph_units_per_pixel.y; + if ((root_mask & 1u) != 0u) { + v_winding -= clamp(crossings.x + 0.5, 0.0, 1.0); + v_edge_weight = max(v_edge_weight, clamp(1.0 - abs(crossings.x) * 2.0, 0.0, 1.0)); + } + if (root_mask > 1u) { + v_winding += clamp(crossings.y + 0.5, 0.0, 1.0); + v_edge_weight = max(v_edge_weight, clamp(1.0 - abs(crossings.y) * 2.0, 0.0, 1.0)); + } + } + } + } + float coverage = max( + abs(h_winding * h_edge_weight + v_winding * v_edge_weight) + / max(h_edge_weight + v_edge_weight, 1.0 / 65536.0), + min(abs(h_winding), abs(v_winding)) + ); + coverage = clamp(coverage, 0.0, 1.0); + float alpha = text_color.a * coverage; + frag_color = vec4(text_color.rgb * alpha, alpha); +} +@end + +@program slug vs fs diff --git a/webpage/slug.jpg b/webpage/slug.jpg new file mode 100644 index 00000000..31c0ea85 Binary files /dev/null and b/webpage/slug.jpg differ