Skip to content

Commit 3d2725d

Browse files
committed
Add runtime glyph cache for Thai font rendering
1 parent f13b2f7 commit 3d2725d

11 files changed

Lines changed: 426 additions & 35 deletions

.DS_Store

0 Bytes
Binary file not shown.

docs/rebases/2.2.9.22.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"stock_sha256": "a03fbea9f68a9de6bc271daabb9f3a41c59053d1086622c76a4e990f829cc561",
55
"stock_url": "https://cdn.evenreal.co/firmware/fc250b05e98a9ff998b4b68f5f99f994.bin",
66
"stock_size": 4476518,
7-
"patched_sha256": "6d10399a7fec28d2389c7be7ffd293b0ea37beb6771f9de470336e826a54600f",
7+
"patched_sha256": "749cf09ceea707620a79b029a1f02e0101b1d7bb44d09d9b700889d77307961b",
88
"component_count": 6,
99
"archive_hardware_validated": false,
1010
"selected_artifact": {

docs/rebases/2.2.9.22.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Expected stock component hashes:
4747
| stock UTF-8 helper | `0x0048B9D4` | `0x00491E24` | 64 exact bytes and decoded behavior |
4848
| glyph release | `0x004DA018` | `0x004E64A0` | 24 exact bytes; descriptor offset evidence |
4949
| glyph descriptor | `0x004DA074` | `0x004E64FC` | 32 exact bytes; IAR short-enum layout |
50+
| LVGL allocator || `0x00458382` | 16 exact bytes; stock font-manager allocation caller |
5051

5152
The rebase uses separate exact shifts (`+0x3890`, `+0x6450`, `+0xC488`); no
5253
global address delta was assumed.
@@ -60,20 +61,22 @@ global address delta was assumed.
6061
`688f2ef20776a1f0286bd73bef4dd5d5c76640f4a7c4f0ea5f7c1b8d87a969b7`);
6162
host-side Raqm shaping extracts combining marks without their dotted-circle
6263
scaffolds before the bitmap fallback is generated;
63-
source rasters use 2× scale with one-pixel edge attenuation for a lighter
64-
visual weight;
64+
source outlines are supersampled, geometrically thinned by half a device
65+
pixel per contour, and downsampled for a lighter visual weight;
6566
its embedded copyright is "All rights reserved" and its inclusion is
6667
user-authorized for this repository only;
6768
- patched output size: `4,606,432` bytes;
68-
- patched output SHA-256: `6d10399a7fec28d2389c7be7ffd293b0ea37beb6771f9de470336e826a54600f`;
69-
- patched main payload: `3,832,742` bytes;
70-
- patched programmed end: `0x007DFB86`; safe headroom: `66,682` bytes;
71-
- `make check`: 36 tests passed, including device-path emulation of the built
69+
- patched output SHA-256: `749cf09ceea707620a79b029a1f02e0101b1d7bb44d09d9b700889d77307961b`;
70+
- patched main payload: `3,834,766` bytes;
71+
- patched programmed end: `0x007E036E`; safe headroom: `64,658` bytes;
72+
- `make check`: 44 tests passed, including device-path emulation of the built
7273
payload (decoder dispatch, NULL-offset lookahead, tone+SARA AM remap, and a
7374
full 8-size glyph rasterization sweep through aligned and unaligned buffers
74-
with guards);
75+
with guards), plus allocator failure/out-of-range fallback, runtime-font
76+
idempotence, cache hit after source erasure, LRU eviction, busy fallback, and
77+
artifact-level chain-to-render integration;
7578
- non-main component payloads remain byte-identical to stock;
76-
- final hooks decode to chain wrapper `0x007BF0A0` and text wrapper `0x007BEE5C`;
79+
- final hooks decode to chain wrapper `0x007BF888` and text wrapper `0x007BF368`;
7780
- Case-USB writer requires the exact committed whole-bundle and main-payload pins;
7881
- two lenses were flashed with earlier broken artifacts; see the crash
7982
post-mortem below.

docs/research.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ cache flush. The only context-sensitive decoder case—tone marks after upper
7070
Thai marks—now identifies the immediately preceding fixed-width Thai UTF-8
7171
sequence directly instead of re-decoding the text prefix.
7272

73+
The fallback also keeps four bounded A8 glyph slots in a writable runtime font
74+
clone allocated during stock font-chain initialization. A cache hit copies the
75+
already-expanded pixels into LVGL's caller-owned draw buffer, so descriptor
76+
ownership, cache-entry fields, release callbacks, and the stock cache flush
77+
remain unchanged. Slot capacity is derived from the largest bitmap in the
78+
selected size; a typical 28 px chain uses 2,768 bytes including metadata. If
79+
the authenticated stock allocator fails, returns outside SRAM, or any runtime
80+
header or slot bound is invalid, the chain attaches the original read-only font
81+
or the bitmap callback uses the uncached A4 path. The cache has a busy guard so
82+
re-entrant rendering also falls back instead of sharing mutable slot state.
83+
7384
## Thai shaping boundary
7485

7586
LVGL decodes UTF-8 and supports font fallback, but it does not provide general

patches/thai_font.c

Lines changed: 171 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define FONT_DATA_MAGIC 0xA11D0003u
1616
#define LV_FONT_GLYPH_FORMAT_A8 0x08u
1717
#define STOCK_CHAIN_BUILD_THUMB 0x00470989u
18+
#define LV_MALLOC_THUMB 0x00458383u
1819
/* The stock letter helper loads its UTF-8 decoder through the double pointer
1920
at 0x00491F14 (slot address, then function pointer) so it always matches
2021
the firmware's active text encoder. Calling any hardcoded entry instead
@@ -27,6 +28,10 @@
2728
#define GLYPH_DSC_SIZE 32u
2829
#define GLYPH_DSC_FORMAT_OFFSET 14u
2930
#define GLYPH_DSC_GID_OFFSET 24u
31+
#define THAI_RUNTIME_MAGIC 0x43414854u
32+
#define THAI_CACHE_SLOTS 4u
33+
#define THAI_CACHE_BUDGET 8192u
34+
#define FONT_WORD_COUNT 9u
3035

3136
typedef struct {
3237
uint32_t bitmap_offset;
@@ -42,6 +47,24 @@ typedef struct {
4247
typedef void *(*stock_chain_build_fn)(void *configs, uint32_t count);
4348
typedef uint32_t (*stock_decode_fn)(const char *text, uint32_t *offset);
4449
typedef void (*flush_cache_fn)(const void *draw_buf, const void *area);
50+
typedef void *(*lv_malloc_fn)(uint32_t size);
51+
typedef uint32_t alias_u32_t __attribute__((__may_alias__));
52+
53+
typedef struct {
54+
uint32_t codepoint;
55+
uint32_t stamp;
56+
} thai_cache_slot_t;
57+
58+
typedef struct {
59+
uint32_t font[FONT_WORD_COUNT];
60+
uint32_t magic;
61+
uint32_t clock;
62+
uint16_t slot_bytes;
63+
uint8_t slot_count;
64+
uint8_t busy;
65+
thai_cache_slot_t slots[THAI_CACHE_SLOTS];
66+
uint8_t pixels[];
67+
} thai_runtime_font_t;
4568

4669
static const uint8_t a4_to_a8[16] = {
4770
0u, 17u, 34u, 51u, 68u, 85u, 102u, 119u,
@@ -109,6 +132,26 @@ static void write_u32(uint8_t *p, uint32_t value) {
109132
p[3] = (uint8_t)(value >> 24);
110133
}
111134

135+
static int writable_ram_range(const void *pointer, uint32_t size) {
136+
uintptr_t address = (uintptr_t)pointer;
137+
uint32_t capacity = WRITABLE_RAM_END - WRITABLE_RAM_BASE;
138+
return size <= capacity && address >= WRITABLE_RAM_BASE &&
139+
address <= WRITABLE_RAM_END - size;
140+
}
141+
142+
static void copy_bytes(uint8_t *destination, const uint8_t *source, uint32_t count) {
143+
if((((uintptr_t)destination | (uintptr_t)source) & 3u) == 0u) {
144+
while(count >= 4u) {
145+
*(alias_u32_t *)(uintptr_t)destination =
146+
*(const alias_u32_t *)(uintptr_t)source;
147+
destination += 4;
148+
source += 4;
149+
count -= 4u;
150+
}
151+
}
152+
while(count--) *destination++ = *source++;
153+
}
154+
112155
static const uint32_t *font_for_line_height(uint32_t height) {
113156
if(height < 28u) return thai_font_16;
114157
if(height < 34u) return thai_font_20;
@@ -139,6 +182,116 @@ static const thai_glyph_t *glyph_record(const uint32_t *font, uint32_t codepoint
139182
return (const thai_glyph_t *)(data + records_offset + record_index * 12u);
140183
}
141184

185+
static uint16_t cache_slot_bytes_for_font(const uint32_t *font) {
186+
const uint8_t *data = (const uint8_t *)(uintptr_t)thai_font_data_address;
187+
uint32_t size_index = font[8];
188+
if(size_index >= SIZE_COUNT) return 0;
189+
uint32_t records_offset = read_u32(data + 12);
190+
uint32_t largest = 0;
191+
for(uint32_t index = 0; index < GLYPH_COUNT; index++) {
192+
const thai_glyph_t *glyph = (const thai_glyph_t *)(
193+
data + records_offset + (size_index * GLYPH_COUNT + index) * 12u);
194+
if(!glyph->present) continue;
195+
uint32_t cache_stride = ((uint32_t)glyph->box_w + 3u) & ~3u;
196+
uint32_t bytes = cache_stride * glyph->box_h;
197+
if(bytes > largest) largest = bytes;
198+
}
199+
if(!largest || largest > THAI_CACHE_BUDGET || largest > 0xFFFFu) return 0;
200+
return (uint16_t)largest;
201+
}
202+
203+
static thai_runtime_font_t *runtime_font_from(const uint32_t *font) {
204+
if(!writable_ram_range(font, sizeof(thai_runtime_font_t))) return 0;
205+
thai_runtime_font_t *runtime = (thai_runtime_font_t *)(uintptr_t)font;
206+
if(runtime->magic != THAI_RUNTIME_MAGIC || !runtime->slot_bytes ||
207+
!runtime->slot_count || runtime->slot_count > THAI_CACHE_SLOTS) return 0;
208+
uint32_t cache_bytes = (uint32_t)runtime->slot_bytes * runtime->slot_count;
209+
if(cache_bytes > THAI_CACHE_BUDGET ||
210+
!writable_ram_range(runtime, sizeof(thai_runtime_font_t) + cache_bytes)) return 0;
211+
return runtime;
212+
}
213+
214+
static const uint32_t *runtime_font_create(const uint32_t *base_font) {
215+
uint16_t slot_bytes = cache_slot_bytes_for_font(base_font);
216+
if(!slot_bytes) return base_font;
217+
uint32_t slot_count = THAI_CACHE_BUDGET / slot_bytes;
218+
if(slot_count > THAI_CACHE_SLOTS) slot_count = THAI_CACHE_SLOTS;
219+
if(!slot_count) return base_font;
220+
uint32_t total = sizeof(thai_runtime_font_t) + slot_count * slot_bytes;
221+
lv_malloc_fn allocate = (lv_malloc_fn)(uintptr_t)LV_MALLOC_THUMB;
222+
thai_runtime_font_t *runtime = (thai_runtime_font_t *)allocate(total);
223+
if(!runtime || !writable_ram_range(runtime, total)) return base_font;
224+
uint8_t *bytes = (uint8_t *)runtime;
225+
for(uint32_t index = 0; index < total; index++) bytes[index] = 0;
226+
for(uint32_t index = 0; index < FONT_WORD_COUNT; index++) {
227+
runtime->font[index] = base_font[index];
228+
}
229+
runtime->magic = THAI_RUNTIME_MAGIC;
230+
runtime->slot_bytes = slot_bytes;
231+
runtime->slot_count = (uint8_t)slot_count;
232+
return runtime->font;
233+
}
234+
235+
static uint8_t *cache_pixels(thai_runtime_font_t *runtime, uint32_t slot) {
236+
return runtime->pixels + slot * runtime->slot_bytes;
237+
}
238+
239+
static uint32_t cache_tick(thai_runtime_font_t *runtime) {
240+
runtime->clock++;
241+
if(!runtime->clock) {
242+
runtime->clock = 1u;
243+
for(uint32_t index = 0; index < runtime->slot_count; index++) {
244+
runtime->slots[index].stamp = 0;
245+
}
246+
}
247+
return runtime->clock;
248+
}
249+
250+
static int cache_restore(thai_runtime_font_t *runtime, const thai_glyph_t *glyph,
251+
uint32_t codepoint, uint8_t *target, uint16_t stride) {
252+
if(!runtime || runtime->busy || stride < glyph->box_w) return 0;
253+
uint32_t cache_stride = ((uint32_t)glyph->box_w + 3u) & ~3u;
254+
if(cache_stride * glyph->box_h > runtime->slot_bytes) return 0;
255+
for(uint32_t slot = 0; slot < runtime->slot_count; slot++) {
256+
if(runtime->slots[slot].codepoint != codepoint) continue;
257+
runtime->busy = 1u;
258+
const uint8_t *source = cache_pixels(runtime, slot);
259+
for(uint32_t y = 0; y < glyph->box_h; y++) {
260+
copy_bytes(target + y * stride, source + y * cache_stride, glyph->box_w);
261+
}
262+
runtime->slots[slot].stamp = cache_tick(runtime);
263+
runtime->busy = 0u;
264+
return 1;
265+
}
266+
return 0;
267+
}
268+
269+
static void cache_store(thai_runtime_font_t *runtime, const thai_glyph_t *glyph,
270+
uint32_t codepoint, const uint8_t *source, uint16_t stride) {
271+
if(!runtime || runtime->busy || stride < glyph->box_w) return;
272+
uint32_t cache_stride = ((uint32_t)glyph->box_w + 3u) & ~3u;
273+
if(cache_stride * glyph->box_h > runtime->slot_bytes) return;
274+
uint32_t selected = 0;
275+
for(uint32_t slot = 0; slot < runtime->slot_count; slot++) {
276+
if(!runtime->slots[slot].codepoint) {
277+
selected = slot;
278+
break;
279+
}
280+
if(runtime->slots[slot].stamp < runtime->slots[selected].stamp) selected = slot;
281+
}
282+
runtime->busy = 1u;
283+
uint8_t *destination = cache_pixels(runtime, selected);
284+
for(uint32_t y = 0; y < glyph->box_h; y++) {
285+
copy_bytes(destination + y * cache_stride, source + y * stride, glyph->box_w);
286+
for(uint32_t x = glyph->box_w; x < cache_stride; x++) {
287+
destination[y * cache_stride + x] = 0;
288+
}
289+
}
290+
runtime->slots[selected].codepoint = codepoint;
291+
runtime->slots[selected].stamp = cache_tick(runtime);
292+
runtime->busy = 0u;
293+
}
294+
142295
__attribute__((used, noinline))
143296
bool thai_get_glyph_dsc(const uint32_t *font, void *glyph_dsc,
144297
uint32_t codepoint, uint32_t next_codepoint) {
@@ -173,6 +326,16 @@ const void *thai_get_glyph_bitmap(void *glyph_dsc, void *draw_buf) {
173326
uint8_t *draw = (uint8_t *)draw_buf;
174327
uint16_t stride = read_u16(draw + 8);
175328
uint8_t *target = (uint8_t *)(uintptr_t)read_u32(draw + 16);
329+
if(!target || stride < glyph->box_w) return 0;
330+
331+
thai_runtime_font_t *runtime = runtime_font_from(font);
332+
if(cache_restore(runtime, glyph, codepoint, target, stride)) {
333+
const uint32_t *handlers = (const uint32_t *)(uintptr_t)read_u32(draw + 24);
334+
if(handlers && handlers[4]) {
335+
((flush_cache_fn)(uintptr_t)handlers[4])(draw_buf, 0);
336+
}
337+
return draw_buf;
338+
}
176339

177340
for(uint32_t y = 0; y < glyph->box_h; y++) {
178341
const uint8_t *row = source + y * glyph->row_bytes;
@@ -197,6 +360,8 @@ const void *thai_get_glyph_bitmap(void *glyph_dsc, void *draw_buf) {
197360
}
198361
}
199362

363+
cache_store(runtime, glyph, codepoint, target, stride);
364+
200365
const uint32_t *handlers = (const uint32_t *)(uintptr_t)read_u32(draw + 24);
201366
if(handlers && handlers[4]) {
202367
((flush_cache_fn)(uintptr_t)handlers[4])(draw_buf, 0);
@@ -247,15 +412,15 @@ void thai_text_encoded_letter_next_2(const char *text, uint32_t *letter,
247412
}
248413

249414
static int is_thai_font(const uint32_t *font) {
250-
return font == thai_font_16 || font == thai_font_20 ||
415+
if(font == thai_font_16 || font == thai_font_20 ||
251416
font == thai_font_24 || font == thai_font_28 ||
252417
font == thai_font_32 || font == thai_font_36 ||
253-
font == thai_font_40 || font == thai_font_48;
418+
font == thai_font_40 || font == thai_font_48) return 1;
419+
return runtime_font_from(font) != 0;
254420
}
255421

256422
static int writable_ram_node(const uint32_t *node) {
257-
uintptr_t address = (uintptr_t)node;
258-
return address >= WRITABLE_RAM_BASE && address < WRITABLE_RAM_END;
423+
return writable_ram_range(node, 32u);
259424
}
260425

261426
/* Appending must be idempotent: the stock builder runs again whenever a new
@@ -274,7 +439,8 @@ void *thai_chain_append(void *chain_ptr) {
274439
/* Only a verified writable stock node may receive the fallback
275440
pointer; XIP or flash tails are left untouched. */
276441
if(!writable_ram_node(last)) return chain_ptr;
277-
last[7] = (uint32_t)(uintptr_t)font_for_line_height(root[3]);
442+
const uint32_t *base_font = font_for_line_height(root[3]);
443+
last[7] = (uint32_t)(uintptr_t)runtime_font_create(base_font);
278444
return chain_ptr;
279445
}
280446
if(is_thai_font((const uint32_t *)(uintptr_t)next)) return chain_ptr;

patches/thai_patches.json

Lines changed: 17 additions & 16 deletions
Large diffs are not rendered by default.

patches/webflasher_case_usb_thai.patch

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ index 0000000..3e96428
146146
+
147147
+export const LOCAL_G2_TEMPLE_FLASH_TARGETS = Object.freeze([
148148
+ Object.freeze({
149-
+ imageSha256: "6d10399a7fec28d2389c7be7ffd293b0ea37beb6771f9de470336e826a54600f",
150-
+ mainSha256: "5a78331d285c5e4aaaca8acfeed252f7f02d6942ffe917c8701c9470b136041b",
151-
+ mainBytes: 3832742,
149+
+ imageSha256: "749cf09ceea707620a79b029a1f02e0101b1d7bb44d09d9b700889d77307961b",
150+
+ mainSha256: "f4b1a36f7d2e1837def914c8b8eefc8c6cae66bea6fc44097f1fcf925d9b0e3c",
151+
+ mainBytes: 3834766,
152152
+ version: "2.2.9.22",
153153
+ reportedVersion: "2.2.9.22",
154154
+ label: "Thai G2 2.2.9.22 · 2005_iannnnnAMD · local Case USB only",

tests/test_patch.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
VALID_CODEPOINTS,
2222
_thin_mask,
2323
)
24-
from generate_patch import encode_bl, encode_bw
24+
from generate_patch import LV_MALLOC_SITE, encode_bl, encode_bw
2525
from render_preview import shape_for_preview
2626
from verify_firmware import verify
2727

@@ -228,13 +228,19 @@ def test_callback_pins_authenticated_iar_descriptor_layout(self) -> None:
228228
self.assertIn("#define GLYPH_DSC_GID_OFFSET 24u", source)
229229
self.assertIn("#define STOCK_CHAIN_BUILD_THUMB 0x00470989u", source)
230230
self.assertIn("#define STOCK_DECODE_SLOT_INDIRECT 0x00491F14u", source)
231+
self.assertIn("#define LV_MALLOC_THUMB 0x00458383u", source)
231232
self.assertNotIn("STOCK_UTF8_NEXT_THUMB", source)
232233
self.assertIn("uint32_t *active_offset = offset ? offset : &local_offset;", source)
233234
self.assertIn("next = decode(text + *active_offset, 0);", source)
234235
self.assertIn("if(!raise_tone && *active_offset >= 6u)", source)
235236
self.assertNotIn("previous_codepoint(", source)
236237
self.assertIn("a4_to_a8[16]", source)
237238
self.assertIn("a4_to_a8_pair[256]", source)
239+
self.assertIn("static int cache_restore(", source)
240+
self.assertIn("static void cache_store(", source)
241+
self.assertIn("return runtime->font;", source)
242+
self.assertEqual(self.spec["metadata"]["lv_malloc_address"], "0x00458382")
243+
self.assertEqual(LV_MALLOC_SITE[0], 0x00458382)
238244

239245
def test_chain_append_only_writes_writable_ram(self) -> None:
240246
source = (ROOT / "patches/thai_font.c").read_text()

0 commit comments

Comments
 (0)