web-font/backend/font_util/gsub-reachable.ts
崮生(子虚) db33685475 perf(gsub): serializeSingleSubst 大 coverage 反转遍历,思源 -63%
思源 GSUB 56 lookup 中 4 个非空 type1 lookup(locl 地区变体)的 coverage
极大(lookup#43 = 8881 gid,#0=729,#42=636),但子集仅命中个位数。
原 serializeSingleSubst 用 readCoverageGids 全量展开 coverage 再逐个查
gidLookup(8881 次几乎全未命中),占 subsetGSUB 84%(0.184ms/call)。

改为遍历子集原始 gid(currentSortedSubsetGids,仅 19 项)+ coverageIndexOf
在 coverage 中二分定位下标(format2 下标对应 substituteGlyphIDs 数组),
O(subsetSize × log covCount) 替代 O(covCount)。与 reachable 侧
[[gsub-reachable-bigcov-byte-bsearch]] 同一类「展开全量消费极少」浪费。

仅当 covGidCount > subsetSize×4 且 >16 时启用反转,短 coverage(FiraCode
等)走原路径避免二分开销。导出 coverageIndexOf/coverageCount 供复用。

subsetGSUB 思源 min 0.204→0.075ms(-63%),otf-思源 woff2 avg 2.7→1.0ms。
输出逐字节一致(思源 GSUB 6560 bytes 差异 0),全字体 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 02:00:05 +08:00

678 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 计算子集 glyph 通过 GSUB 替换链可达的全部 target glyph id
*
* GSUB连字/上下文替换)的替换目标常为无 unicode 的纯字形 glyph如 FiraCode 的
* `greater_equal.liga`unicode=None。子集化基于 codepoint 无法保留这些 glyph
* 导致替换规则 target 失效、连字不渲染。本模块在子集化前解析原始 GSUB找出从子集起始
* gid 出发、经替换链可达的全部 target gid供调用方注入子集extraSubsetGids
*
* 处理的 lookup 类型:
* - type1 SingleSubst: coverage gid在子集→ target gid
* - type2 MultipleSubst: coverage gid在子集→ 多个 target gid
* - type3 AlternateSubst: coverage gid在子集→ 多个候选 target gid全部保留
* - type4 LigatureSubst: 全部 component 在子集 → target gid多对一target 为新字形)
* - type6 ChainedContext: 匹配上下文后调用其他 lookup递归处理被引用 lookup
* - type7 Extension: 解包后递归
*
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/gsub
*/
import { OTReader } from "./ot-bytes.js";
/** GSUB lookup 类型常量 */
const LT_SINGLE = 1;
const LT_MULTIPLE = 2;
const LT_ALTERNATE = 3;
const LT_LIGATURE = 4;
const LT_CHAIN = 6;
const LT_EXTENSION = 7;
/**
* Coverage 解析缓存off → 原 gid 数组)。
* 不动点迭代每轮对全部 lookup 的 coverage 重复解析,但 coverage 字节不变故结果稳定。
* 实测 FiraCode 迭代多轮,缓存消除绝大部分重复 u16 读取与数组分配reachable 阶段主热点)。
*/
type CoverageCache = Map<number, number[]>;
/** 读取 Coverage 表的 gid 列表。传入 cache 时按绝对偏移缓存解析结果。 */
function readCoverageGids(r: OTReader, off: number, cache: CoverageCache): number[] {
const hit = cache.get(off);
if (hit !== undefined) return hit;
const dv = r.dv;
const format = dv.getUint16(off, false);
const gids: number[] = [];
if (format === 1) {
const count = dv.getUint16(off + 2, false);
/** format1 gid 列表是连续 count 个大端 u16。若 2 字节对齐,用 Uint16Array view 共享 buffer
* 读取 + 内联翻转(与 readCoverageRemapped/hmtx/loca 同思路),比逐次 dv.getUint16 的边界检查 + 大端组装更快 */
const base = off + 4;
const byteOff = dv.byteOffset + base;
if (count > 8 && (byteOff & 1) === 0) {
const src16 = new Uint16Array(dv.buffer, byteOff, count);
for (let i = 0; i < count; i++) {
const raw = src16[i];
gids.push(((raw & 0xff) << 8) | (raw >> 8));
}
} else {
for (let i = 0; i < count; i++) gids.push(dv.getUint16(base + i * 2, false));
}
} else if (format === 2) {
const rangeCount = dv.getUint16(off + 2, false);
let p = off + 4;
for (let i = 0; i < rangeCount; i++) {
const start = dv.getUint16(p, false);
const end = dv.getUint16(p + 2, false);
for (let g = start; g <= end; g++) gids.push(g);
p += 6;
}
}
cache.set(off, gids);
return gids;
}
/**
* 找 coverage 中第一个不在子集inSubset 返回 false的 gid无需展开完整数组。
*
* format1 list逐项 u16 读取 + inSubset命中即返回。
* format2 range逐 range 内 gid 生成start..end+ inSubset命中即返回避免 readCoverageGids
* 的完整展开与数组分配。初夏纯标点 280 个 format3 首轮全失败,第一个 coverage 的首个 gid
* 往往就排除,本函数短路返回省掉全量展开。
*
* 全部 gid 都在子集时返回 -1coverage「全包含」
*
* 优化310: cache 命中时遍历已缓存数组(与其他 lookup type 共享解析结果);
* cache miss 时**直接边解析边查、不调 readCoverageGids 也不填 cache**。
* 原实现 `readCoverageGids(off, cache)` 会完整展开 coverage 到数组再遍历——对 format2 range
* (初夏明朝 coverage 常覆盖上千 gid即使首个 gid 就 excluded 也要展开全量,是「展开全量命中极少」浪费
* (同类见 [[gsub-classdef-format2-bsearch]]。fmt3 失败路径的 coverage 永不被 collectSubtableTargets
* 读(失败不产 target故不填 cache 无碍;仅 triggerable=true 的 coverage 才在后续 readCoverageGids
* 收集 contextGids 时缓存。
*
* @param off coverage 绝对偏移
* @param inSubset 判定 gid 是否在子集
* @returns 第一个不在子集的 gid全在子集返回 -1
*/
function coverageFirstExcludedGid(
r: OTReader,
off: number,
cache: CoverageCache,
inSubset: (gid: number) => boolean,
): number {
/** cache 命中:遍历已缓存数组(复用其他 lookup 的解析结果) */
const cached = cache.get(off);
if (cached !== undefined) {
for (const g of cached) if (!inSubset(g)) return g;
return -1;
}
/** cache miss边解析边查不分配数组、不填 cache */
const dv = r.dv;
const format = dv.getUint16(off, false);
if (format === 1) {
const count = dv.getUint16(off + 2, false);
const base = off + 4;
for (let i = 0; i < count; i++) {
const g = dv.getUint16(base + i * 2, false);
if (!inSubset(g)) return g;
}
return -1;
} else if (format === 2) {
const rangeCount = dv.getUint16(off + 2, false);
let p = off + 4;
for (let i = 0; i < rangeCount; i++) {
const start = dv.getUint16(p, false);
const end = dv.getUint16(p + 2, false);
for (let g = start; g <= end; g++) {
if (!inSubset(g)) return g;
}
p += 6;
}
return -1;
}
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
*/
export 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)。
*/
export 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 本身)。
*
* @param gsubBytes 原始 GSUB 表字节
* @param seedGids 子集起始 gid 集合(子集 codepoint 对应的 gid
* @returns 需额外保留的 target gid 集合
*/
export function collectReachableGsubTargets(
gsubBytes: Uint8Array,
seedGids: Set<number>,
): Set<number> {
const dv = new DataView(gsubBytes.buffer, gsubBytes.byteOffset, gsubBytes.byteLength);
const r = new OTReader(dv);
const covCache: CoverageCache = new Map();
const major = r.u16(0);
const minor = r.u16(2);
if (major !== 1 || minor > 1) return new Set();
const lookupListOff = r.u16(8);
const lookupCount = r.u16(lookupListOff);
/** 解析每个 lookup 的 (effectiveType, subtableAbsOffs)。
* 合并 lookupRelOffs 读取到主循环(避免先收集到中间数组再按下标回读)。 */
interface LookupParsed {
effectiveType: number;
subtableAbsOffs: number[];
}
const lookups: LookupParsed[] = [];
for (let i = 0; i < lookupCount; i++) {
const lOff = lookupListOff + dv.getUint16(lookupListOff + 2 + i * 2, false);
const lookupType = dv.getUint16(lOff, false);
const subTableCount = dv.getUint16(lOff + 4, false);
const subtableAbsOffs: number[] = [];
let effectiveType = lookupType;
for (let j = 0; j < subTableCount; j++) {
const subOff = lOff + dv.getUint16(lOff + 6 + j * 2, false);
if (lookupType === LT_EXTENSION) {
if (dv.getUint16(subOff, false) !== 1) {
effectiveType = -1;
continue;
}
effectiveType = dv.getUint16(subOff + 2, false);
subtableAbsOffs.push(subOff + r.u32(subOff + 4));
} else {
subtableAbsOffs.push(subOff);
}
}
lookups.push({ effectiveType, subtableAbsOffs });
}
/** 固定点迭代:不断扩展 reachable 集合。
* type6 ChainedContext 规则:若其全部 input/backtrack/lookahead gid 都在子集内,
* 则规则可触发——此时被引用 lookup 的 target 需保留(通过 chainRefs 在迭代中处理 type1/2/3/4 target
* type1/2/3/4 规则coverage gid 在子集 → target 保留。
* 这模拟了 pyftsubset 的「实际触发路径」分析,避免保留无关 calt 规则的海量 context glyph。
*
* 优化(单 Set inSubsetreachable 初始即含 seedGidsinSubset 只查 reachable 一个 Set
* (原为 seedGids.has || reachable.has 两次 Set 查询,热路径每 subtable 多次调用)。
* 返回的 reachable 可能含 seed gid——调用方注入 extraSubsetGids 后 Font.create 会去重,无害。 */
const reachable = new Set<number>(seedGids);
let changed = true;
const inSubset = (gid: number) => reachable.has(gid);
/** 复用临时 Set避免每个 type6 subtable 两次 new Set 的 GC 压力(初夏明朝 51 lookup 多轮迭代) */
const refsReuse: Set<number> = new Set<number>();
const ctxGidsReuse: Set<number> = new Set<number>();
/**
* 优化316固定点迭代跨轮稳定性记忆。
* collectChainRefs 对 format2保守全收集与 format3 triggerable=true 返回 true表示该 subtable
* 的 refs/contextGids 已收全、不随 reachable 扩展而变化。记录其偏移到 settledChain后续轮直接跳过
* 避免重复扫描(初夏纯标点 280 个 format3第 2 轮全部可跳过。triggerable=false 的不记忆,因
* reachable 扩展可能使缺失的 coverage gid 进入、令其转为触发。
*/
const settledChain: Set<number> = new Set<number>();
/**
* 优化format3 失败 gid 跨轮跳过format3 triggerable=false 的 subtable记录使其失败的第一个
* coverage gid。下一轮若该 gid 仍未进 reachable本 subtable 必定仍 false直接跳过重扫。
* 初夏纯标点 280 个 format3 全 false原 3 轮 × 280 = 840 次 collectChainRefs改后第 2 轮起全部跳过。
* 失败 gid 进 reachable 时重扫(可能仍 false 则更新失败 gid可能变 true 则走 settledChain 记忆)。
*/
const failGidMap: Map<number, number> = new Map<number, number>();
/** 复用对象,避免每次调用 collectChainRefs 分配 */
const failGidBox: { v: number } = { v: -1 };
while (changed) {
changed = false;
for (let i = 0; i < lookupCount; i++) {
const lk = lookups[i];
for (const subAbs of lk.subtableAbsOffs) {
if (lk.effectiveType === LT_CHAIN) {
if (settledChain.has(subAbs)) continue;
/** 已知失败 gid 仍未进 reachable → 必定仍 triggerable=false跳过重扫 */
const knownFail = failGidMap.get(subAbs);
if (knownFail !== undefined && !reachable.has(knownFail)) continue;
/** type6收集可触发规则引用的 lookup index 与所需 context gid */
refsReuse.clear();
ctxGidsReuse.clear();
failGidBox.v = -1;
const stable = collectChainRefs(r, subAbs, refsReuse, ctxGidsReuse, inSubset, covCache, failGidBox);
for (const g of ctxGidsReuse) {
if (!reachable.has(g)) { reachable.add(g); changed = true; }
}
for (const li of refsReuse) {
const refLk = lookups[li];
if (!refLk) continue;
for (const refSub of refLk.subtableAbsOffs) {
const refTargets = collectSubtableTargets(r, refSub, refLk.effectiveType, inSubset, covCache, reachable);
for (const g of refTargets) {
if (!reachable.has(g)) { reachable.add(g); changed = true; }
}
}
}
if (stable) {
settledChain.add(subAbs);
failGidMap.delete(subAbs);
} else if (failGidBox.v >= 0) {
/** triggerable=false记录失败 gid下轮据此跳过 */
failGidMap.set(subAbs, failGidBox.v);
}
} else {
const newTargets = collectSubtableTargets(r, subAbs, lk.effectiveType, inSubset, covCache, reachable);
for (const g of newTargets) {
if (!reachable.has(g)) {
reachable.add(g);
changed = true;
}
}
}
}
}
if (r.errorFlag) break;
}
return reachable;
}
/** 从单个 subtable 收集 target gidtype1/2/3/4type6 不直接产生 target通过 chainRefs 间接) */
function collectSubtableTargets(
r: OTReader,
off: number,
type: number,
inSubset: (gid: number) => boolean,
covCache: CoverageCache,
reachable: Set<number>,
): number[] {
const targets: number[] = [];
/** 缓存 dv 供内层循环连续 u16 读取直接调用 getUint16省去 r.u16 的方法调用 + 边界检查开销
* collectReachableGsubTargets 是 FiraCode 等 GSUB 重字体的 #1 热点,循环内 u16 调用密集)。
* offset 均为 GSUB 表内有效偏移getUint16 与 u16 行为一致format/count 等结构判定仍用 r.u16 保持 errorFlag 安全网 */
const dv = r.dv;
if (type === LT_SINGLE) {
/** SingleSubst: format1 coverage+delta / format2 coverage+gidArray */
const format = r.u16(off);
const covOff = off + r.u16(off + 2);
if (format === 1) {
const delta = r.i16(off + 4);
/** 条件反转:仅当 covGids 明显多于 reachable 时反转遍历方向(遍历小集合二分查大集合)。
* covGids 短FiraCode type1 avg 5.5)时原 Set.has 路径更快(二分开销 > 遍历)。 */
if (coverageCount(r, covOff) > reachable.size) {
/** 反转路径(大 coveragecoverageIndexOf 直接对 coverage 字节二分,不展开 gid 数组。
* format1 delta 替换target = (gid + delta) & 0xffff无需 index 查表。 */
for (const g of reachable) {
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 路径。 */
const count = r.u16(off + 4);
const gidArrBase = off + 6;
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) {
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));
}
}
}
} else if (type === LT_MULTIPLE) {
const covOff = off + r.u16(off + 2);
const seqCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff, covCache);
for (let i = 0; i < covGids.length && i < seqCount; i++) {
if (!inSubset(covGids[i])) continue;
const seqOff = off + dv.getUint16(off + 6 + i * 2, false);
const glyphCount = dv.getUint16(seqOff, false);
for (let k = 0; k < glyphCount; k++) targets.push(dv.getUint16(seqOff + 2 + k * 2, false));
}
} else if (type === LT_ALTERNATE) {
/** 条件反转:初夏 type3 covLen 987/3 callsavg 329命中率 2.3%covGids 短时走原路径。 */
const covOff = off + r.u16(off + 2);
const altCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff, covCache);
const lim = covGids.length < altCount ? covGids.length : altCount;
if (covGids.length > reachable.size) {
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 {
const altOff = off + dv.getUint16(off + 6 + mid * 2, false);
const cnt = dv.getUint16(altOff, false);
for (let k = 0; k < cnt; k++) targets.push(dv.getUint16(altOff + 2 + k * 2, false));
break;
}
}
}
} else {
for (let i = 0; i < lim; i++) {
if (!inSubset(covGids[i])) continue;
const altOff = off + dv.getUint16(off + 6 + i * 2, false);
const cnt = dv.getUint16(altOff, false);
for (let k = 0; k < cnt; k++) targets.push(dv.getUint16(altOff + 2 + k * 2, false));
}
}
} else if (type === LT_LIGATURE) {
/** LigatureSubst: 全部 component 在子集 → target gid */
const covOff = off + r.u16(off + 2);
const setCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff, covCache);
for (let i = 0; i < covGids.length && i < setCount; i++) {
/** 第一分量coverage gid不在子集时整条 ligature 不触发,跳过整个 LigatureSet
* 不展开其内部 ligature省 ligCount/ligOff/compCount/target 等无谓 u16 读取)。
* 初夏明朝 type4 first gid 常全不在子集(首轮覆盖 glyph 多为非子集字形),此短路省掉每条
* ligature 的多次 u16 读 + inSubset 判定。 */
if (!inSubset(covGids[i])) continue;
const setOff = off + dv.getUint16(off + 6 + i * 2, false);
const ligCount = dv.getUint16(setOff, false);
for (let j = 0; j < ligCount; j++) {
const ligOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
const compCount = dv.getUint16(ligOff, false);
const target = dv.getUint16(ligOff + 2, false);
let allIn = true;
for (let k = 0; k < compCount - 1; k++) {
if (!inSubset(dv.getUint16(ligOff + 4 + k * 2, false))) {
allIn = false;
break;
}
}
if (allIn) targets.push(target);
}
}
}
/** type5 ReverseChain / type6 ChainedContext 不直接产生 targettype6 通过 chainRefs 间接,
* 被引用 lookup 的 target 在其自身 subtable 收集ReverseChain 罕见且 target 提取复杂,暂忽略) */
return targets;
}
/**
* 收集 type6 ChainedContext subtable 中「可触发规则」引用的 lookup index 与所需 context gid。
*
* 一条 ChainSubstRule 可触发,当且仅当其全部 backtrack/input/lookahead gid 都在当前子集内。
* format1元素是直接 gid用 inSubset 逐项判断context gid 收集到 contextGids保留它们使规则可触发
* format2元素是 class index非 gidclass 匹配的 gid 由 ClassDef 决定class index 始终有效,
* 但规则是否「可能触发」取决于该 class 是否含子集内 gid保守起见视为可触发收集 ClassDef gid
* format3元素是 coveragegid 全在子集则可触发。
*
* 只对可触发规则收集其 SubstLookupRecord 引用的 lookup indexrefs供调用方进一步追踪 target。
* 这模拟 pyftsubset 的触发路径分析,避免保留无关规则的海量 context glyph。
*/
function collectChainRefs(
r: OTReader,
off: number,
refs: Set<number>,
contextGids: Set<number>,
inSubset: (gid: number) => boolean,
covCache: CoverageCache,
/** out: triggerable=false 时记录第一个不在 reachable 的 coverage gid跨轮跳过判定用 */
failGid: { v: number },
): boolean {
const format = r.u16(off);
/** 缓存 dv 供循环内连续 u16 读取直接调用 getUint16format3 是 FiraCode 最多 subtable 类型,循环密集) */
const dv = r.dv;
if (format === 1) {
/** format1: coverage(gid) + SubRuleSet 数组,按 coverage gid 索引 */
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff, covCache);
const setCount = r.u16(off + 4);
for (let i = 0; i < covGids.length && i < setCount; i++) {
/** 第一分量coverage gid须在子集否则该 SubRuleSet 不触发 */
if (!inSubset(covGids[i])) continue;
contextGids.add(covGids[i]);
const setOffRel = dv.getUint16(off + 6 + i * 2, false);
if (setOffRel === 0) continue;
const setOff = off + setOffRel;
const ruleCount = dv.getUint16(setOff, false);
for (let j = 0; j < ruleCount; j++) {
const ruleOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
collectChainRuleRefs(r, ruleOff, refs, contextGids, inSubset, true);
}
}
/** format1 按 coverage gid 分派 SubRuleSet部分触发部分未触发未触发部分可能随 reachable
* 扩展而新增触发,故保守返回 false不跨轮跳过。FiraCode 90 个 format1影响有限。 */
return false;
} else if (format === 2) {
/** format2: 三个 ClassDef + 按 input 第一分量 class 索引的 SubClassSet。
* class index 不重映射,规则的 backtrack/input/lookahead 是 class index非 gid
* 无法直接用 inSubset 判断class 可含任意 gid。保守收集被引用 lookup 与 ClassDef gid。 */
const classSetCount = r.u16(off + 10);
for (let i = 0; i < classSetCount; i++) {
const setOffRel = dv.getUint16(off + 12 + i * 2, false);
if (setOffRel === 0) continue;
const setOff = off + setOffRel;
const ruleCount = dv.getUint16(setOff, false);
for (let j = 0; j < ruleCount; j++) {
const ruleOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
collectChainRuleRefs(r, ruleOff, refs, contextGids, inSubset, false);
}
}
/** format2 保守收集全部 ClassDef gid不判 inSubset首轮即收全结果不随 reachable 扩展变化。 */
return true;
} else if (format === 3) {
/** format3: 显式 coverage 数组 + SubstLookupRecords。
* 三个 coverage 数组backtrack/input/lookahead的 gid 须全在子集才触发。
*
* 优化(失败短路,跳过 coverage 展开与 contextGids 收集format3 triggerable=false 是常态
* (思源黑体 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 */
for (let seg = 0; seg < 3 && triggerable; seg++) {
const cnt = r.u16(p);
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;
/** 记录使其失败的 gid供调用方跨轮跳过该 gid 未进 reachable 前,本 subtable 必定仍 false。 */
failGid.v = excluded;
break;
}
}
p += cnt * 2;
}
if (triggerable) {
/** 第二遍(仅 triggerable=true收集三段 coverage 的全部 gid 到 contextGids。
* 此刻 coverage 全部 gid 都在 reachable已被 coverageFirstExcludedGid 确认),展开安全且量小。 */
let p2 = off + 2;
for (let seg = 0; seg < 3; seg++) {
const cnt = r.u16(p2);
p2 += 2;
for (let k = 0; k < cnt; k++) {
const covGids = readCoverageGids(r, off + dv.getUint16(p2 + k * 2, false), covCache);
for (const g of covGids) contextGids.add(g);
}
p2 += cnt * 2;
}
const substCount = r.u16(p2);
for (let k = 0; k < substCount; k++) refs.add(dv.getUint16(p2 + 2 + k * 4 + 2, false));
/** triggerable=true三段 coverage gid 全在 reachablereachable 单调,后续仍全在),
* refs/contextGids 已确定,不随 reachable 扩展变化 → 稳定,可跨轮跳过。 */
return true;
}
/** triggerable=false某 coverage gid 不在 reachable后续 reachable 扩展可能使其进入 → 不稳定。 */
return false;
}
return false;
}
/**
* 从一条 ChainSubstRule 收集引用的 lookup index 与 context gid。
* @param isGidFormat true=format1元素为 gid需 inSubset 判断且收集 gid
* false=format2元素为 class index原样保留不判断不收集
* 规则全部 context gid 在子集时才收集 refsformat1format2 class index 始终收集。
*
* 优化(消除中间数组):原实现先读全部 backtrack/input/lookahead 到 3 个临时数组再判断,
* format1 每 rule 分配 3 个 number[]FiraCode 424 次/call × 3 = 1272 次数组分配)。
* 改为两遍扫描:第一遍仅用 inSubset 校验全部 gid不分配数组遇子集外 gid 即 return
* 通过校验后再第二遍收集 context gid 并读 subst records。format1 规则触发是少数,第二遍开销可忽略。
*/
function collectChainRuleRefs(
r: OTReader,
ruleOff: number,
refs: Set<number>,
contextGids: Set<number>,
inSubset: (gid: number) => boolean,
isGidFormat: boolean,
): void {
/** format1/2 rule: backtrackCount + backtrack[] + inputCount + input[] + lookaheadCount + lookahead[] + substCount + substRecords[] */
/** 缓存 dvchain format1 规则密集调用FiraCode 180 fmt1 规则 × 6+ 次 u16直接 getUint16 省方法调用+边界检查 */
const dv = r.dv;
let p = ruleOff;
const backtrackCount = r.u16(p); p += 2;
const backtrackEnd = p + backtrackCount * 2;
const inputCount = r.u16(backtrackEnd); p = backtrackEnd + 2;
/** input 数组长度 = inputCount - 1第一分量在 coveragerule 内只存后续分量) */
const inputLen = inputCount > 0 ? inputCount - 1 : 0;
const inputEnd = p + inputLen * 2;
const lookaheadCount = r.u16(inputEnd); p = inputEnd + 2;
const lookaheadEnd = p + lookaheadCount * 2;
const substCount = r.u16(lookaheadEnd); p = lookaheadEnd + 2;
/** format1全部 context gid 在子集才触发先校验再收集避免中间数组format2class index 始终「可触发」(保守) */
if (isGidFormat) {
/** 第一遍:校验 backtrack + input + lookahead 全部 gid 在子集(遇子集外即放弃规则) */
let q = ruleOff + 2;
for (let k = 0; k < backtrackCount; k++) {
if (!inSubset(dv.getUint16(q + k * 2, false))) return;
}
q += backtrackCount * 2 + 2;
for (let k = 0; k < inputLen; k++) {
if (!inSubset(dv.getUint16(q + k * 2, false))) return;
}
q += inputLen * 2 + 2;
for (let k = 0; k < lookaheadCount; k++) {
if (!inSubset(dv.getUint16(q + k * 2, false))) return;
}
/** 第二遍:全部在子集,收集 context gid */
q = ruleOff + 2;
for (let k = 0; k < backtrackCount; k++) contextGids.add(dv.getUint16(q + k * 2, false));
q += backtrackCount * 2 + 2;
for (let k = 0; k < inputLen; k++) contextGids.add(dv.getUint16(q + k * 2, false));
q += inputLen * 2 + 2;
for (let k = 0; k < lookaheadCount; k++) contextGids.add(dv.getUint16(q + k * 2, false));
}
for (let k = 0; k < substCount; k++) {
/** SubstLookupRecord: sequenceIndex(2) + lookupListIndex(2) */
refs.add(dv.getUint16(p + k * 4 + 2, false));
}
}