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>
This commit is contained in:
崮生(子虚) 2026-07-25 02:00:05 +08:00
parent baa3a026ed
commit db33685475
2 changed files with 54 additions and 17 deletions

View File

@ -145,7 +145,7 @@ function coverageFirstExcludedGid(
* @param gid gid
* @returns gid coverage -1
*/
function coverageIndexOf(r: OTReader, off: number, gid: number): number {
export function coverageIndexOf(r: OTReader, off: number, gid: number): number {
const dv = r.dv;
const format = dv.getUint16(off, false);
if (format === 1) {
@ -195,7 +195,7 @@ function coverageIndexOf(r: OTReader, off: number, gid: number): number {
* coverage reachable gid locl coverage
* gid #1 format1 countformat2 range (end-start+1)
*/
function coverageCount(r: OTReader, off: number): number {
export function coverageCount(r: OTReader, off: number): number {
const dv = r.dv;
const format = dv.getUint16(off, false);
if (format === 1) {

View File

@ -22,6 +22,7 @@
*/
import { OTWriter as Writer, OTReader as Reader, serializeScriptList, serializeFeatureList, scriptListSpan, featureListSpan } from "./ot-bytes.js";
import { coverageIndexOf, coverageCount } from "./gsub-reachable.js";
/** GSUB lookup 类型常量 */
const LT_SINGLE = 1;
@ -371,26 +372,62 @@ function serializeSingleSubst(
const dv = r.dv;
const format = r.u16(off);
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff);
/** 收集 (from新gid → to新gid) 有效项,子集外的剔除 */
const entries: Array<{ from: number; to: number }> = [];
if (format === 1) {
const delta = r.i16(off + 4);
for (const g of covGids) {
const fromNew = gidLookup[g];
const toNew = gidLookup[(g + delta) & 0xffff];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
}
} else if (format === 2) {
const count = r.u16(off + 4);
for (let i = 0; i < covGids.length && i < count; i++) {
const fromNew = gidLookup[covGids[i]];
const toNew = gidLookup[dv.getUint16(off + 6 + i * 2, false)];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
/** locl type1 lookup coverage ~ gidlookup#43 = 8881
* readCoverageGids coverage gidLookup8881
* gidcurrentSortedSubsetGids 19 coverageIndexOf
* coverage targetO(subsetSize × log covCount) O(covCount)
* [[gsub-reachable-type1-fmt2-reverse-iter]] coverage
* coverage > */
const subsetGids = currentSortedSubsetGids;
const covGidCount = coverageCount(r, covOff);
/** 阈值coverage gid 数 / 子集 gid 数 > 4 且 coverage 较大时启用反转 */
const useReverse = covGidCount > subsetGids.length * 4 && covGidCount > 16;
if (useReverse) {
if (format === 1) {
const delta = r.i16(off + 4);
for (const g of subsetGids) {
/** from gid 必须在 coverage 中SingleSubst 仅对 coverage 内 gid 生效) */
if (coverageIndexOf(r, covOff, g) < 0) continue;
const fromNew = gidLookup[g];
const toNew = gidLookup[(g + delta) & 0xffff];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
}
} else if (format === 2) {
for (const g of subsetGids) {
/** idx = gid 在 coverage 中的序号,与 substituteGlyphIDs 数组下标一一对应 */
const idx = coverageIndexOf(r, covOff, g);
if (idx < 0) continue;
const fromNew = gidLookup[g];
const toNew = gidLookup[dv.getUint16(off + 6 + idx * 2, false)];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
}
} else {
return false;
}
} else {
return false;
const covGids = readCoverageGids(r, covOff);
if (format === 1) {
const delta = r.i16(off + 4);
for (const g of covGids) {
const fromNew = gidLookup[g];
const toNew = gidLookup[(g + delta) & 0xffff];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
}
} else if (format === 2) {
const count = r.u16(off + 4);
for (let i = 0; i < covGids.length && i < count; i++) {
const fromNew = gidLookup[covGids[i]];
const toNew = gidLookup[dv.getUint16(off + 6 + i * 2, false)];
if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew });
}
} else {
return false;
}
}
if (entries.length === 0) return false;