java-topology/defects/raylib/unit/test_raylib_0001.py
russell@unturf.com 5dc86a9bb1 game-engines: raylib-0001 GetGlyphIndex O(T×G), sdl3-0001 TRACK_RESOURCE O(N²); 2 new defects
raylib-0001: GetGlyphIndex scans all G glyphs per character in every
DrawText/MeasureText call — O(T×G) per render. Fix: hash map at load time.
228× speedup at G=1000, 814× at G=4000. UNDF-2026-000000259.

sdl3-0001: SDL3 GPU TRACK_RESOURCE macro does linear scan for duplicate
check before adding a resource to the command buffer tracked list — O(N²)
total across all bind calls per frame. Affects Vulkan, D3D12, and Metal
backends identically. Fix: hash set keyed by pointer identity. 41× at N=1000.
UNDF-2026-000000273.

All 10/10 unit tests PASS.
2026-03-29 22:04:06 -04:00

190 lines
6 KiB
Python
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.

"""
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")