#import "modules/std.sx"; #import "modules/opengl.sx"; #import "ui/types.sx"; // Cached glyph data with UV coordinates into the atlas texture CachedGlyph :: struct { uv_x: f32; uv_y: f32; uv_w: f32; uv_h: f32; width: f32; height: f32; offset_x: f32; offset_y: f32; advance: f32; } // Cache entry: key + glyph data GlyphEntry :: struct { key: u32; glyph: CachedGlyph; } // Quantize font size to half-point increments to limit cache entries. // e.g., 13.0 -> 26, 13.5 -> 27, 14.0 -> 28 quantize_size :: (font_size: f32) -> u16 { xx (font_size * 2.0 + 0.5); } dequantize_size :: (q: u16) -> f32 { xx q / 2.0; } // Pack (glyph_index, size_quantized) into a single u32 for fast comparison make_glyph_key :: (glyph_index: u16, size_quantized: u16) -> u32 { (xx glyph_index << 16) | xx size_quantized; } // Shaped glyph — output of text shaping (positioned glyph with index) ShapedGlyph :: struct { glyph_index: u16; x: f32; // horizontal position (logical units, cumulative) y: f32; // vertical offset (logical units) advance: f32; // advance width (logical units) } is_ascii :: (text: string) -> bool { i : s64 = 0; while i < text.len { if text[i] >= 128 { return false; } i += 1; } true; } // kbts constants (C enum values) KBTS_DIRECTION_DONT_KNOW :u32: 0; KBTS_LANGUAGE_DONT_KNOW :u32: 0; KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX :u32: 0; // SX structs matching kbts C struct layouts (64-bit). // We define these in SX to access fields directly, casting from opaque C pointers. KbtsGlyphIterator :: struct { glyph_storage: *void; current_glyph: *void; last_advance_x: s32; x: s32; y: s32; } KbtsRun :: struct { font: *void; script: u32; paragraph_direction: u32; direction: u32; flags: u32; glyphs: KbtsGlyphIterator; } KbtsGlyph :: struct { prev: *void; next: *void; codepoint: u32; id: u16; uid: u16; user_id_or_codepoint_index: s32; offset_x: s32; offset_y: s32; advance_x: s32; advance_y: s32; } // kbts_font_info2 base (simplified — we only need the Size field for dispatch) KBTS_FONT_INFO_STRING_ID_COUNT :s32: 7; KbtsFontInfo2 :: struct { size: u32; strings: [7]*void; // char* array string_lengths: [7]u16; style_flags: u32; weight: u32; width: u32; } KbtsFontInfo2_1 :: struct { base: KbtsFontInfo2; units_per_em: u16; x_min: s16; y_min: s16; x_max: s16; y_max: s16; ascent: s16; descent: s16; line_gap: s16; } GLYPH_ATLAS_W :s32: 1024; GLYPH_ATLAS_H :s32: 1024; FONTINFO_SIZE :s64: 256; PackResult :: struct { x, y: s32; } // Dynamic glyph cache with on-demand rasterization and texture atlas packing. GlyphCache :: struct { // Font data font_info: *void; // heap-allocated stbtt_fontinfo (256 bytes) font_data: *void; // raw TTF file bytes (kept alive for stbtt) // Atlas texture (GPU) texture_id: u32; atlas_width: s32; atlas_height: s32; // Atlas bitmap (CPU-side for updates) bitmap: [*]u8; // Shelf packer state shelf_y: s32; shelf_height: s32; cursor_x: s32; padding: s32; // Glyph lookup cache entries: List(GlyphEntry); // Hash table for O(1) glyph lookup (open addressing, linear probing) hash_keys: [*]u32; // key per slot (0 = empty sentinel) hash_vals: [*]s32; // index into entries list hash_cap: s64; // table capacity (power of 2) // Dirty tracking for texture upload dirty: bool; // Font vertical metrics (at reference size 1.0 — scale by font_size) ascent: f32; descent: f32; line_gap: f32; // HiDPI: physical pixels per logical pixel (e.g. 2.0 on Retina) dpi_scale: f32; inv_dpi: f32; // Text shaping (kb_text_shape) shape_ctx: *void; shape_font: *void; units_per_em: u16; font_data_size: s32; shaped_buf: List(ShapedGlyph); // Shape cache: skip reshaping if same text + size as last call last_shape_ptr: [*]u8; last_shape_len: s64; last_shape_size_q: u16; init :: (self: *GlyphCache, path: [:0]u8, default_size: f32) { // Zero out the entire struct first (parent may be uninitialized with = ---) memset(self, 0, size_of(GlyphCache)); // Load font file file_size : s32 = 0; font_data := read_file_bytes(path, @file_size); if xx font_data == 0 { out("Failed to load font: "); out(path); out("\n"); return; } self.font_data = xx font_data; self.font_data_size = file_size; // Init stbtt_fontinfo self.font_info = context.allocator.alloc(FONTINFO_SIZE); memset(self.font_info, 0, FONTINFO_SIZE); stbtt_InitFont(self.font_info, font_data, 0); // Get font vertical metrics (in unscaled font units) ascent_i : s32 = 0; descent_i : s32 = 0; linegap_i : s32 = 0; stbtt_GetFontVMetrics(self.font_info, @ascent_i, @descent_i, @linegap_i); // Store unscaled metrics — we'll scale per font_size in measure_text self.ascent = xx ascent_i; self.descent = xx descent_i; self.line_gap = xx linegap_i; // Init text shaping context self.shape_ctx = xx kbts_CreateShapeContext(xx 0, xx 0); if xx self.shape_ctx != 0 { self.shape_font = xx kbts_ShapePushFontFromMemory(xx self.shape_ctx, self.font_data, file_size, 0); // Get font metrics (units_per_em) from kbts kb_info : KbtsFontInfo2_1 = ---; memset(@kb_info, 0, size_of(KbtsFontInfo2_1)); kb_info.base.size = xx size_of(KbtsFontInfo2_1); kbts_GetFontInfo2(xx self.shape_font, xx @kb_info); self.units_per_em = kb_info.units_per_em; } // Allocate atlas bitmap self.atlas_width = GLYPH_ATLAS_W; self.atlas_height = GLYPH_ATLAS_H; bitmap_size : s64 = xx self.atlas_width * xx self.atlas_height; self.bitmap = xx context.allocator.alloc(bitmap_size); memset(self.bitmap, 0, bitmap_size); // Shelf packer init self.shelf_y = 0; self.shelf_height = 0; self.cursor_x = 0; self.padding = 1; self.dirty = false; self.dpi_scale = 1.0; self.inv_dpi = 1.0; // Init hash table (256 slots) self.hash_cap = 256; hash_bytes : s64 = self.hash_cap * 4; // u32 per slot self.hash_keys = xx context.allocator.alloc(hash_bytes); memset(self.hash_keys, 0, hash_bytes); val_bytes : s64 = self.hash_cap * 8; // s64 per slot (s32 would suffice but alignment) self.hash_vals = xx context.allocator.alloc(val_bytes); // Create OpenGL texture glGenTextures(1, @self.texture_id); glBindTexture(GL_TEXTURE_2D, self.texture_id); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, self.atlas_width, self.atlas_height, 0, GL_RED, GL_UNSIGNED_BYTE, self.bitmap); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE); out("GlyphCache initialized: "); out(path); out("\n"); } // Look up or rasterize a glyph, returning a pointer to its cached entry. // Returns null for glyphs with no outline AND zero advance (shouldn't happen for valid chars). get_or_rasterize :: (self: *GlyphCache, glyph_index: u16, font_size: f32) -> *CachedGlyph { size_q := quantize_size(font_size); key := make_glyph_key(glyph_index, size_q); // Hash table lookup (open addressing, linear probing) mask := self.hash_cap - 1; slot : s64 = xx ((key * 2654435761) >> 24) & xx mask; while self.hash_keys[slot] != 0 { if self.hash_keys[slot] == key { return @self.entries.items[self.hash_vals[slot]].glyph; } slot = (slot + 1) & mask; } // Cache miss — rasterize actual_size := dequantize_size(size_q); scale := stbtt_ScaleForPixelHeight(self.font_info, actual_size); // Get glyph bounding box x0 : s32 = 0; y0 : s32 = 0; x1 : s32 = 0; y1 : s32 = 0; stbtt_GetGlyphBitmapBox(self.font_info, xx glyph_index, scale, scale, @x0, @y0, @x1, @y1); glyph_w := if x1 > x0 then x1 - x0 else 0; glyph_h := if y1 > y0 then y1 - y0 else 0; // Get horizontal metrics advance_i : s32 = 0; lsb_i : s32 = 0; stbtt_GetGlyphHMetrics(self.font_info, xx glyph_index, @advance_i, @lsb_i); advance : f32 = xx advance_i * scale; // Zero-size glyph (e.g. space) — cache with advance only if glyph_w == 0 or glyph_h == 0 { entry := GlyphEntry.{ key = key, glyph = CachedGlyph.{ uv_x = 0.0, uv_y = 0.0, uv_w = 0.0, uv_h = 0.0, width = 0.0, height = 0.0, offset_x = xx x0, offset_y = xx y0, advance = advance } }; self.entries.append(entry); self.hash_insert(key, self.entries.len - 1); return @self.entries.items[self.entries.len - 1].glyph; } // Pack into atlas pack := self.try_pack(glyph_w, glyph_h); if pack.x < 0 { // Atlas full — grow and retry self.grow(); return self.get_or_rasterize(glyph_index, font_size); } // Rasterize directly into atlas bitmap dest_offset : s64 = xx pack.y * xx self.atlas_width + xx pack.x; stbtt_MakeGlyphBitmap( self.font_info, @self.bitmap[dest_offset], glyph_w, glyph_h, self.atlas_width, scale, scale, xx glyph_index ); self.dirty = true; // Compute normalized UV coordinates atlas_wf : f32 = xx self.atlas_width; atlas_hf : f32 = xx self.atlas_height; entry := GlyphEntry.{ key = key, glyph = CachedGlyph.{ uv_x = xx pack.x / atlas_wf, uv_y = xx pack.y / atlas_hf, uv_w = xx glyph_w / atlas_wf, uv_h = xx glyph_h / atlas_hf, width = xx glyph_w, height = xx glyph_h, offset_x = xx x0, offset_y = xx y0, advance = advance } }; self.entries.append(entry); self.hash_insert(key, self.entries.len - 1); return @self.entries.items[self.entries.len - 1].glyph; } // Insert a key→index mapping into the hash table, growing if needed hash_insert :: (self: *GlyphCache, key: u32, index: s64) { // Grow if load factor > 70% if self.entries.len * 10 > self.hash_cap * 7 { self.hash_grow(); } mask := self.hash_cap - 1; slot : s64 = xx ((key * 2654435761) >> 24) & xx mask; while self.hash_keys[slot] != 0 { slot = (slot + 1) & mask; } self.hash_keys[slot] = key; self.hash_vals[slot] = xx index; } // Double the hash table and rehash all entries hash_grow :: (self: *GlyphCache) { old_cap := self.hash_cap; old_keys := self.hash_keys; old_vals := self.hash_vals; self.hash_cap = old_cap * 2; hash_bytes : s64 = self.hash_cap * 4; self.hash_keys = xx context.allocator.alloc(hash_bytes); memset(self.hash_keys, 0, hash_bytes); val_bytes : s64 = self.hash_cap * 8; self.hash_vals = xx context.allocator.alloc(val_bytes); // Rehash mask := self.hash_cap - 1; i : s64 = 0; while i < old_cap { k := old_keys[i]; if k != 0 { slot : s64 = xx ((k * 2654435761) >> 24) & xx mask; while self.hash_keys[slot] != 0 { slot = (slot + 1) & mask; } self.hash_keys[slot] = k; self.hash_vals[slot] = old_vals[i]; } i += 1; } context.allocator.dealloc(old_keys); context.allocator.dealloc(old_vals); } // Upload dirty atlas to GPU flush :: (self: *GlyphCache) { if self.dirty == false { return; } glBindTexture(GL_TEXTURE_2D, self.texture_id); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap); self.dirty = false; } // Shelf-based rectangle packer. // Returns PackResult with x >= 0 on success, x = -1 if no space. try_pack :: (self: *GlyphCache, w: s32, h: s32) -> PackResult { padded_w := w + self.padding; padded_h := h + self.padding; // Try fitting on the current shelf eff_h := if self.shelf_height > padded_h then self.shelf_height else padded_h; if self.cursor_x + padded_w <= self.atlas_width and self.shelf_y + eff_h <= self.atlas_height { result := PackResult.{ x = self.cursor_x, y = self.shelf_y }; self.cursor_x += padded_w; if padded_h > self.shelf_height { self.shelf_height = padded_h; } return result; } // Start a new shelf new_shelf_y := self.shelf_y + self.shelf_height; if new_shelf_y + padded_h <= self.atlas_height and padded_w <= self.atlas_width { self.shelf_y = new_shelf_y; self.shelf_height = padded_h; self.cursor_x = padded_w; return PackResult.{ x = 0, y = new_shelf_y }; } // No space PackResult.{ x = 0 - 1, y = 0 - 1 }; } // Grow the atlas by doubling dimensions grow :: (self: *GlyphCache) { new_w := self.atlas_width * 2; new_h := self.atlas_height * 2; new_size : s64 = xx new_w * xx new_h; new_bitmap : [*]u8 = xx context.allocator.alloc(new_size); memset(new_bitmap, 0, new_size); // Copy old rows into new bitmap y : s32 = 0; while y < self.atlas_height { old_off : s64 = xx y * xx self.atlas_width; new_off : s64 = xx y * xx new_w; memcpy(@new_bitmap[new_off], @self.bitmap[old_off], xx self.atlas_width); y += 1; } context.allocator.dealloc(self.bitmap); self.bitmap = new_bitmap; self.atlas_width = new_w; self.atlas_height = new_h; // Recreate GL texture glDeleteTextures(1, @self.texture_id); glGenTextures(1, @self.texture_id); glBindTexture(GL_TEXTURE_2D, self.texture_id); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, new_w, new_h, 0, GL_RED, GL_UNSIGNED_BYTE, new_bitmap); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE); // Recompute UV coordinates for all cached glyphs atlas_wf : f32 = xx new_w; atlas_hf : f32 = xx new_h; i : s64 = 0; while i < self.entries.len { g := @self.entries.items[i].glyph; if g.width > 0.0 { g.uv_x = g.uv_x / 2.0; g.uv_y = g.uv_y / 2.0; g.uv_w = g.width / atlas_wf; g.uv_h = g.height / atlas_hf; } i += 1; } self.dirty = false; out("GlyphCache atlas grown\n"); } set_dpi_scale :: (self: *GlyphCache, scale: f32) { self.dpi_scale = scale; self.inv_dpi = 1.0 / scale; } // Get the scale factor for a logical font size scale_for_size :: (self: *GlyphCache, font_size: f32) -> f32 { stbtt_ScaleForPixelHeight(self.font_info, font_size); } // Get scaled ascent for a logical font size get_ascent :: (self: *GlyphCache, font_size: f32) -> f32 { self.ascent * self.scale_for_size(font_size); } // Get scaled line height for a logical font size get_line_height :: (self: *GlyphCache, font_size: f32) -> f32 { s := self.scale_for_size(font_size); (self.ascent - self.descent + self.line_gap) * s; } // Shape text into positioned glyphs. // Uses ASCII fast-path (stbtt byte-by-byte) for pure ASCII, // full kb_text_shape pipeline for Unicode/complex scripts. // Results stored in self.shaped_buf (reused across calls). shape_text :: (self: *GlyphCache, text: string, font_size: f32) { // Check shape cache: skip if same text + size as last call size_q := quantize_size(font_size); if text.len > 0 and text.ptr == self.last_shape_ptr and text.len == self.last_shape_len and size_q == self.last_shape_size_q { return; // shaped_buf already has the result } self.shaped_buf.len = 0; if text.len == 0 { return; } if is_ascii(text) { self.shape_ascii(text, font_size); } else { self.shape_with_kb(text, font_size); } // Update shape cache self.last_shape_ptr = text.ptr; self.last_shape_len = text.len; self.last_shape_size_q = size_q; } shape_ascii :: (self: *GlyphCache, text: string, font_size: f32) { scale := stbtt_ScaleForPixelHeight(self.font_info, font_size); total : f32 = 0.0; i : s64 = 0; while i < text.len { ch : s32 = xx text[i]; glyph_index : u16 = xx stbtt_FindGlyphIndex(self.font_info, ch); advance_i : s32 = 0; lsb_i : s32 = 0; stbtt_GetGlyphHMetrics(self.font_info, xx glyph_index, @advance_i, @lsb_i); adv : f32 = xx advance_i * scale; self.shaped_buf.append(ShapedGlyph.{ glyph_index = glyph_index, x = total, y = 0.0, advance = adv }); total += adv; i += 1; } } shape_with_kb :: (self: *GlyphCache, text: string, font_size: f32) { if xx self.shape_ctx == 0 { self.shape_ascii(text, font_size); return; } scale : f32 = font_size / xx self.units_per_em; total : f32 = 0.0; kbts_ShapeBegin(xx self.shape_ctx, KBTS_DIRECTION_DONT_KNOW, KBTS_LANGUAGE_DONT_KNOW); kbts_ShapeUtf8(xx self.shape_ctx, xx text.ptr, xx text.len, KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX); kbts_ShapeEnd(xx self.shape_ctx); run : KbtsRun = ---; while kbts_ShapeRun(xx self.shape_ctx, xx @run) != 0 { glyph_ptr : *KbtsGlyph = xx 0; while kbts_GlyphIteratorNext(xx @run.glyphs, xx @glyph_ptr) != 0 { if xx glyph_ptr == 0 { continue; } gx := total + xx glyph_ptr.offset_x * scale; gy : f32 = xx glyph_ptr.offset_y * scale; adv : f32 = xx glyph_ptr.advance_x * scale; self.shaped_buf.append(ShapedGlyph.{ glyph_index = glyph_ptr.id, x = gx, y = gy, advance = adv }); total += adv; } } } // Measure text at a logical font size using text shaping. // Rasterizes at physical resolution (font_size * dpi_scale), returns logical dimensions. measure_text :: (self: *GlyphCache, text: string, font_size: f32) -> Size { self.shape_text(text, font_size); width : f32 = 0.0; i : s64 = 0; while i < self.shaped_buf.len { width += self.shaped_buf.items[i].advance; i += 1; } Size.{ width = width, height = self.get_line_height(font_size) }; } }