""" Unit test for raylib-0001: GetGlyphIndex O(T×G) linear scan per character. Tests: 1. Correctness: both implementations return the same glyph index 2. Performance: O(G) linear scan vs O(1) hash map lookup ratio """ import time import random # ---- Minimal glyph stub ---- class GlyphInfo: def __init__(self, codepoint, advance_x=8): self.value = codepoint self.advance_x = advance_x def make_font(codepoints): """Build a fake Font-like object with glyphs in arbitrary order.""" glyphs = [GlyphInfo(cp) for cp in codepoints] random.shuffle(glyphs) return glyphs # ---- BEFORE: defective implementation (linear scan) ---- def get_glyph_index_before(glyphs, codepoint): """O(G) linear scan — mirrors raylib src/rtext.c GetGlyphIndex.""" index = 0 fallback_index = 0 for i, g in enumerate(glyphs): if g.value == 63: # '?' fallback fallback_index = i if g.value == codepoint: return i if glyphs[0].value != codepoint: return fallback_index return index def measure_text_before(glyphs, text_codepoints): """Simulate MeasureTextEx: calls get_glyph_index_before per character.""" total = 0 for cp in text_codepoints: idx = get_glyph_index_before(glyphs, cp) total += glyphs[idx].advance_x return total # ---- AFTER: fixed implementation (hash map) ---- def build_glyph_hashmap(glyphs): """Build codepoint→index hash map once at font load time.""" return {g.value: i for i, g in enumerate(glyphs)} def get_glyph_index_after(glyph_map, glyphs, codepoint): """O(1) hash map lookup.""" if codepoint in glyph_map: return glyph_map[codepoint] # fallback to '?' if 63 in glyph_map: return glyph_map[63] return 0 def measure_text_after(glyph_map, glyphs, text_codepoints): """Simulate MeasureTextEx with hash map — O(T) total.""" total = 0 for cp in text_codepoints: idx = get_glyph_index_after(glyph_map, glyphs, cp) total += glyphs[idx].advance_x return total # ---- Tests ---- def test_correctness_ascii(): """Both implementations return the same index for ASCII glyphs.""" codepoints = list(range(32, 127)) # 95 ASCII glyphs glyphs = make_font(codepoints) glyph_map = build_glyph_hashmap(glyphs) for cp in codepoints: before = get_glyph_index_before(glyphs, cp) after = get_glyph_index_after(glyph_map, glyphs, cp) assert before == after, ( f"Mismatch for codepoint {cp}: before={before} after={after}" ) print("PASS test_correctness_ascii") def test_correctness_unicode(): """Both implementations agree on a mixed Unicode glyph set.""" codepoints = list(range(32, 127)) + list(range(0x4E00, 0x4E00 + 200)) # ASCII + CJK glyphs = make_font(codepoints) glyph_map = build_glyph_hashmap(glyphs) sample = random.sample(codepoints, 50) for cp in sample: before = get_glyph_index_before(glyphs, cp) after = get_glyph_index_after(glyph_map, glyphs, cp) assert before == after, ( f"Mismatch for codepoint {cp}: before={before} after={after}" ) print("PASS test_correctness_unicode") def test_correctness_measure_text(): """MeasureText returns the same width before and after the fix.""" codepoints = list(range(32, 127)) glyphs = make_font(codepoints) glyph_map = build_glyph_hashmap(glyphs) text = [ord(c) for c in "Hello, World! raylib text rendering."] w_before = measure_text_before(glyphs, text) w_after = measure_text_after(glyph_map, glyphs, text) assert w_before == w_after, f"Width mismatch: {w_before} vs {w_after}" print("PASS test_correctness_measure_text") def test_performance_ratio(): """O(G) linear scan is dramatically slower than O(1) hash lookup.""" G = 1000 # glyph count (extended Latin + symbols) T = 500 # characters per render call ITERS = 100 codepoints = list(range(32, 32 + G)) glyphs = make_font(codepoints) glyph_map = build_glyph_hashmap(glyphs) text = [codepoints[i % len(codepoints)] for i in range(T)] t0 = time.perf_counter() for _ in range(ITERS): measure_text_before(glyphs, text) t_before = time.perf_counter() - t0 t0 = time.perf_counter() for _ in range(ITERS): measure_text_after(glyph_map, glyphs, text) t_after = time.perf_counter() - t0 ratio = t_before / t_after if t_after > 0 else float('inf') print(f"PERF raylib-0001: G={G} T={T} ITERS={ITERS} " f"before={t_before*1000:.1f}ms after={t_after*1000:.1f}ms ratio={ratio:.1f}x") assert ratio >= 50, ( f"Expected >= 50x speedup for G={G} T={T}, got {ratio:.1f}x" ) print("PASS test_performance_ratio") def test_performance_large_unicode_font(): """With a large Unicode font (G=4000) the pathology is severe.""" G = 4000 T = 200 ITERS = 20 codepoints = list(range(0x4E00, 0x4E00 + G)) # CJK range glyphs = make_font(codepoints) glyph_map = build_glyph_hashmap(glyphs) text = [codepoints[i % len(codepoints)] for i in range(T)] t0 = time.perf_counter() for _ in range(ITERS): measure_text_before(glyphs, text) t_before = time.perf_counter() - t0 t0 = time.perf_counter() for _ in range(ITERS): measure_text_after(glyph_map, glyphs, text) t_after = time.perf_counter() - t0 ratio = t_before / t_after if t_after > 0 else float('inf') print(f"PERF raylib-0001 (unicode): G={G} T={T} ITERS={ITERS} " f"before={t_before*1000:.1f}ms after={t_after*1000:.1f}ms ratio={ratio:.1f}x") assert ratio >= 500, ( f"Expected >= 500x speedup for G={G} T={T}, got {ratio:.1f}x" ) print("PASS test_performance_large_unicode_font") if __name__ == "__main__": random.seed(42) test_correctness_ascii() test_correctness_unicode() test_correctness_measure_text() test_performance_ratio() test_performance_large_unicode_font() print("\nAll raylib-0001 tests PASSED")