ttf.c: glyph raster cache, fixed slots, no allocation (item 4.3.7d)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
095860251a
commit
2db4603d04
@@ -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
|
> 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.
|
> 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:*
|
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
|
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:*
|
counter or timing difference), verified live, not just "code that should cache." *Refs:*
|
||||||
§27.7.
|
§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 -- )`,
|
- [ ] **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
|
analogous to 4.3.6f's `TEXT` but TrueType-backed — becomes the primary text-rendering path
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Capsule Block Manifest — Auto-generated
|
# Capsule Block Manifest — Auto-generated
|
||||||
<!-- Generated by mkcapsule --manifest 2026-08-11T02:22:42Z -->
|
<!-- Generated by mkcapsule --manifest 2026-08-11T02:31:16Z -->
|
||||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||||
<!-- Hand-written justifications and immutability notes live -->
|
<!-- Hand-written justifications and immutability notes live -->
|
||||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||||
|
|||||||
@@ -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,
|
q48_16_t scale, q48_16_t origin_x, q48_16_t origin_y,
|
||||||
uint8_t fill_value, ttf_bitmap_t *out);
|
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__
|
#ifdef __STARKERNEL__
|
||||||
#include "capsule.h"
|
#include "capsule.h"
|
||||||
|
|
||||||
|
|||||||
@@ -813,6 +813,96 @@ int ttf_rasterize_glyph(const ttf_font_t *font, uint32_t glyph_index,
|
|||||||
return TTF_OK;
|
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)
|
* 4.3.7b — font data ingestion (capsule-backed, zero-copy)
|
||||||
* ===========================================================================
|
* ===========================================================================
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#include "../include/starkernel/ttf.h"
|
#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);
|
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) {
|
int main(int argc, char **argv) {
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
long size;
|
long size;
|
||||||
@@ -287,6 +336,8 @@ int main(int argc, char **argv) {
|
|||||||
test_rasterize(&font, 0x002E, "'.'");
|
test_rasterize(&font, 0x002E, "'.'");
|
||||||
test_rasterize(&font, 0x0061, "'a'");
|
test_rasterize(&font, 0x0061, "'a'");
|
||||||
|
|
||||||
|
test_raster_cache(&font);
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
|
|
||||||
if (failures) {
|
if (failures) {
|
||||||
|
|||||||
Reference in New Issue
Block a user