Tests for cached rendering performance
This commit is contained in:
parent
42fcd3417e
commit
0545dd4861
20 changed files with 1740562 additions and 0 deletions
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
File diff suppressed because it is too large
Load diff
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
File diff suppressed because it is too large
Load diff
51
tests/benchmarks/baseline/headless_summary.json
Normal file
51
tests/benchmarks/baseline/headless_summary.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"timestamp": "2025-11-28T19:22:01.900442",
|
||||
"mode": "headless",
|
||||
"results": {
|
||||
"many_frames": {
|
||||
"avg_work_ms": 0.5644053203661328,
|
||||
"max_work_ms": 1.78,
|
||||
"frame_count": 3496
|
||||
},
|
||||
"many_sprites": {
|
||||
"avg_work_ms": 0.14705301494330555,
|
||||
"max_work_ms": 11.814,
|
||||
"frame_count": 13317
|
||||
},
|
||||
"many_captions": {
|
||||
"avg_work_ms": 0.49336296106557376,
|
||||
"max_work_ms": 2.202,
|
||||
"frame_count": 3904
|
||||
},
|
||||
"deep_nesting": {
|
||||
"avg_work_ms": 0.3517734925606891,
|
||||
"max_work_ms": 145.75,
|
||||
"frame_count": 10216
|
||||
},
|
||||
"deep_nesting_cached": {
|
||||
"avg_work_ms": 0.0942947468905298,
|
||||
"max_work_ms": 100.242,
|
||||
"frame_count": 35617
|
||||
},
|
||||
"large_grid": {
|
||||
"avg_work_ms": 2.2851537544696066,
|
||||
"max_work_ms": 11.534,
|
||||
"frame_count": 839
|
||||
},
|
||||
"animation_stress": {
|
||||
"avg_work_ms": 0.0924456547145996,
|
||||
"max_work_ms": 11.933,
|
||||
"frame_count": 21391
|
||||
},
|
||||
"static_scene": {
|
||||
"avg_work_ms": 2.022726128016789,
|
||||
"max_work_ms": 17.275,
|
||||
"frame_count": 953
|
||||
},
|
||||
"static_scene_cached": {
|
||||
"avg_work_ms": 2.694431129476584,
|
||||
"max_work_ms": 22.059,
|
||||
"frame_count": 726
|
||||
}
|
||||
}
|
||||
}
|
||||
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"timestamp": "2025-11-28T16:53:30.850948",
|
||||
"mode": "windowed",
|
||||
"results": {
|
||||
"many_frames": {
|
||||
"avg_work_ms": 1.5756444444444444,
|
||||
"max_work_ms": 3.257,
|
||||
"frame_count": 90
|
||||
},
|
||||
"many_sprites": {
|
||||
"avg_work_ms": 0.6889555555555555,
|
||||
"max_work_ms": 1.533,
|
||||
"frame_count": 90
|
||||
},
|
||||
"many_captions": {
|
||||
"avg_work_ms": 1.2975777777777777,
|
||||
"max_work_ms": 3.386,
|
||||
"frame_count": 90
|
||||
},
|
||||
"deep_nesting": {
|
||||
"avg_work_ms": 0.6173444444444445,
|
||||
"max_work_ms": 1.4,
|
||||
"frame_count": 90
|
||||
},
|
||||
"large_grid": {
|
||||
"avg_work_ms": 3.6094,
|
||||
"max_work_ms": 6.631,
|
||||
"frame_count": 90
|
||||
},
|
||||
"animation_stress": {
|
||||
"avg_work_ms": 0.5419333333333334,
|
||||
"max_work_ms": 1.081,
|
||||
"frame_count": 90
|
||||
},
|
||||
"static_scene": {
|
||||
"avg_work_ms": 3.321588888888889,
|
||||
"max_work_ms": 11.905,
|
||||
"frame_count": 90
|
||||
}
|
||||
}
|
||||
}
|
||||
343
tests/benchmarks/stress_test_suite.py
Normal file
343
tests/benchmarks/stress_test_suite.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stress Test Suite for McRogueFace Performance Analysis
|
||||
|
||||
Establishes baseline performance data before implementing texture caching (#144).
|
||||
Uses a single repeating timer pattern to avoid callback chain issues.
|
||||
|
||||
Usage:
|
||||
./mcrogueface --headless --exec tests/benchmarks/stress_test_suite.py
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Configuration
|
||||
TEST_DURATION_MS = 2000
|
||||
TIMER_INTERVAL_MS = 50
|
||||
OUTPUT_DIR = "../tests/benchmarks/baseline"
|
||||
IS_HEADLESS = True # Assume headless for automated testing
|
||||
|
||||
class StressTestRunner:
|
||||
def __init__(self):
|
||||
self.tests = []
|
||||
self.current_test = -1
|
||||
self.results = {}
|
||||
self.frames_counted = 0
|
||||
self.mode = "headless" if IS_HEADLESS else "windowed"
|
||||
|
||||
def add_test(self, name, setup_fn, description=""):
|
||||
self.tests.append({'name': name, 'setup': setup_fn, 'description': description})
|
||||
|
||||
def tick(self, runtime):
|
||||
"""Single timer callback that manages all test flow"""
|
||||
self.frames_counted += 1
|
||||
|
||||
# Check if current test should end
|
||||
if self.current_test >= 0 and self.frames_counted * TIMER_INTERVAL_MS >= TEST_DURATION_MS:
|
||||
self.end_current_test()
|
||||
self.start_next_test()
|
||||
elif self.current_test < 0:
|
||||
self.start_next_test()
|
||||
|
||||
def start_next_test(self):
|
||||
self.current_test += 1
|
||||
|
||||
if self.current_test >= len(self.tests):
|
||||
self.finish_suite()
|
||||
return
|
||||
|
||||
test = self.tests[self.current_test]
|
||||
print(f"\n[{self.current_test + 1}/{len(self.tests)}] {test['name']}")
|
||||
print(f" {test['description']}")
|
||||
|
||||
# Setup scene
|
||||
scene_name = f"stress_{self.current_test}"
|
||||
mcrfpy.createScene(scene_name)
|
||||
|
||||
# Start benchmark
|
||||
mcrfpy.start_benchmark()
|
||||
mcrfpy.log_benchmark(f"TEST: {test['name']}")
|
||||
|
||||
# Run setup
|
||||
try:
|
||||
test['setup'](scene_name)
|
||||
except Exception as e:
|
||||
print(f" SETUP ERROR: {e}")
|
||||
|
||||
mcrfpy.setScene(scene_name)
|
||||
self.frames_counted = 0
|
||||
|
||||
def end_current_test(self):
|
||||
if self.current_test < 0:
|
||||
return
|
||||
|
||||
test = self.tests[self.current_test]
|
||||
try:
|
||||
filename = mcrfpy.end_benchmark()
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
frames = data['frames'][30:] # Skip warmup
|
||||
if frames:
|
||||
avg_work = sum(f['work_time_ms'] for f in frames) / len(frames)
|
||||
avg_frame = sum(f['frame_time_ms'] for f in frames) / len(frames)
|
||||
max_work = max(f['work_time_ms'] for f in frames)
|
||||
|
||||
self.results[test['name']] = {
|
||||
'avg_work_ms': avg_work,
|
||||
'max_work_ms': max_work,
|
||||
'frame_count': len(frames),
|
||||
}
|
||||
print(f" Work: {avg_work:.2f}ms avg, {max_work:.2f}ms max ({len(frames)} frames)")
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
new_name = f"{OUTPUT_DIR}/{self.mode}_{test['name']}.json"
|
||||
os.rename(filename, new_name)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
self.results[test['name']] = {'error': str(e)}
|
||||
|
||||
def finish_suite(self):
|
||||
mcrfpy.delTimer("tick")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("STRESS TEST COMPLETE")
|
||||
print("="*50)
|
||||
|
||||
for name, r in self.results.items():
|
||||
if 'error' in r:
|
||||
print(f" {name}: ERROR")
|
||||
else:
|
||||
print(f" {name}: {r['avg_work_ms']:.2f}ms avg")
|
||||
|
||||
# Save summary
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
with open(f"{OUTPUT_DIR}/{self.mode}_summary.json", 'w') as f:
|
||||
json.dump({
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'mode': self.mode,
|
||||
'results': self.results
|
||||
}, f, indent=2)
|
||||
|
||||
print(f"\nResults saved to {OUTPUT_DIR}/")
|
||||
sys.exit(0)
|
||||
|
||||
def start(self):
|
||||
print("="*50)
|
||||
print("McRogueFace Stress Test Suite")
|
||||
print("="*50)
|
||||
print(f"Tests: {len(self.tests)}, Duration: {TEST_DURATION_MS}ms each")
|
||||
|
||||
mcrfpy.createScene("init")
|
||||
ui = mcrfpy.sceneUI("init")
|
||||
ui.append(mcrfpy.Frame(pos=(0,0), size=(10,10))) # Required for timer to fire
|
||||
mcrfpy.setScene("init")
|
||||
mcrfpy.setTimer("tick", self.tick, TIMER_INTERVAL_MS)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TEST SETUP FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def test_many_frames(scene_name):
|
||||
"""1000 Frame elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(1000):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=((i % 32) * 32, (i // 32) * 24),
|
||||
size=(30, 22),
|
||||
fill_color=mcrfpy.Color((i*7)%256, (i*13)%256, (i*17)%256)
|
||||
)
|
||||
ui.append(frame)
|
||||
mcrfpy.log_benchmark("1000 frames created")
|
||||
|
||||
def test_many_sprites(scene_name):
|
||||
"""500 Sprite elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
for i in range(500):
|
||||
sprite = mcrfpy.Sprite(
|
||||
pos=((i % 20) * 48 + 10, (i // 20) * 28 + 10),
|
||||
texture=texture,
|
||||
sprite_index=i % 128
|
||||
)
|
||||
sprite.scale_x = 2.0
|
||||
sprite.scale_y = 2.0
|
||||
ui.append(sprite)
|
||||
mcrfpy.log_benchmark("500 sprites created")
|
||||
|
||||
def test_many_captions(scene_name):
|
||||
"""500 Caption elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(500):
|
||||
caption = mcrfpy.Caption(
|
||||
text=f"Text #{i}",
|
||||
pos=((i % 20) * 50 + 5, (i // 20) * 28 + 5)
|
||||
)
|
||||
ui.append(caption)
|
||||
mcrfpy.log_benchmark("500 captions created")
|
||||
|
||||
def test_deep_nesting(scene_name):
|
||||
"""15-level nested frames"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
current = ui
|
||||
for level in range(15):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(20, 20),
|
||||
size=(1024 - level * 60, 768 - level * 45),
|
||||
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||
)
|
||||
current.append(frame)
|
||||
# Add children at each level
|
||||
for j in range(3):
|
||||
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||
frame.children.append(child)
|
||||
current = frame.children
|
||||
mcrfpy.log_benchmark("15-level nesting created")
|
||||
|
||||
def test_large_grid(scene_name):
|
||||
"""100x100 grid with 500 entities"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
grid = mcrfpy.Grid(pos=(50, 50), size=(900, 650), grid_size=(100, 100), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
for y in range(100):
|
||||
for x in range(100):
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = (x + y) % 64
|
||||
|
||||
for i in range(500):
|
||||
entity = mcrfpy.Entity(
|
||||
grid_pos=((i * 7) % 100, (i * 11) % 100),
|
||||
texture=texture,
|
||||
sprite_index=(i * 3) % 128,
|
||||
grid=grid
|
||||
)
|
||||
mcrfpy.log_benchmark("100x100 grid with 500 entities created")
|
||||
|
||||
def test_animation_stress(scene_name):
|
||||
"""100 frames with 200 animations"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(100):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=((i % 10) * 100 + 10, (i // 10) * 70 + 10),
|
||||
size=(80, 50),
|
||||
fill_color=mcrfpy.Color(100, 150, 200)
|
||||
)
|
||||
ui.append(frame)
|
||||
|
||||
# Two animations per frame
|
||||
anim_x = mcrfpy.Animation("x", float((i % 10) * 100 + 50), 1.5, "easeInOut")
|
||||
anim_x.start(frame)
|
||||
anim_o = mcrfpy.Animation("fill_color.a", 128 + (i % 128), 2.0, "linear")
|
||||
anim_o.start(frame)
|
||||
mcrfpy.log_benchmark("100 frames with 200 animations")
|
||||
|
||||
def test_static_scene(scene_name):
|
||||
"""Static game scene (ideal for caching)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50))
|
||||
ui.append(bg)
|
||||
|
||||
# UI panel
|
||||
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70))
|
||||
ui.append(panel)
|
||||
for i in range(10):
|
||||
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||
panel.children.append(caption)
|
||||
|
||||
# Grid
|
||||
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||
ui.append(grid)
|
||||
for y in range(30):
|
||||
for x in range(40):
|
||||
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||
|
||||
for i in range(20):
|
||||
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||
mcrfpy.log_benchmark("Static game scene created")
|
||||
|
||||
|
||||
def test_static_scene_cached(scene_name):
|
||||
"""Static game scene with cache_subtree enabled (#144)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Background with caching enabled
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50), cache_subtree=True)
|
||||
ui.append(bg)
|
||||
|
||||
# UI panel with caching enabled
|
||||
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70), cache_subtree=True)
|
||||
ui.append(panel)
|
||||
for i in range(10):
|
||||
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||
panel.children.append(caption)
|
||||
|
||||
# Grid (not cached - grids handle their own optimization)
|
||||
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||
ui.append(grid)
|
||||
for y in range(30):
|
||||
for x in range(40):
|
||||
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||
|
||||
for i in range(20):
|
||||
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||
mcrfpy.log_benchmark("Static game scene with cache_subtree created")
|
||||
|
||||
|
||||
def test_deep_nesting_cached(scene_name):
|
||||
"""15-level nested frames with cache_subtree on outer frame (#144)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
|
||||
# Outer frame with caching - entire subtree cached
|
||||
outer = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(0, 100, 255, 200),
|
||||
cache_subtree=True # Cache entire nested hierarchy
|
||||
)
|
||||
ui.append(outer)
|
||||
|
||||
current = outer.children
|
||||
for level in range(15):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(20, 20),
|
||||
size=(1024 - level * 60, 768 - level * 45),
|
||||
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||
)
|
||||
current.append(frame)
|
||||
# Add children at each level
|
||||
for j in range(3):
|
||||
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||
frame.children.append(child)
|
||||
current = frame.children
|
||||
mcrfpy.log_benchmark("15-level nesting with cache_subtree created")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
runner = StressTestRunner()
|
||||
runner.add_test("many_frames", test_many_frames, "1000 Frame elements")
|
||||
runner.add_test("many_sprites", test_many_sprites, "500 Sprite elements")
|
||||
runner.add_test("many_captions", test_many_captions, "500 Caption elements")
|
||||
runner.add_test("deep_nesting", test_deep_nesting, "15-level nested hierarchy")
|
||||
runner.add_test("deep_nesting_cached", test_deep_nesting_cached, "15-level nested (cache_subtree)")
|
||||
runner.add_test("large_grid", test_large_grid, "100x100 grid, 500 entities")
|
||||
runner.add_test("animation_stress", test_animation_stress, "100 frames, 200 animations")
|
||||
runner.add_test("static_scene", test_static_scene, "Static game scene (no caching)")
|
||||
runner.add_test("static_scene_cached", test_static_scene_cached, "Static game scene (cache_subtree)")
|
||||
runner.start()
|
||||
140
tests/benchmarks/tcod_scale_test.py
Normal file
140
tests/benchmarks/tcod_scale_test.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
TCOD Scaling Benchmark - Test pathfinding/FOV on large grids
|
||||
|
||||
Tests whether TCOD operations scale acceptably on 1000x1000 grids,
|
||||
to determine if TCOD data needs chunking or can stay as single logical grid.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Grid sizes to test
|
||||
SIZES = [(100, 100), (250, 250), (500, 500), (1000, 1000)]
|
||||
ITERATIONS = 10
|
||||
|
||||
def benchmark_grid_size(grid_x, grid_y):
|
||||
"""Benchmark TCOD operations for a given grid size"""
|
||||
results = {}
|
||||
|
||||
# Create scene and grid
|
||||
scene_name = f"bench_{grid_x}x{grid_y}"
|
||||
mcrfpy.createScene(scene_name)
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Time grid creation
|
||||
t0 = time.perf_counter()
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(0, 0),
|
||||
size=(800, 600),
|
||||
grid_size=(grid_x, grid_y),
|
||||
texture=texture
|
||||
)
|
||||
ui.append(grid)
|
||||
results['create_ms'] = (time.perf_counter() - t0) * 1000
|
||||
|
||||
# Set up some walkability (maze-like pattern)
|
||||
t0 = time.perf_counter()
|
||||
for y in range(grid_y):
|
||||
for x in range(grid_x):
|
||||
cell = grid.at(x, y)
|
||||
# Create a simple maze: every 3rd cell is a wall
|
||||
cell.walkable = not ((x % 3 == 0) and (y % 3 == 0))
|
||||
cell.transparent = cell.walkable
|
||||
results['setup_walkability_ms'] = (time.perf_counter() - t0) * 1000
|
||||
|
||||
# Add an entity for FOV perspective
|
||||
entity = mcrfpy.Entity(
|
||||
grid_pos=(grid_x // 2, grid_y // 2),
|
||||
texture=texture,
|
||||
sprite_index=64,
|
||||
grid=grid
|
||||
)
|
||||
|
||||
# Benchmark FOV computation
|
||||
fov_times = []
|
||||
for i in range(ITERATIONS):
|
||||
# Move entity to different positions
|
||||
ex, ey = (i * 7) % (grid_x - 20) + 10, (i * 11) % (grid_y - 20) + 10
|
||||
t0 = time.perf_counter()
|
||||
grid.compute_fov(ex, ey, radius=15)
|
||||
fov_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['fov_avg_ms'] = sum(fov_times) / len(fov_times)
|
||||
results['fov_max_ms'] = max(fov_times)
|
||||
|
||||
# Benchmark A* pathfinding (corner to corner)
|
||||
path_times = []
|
||||
for i in range(ITERATIONS):
|
||||
# Path from near origin to near opposite corner
|
||||
x1, y1 = 1, 1
|
||||
x2, y2 = grid_x - 2, grid_y - 2
|
||||
t0 = time.perf_counter()
|
||||
path = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
path_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['astar_avg_ms'] = sum(path_times) / len(path_times)
|
||||
results['astar_max_ms'] = max(path_times)
|
||||
results['astar_path_len'] = len(path) if path else 0
|
||||
|
||||
# Benchmark Dijkstra (full map distance calculation)
|
||||
dijkstra_times = []
|
||||
for i in range(ITERATIONS):
|
||||
cx, cy = grid_x // 2, grid_y // 2
|
||||
t0 = time.perf_counter()
|
||||
grid.compute_dijkstra(cx, cy)
|
||||
dijkstra_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['dijkstra_avg_ms'] = sum(dijkstra_times) / len(dijkstra_times)
|
||||
results['dijkstra_max_ms'] = max(dijkstra_times)
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("TCOD Scaling Benchmark")
|
||||
print("=" * 60)
|
||||
print(f"Testing grid sizes: {SIZES}")
|
||||
print(f"Iterations per test: {ITERATIONS}")
|
||||
print()
|
||||
|
||||
all_results = {}
|
||||
|
||||
for grid_x, grid_y in SIZES:
|
||||
print(f"\n--- Grid {grid_x}x{grid_y} ({grid_x * grid_y:,} cells) ---")
|
||||
try:
|
||||
results = benchmark_grid_size(grid_x, grid_y)
|
||||
all_results[f"{grid_x}x{grid_y}"] = results
|
||||
|
||||
print(f" Creation: {results['create_ms']:.2f}ms")
|
||||
print(f" Walkability: {results['setup_walkability_ms']:.2f}ms")
|
||||
print(f" FOV (r=15): {results['fov_avg_ms']:.3f}ms avg, {results['fov_max_ms']:.3f}ms max")
|
||||
print(f" A* path: {results['astar_avg_ms']:.2f}ms avg, {results['astar_max_ms']:.2f}ms max (len={results['astar_path_len']})")
|
||||
print(f" Dijkstra: {results['dijkstra_avg_ms']:.2f}ms avg, {results['dijkstra_max_ms']:.2f}ms max")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
all_results[f"{grid_x}x{grid_y}"] = {'error': str(e)}
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY - Per-frame budget analysis (targeting 16ms for 60fps)")
|
||||
print("=" * 60)
|
||||
|
||||
for size, results in all_results.items():
|
||||
if 'error' in results:
|
||||
print(f" {size}: ERROR")
|
||||
else:
|
||||
total_logic = results['fov_avg_ms'] + results['astar_avg_ms']
|
||||
print(f" {size}: FOV+A* = {total_logic:.2f}ms ({total_logic/16*100:.0f}% of frame budget)")
|
||||
|
||||
print("\nDone.")
|
||||
sys.exit(0)
|
||||
|
||||
# Run immediately (no timer needed for this test)
|
||||
mcrfpy.createScene("init")
|
||||
mcrfpy.setScene("init")
|
||||
|
||||
# Use a timer to let the engine initialize
|
||||
def run_benchmark(runtime):
|
||||
main()
|
||||
|
||||
mcrfpy.setTimer("bench", run_benchmark, 100)
|
||||
Loading…
Add table
Add a link
Reference in a new issue