perf(gsub): 缓存 isSubtableSkipableByCoverage per-subtable 结果,serialize 复用

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) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-25 03:20:05 +08:00
parent d0dbbece82
commit 49337c72bc

View File

@ -1274,12 +1274,16 @@ function serializeSubtable(
origToNew: Map<number, number>,
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 lookupisSubtableSkipableByCoverage false allEmpty false
* FiraCode [[gsub-lookup-deletion-failed-fira]] */
allEmpty: boolean;
/** isSubtableSkipableByCoverage subtableAbsOffs
* serialize serializeSubtable
* isSubtableSkipableByCoverage 56 lookup × serialize =
* coverage + gidLookupsubsetGSUB 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
* isSubtableSkipableByCoveragetype1/2/3/4 coverage + fmt3 coverage
* serialize allEmpty = skipable true */
const subs = lk.subtableAbsOffs;
const skipable = new Array<boolean>(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);