From 2db4603d0476524c073315c43263e9845b5f44d1 Mon Sep 17 00:00:00 2001 From: Robert Allan James Date: Mon, 10 Aug 2026 22:31:56 -0400 Subject: [PATCH] ttf.c: glyph raster cache, fixed slots, no allocation (item 4.3.7d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Punch list §25 item 4.3.7d complete. ttf_raster_cache_get() looks up (font, codepoint, size_px) in a caller-owned fixed slot array, evicting round-robin once full, rasterizing into a slot on a miss. Verified live in tools/ttftest.c: an identical (font, 'A', 24px) call made twice returns was_hit=0 then was_hit=1, and the slot's own hits counter reads exactly 1 afterward -- checked programmatically. A different-codepoint call misses again, proving the key actually discriminates. Wall-clock timing (miss 0.040ms vs hit 0.001ms) is printed as informational corroboration only, not the load-bearing check. Co-Authored-By: Claude Sonnet 5 --- FABRIC.md | 27 +++++++++++- capsules/BLOCK_MAP.md | 2 +- include/starkernel/ttf.h | 51 +++++++++++++++++++++++ src/starkernel/hal/ttf.c | 90 ++++++++++++++++++++++++++++++++++++++++ tools/ttftest.c | 51 +++++++++++++++++++++++ 5 files changed, 219 insertions(+), 2 deletions(-) diff --git a/FABRIC.md b/FABRIC.md index 47bd763..e7c0d42 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -4849,11 +4849,36 @@ document and committing that amendment as its own item.* > job), but `ttf.c` builds into all three via the `hal/*.c` wildcard so a portability > compile check costs little and catches real bugs early. -- [ ] **4.3.7d — Glyph raster cache.** Rasterizing on every draw call is too slow for +- [x] **4.3.7d — Glyph raster cache.** Rasterizing on every draw call is too slow for repeated text; cache rasterized bitmaps keyed by (font, codepoint, size). *Done when:* drawing the same codepoint/size twice measurably hits the cache on the second call (e.g. a counter or timing difference), verified live, not just "code that should cache." *Refs:* §27.7. + > **Done 2026-08-10.** `ttf_raster_cache_get()`/`ttf_raster_cache_init()` in + > `src/starkernel/hal/ttf.c`/`ttf.h`: a fixed, caller-owned slot array (no allocation, same + > convention as the rest of this module), linear-scan lookup keyed by (font pointer, + > codepoint, size_px), round-robin eviction once every slot is full. Each slot is a fixed + > `TTF_CACHE_BITMAP_DIM` (80×80) square, rasterized at a fixed origin + > (`TTF_CACHE_MARGIN`, `size_px + TTF_CACHE_MARGIN`) regardless of the glyph's own bounding + > box — a caller doing real text layout (4.3.7e) needs to know this fixed convention, not + > assume the bitmap is tightly cropped to the glyph. + > + > **Verified live, not just "code that should cache"** (this item's own bar): extended + > `tools/ttftest.c`'s `test_raster_cache()` calls `ttf_raster_cache_get()` twice for the + > identical (font, `'A'`, 24px) key — first call returns `was_hit=0` (rasterized), second + > returns `was_hit=1` (cache hit), and the slot's own `hits` counter reads exactly 1 + > afterward, checked programmatically, not just printed. A third call for a different + > codepoint (`'a'`) at the same size misses again, proving the key actually discriminates + > rather than the cache just always reporting "hit". Wall-clock `clock()` timing is also + > printed as corroborating evidence (miss 0.040ms vs. hit 0.001ms on this run) but is + > explicitly labeled informational-only in the test's own output, since host `clock()` + > resolution is coarse and this repo doesn't treat unverified timing claims as proof on + > their own — the hit counter is the load-bearing check. + > + > No kernel-boot/screendump verification needed — this item's own "done when" only asks + > for a measurable hit, which the host test above demonstrates directly; unlike 4.3.7c, + > nothing here is CANVAS-visual. Compile-checked clean (`-Wall -Werror -Wextra`) on all + > three architectures, with and without `-D__STARKERNEL__`. - [ ] **4.3.7e — `TTF-TEXT` entry point.** `TTF-TEXT ( c-addr u x y size color -- )`, analogous to 4.3.6f's `TEXT` but TrueType-backed — becomes the primary text-rendering path diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index 4421dcd..eb591fa 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,5 @@ # Capsule Block Manifest — Auto-generated - + diff --git a/include/starkernel/ttf.h b/include/starkernel/ttf.h index 710adcf..185a41e 100644 --- a/include/starkernel/ttf.h +++ b/include/starkernel/ttf.h @@ -218,6 +218,57 @@ int ttf_rasterize_glyph(const ttf_font_t *font, uint32_t glyph_index, q48_16_t scale, q48_16_t origin_x, q48_16_t origin_y, uint8_t fill_value, ttf_bitmap_t *out); +/* Glyph raster cache (FABRIC.md item 4.3.7d). Fixed-size, caller-owned + * slot array -- no allocation, same convention as the rest of this + * module. Every cached bitmap is a fixed TTF_CACHE_BITMAP_DIM square, + * rasterized with the fixed origin (TTF_CACHE_MARGIN, + * size_px + TTF_CACHE_MARGIN) -- i.e. glyph (0,0)/baseline sits at that + * pixel within the bitmap on every cache entry, not just fitted to each + * glyph's own bounding box. Callers positioning text (4.3.7e) need to + * know this fixed convention. */ +#define TTF_CACHE_MAX_SIZE_PX 64 +#define TTF_CACHE_MARGIN 8 +#define TTF_CACHE_BITMAP_DIM (TTF_CACHE_MAX_SIZE_PX + TTF_CACHE_MARGIN * 2) +#define TTF_CACHE_BITMAP_BYTES (TTF_CACHE_BITMAP_DIM * TTF_CACHE_BITMAP_DIM) + +typedef struct { + int valid; + const ttf_font_t *font; + uint32_t codepoint; + uint32_t size_px; + uint32_t width, height; + uint32_t hits; /* incremented on every cache hit; 4.3.7d's own + * "measurable" verification reads this. */ + uint8_t pixels[TTF_CACHE_BITMAP_BYTES]; +} ttf_raster_cache_slot_t; + +typedef struct { + ttf_raster_cache_slot_t *slots; + uint32_t slot_count; + uint32_t evict_next; /* round-robin index used once every slot is full */ +} ttf_raster_cache_t; + +/** ttf_raster_cache_init - Bind a caller-supplied slot array to `cache` + * and mark every slot empty. */ +void ttf_raster_cache_init(ttf_raster_cache_t *cache, ttf_raster_cache_slot_t *slots, + uint32_t slot_count); + +/** + * ttf_raster_cache_get - Look up (font, codepoint, size_px); on a miss, + * rasterize and insert (evicting round-robin if every slot is full). + * `out` borrows the winning slot's buffer directly -- valid until that + * slot is evicted by a later call. + * + * @param was_hit If non-NULL, set to 1 on a cache hit, 0 if this call + * rasterized and inserted + * @return TTF_OK, TTF_ERR_UNSUPPORTED if size_px > TTF_CACHE_MAX_SIZE_PX, + * or a ttf_rasterize_glyph() TTF_ERR_* code on a miss that failed + * to rasterize + */ +int ttf_raster_cache_get(ttf_raster_cache_t *cache, const ttf_font_t *font, + uint32_t codepoint, uint32_t size_px, + ttf_bitmap_t *out, int *was_hit); + #ifdef __STARKERNEL__ #include "capsule.h" diff --git a/src/starkernel/hal/ttf.c b/src/starkernel/hal/ttf.c index cf95980..f1aba30 100644 --- a/src/starkernel/hal/ttf.c +++ b/src/starkernel/hal/ttf.c @@ -813,6 +813,96 @@ int ttf_rasterize_glyph(const ttf_font_t *font, uint32_t glyph_index, return TTF_OK; } +/* =========================================================================== + * 4.3.7d — glyph raster cache: fixed caller-owned slots, no allocation. + * =========================================================================== + */ + +void ttf_raster_cache_init(ttf_raster_cache_t *cache, ttf_raster_cache_slot_t *slots, + uint32_t slot_count) { + uint32_t i; + cache->slots = slots; + cache->slot_count = slot_count; + cache->evict_next = 0; + for (i = 0; i < slot_count; i++) { + slots[i].valid = 0; + slots[i].hits = 0; + } +} + +int ttf_raster_cache_get(ttf_raster_cache_t *cache, const ttf_font_t *font, + uint32_t codepoint, uint32_t size_px, + ttf_bitmap_t *out, int *was_hit) { + uint32_t i, slot_idx; + ttf_raster_cache_slot_t *slot; + int rc; + + if (!cache || !font || !out) return TTF_ERR_BAD_TABLE; + if (size_px > TTF_CACHE_MAX_SIZE_PX) return TTF_ERR_UNSUPPORTED; + + for (i = 0; i < cache->slot_count; i++) { + slot = &cache->slots[i]; + if (slot->valid && slot->font == font && slot->codepoint == codepoint + && slot->size_px == size_px) { + slot->hits++; + out->pixels = slot->pixels; + out->width = slot->width; + out->height = slot->height; + if (was_hit) *was_hit = 1; + return TTF_OK; + } + } + + /* Miss: use a free slot if one exists, else evict round-robin. */ + slot_idx = cache->slot_count; + for (i = 0; i < cache->slot_count; i++) { + if (!cache->slots[i].valid) { slot_idx = i; break; } + } + if (slot_idx == cache->slot_count) { + slot_idx = cache->evict_next; + cache->evict_next = (cache->evict_next + 1) % cache->slot_count; + } + slot = &cache->slots[slot_idx]; + + { + uint32_t j; + ttf_bitmap_t bmp; + q48_16_t scale; + uint32_t gid; + + for (j = 0; j < TTF_CACHE_BITMAP_BYTES; j++) slot->pixels[j] = 0; + + gid = ttf_codepoint_to_glyph(font, codepoint); + scale = q48_div(q48_from_u64(size_px), q48_from_u64(font->units_per_em)); + + bmp.pixels = slot->pixels; + bmp.width = TTF_CACHE_BITMAP_DIM; + bmp.height = TTF_CACHE_BITMAP_DIM; + rc = ttf_rasterize_glyph(font, gid, scale, + q48_from_u64(TTF_CACHE_MARGIN), + q48_from_u64(size_px + TTF_CACHE_MARGIN), + 255, &bmp); + if (rc != TTF_OK) { + slot->valid = 0; + return rc; + } + + slot->valid = 1; + slot->font = font; + slot->codepoint = codepoint; + slot->size_px = size_px; + slot->width = TTF_CACHE_BITMAP_DIM; + slot->height = TTF_CACHE_BITMAP_DIM; + slot->hits = 0; + } + + out->pixels = slot->pixels; + out->width = slot->width; + out->height = slot->height; + if (was_hit) *was_hit = 0; + return TTF_OK; +} + /* =========================================================================== * 4.3.7b — font data ingestion (capsule-backed, zero-copy) * =========================================================================== diff --git a/tools/ttftest.c b/tools/ttftest.c index 8d566d1..35b1bef 100644 --- a/tools/ttftest.c +++ b/tools/ttftest.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "../include/starkernel/ttf.h" @@ -221,6 +222,54 @@ static void test_rasterize(const ttf_font_t *font, uint32_t codepoint, const cha free(pixels); } +/* Raster cache (4.3.7d): first ttf_raster_cache_get() for a given + * (font, codepoint, size) must miss and rasterize; the second, identical + * call must hit -- checked both via the returned *was_hit flag and the + * slot's own hits counter (the item's own "measurable... counter or + * timing difference" bar), plus a wall-clock comparison as corroborating + * (not load-bearing -- host timing is noisy) evidence. A third call for + * a different codepoint at the same size must miss again, proving the + * cache actually discriminates on key, not just "always says hit". */ +static void test_raster_cache(const ttf_font_t *font) { + enum { SLOTS = 4 }; + ttf_raster_cache_slot_t slot_storage[SLOTS]; + ttf_raster_cache_t cache; + ttf_bitmap_t bmp; + int was_hit, rc; + clock_t t0, t1, t2; + + ttf_raster_cache_init(&cache, slot_storage, SLOTS); + + t0 = clock(); + rc = ttf_raster_cache_get(&cache, font, 0x0041, 24, &bmp, &was_hit); + t1 = clock(); + if (rc != TTF_OK || was_hit) { + printf(" FAIL raster_cache: first call rc=%d was_hit=%d (want TTF_OK, miss)\n", + rc, was_hit); + failures++; + } + + rc = ttf_raster_cache_get(&cache, font, 0x0041, 24, &bmp, &was_hit); + t2 = clock(); + if (rc != TTF_OK || !was_hit) { + printf(" FAIL raster_cache: second (identical) call rc=%d was_hit=%d (want TTF_OK, hit)\n", + rc, was_hit); + failures++; + } + check_eq_i("raster_cache hits", 0x0041, slot_storage[0].hits, 1); + + printf("raster_cache: miss=%.3fms hit=%.3fms (hit expected <= miss on a real cache;" + " host clock() is coarse/noisy, informational only)\n", + 1000.0 * (t1 - t0) / CLOCKS_PER_SEC, 1000.0 * (t2 - t1) / CLOCKS_PER_SEC); + + rc = ttf_raster_cache_get(&cache, font, 0x0061, 24, &bmp, &was_hit); + if (rc != TTF_OK || was_hit) { + printf(" FAIL raster_cache: different codepoint call rc=%d was_hit=%d (want TTF_OK, miss)\n", + rc, was_hit); + failures++; + } +} + int main(int argc, char **argv) { FILE *fp; long size; @@ -287,6 +336,8 @@ int main(int argc, char **argv) { test_rasterize(&font, 0x002E, "'.'"); test_rasterize(&font, 0x0061, "'a'"); + test_raster_cache(&font); + free(buf); if (failures) {