feat(sdk): SDK 矢量字(meow_ft_*,§10.1)——上收阅读器 FreeType 管线

问题:SDK 位图字库(meow_font)只有离散字号档位,全字形 atlas 稳态 17.3MiB
(27MB 整机装不下)→ 大字库/大字号必出豆腐。阅读器 R1/R2 的 FreeType 管线已真机验证
(MiSans OTF),本 commit 将其上收 SDK,让任意 App 用上无级字号 + CJK 全字形。

设计(追加式,与位图字库并存):
- meow_ui_ft.c(独立 .o):meow_ft_load/unload/loaded/text/width/line_height/
  ascender/descender/cache_set_slots/cache_slots/cache_used。底层按 READER_ENGINE_NOTES §5:
  FT_New_Face(path) 按需读文件(零整载 anon);灰阶 FT_LOAD_RENDER|NO_HINTING|TARGET_LIGHT;
  glyph LRU key=(px<<32)|cp,默认 512(~0.46MB),可配 256/1024;测宽不栅格化。
- meow_ui_gfx.c 只加两个薄封装 meow_gfx_ft_blend_px/dirty(写 back_buf + 标脏),
  使 meow_ui_ft.o 不引入新依赖——「不调 meow_ft_* 的 App 零增量」(freetype 不拉入,已验证)。
- 降级:加载失败/无卡 → meow_ft_loaded()==0 → meow_ft_text 自动改走 meow_ui_text(位图字库,
  5x7 兜底),绝不豆腐不崩;meow_ft_width/line_height 降级位图字库度量。
- FreeType 2.14.3 vendor 进 patch_v2/sdk/vendor/(gitignore)+ build_sdk.sh 可复现拉取+裁剪
  构建(-DFT_DISABLE_ZLIB/BZIP2/PNG/HARFBUZZ/BROTLI=TRUE,静态 musl),freetype .o 编入
  meow_ui.a,App 链接命令不变(meow_ui.a libz.a -lpthread)。

验证:
- 宿主 test_meow_ui_ft 5 项全 PASS(加载/渲染像素非空/宽度测量/LRU 淘汰/降级回退)。
- 全量 SDK 回归:widgets/sdk_fixes/dispatch/notif_gesture/overlay_suppress/listvirt/box/anim/
  scroll/ime_fix/pinyin/notif_open/sb_collapse/theme/notify_ipc/audio_host/vol/str/config/
  sdk_input_keys 全 PASS。
- 交叉 -Wall -Wextra 全 SDK TU 0 warning;qemu-mipsel 冒烟 PASS(mipsel FreeType 真光栅化)。
- meow_ui.a 新 md5 6ff5be9455f9e197b35fa120be012041(未重链不部署 App,四 App 免重链)。

MEOW_APP_SDK.md 补 §10.1 矢量字一节 + 公共头索引/§9 构建约定更新;.gitignore 忽略
patch_v2/sdk/vendor/ 与 test_meow_ui_ft 二进制。
This commit is contained in:
AskaEth committed 2026-09-02 09:51:23 +08:00
1 parent 121d30ec95
commit 54750a63c5
9 files changed
+542 -6

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
/*
* meow_ui_ft.c — SDK 矢量字(FreeType)封装。
*
* 上收阅读器 R1/R2 已验证的 FreeType 管线(READER_ENGINE_NOTES §5),让任何 App 用上
* 无级字号 + CJK 全字形(解决大字库豆腐类问题的根)。与位图字库**并存**:App 显式调用
* meow_ft_* 才用矢量;既有 meow_ui_text 路径零改动。
*
* 内存纪律(对齐 READER_ENGINE_NOTES §5.1/§5.4):
* - FT_New_Face(path) 按需读文件(stream->base=NULL,不整文件载入 → 零大块 anon;
* 文件是 disk-backed,可被内核回收重读)。不要 mmap 后 FT_New_Memory_Face。
* - 灰阶 FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT(平滑矢量)。
* - glyph LRU 缓存:key=(px<<32)|cp;默认 512(~0.46MB,28px CJK 约 900B/字),
* 可配 256 省内存 / 1024 提命中。目标单 App 增量 ≤1.5MB(FreeType 本体按需拉取)。
* - 测宽(meow_ft_advance)只 FT_Load_Glyph 不 RENDER,不占 LRU 槽。
*
* 本 TU 只含 FreeType 封装逻辑(load/glyph/advance/度量/cache),渲染进 back_buf 的
* meow_ft_text / meow_ft_width 在 meow_ui_gfx.c(需访问 back_buf/clip/put_px_blend)。
* 无 gfx 依赖 —— 纯宿主编译 + libfreetype.a 即可单测(见 test_meow_ui_ft.c)。
*/
#include "meow_ui.h" /* 公共原型(meow_ui_font.h 含 meow_ft_*) */
#include "meow_ui_internal.h" /* meow_ft_glyph/advance 内部签名 */
#include <ft2build.h>
#include FT_FREETYPE_H
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
uint64_t key; /* (px<<32)|cp */
uint64_t stamp; /* LRU 单调戳 */
int adv; /* 水平 advance(px) */
uint8_t *bmp; /* 灰阶位图(紧凑,每行 bw 字节;bw==0 时 NULL) */
int bw, bh, left, top;
} mft_glyph;
typedef struct {
FT_Library lib;
FT_Face face;
int cur_px; /* 当前 FT_Set_Pixel_Sizes 字号(0=未设) */
mft_glyph *slots;
int nslots;
int used;
uint64_t clock;
} mft;
static mft *g_mft = NULL; /* App 单进程单例(主线程渲染,无并发) */
/* 惰性初始化 FreeType 运行时(幂等)。 */
static mft *mft_get(void)
{
if (g_mft) return g_mft;
mft *x = (mft *)calloc(1, sizeof *x);
if (!x) return NULL;
if (FT_Init_FreeType(&x->lib) != 0) { free(x); return NULL; }
x->nslots = 512; /* 默认 */
x->slots = (mft_glyph *)calloc((size_t)x->nslots, sizeof(mft_glyph));
if (!x->slots) { FT_Done_FreeType(x->lib); free(x); return NULL; }
x->used = 0;
x->clock = 1;
g_mft = x;
return x;
}
/* 清空 glyph 缓存并释放各槽位图(换字体/换字号/换容量时)。 */
static void mft_cache_flush(mft *x)
{
for (int i = 0; i < x->nslots; i++) free(x->slots[i].bmp);
memset(x->slots, 0, (size_t)x->nslots * sizeof(mft_glyph));
x->used = 0;
}
int meow_ft_load(const char *path)
{
if (!path) return -1;
mft *x = mft_get();
if (!x) return -3;
if (x->face) { FT_Done_Face(x->face); x->face = NULL; }
mft_cache_flush(x);
x->cur_px = 0;
if (FT_New_Face(x->lib, path, 0, &x->face) != 0) return -1; /* 无字体 → loaded()==0 → 降级位图 */
return 0;
}
void meow_ft_unload(void)
{
if (!g_mft) return;
if (g_mft->face) { FT_Done_Face(g_mft->face); g_mft->face = NULL; }
mft_cache_flush(g_mft);
g_mft->cur_px = 0;
}
int meow_ft_loaded(void) { return (g_mft && g_mft->face) ? 1 : 0; }
/* 定像素字号(仅当与当前不同才调 FT_Set_Pixel_Sizes)。
* 不在此清缓存:glyph key 已含字号,不同字号位图可共存于 LRU(同 face 的 size 切换只影响
* 后续光栅化的 26.6 度量,已缓存的位图是独立拷贝,仍有效)。排序器/同页多字号不互相驱逐。 */
static int mft_setpx(mft *x, int px)
{
if (!x->face || px <= 0) return -1;
if (x->cur_px != px) {
FT_Set_Pixel_Sizes(x->face, 0, (FT_UInt)px);
x->cur_px = px;
}
return 0;
}
static mft_glyph *mft_lru_find(mft *x, uint64_t key)
{
for (int i = 0; i < x->nslots; i++)
if (x->slots[i].key == key && x->slots[i].stamp) return &x->slots[i];
return NULL;
}
/* 取一个槽位:优先空槽(stamp==0),否则逐出最久未用;返回槽指针。 */
static mft_glyph *mft_lru_evict(mft *x)
{
mft_glyph *oldest = NULL;
uint64_t mins = UINT64_MAX;
for (int i = 0; i < x->nslots; i++) {
if (!x->slots[i].stamp) { x->used++; return &x->slots[i]; }
if (x->slots[i].stamp < mins) { mins = x->slots[i].stamp; oldest = &x->slots[i]; }
}
free(oldest->bmp);
oldest->bmp = NULL; oldest->key = 0; oldest->stamp = 0;
return oldest;
}
int meow_ft_glyph(int px, uint32_t cp, int *adv_x,
const uint8_t **bmp, int *bw, int *bh, int *left, int *top)
{
if (!adv_x || !bmp) return -1;
mft *x = g_mft;
if (!x || !x->face) return -1; /* 无已载字体 → 调用方降级位图 */
mft_setpx(x, px);
if (x->clock == UINT64_MAX) x->clock = 1; /* 时钟回绕(罕见) */
uint64_t key = ((uint64_t)(uint32_t)px << 32) | cp;
mft_glyph *s = mft_lru_find(x, key);
if (s) {
s->stamp = x->clock++;
*adv_x = s->adv; *bmp = s->bmp; *bw = s->bw; *bh = s->bh; *left = s->left; *top = s->top;
return 0;
}
FT_UInt gi = FT_Get_Char_Index(x->face, cp);
if (FT_Load_Glyph(x->face, gi, FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT) != 0) {
*adv_x = (int)(x->face->glyph->advance.x >> 6);
*bmp = NULL; *bw = *bh = *left = *top = 0;
return 0;
}
FT_GlyphSlot g = x->face->glyph;
int a = (int)((g->advance.x >= 0 ? g->advance.x + 0x20 : g->advance.x - 0x20) >> 6);
if (a < 0) a = 0;
s = mft_lru_evict(x);
s->key = key; s->stamp = x->clock++; s->adv = a;
s->bmp = NULL; s->bw = s->bh = s->left = s->top = 0;
s->left = g->bitmap_left; s->top = g->bitmap_top;
if (g->bitmap.buffer && g->bitmap.width > 0 && g->bitmap.rows > 0) {
int w = (int)g->bitmap.width, rows = (int)g->bitmap.rows, pitch = (int)g->bitmap.pitch;
uint8_t *b = (uint8_t *)malloc((size_t)w * (size_t)rows);
if (b) {
for (int rr = 0; rr < rows; rr++)
memcpy(b + (size_t)rr * (size_t)w, g->bitmap.buffer + (size_t)rr * (size_t)pitch, (size_t)w);
s->bmp = b; s->bw = w; s->bh = rows; /* 紧凑:每行 bw 字节 */
}
}
*adv_x = s->adv; *bmp = s->bmp; *bw = s->bw; *bh = s->bh; *left = s->left; *top = s->top;
return 0;
}
int meow_ft_advance(int px, uint32_t cp, int *adv_x)
{
if (!adv_x) return -1;
mft *x = g_mft;
if (!x || !x->face) return -1; /* 无已载字体 → 调用方降级位图 */
mft_setpx(x, px);
FT_UInt gi = FT_Get_Char_Index(x->face, cp);
if (FT_Load_Glyph(x->face, gi, FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT) != 0) {
*adv_x = (int)(x->face->glyph->advance.x >> 6);
return 0;
}
int a = (int)((x->face->glyph->advance.x >= 0 ? x->face->glyph->advance.x + 0x20
: x->face->glyph->advance.x - 0x20) >> 6);
*adv_x = a < 0 ? 0 : a;
return 0;
}
int meow_ft_ascender(int px)
{
mft *x = g_mft;
if (!x || !x->face) return px > 0 ? px : 0; /* 无字体:px 近似(降级) */
mft_setpx(x, px);
return (int)(x->face->size->metrics.ascender >> 6);
}
int meow_ft_descender(int px)
{
mft *x = g_mft;
if (!x || !x->face) return 0;
mft_setpx(x, px);
return (int)(x->face->size->metrics.descender >> 6);
}
int meow_ft_line_height(int px)
{
return meow_ft_ascender(px) - meow_ft_descender(px) + 1;
}
int meow_ft_cache_set_slots(int slots)
{
mft *x = mft_get();
if (!x) return 0;
if (slots < 1) slots = 1;
if (slots > 4096) slots = 4096;
if (slots == x->nslots) return x->nslots;
mft_glyph *na = (mft_glyph *)calloc((size_t)slots, sizeof(mft_glyph));
if (!na) return x->nslots;
for (int i = 0; i < x->nslots; i++) free(x->slots[i].bmp);
free(x->slots);
x->slots = na; x->nslots = slots; x->used = 0;
return x->nslots;
}
int meow_ft_cache_slots(void) { return g_mft ? g_mft->nslots : 0; }
int meow_ft_cache_used(void) { return g_mft ? g_mft->used : 0; }
/* ---------------- UTF-8 解码(渲染/测宽逐码点) ---------------- */
static int ft_utf8_next(const unsigned char **pp)
{
const unsigned char *s = *pp;
unsigned char c = s[0];
if (c < 0x80) { *pp += 1; return c; }
int n; uint32_t cp;
if ((c & 0xE0) == 0xC0) { n = 2; cp = c & 0x1F; }
else if ((c & 0xF0) == 0xE0) { n = 3; cp = c & 0x0F; }
else if ((c & 0xF8) == 0xF0) { n = 4; cp = c & 0x07; }
else { *pp += 1; return -1; }
for (int i = 1; i < n; i++) {
if ((s[i] & 0xC0) != 0x80) { *pp += 1; return -1; }
cp = (cp << 6) | (s[i] & 0x3F);
}
*pp += n;
return (int)cp;
}
int meow_ft_text(int x, int y, const char *utf8, int px, uint32_t color)
{
if (!utf8) return -2;
if (!meow_ft_loaded()) {
meow_ui_text(x, y, utf8, px, color); /* 降级位图字库(meow_font/5x7 兜底),不出豆腐 */
return -1;
}
int baseline = y + meow_ft_ascender(px);
int pen = 0;
const unsigned char *p = (const unsigned char *)utf8;
int cp;
while (*p && (cp = ft_utf8_next(&p)) > 0) {
int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
const uint8_t *bmp = NULL;
if (meow_ft_glyph(px, (uint32_t)cp, &adv, &bmp, &bw, &bh, &left, &top) != 0) break;
if (bmp && bw > 0 && bh > 0) {
int bx = x + pen + left;
int ty = baseline - top;
for (int row = 0; row < bh; row++) {
const uint8_t *src = bmp + (size_t)row * (size_t)bw;
int py = ty + row;
for (int col = 0; col < bw; col++)
if (src[col]) meow_gfx_ft_blend_px(bx + col, py, color, (int)src[col]);
}
}
pen += adv;
}
if (pen > 0) meow_gfx_ft_dirty(x, y, pen, meow_ft_line_height(px));
return 0;
}
int meow_ft_width(const char *utf8, int px)
{
if (!utf8) return 0;
if (!meow_ft_loaded()) return meow_ui_text_width(utf8, px); /* 降级位图字库 */
int total = 0;
const unsigned char *p = (const unsigned char *)utf8;
int cp;
while (*p && (cp = ft_utf8_next(&p)) > 0) {
int a = 0;
if (meow_ft_advance(px, (uint32_t)cp, &a) != 0) continue;
total += a;
}
return total;
}