diff --git a/CODING.md b/CODING.md index 433b806..1bd2bfe 100644 --- a/CODING.md +++ b/CODING.md @@ -42,3 +42,7 @@ Assume memory allocation will eventually fail. Assume the filesystem will eventu - Avoid separate functions for each method handler. Inline them into the single handler that handles dispatch. This not only net-shrinks the codebase, but also means that navigating by searching for `== NN__)` (in NCL) or `== NN_NUM_)` (in NN) will show the dispatch logic and implementation. +- Properties which are always equal to a constant from the config, or simply has no dependency on state, must not be requests, and instead handled by +the component class directly. +- All loops inside methods must be bound by something controlled by either the config or a compile-time constant, to prevent denial of service attacks on +the worker thread. diff --git a/src/glyphcache.c b/src/glyphcache.c new file mode 100644 index 0000000..cba5a21 --- /dev/null +++ b/src/glyphcache.c @@ -0,0 +1,212 @@ +#include +#include "glyphcache.h" +#include +#include +#include + +/* + * Dynamic glyph cache for raylib + * (C) - "Raylib's text renderer handles unicode like garbage" + * Problem: raylib's LoadFont only loads ~95 ASCII glyphs. + * LoadFontEx can load arbitrary codepoints, but you must know + * them upfront, and rebuilding every frame is expensive. + * + * So we should lazily collect codepoints the screen actually uses, + * rebuild the font atlas only when new ones appear. + * Typically this stabilises after the first few frames. + * +*/ + +#define GC_INITIAL_CAP 4096 +#define GC_BUCKET_COUNT 8192 // must be power of 2 +#define GC_BUCKET_MASK (GC_BUCKET_COUNT - 1) + +// Codepoint set (open-addressing hash set) + +typedef struct { + int *slots; // 0 = empty sentinel (U+0000 never needed) + size_t count; + size_t cap; // always a power of 2 +} CpSet; + +static void cpset_init(CpSet *s) { + s->cap = GC_BUCKET_COUNT; + s->count = 0; + s->slots = calloc(s->cap, sizeof(int)); +} + +static void cpset_free(CpSet *s) { + free(s->slots); + s->slots = NULL; + s->count = 0; +} + +static bool cpset_contains(const CpSet *s, int cp) { + size_t idx = (unsigned)cp & (s->cap - 1); + for(size_t i = 0; i < s->cap; i++) { + size_t j = (idx + i) & (s->cap - 1); + if(s->slots[j] == 0) return false; + if(s->slots[j] == cp) return true; + } + return false; +} + +static void cpset_grow(CpSet *s); + +// Returns true if the codepoint was newly inserted. +static bool cpset_insert(CpSet *s, int cp) { + if(cp == 0) return false; // sentinel + if(cpset_contains(s, cp)) return false; + + if(s->count * 4 >= s->cap * 3) cpset_grow(s); + + size_t idx = (unsigned)cp & (s->cap - 1); + for(size_t i = 0; i < s->cap; i++) { + size_t j = (idx + i) & (s->cap - 1); + if(s->slots[j] == 0) { + s->slots[j] = cp; + s->count++; + return true; + } + } + return false; // should never happen after grow +} + +static void cpset_grow(CpSet *s) { + size_t oldCap = s->cap; + int *old = s->slots; + + s->cap *= 2; + s->slots = calloc(s->cap, sizeof(int)); + s->count = 0; + + for(size_t i = 0; i < oldCap; i++) + if(old[i] != 0) cpset_insert(s, old[i]); + + free(old); +} + +// Fill dst (must hold at least s->count ints). +static void cpset_collect(const CpSet *s, int *dst) { + size_t n = 0; + for(size_t i = 0; i < s->cap; i++) + if(s->slots[i] != 0) dst[n++] = s->slots[i]; +} + +// Glyph cache + +struct ncl_GlyphCache { + char *fontPath; + int fontSize; + Font font; + bool fontLoaded; + bool dirty; // new codepoints since last rebuild + CpSet known; // all codepoints we have glyphs for +}; + +// Pre-seed the most common ranges so the first frame is not barren. +static void gc_seed(ncl_GlyphCache *gc) { + // ASCII printable + for(int i = 0x0020; i <= 0x007E; i++) cpset_insert(&gc->known, i); + + // Latin-1 Supplement + for(int i = 0x00A0; i <= 0x00FF; i++) cpset_insert(&gc->known, i); + + // Cyrillic (common) + for(int i = 0x0400; i <= 0x04FF; i++) cpset_insert(&gc->known, i); + + // General punctuation + for(int i = 0x2010; i <= 0x2027; i++) cpset_insert(&gc->known, i); + + // Box drawing + for(int i = 0x2500; i <= 0x257F; i++) cpset_insert(&gc->known, i); + + // Block elements + for(int i = 0x2580; i <= 0x259F; i++) cpset_insert(&gc->known, i); + + // Geometric shapes (partial) + for(int i = 0x25A0; i <= 0x25FF; i++) cpset_insert(&gc->known, i); + + // Braille patterns + for(int i = 0x2800; i <= 0x28FF; i++) cpset_insert(&gc->known, i); + + // Powerline / private use (common in OC themes) + for(int i = 0xE000; i <= 0xE0FF; i++) cpset_insert(&gc->known, i); + + gc->dirty = true; +} + +static void gc_rebuild(ncl_GlyphCache *gc) { + if(gc->fontLoaded) UnloadFont(gc->font); + + size_t n = gc->known.count; + if(n == 0) { gc->fontLoaded = false; gc->dirty = false; return; } + + int *cps = malloc(sizeof(int) * n); + cpset_collect(&gc->known, cps); + + gc->font = LoadFontEx(gc->fontPath, gc->fontSize, cps, (int)n); + gc->fontLoaded = true; + gc->dirty = false; + + // Let raylib use bilinear for scaled glyphs, nearest for 1:1. + SetTextureFilter(gc->font.texture, TEXTURE_FILTER_POINT); + + free(cps); + + fprintf(stderr, "[glyphcache] rebuilt atlas: %zu glyphs, tex %dx%d\n", + n, gc->font.texture.width, gc->font.texture.height); +} + +// Public API + +ncl_GlyphCache *ncl_createGlyphCache(const char *fontPath, int fontSize) { + ncl_GlyphCache *gc = calloc(1, sizeof(*gc)); + gc->fontPath = strdup(fontPath); + gc->fontSize = fontSize; + gc->fontLoaded = false; + gc->dirty = false; + cpset_init(&gc->known); + gc_seed(gc); + gc_rebuild(gc); + return gc; +} + +void ncl_destroyGlyphCache(ncl_GlyphCache *gc) { + if(!gc) return; + if(gc->fontLoaded) UnloadFont(gc->font); + cpset_free(&gc->known); + free(gc->fontPath); + free(gc); +} + +Font ncl_getFont(ncl_GlyphCache *gc) { + return gc->font; +} + +void ncl_flushGlyphs(ncl_GlyphCache *gc) { + if(gc->dirty) gc_rebuild(gc); +} + +void ncl_needGlyph(ncl_GlyphCache *gc, nn_codepoint cp) { + if(cp == 0) return; + if(cpset_insert(&gc->known, (int)cp)) + gc->dirty = true; +} + +void ncl_drawGlyph(ncl_GlyphCache *gc, nn_codepoint cp, + Vector2 pos, float size, Color tint) +{ + ncl_needGlyph(gc, cp); + DrawTextCodepoint(gc->font, (int)cp, pos, size, tint); +} + +int ncl_cellWidth(ncl_GlyphCache *gc) { + // Measure 'A' as the reference cell. + if(!gc->fontLoaded) return 8; + return MeasureTextEx(gc->font, "A", (float)gc->fontSize, 0).x; +} + +int ncl_cellHeight(ncl_GlyphCache *gc) { + return gc->fontSize; +} \ No newline at end of file diff --git a/src/glyphcache.h b/src/glyphcache.h new file mode 100644 index 0000000..c3ba199 --- /dev/null +++ b/src/glyphcache.h @@ -0,0 +1,19 @@ +#ifndef NCL_GLYPHCACHE_H +#define NCL_GLYPHCACHE_H + +#include +#include "neonucleus.h" + +typedef struct ncl_GlyphCache ncl_GlyphCache; + +ncl_GlyphCache *ncl_createGlyphCache(const char *fontPath, int fontSize); +void ncl_destroyGlyphCache(ncl_GlyphCache *gc); +Font ncl_getFont(ncl_GlyphCache *gc); +void ncl_flushGlyphs(ncl_GlyphCache *gc); +void ncl_needGlyph(ncl_GlyphCache *gc, nn_codepoint cp); +void ncl_drawGlyph(ncl_GlyphCache *gc, nn_codepoint cp, + Vector2 pos, float size, Color tint); +int ncl_cellWidth(ncl_GlyphCache *gc); +int ncl_cellHeight(ncl_GlyphCache *gc); + +#endif \ No newline at end of file diff --git a/src/ncomplib.c b/src/ncomplib.c index 4722af0..e19156e 100644 --- a/src/ncomplib.c +++ b/src/ncomplib.c @@ -1573,7 +1573,7 @@ static nn_Exit ncl_gpuHandler(nn_GPURequest *req) { int h = req->resolution.height; nn_lock(ctx, st->lock); ncl_ScreenState *scr = - ncl_getBoundScreen(st, C); + ncl_getBoundScreen(st, C); nn_unlock(ctx, st->lock); int maxW, maxH; char maxD; if(scr != NULL) ncl_lockScreen(scr); diff --git a/src/neonucleus.c b/src/neonucleus.c index 95319d4..77526bc 100644 --- a/src/neonucleus.c +++ b/src/neonucleus.c @@ -3215,6 +3215,9 @@ typedef enum nn_EENum { NN_EENUM_SETDATA, NN_EENUM_SETLABEL, NN_EENUM_SETARCH, + NN_EENUM_ISRO, + NN_EENUM_GETCHKSUM, + NN_EENUM_MKRO, NN_EENUM_COUNT, } nn_EENum; @@ -3359,6 +3362,9 @@ nn_Component *nn_createEEPROM(nn_Universe *universe, const char *address, const [NN_EENUM_SETDATA] = {"setData", "function(data: string) - Set the data on the EEPROM", NN_INDIRECT}, [NN_EENUM_SETLABEL] = {"setLabel", "function(label?: string) - Set the label", NN_INDIRECT}, [NN_EENUM_SETARCH] = {"setArchitecture", "function(arch?: string) - Set the desired architecture", NN_INDIRECT}, + [NN_EENUM_ISRO] = {"isReadonly", "function(): boolean - Returns whether the EEPROM is read-only.", NN_DIRECT}, + [NN_EENUM_GETCHKSUM] = {"getChecksum", "function(): string - Returns a checksum of the EEPROM code.", NN_DIRECT}, + [NN_EENUM_MKRO] = {"makeReadonly", "function(checksum: string): boolean - Make the EEPROM read-only if checksum passes.", NN_INDIRECT}, }; nn_Exit e = nn_setComponentMethodsArray(c, methods, NN_EENUM_COUNT); if(e) { @@ -4431,14 +4437,16 @@ static nn_Exit nn_gpuHandler(nn_ComponentRequest *req) { g.resolution.height = nn_tointeger(C, 1); e = cls->handler(&g); if(e) return e; - // push screen_resized via getScreen + // signal is best-effort, resolution is + // already changed at this point nn_GPURequest s = g; s.action = NN_GPU_GETSCREEN; s.screenAddr[0] = '\0'; - cls->handler(&s); - if(s.screenAddr[0] != '\0') { + if(cls->handler(&s) == NN_OK + && s.screenAddr[0] != '\0') { nn_pushScreenResized(C, s.screenAddr, - g.resolution.width, g.resolution.height); + g.resolution.width, + g.resolution.height); } req->returnCount = 1; return nn_pushbool(C, true);