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.
208 lines
6.9 KiB
Python
208 lines
6.9 KiB
Python
"""
|
|
Unit test for sdl3-0001: SDL3 GPU TRACK_RESOURCE O(N²) linear dedup.
|
|
|
|
Tests:
|
|
1. Correctness: both implementations produce the same unique resource list
|
|
2. Performance: O(N²) linear scan vs O(N) hash set ratio
|
|
"""
|
|
import time
|
|
import random
|
|
|
|
|
|
# ---- Minimal resource stub ----
|
|
|
|
class FakeResource:
|
|
"""Simulates a VulkanTexture* or D3D12Texture* pointer."""
|
|
def __init__(self, resource_id):
|
|
self.resource_id = resource_id
|
|
self.ref_count = 0
|
|
|
|
|
|
# ---- BEFORE: defective implementation (linear scan dedup) ----
|
|
|
|
def track_resource_before(used_list, resource):
|
|
"""
|
|
Mirrors the TRACK_RESOURCE macro in SDL_gpu_vulkan.c and SDL_gpu_d3d12.c.
|
|
O(N) linear scan for duplicate check before adding.
|
|
"""
|
|
for r in reversed(used_list): # Vulkan scans in reverse
|
|
if r is resource:
|
|
return # duplicate, skip
|
|
used_list.append(resource)
|
|
resource.ref_count += 1
|
|
|
|
|
|
def record_frame_before(resources_per_draw, draw_count):
|
|
"""
|
|
Simulate recording a frame: for each draw call, bind a set of resources.
|
|
Returns the final tracked list.
|
|
"""
|
|
used_textures = []
|
|
for draw_i in range(draw_count):
|
|
for resource in resources_per_draw[draw_i]:
|
|
track_resource_before(used_textures, resource)
|
|
return used_textures
|
|
|
|
|
|
# ---- AFTER: fixed implementation (hash set dedup) ----
|
|
|
|
def track_resource_after(used_set, used_list, resource):
|
|
"""
|
|
O(1) hash set membership check.
|
|
"""
|
|
resource_id = id(resource)
|
|
if resource_id not in used_set:
|
|
used_set.add(resource_id)
|
|
used_list.append(resource)
|
|
resource.ref_count += 1
|
|
|
|
|
|
def record_frame_after(resources_per_draw, draw_count):
|
|
"""
|
|
Same frame recording but using hash set for dedup.
|
|
"""
|
|
used_textures = []
|
|
used_set = set()
|
|
for draw_i in range(draw_count):
|
|
for resource in resources_per_draw[draw_i]:
|
|
track_resource_after(used_set, used_textures, resource)
|
|
return used_textures
|
|
|
|
|
|
# ---- Tests ----
|
|
|
|
def test_correctness_no_duplicates():
|
|
"""All unique resources are tracked exactly once."""
|
|
resources = [FakeResource(i) for i in range(50)]
|
|
draw_schedule = [resources[i:i+5] for i in range(0, 50, 5)]
|
|
result = record_frame_before(draw_schedule, len(draw_schedule))
|
|
# All 50 resources should appear exactly once
|
|
assert len(result) == 50, f"Expected 50, got {len(result)}"
|
|
assert all(r.ref_count == 1 for r in resources), "Ref counts corrupted"
|
|
print("PASS test_correctness_no_duplicates")
|
|
|
|
|
|
def test_correctness_with_duplicates():
|
|
"""Resources reused across draw calls are tracked only once."""
|
|
r0 = FakeResource(0)
|
|
r1 = FakeResource(1)
|
|
r2 = FakeResource(2)
|
|
# Same textures reused across many draw calls (typical render pass)
|
|
draw_schedule = [[r0, r1, r2]] * 10
|
|
result = record_frame_before(draw_schedule, len(draw_schedule))
|
|
assert len(result) == 3, f"Expected 3 unique, got {len(result)}"
|
|
assert r0.ref_count == 1 and r1.ref_count == 1 and r2.ref_count == 1
|
|
print("PASS test_correctness_with_duplicates")
|
|
|
|
|
|
def test_correctness_before_after_agree():
|
|
"""Before and after produce identical resource lists."""
|
|
n_resources = 100
|
|
n_draws = 50
|
|
resources = [FakeResource(i) for i in range(n_resources)]
|
|
rng = random.Random(42)
|
|
draw_schedule = [
|
|
[resources[rng.randint(0, n_resources - 1)] for _ in range(5)]
|
|
for _ in range(n_draws)
|
|
]
|
|
|
|
# Reset ref counts
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
before_result = record_frame_before(draw_schedule, n_draws)
|
|
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
after_result = record_frame_after(draw_schedule, n_draws)
|
|
|
|
assert set(id(r) for r in before_result) == set(id(r) for r in after_result), (
|
|
"Before and after tracked different resource sets"
|
|
)
|
|
print("PASS test_correctness_before_after_agree")
|
|
|
|
|
|
def test_performance_typical_scene():
|
|
"""Worst-case: all N resources tracked in sequence (each new, no duplicates).
|
|
This directly measures the O(N²) sum(1+2+...+N) vs O(N) hash cost."""
|
|
N = 200 # unique textures bound across the command buffer
|
|
ITERS = 2000
|
|
|
|
resources = [FakeResource(i) for i in range(N)]
|
|
# Each draw binds exactly one new resource — worst-case for linear scan
|
|
draw_schedule = [[resources[i]] for i in range(N)]
|
|
|
|
t0 = time.perf_counter()
|
|
for _ in range(ITERS):
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
record_frame_before(draw_schedule, N)
|
|
t_before = time.perf_counter() - t0
|
|
|
|
t0 = time.perf_counter()
|
|
for _ in range(ITERS):
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
record_frame_after(draw_schedule, N)
|
|
t_after = time.perf_counter() - t0
|
|
|
|
ratio = t_before / t_after if t_after > 0 else float('inf')
|
|
print(f"PERF sdl3-0001 (typical): N={N} ITERS={ITERS} "
|
|
f"before={t_before*1000:.1f}ms after={t_after*1000:.1f}ms ratio={ratio:.1f}x")
|
|
assert ratio >= 5, f"Expected >= 5x speedup, got {ratio:.1f}x"
|
|
print("PASS test_performance_typical_scene")
|
|
|
|
|
|
def test_performance_heavy_scene():
|
|
"""Heavy scene: 1000 unique resources tracked sequentially — pure O(N²) vs O(N)."""
|
|
N = 1000 # unique GPU resources per command buffer
|
|
ITERS = 500
|
|
|
|
resources = [FakeResource(i) for i in range(N)]
|
|
# Every resource is new — forces full O(N) scan each time
|
|
draw_schedule = [[resources[i]] for i in range(N)]
|
|
|
|
t0 = time.perf_counter()
|
|
for _ in range(ITERS):
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
record_frame_before(draw_schedule, N)
|
|
t_before = time.perf_counter() - t0
|
|
|
|
t0 = time.perf_counter()
|
|
for _ in range(ITERS):
|
|
for r in resources:
|
|
r.ref_count = 0
|
|
record_frame_after(draw_schedule, N)
|
|
t_after = time.perf_counter() - t0
|
|
|
|
ratio = t_before / t_after if t_after > 0 else float('inf')
|
|
print(f"PERF sdl3-0001 (heavy): N={N} ITERS={ITERS} "
|
|
f"before={t_before*1000:.1f}ms after={t_after*1000:.1f}ms ratio={ratio:.1f}x")
|
|
assert ratio >= 30, f"Expected >= 30x speedup for N={N}, got {ratio:.1f}x"
|
|
print("PASS test_performance_heavy_scene")
|
|
|
|
|
|
def test_no_ref_count_double_increment():
|
|
"""A resource reused across 100 draws is ref-counted exactly once."""
|
|
shared = FakeResource(0)
|
|
other = [FakeResource(i + 1) for i in range(10)]
|
|
draw_schedule = [[shared] + other[:3]] * 100
|
|
for r in [shared] + other:
|
|
r.ref_count = 0
|
|
record_frame_before(draw_schedule, len(draw_schedule))
|
|
assert shared.ref_count == 1, (
|
|
f"shared.ref_count should be 1 (not {shared.ref_count}); "
|
|
"double-tracking corrupts refcounts"
|
|
)
|
|
print("PASS test_no_ref_count_double_increment")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
random.seed(42)
|
|
test_correctness_no_duplicates()
|
|
test_correctness_with_duplicates()
|
|
test_correctness_before_after_agree()
|
|
test_performance_typical_scene()
|
|
test_performance_heavy_scene()
|
|
test_no_ref_count_double_increment()
|
|
print("\nAll sdl3-0001 tests PASSED")
|