From 49337c72bcf64d8fca56ea85840875d47d066c26 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:20:05 +0800 Subject: [PATCH] =?UTF-8?q?perf(gsub):=20=E7=BC=93=E5=AD=98=20isSubtableSk?= =?UTF-8?q?ipableByCoverage=20per-subtable=20=E7=BB=93=E6=9E=9C=EF=BC=8Cse?= =?UTF-8?q?rialize=20=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subsetGSUB 预扫描对每个子表调 isSubtableSkipableByCoverage(判全空),serialize 阶段 serializeSubtable 内部对同一子表再调一次——重复。预扫描原 allEmpty=false 时 break 早退, 仅记录 lookup 级 allEmpty 布尔,serialize 无法复用逐子表结果。 改为预扫描完整扫描填 subtableSkipable: boolean[](与 subtableAbsOffs 同序),serializeSubtable 加 preCheckedSkipable 参数,serialize 主循环传 lk.subtableSkipable[j] 跳过内部重复预检。 allEmpty 从数组派生(全 true)。 安全性依赖 [[covCache 修复]](上一提交):isSubtableSkipableByCoverage 对 fmt3 调 readCoverageRemapped,covCache 顺序依赖 bug 已修复后结果真正顺序无关,故完整预扫描 改变填充顺序不再触发 FiraCode calt 输出变化。 A/B:6 字体(思源/白狐/令东/思源ttf/FiraCode/初夏)× ttf/woff2 共 12 产物逐字节 IDENTICAL。 性能(best-of-min):otf-思源 -9.2%、FiraCode -2.9%、初夏纯标点 -3.8%、白狐 -2.0%、 思源ttf -1.3%、令东持平(无大 GSUB)。基准测试全通过,SSIM 维持。 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/font_util/gsub-subset.ts | 36 +++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/backend/font_util/gsub-subset.ts b/backend/font_util/gsub-subset.ts index 0fe769d..cbada0e 100644 --- a/backend/font_util/gsub-subset.ts +++ b/backend/font_util/gsub-subset.ts @@ -1274,12 +1274,16 @@ function serializeSubtable( origToNew: Map, covCache: CoverageCache, gidLookup: GidLookup, + /** 预扫描已判定的 skipable 结果。传入时跳过内部 isSubtableSkipableByCoverage 重复预检 + * (subsetGSUB 主循环预扫描已对每个子表判过,结果只依赖 coverage 字节+gidLookup 不变,可复用)。 */ + preCheckedSkipable: boolean | undefined, ): boolean { r.clearError(); /** 预检:主 coverage(或 fmt3 的三组 coverage)全子集外则直接判失败(输出空 subtable), * 跳过昂贵的深度解析。FiraCode 403 lookup 中 ~330 个可预检跳过(type1-4 + fmt1 + fmt3 失效), - * format2 不预检(class 驱动,主 coverage 非充分条件)。 */ - if (isSubtableSkipableByCoverage(r, off, type, gidLookup, covCache)) return false; + * format2 不预检(class 驱动,主 coverage 非充分条件)。 + * 预扫描已判时直接用其结果(消除重复预检),否则现场判。 */ + if (preCheckedSkipable !== undefined ? preCheckedSkipable : isSubtableSkipableByCoverage(r, off, type, gidLookup, covCache)) return false; let ok: boolean; switch (type) { case LT_SINGLE: @@ -1365,6 +1369,12 @@ export function subsetGSUB( * 含 format2 class 驱动子表的 lookup,isSubtableSkipableByCoverage 返回 false,故 allEmpty 必为 false, * 走原逐子表路径(保守,与 FiraCode 连字安全要求一致 [[gsub-lookup-deletion-failed-fira]])。 */ allEmpty: boolean; + /** 每个子表的 isSubtableSkipableByCoverage 预扫描结果(与 subtableAbsOffs 同序)。 + * 预扫描(下方)填,serialize 阶段复用——避免 serializeSubtable 内部对同一子表再调一次 + * isSubtableSkipableByCoverage(思源 56 lookup × 多子表,预扫描与 serialize 各判一次=重复)。 + * 结果只依赖 coverage 字节 + gidLookup(subsetGSUB 内均不变),与 covCache 状态无关 + * (covCache 顺序依赖 bug 已修复,readCoverageRemapped 命中任意 entry 结果正确)。 */ + subtableSkipable: boolean[]; } const lookups: LookupInfo[] = []; for (let i = 0; i < lookupCount; i++) { @@ -1395,7 +1405,7 @@ export function subsetGSUB( effectiveType === LT_ALTERNATE || effectiveType === LT_LIGATURE || effectiveType === LT_CHAIN; - lookups.push({ supported, effectiveType, subtableAbsOffs, origLookupOff: lOff, allEmpty: false }); + lookups.push({ supported, effectiveType, subtableAbsOffs, origLookupOff: lOff, allEmpty: false, subtableSkipable: [] }); } /** @@ -1412,13 +1422,19 @@ export function subsetGSUB( for (let i = 0; i < lookupCount; i++) { const lk = lookups[i]; if (!lk.supported) continue; - let allEmpty = lk.subtableAbsOffs.length > 0; - for (let j = 0; j < lk.subtableAbsOffs.length; j++) { - if (!isSubtableSkipableByCoverage(r, lk.subtableAbsOffs[j], lk.effectiveType, gidLookup, covCache)) { - allEmpty = false; - break; - } + /** 完整扫描每个子表记录 skipable(供 serialize 复用,避免 serializeSubtable 内部重复预检)。 + * 原实现 allEmpty=false 时 break 早退省后续判定,但 serialize 阶段对每个子表再调一次 + * isSubtableSkipableByCoverage(type1/2/3/4 主 coverage + fmt3 三组 coverage)。缓存完整结果后 + * serialize 直接查数组,消除第二次预检。allEmpty = 全部 skipable(数组全 true)。 */ + const subs = lk.subtableAbsOffs; + const skipable = new Array(subs.length); + let allEmpty = subs.length > 0; + for (let j = 0; j < subs.length; j++) { + const sk = isSubtableSkipableByCoverage(r, subs[j], lk.effectiveType, gidLookup, covCache); + skipable[j] = sk; + if (!sk) allEmpty = false; } + lk.subtableSkipable = skipable; lk.allEmpty = allEmpty; } @@ -1529,7 +1545,7 @@ export function subsetGSUB( * 不再用 copyBytesBlock 按估算范围拷贝——原始 subtable 数据可能与其他 lookup 物理交错, * 按 lookup 边界估算会拷贝到错误字节(霞鹜文楷实测 subtable 在表头区之后)。 */ const before = w.length; - const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, origToNew, covCache, gidLookup); + const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, origToNew, covCache, gidLookup, lk.subtableSkipable[j]); if (!ok) { w.rollback(before); writeEmptySubtable(w, lk.effectiveType);