java-topology/docs/tickets/libgdx-0004-kerning-gpos-intarray-contains.md

2.6 KiB
Raw Permalink Blame History

libgdx-0004: Kerning.readSubtable2 — O(N²) IntArray.contains() in GPOS coverage loop

Severity: MEDIUM File: extensions/gdx-tools/src/com/badlogic/gdx/tools/hiero/Kerning.java Line: 236244 Status: PATCHED

Description

In Kerning.java, the GPOS lookup type 2 (pair adjustment / class-based kerning) handler at lines 236244 iterates over every covered glyph and, for each glyph, performs a linear scan through all class-1 glyph arrays to find which class the glyph belongs to.

IntArray.contains(int) is an O(K) linear scan. With C coverage glyphs and N class-1 definitions each containing an average of G glyphs, total cost is O(C × N × G) — cubic in glyph/class count.

This runs at font load time inside the Hiero bitmap font tool, but also inside any Kerning.load() call at runtime. Fonts with large kern class tables (e.g. professional typefaces with 200+ class-1 groups and 5000+ coverage glyphs) will experience multi-second hangs on a path that should be sub-millisecond.

Root Cause

// Kerning.java:236-244
for (int i = 0; i < coverage.length; i++) {
    int glyph = coverage[i];
    boolean found = false;
    for (int j = 1; j < class1Count && !found; j++) {
        found = glyphsByClass1[j].contains(glyph);   // O(K) linear scan per class
    }
    if (!found) {
        glyphsByClass1[0].add(glyph);
    }
}

IntArray.contains(int) iterates the entire backing int[] array. No IntSet (libGDX's O(1) integer hash set) is used.

Fix

Build a single int[] glyphToClass1 array indexed by glyph code (or an IntIntMap) once during readClassDefinition, then use O(1) lookup to check/assign class membership.

// After readClassDefinition, build reverse map:
IntIntMap glyphToClass1 = new IntIntMap();
for (int c = 0; c < class1Count; c++) {
    IntArray glyphs = glyphsByClass1[c];
    for (int k = 0; k < glyphs.size; k++)
        glyphToClass1.put(glyphs.items[k], c);
}

// Replace O(C × N × G) loop with O(C):
for (int i = 0; i < coverage.length; i++) {
    int glyph = coverage[i];
    if (!glyphToClass1.containsKey(glyph)) {    // O(1)
        glyphsByClass1[0].add(glyph);
        glyphToClass1.put(glyph, 0);
    }
}

Speedup

Coverage glyphs Class-1 groups Avg glyphs/class Before After
500 50 20 500 000 ops ~500 ops
2 000 200 50 20 000 000 ops ~2 000 ops

Estimated 1000x speedup for professional typefaces with large kerning class tables.