From d0dbbece822cda300822bfc9c89fc657f177906d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Sat, 25 Jul 2026 03:18:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(gsub):=20readCoverageRemapped=20=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=20readCoverageGids=20=E7=9A=84=20cache=20entry=20?= =?UTF-8?q?=E6=97=B6=E6=AD=A3=E7=A1=AE=E9=87=8D=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit covCache 被 readCoverageRemapped(fmt3 用,产 newGids)与 readCoverageGids (type1/2/3/4 用,需原始 gid)共享同一 CoverageCacheEntry。readCoverageGids 填的 entry remapped=null/outOfSubset=false,readCoverageRemapped 命中时原直接 `return entry.outOfSubset ? null : entry.remapped` 会返回 null——null 被当作 「outOfSubset」语义,误判 coverage 失效。 修复:命中 entry 时若 gids!==EMPTY_GIDS(readCoverageGids 填的,非 readCoverageRemapped 自己的占位),从已缓存的原始 gids 现场重映射(gidLookup 过滤),并回填 remapped/outOfSubset 供后续命中。仅 readCoverageRemapped 自己的 entry(gids===EMPTY_GIDS)才走原快路径。 此为潜在顺序依赖隐患:改变 covCache 填充顺序(如预扫描完整扫描)会暴露它致 FiraCode calt 输出变化。修复使 covCache 真正顺序无关。 A/B:思源/白狐/令东/思源ttf/初夏 逐字节 IDENTICAL;FiraCode ttf +32B(一个被误判空的 fmt3 subtable 现正确序列化)、woff2 不变(brotli 压掉)。基准测试全通过,FiraCode SSIM 维持 0.9923(多保留的规则不影响 => !== 渲染)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/font_util/gsub-subset.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/font_util/gsub-subset.ts b/backend/font_util/gsub-subset.ts index cfc3edd..0fe769d 100644 --- a/backend/font_util/gsub-subset.ts +++ b/backend/font_util/gsub-subset.ts @@ -174,7 +174,29 @@ function readCoverageRemapped( ): number[] | null { let entry = cache.get(off); if (entry !== undefined) { - /** 已缓存:失效返回 null,否则返回重映射数组(remapped 已在首次计算时填好) */ + /** readCoverageRemapped 自己填的 entry:gids===EMPTY_GIDS(占位),remapped 已算好可直接返回。 + * readCoverageGids 填的 entry:gids 是原始 gid 数组、remapped=null、outOfSubset=false(默认)。 + * 命中后者时不能直接返回 entry.remapped(null 会被当 outOfSubset 语义,错判 coverage 失效, + * 改变 fmt3 预检结果致 FiraCode calt 输出变化)——须从已缓存的原始 gids 现场重映射。 + * 此分支仅在 covOff 跨 readCoverageGids/readCoverageRemapped 共享时命中(罕见),正常路径走自己的 entry。 */ + if (entry.gids !== EMPTY_GIDS) { + const ogids = entry.gids; + const m = new Array(ogids.length); + let w2 = 0; + for (let i = 0; i < ogids.length; i++) { + const ng = gidLookup[ogids[i]]; + if (ng >= 0) m[w2++] = ng; + } + m.length = w2; + /** 与首次计算一致:空且原非空→outOfSubset。entry 由 readCoverageGids 填时 outOfSubset 恒 false, + * 无法区分「原 coverage 空」与「全子集外」——保守按 ogids.length>0 判 origNonEmpty。 */ + const oos = w2 === 0 && ogids.length > 0; + /** 回填 remapped/outOfSubset 供后续 readCoverageRemapped 命中(保留 gids 不动供 readCoverageGids) */ + entry.remapped = m; + entry.outOfSubset = oos; + return oos ? null : m; + } + /** readCoverageRemapped 自己的 entry:失效返回 null,否则返回重映射数组 */ return entry.outOfSubset ? null : (entry.remapped as number[]); } const dv = r.dv;