perf(gsub-reachable): 大 coverage 反转路径用字节二分省掉数组展开

collectReachableGsubTargets 的 SingleSubst 反转遍历路径(reachable 小、coverage 大)
原 readCoverageGids 先把 coverage 全部 gid 展开到数组再二分。思源 locl lookup 的 coverage
达 8881/12000/11632 gid,展开是 reach 阶段 #1 热点(0.060ms / 0.165ms)。

新增 coverageIndexOf(format1 二分 / format2 range 二分+累计)与 coverageCount(不展开得
gid 总数),反转路径直接对 coverage 字节二分得 index,完全省掉大数组展开:
- 思源 collectReachable 0.165→0.042ms(-74%)
- 白狐 0.026→0.007ms(-73%)
- FiraCode 持平(coverage 小不走反转路径)

reachable 结果逐字体一致(思源 19 / 白狐 4 / FiraCode 89),基准测试全过:
思源 OTF ssim=1.0000、FiraCode 0.9923 保持。

otf-subset 顺带 findTable 去重(reachable 收集与布局表子集化共用提前解析的 GSUB/GPOS)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-25 00:27:56 +08:00
parent 07432852cf
commit f1c8315b0c
2 changed files with 137 additions and 46 deletions

View File

@ -131,6 +131,89 @@ function coverageFirstExcludedGid(
return -1;
}
/**
* coverage gid 0-based gid gid
*
* SingleSubst format2 / AlternateSubst reachable coverage
* readCoverageGids coverage gid locl lookup coverage 8881/12000/11632 gid
* reachable reach #1 0.060ms / 0.165ms
* coverage
* - format1gid log2(count) getUint16
* - format2range gid range = range range gid + (gid - start)
*
* @param off coverage
* @param gid gid
* @returns gid coverage -1
*/
function coverageIndexOf(r: OTReader, off: number, gid: number): number {
const dv = r.dv;
const format = dv.getUint16(off, false);
if (format === 1) {
const count = dv.getUint16(off + 2, false);
const base = off + 4;
let lo = 0, hi = count;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const mg = dv.getUint16(base + mid * 2, false);
if (mg < gid) lo = mid + 1;
else if (mg > gid) hi = mid;
else return mid;
}
return -1;
} else if (format === 2) {
const rangeCount = dv.getUint16(off + 2, false);
const base = off + 4;
/** 二分找第一个 end >= gid 的 rangerange 按 start 升序,故 end 也升序) */
let lo = 0, hi = rangeCount;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const end = dv.getUint16(base + mid * 6 + 2, false);
if (end < gid) lo = mid + 1;
else hi = mid;
}
if (lo >= rangeCount) return -1;
const rangeOff = base + lo * 6;
const start = dv.getUint16(rangeOff, false);
const end = dv.getUint16(rangeOff + 2, false);
if (gid < start || gid > end) return -1;
/** = range gid + (gid - start)range coverage format1
* 线range 退 O(rangeCount) O(totalGids) */
let prefix = 0;
for (let i = 0; i < lo; i++) {
const s = dv.getUint16(base + i * 6, false);
const e = dv.getUint16(base + i * 6 + 2, false);
prefix += e - s + 1;
}
return prefix + (gid - start);
}
return -1;
}
/**
* coverage gid
*
* coverage reachable gid locl coverage
* gid #1 format1 countformat2 range (end-start+1)
*/
function coverageCount(r: OTReader, off: number): number {
const dv = r.dv;
const format = dv.getUint16(off, false);
if (format === 1) {
return dv.getUint16(off + 2, false);
} else if (format === 2) {
const rangeCount = dv.getUint16(off + 2, false);
const base = off + 4;
let total = 0;
for (let i = 0; i < rangeCount; i++) {
const s = dv.getUint16(base + i * 6, false);
const e = dv.getUint16(base + i * 6 + 2, false);
total += e - s + 1;
}
return total;
}
return 0;
}
/**
* seedGids GSUB target gid seed
*
@ -193,7 +276,6 @@ export function collectReachableGsubTargets(
* reachable seed gid extraSubsetGids Font.create */
const reachable = new Set<number>(seedGids);
let changed = true;
/** 当前「在子集内」= reachable已含 seed */
const inSubset = (gid: number) => reachable.has(gid);
/** 复用临时 Set避免每个 type6 subtable 两次 new Set 的 GC 压力(初夏明朝 51 lookup 多轮迭代) */
@ -287,47 +369,38 @@ function collectSubtableTargets(
/** SingleSubst: format1 coverage+delta / format2 coverage+gidArray */
const format = r.u16(off);
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff, covCache);
if (format === 1) {
const delta = r.i16(off + 4);
/** covGids reachable
* covGids FiraCode type1 avg 5.5 Set.has > */
if (covGids.length > reachable.size) {
const lim = covGids.length;
if (coverageCount(r, covOff) > reachable.size) {
/** coveragecoverageIndexOf coverage gid
* format1 delta target = (gid + delta) & 0xffff index */
for (const g of reachable) {
let lo = 0, hi = lim;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const mg = covGids[mid];
if (mg < g) lo = mid + 1;
else if (mg > g) hi = mid;
else { targets.push((g + delta) & 0xffff); break; }
}
if (coverageIndexOf(r, covOff, g) >= 0) targets.push((g + delta) & 0xffff);
}
} else {
const covGids = readCoverageGids(r, covOff, covCache);
for (const g of covGids) {
if (inSubset(g)) targets.push((g + delta) & 0xffff);
}
}
} else if (format === 2) {
/** coverage fmt2 covLen 9180/roundreachable 53<4%
* covGids reachable reachable covGids index Set.has
* covGids gid fmt1 list / fmt2 range */
* covGids reachable reachable covGids index Set.has */
const count = r.u16(off + 4);
const lim = covGids.length < count ? covGids.length : count;
const gidArrBase = off + 6;
if (lim > reachable.size) {
if (coverageCount(r, covOff) > reachable.size) {
/** coveragecoverageIndexOf coverage index gid
* locl lookup coverage 8881/12000/11632 gid reach #1
* index gidArrBase+index*2 target */
for (const g of reachable) {
let lo = 0, hi = lim;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const mg = covGids[mid];
if (mg < g) lo = mid + 1;
else if (mg > g) hi = mid;
else { targets.push(dv.getUint16(gidArrBase + mid * 2, false)); break; }
}
const idx = coverageIndexOf(r, covOff, g);
if (idx >= 0 && idx < count) targets.push(dv.getUint16(gidArrBase + idx * 2, false));
}
} else {
const covGids = readCoverageGids(r, covOff, covCache);
const lim = covGids.length < count ? covGids.length : count;
for (let i = 0; i < lim; i++) {
if (inSubset(covGids[i])) targets.push(dv.getUint16(gidArrBase + i * 2, false));
}
@ -474,12 +547,16 @@ function collectChainRefs(
* coverage backtrack/input/lookahead gid
*
* coverage contextGids format3 triggerable=false
* 280 format3 false reachable gid
* failGid contextGids break gid reachable
* inSubset=reachable.has reachable.add no-op
* coverageFirstExcludedGid format2 range
* readCoverageGids + triggerable=true coverage
* readCoverageGids contextGids */
* 281 format3 false 280
* reachable gid failGid contextGids break
* gid reachableinSubset=reachable.has reachable.add no-op
* triggerable=true coverage readCoverageGids contextGids
*
* gid format1/2 coverage gid coverage
* format1: off+4format2: off+4 range start fail fmt3
* coverage gid reachable 281/281 2 + 1 inSubset
* coverageFirstExcludedGid + covCache.get Map gid
* reachable coverageFirstExcludedGid coverage gid */
let p = off + 2;
let triggerable = true;
/** 第一遍:逐 coverage 找首个不在子集的 gid全在子集则 triggerable 保持 true */
@ -488,6 +565,20 @@ function collectChainRefs(
p += 2;
for (let k = 0; k < cnt; k++) {
const covOff = off + dv.getUint16(p + k * 2, false);
/** gid coverage format covOff gid covOff+4format1 list[0] format2 range[0].start
* count>0 gidformat 1/2 coverageFirstExcludedGid -1
* gid reachable coverage excluded failGid */
const covFormat = dv.getUint16(covOff, false);
const covCount = dv.getUint16(covOff + 2, false);
if ((covFormat === 1 || covFormat === 2) && covCount > 0) {
const firstGid = dv.getUint16(covOff + 4, false);
if (!inSubset(firstGid)) {
triggerable = false;
failGid.v = firstGid;
break;
}
}
/** 首 gid 在 reachable完整检查该 coverage 的全部 gid首 gid 之外的可能 excluded */
const excluded = coverageFirstExcludedGid(r, covOff, covCache, inSubset);
if (excluded >= 0) {
triggerable = false;

View File

@ -471,6 +471,11 @@ export function subsetOTF(fontBuffer: Uint8Array, codePoints: number[], keepGSUB
/** 子集字形集 + codepoint→新gid */
const { subsetGids, cpToNewGid } = buildSubsetGids(dv, cmap.offset, codePoints);
/** keepGSUB GSUB/GPOS reachable
* findTable 线 + */
const gsub = keepGSUB ? findTable(tables, "GSUB") : undefined;
const gpos = keepGSUB ? findTable(tables, "GPOS") : undefined;
/** GSUB target locl/calt/liga
* CID locl feature localeGSUB
* "天"gid A CN gid A+1 subsetGids target
@ -478,20 +483,17 @@ export function subsetOTF(fontBuffer: Uint8Array, codePoints: number[], keepGSUB
* SSIM 0.9530"天玄宇法海" ink"宙" ink locl
* ttf collectReachableGsubTargets seed GSUB target gid subsetGids
* GSUB GSUB/ */
if (keepGSUB) {
const gsub = findTable(tables, "GSUB");
if (gsub) {
const gsubBytes = new Uint8Array(dv.buffer, dv.byteOffset + gsub.offset, gsub.length);
/** seed = 当前 subsetGids已含 .notdef + 各 codepoint 的 gid */
const seed = new Set<number>(subsetGids);
const reachable = collectReachableGsubTargets(gsubBytes, seed);
if (reachable.size > 0) {
/** 合并去重:新 gid 顺序追加在现有 subsetGids 之后(不破坏 cpToNewGid 已建立的映射) */
for (const gid of reachable) {
if (gid > 0 && !seed.has(gid)) {
seed.add(gid);
subsetGids.push(gid);
}
if (gsub) {
const gsubBytes = new Uint8Array(dv.buffer, dv.byteOffset + gsub.offset, gsub.length);
/** seed = 当前 subsetGids已含 .notdef + 各 codepoint 的 gid */
const seed = new Set<number>(subsetGids);
const reachable = collectReachableGsubTargets(gsubBytes, seed);
if (reachable.size > 0) {
/** 合并去重:新 gid 顺序追加在现有 subsetGids 之后(不破坏 cpToNewGid 已建立的映射) */
for (const gid of reachable) {
if (gid > 0 && !seed.has(gid)) {
seed.add(gid);
subsetGids.push(gid);
}
}
}
@ -558,12 +560,10 @@ export function subsetOTF(fontBuffer: Uint8Array, codePoints: number[], keepGSUB
/** origToNewsubsetGids[新gid] = 原gid反转得 原gid→新gid */
const origToNew = new Map<number, number>();
for (let newGid = 0; newGid < subsetGids.length; newGid++) origToNew.set(subsetGids[newGid], newGid);
const gsub = findTable(tables, "GSUB");
if (gsub) {
const gsubBytes = new Uint8Array(dv.buffer, dv.byteOffset + gsub.offset, gsub.length);
outTables.push({ tag: "GSUB", bytes: subsetGSUB(gsubBytes, origToNew) });
}
const gpos = findTable(tables, "GPOS");
if (gpos) {
const gposBytes = new Uint8Array(dv.buffer, dv.byteOffset + gpos.offset, gpos.length);
/** subsetGPOS 可能对不支持的版本返回 null此时丢弃该表无 GPOS 仅丢失 kerning */