feat: Add dirty flag propagation to all UIDrawables and expand metrics API (#144, #104)

- Add markDirty() calls to setProperty() methods in:
  - UISprite: position, scale, sprite_index changes
  - UICaption: position, font_size, colors, text changes
  - UIGrid: position, size, center, zoom, color changes
  - UILine: thickness, position, endpoints, color changes
  - UICircle: radius, position, colors changes
  - UIArc: radius, angles, position, color changes
  - UIEntity: position changes propagate to parent grid

- Expand getMetrics() Python API to include detailed timing breakdown:
  - grid_render_time, entity_render_time, fov_overlay_time
  - python_time, animation_time
  - grid_cells_rendered, entities_rendered, total_entities

- Add comprehensive benchmark suite (tests/benchmarks/benchmark_suite.py):
  - 6 scenarios: empty, static UI, animated UI, mixed, deep hierarchy, grid stress
  - Automated metrics collection and performance assessment
  - Timing breakdown percentages

This enables proper dirty flag propagation for the upcoming texture caching
system (#144) and provides infrastructure for performance benchmarking (#104).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-28 15:44:09 -05:00
parent 6c496b8732
commit 219a559c35
9 changed files with 442 additions and 3 deletions

View File

@ -1233,17 +1233,29 @@ PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
// Create a dictionary with metrics
PyObject* dict = PyDict_New();
if (!dict) return NULL;
// Add frame time metrics
PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));
// Add draw call metrics
PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));
// #144 - Add detailed timing breakdown (in milliseconds)
PyDict_SetItemString(dict, "grid_render_time", PyFloat_FromDouble(game->metrics.gridRenderTime));
PyDict_SetItemString(dict, "entity_render_time", PyFloat_FromDouble(game->metrics.entityRenderTime));
PyDict_SetItemString(dict, "fov_overlay_time", PyFloat_FromDouble(game->metrics.fovOverlayTime));
PyDict_SetItemString(dict, "python_time", PyFloat_FromDouble(game->metrics.pythonScriptTime));
PyDict_SetItemString(dict, "animation_time", PyFloat_FromDouble(game->metrics.animationTime));
// #144 - Add grid-specific metrics
PyDict_SetItemString(dict, "grid_cells_rendered", PyLong_FromLong(game->metrics.gridCellsRendered));
PyDict_SetItemString(dict, "entities_rendered", PyLong_FromLong(game->metrics.entitiesRendered));
PyDict_SetItemString(dict, "total_entities", PyLong_FromLong(game->metrics.totalEntities));
// Add general metrics
PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));

View File

@ -206,30 +206,36 @@ void UIArc::resize(float w, float h) {
bool UIArc::setProperty(const std::string& name, float value) {
if (name == "radius") {
setRadius(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "start_angle") {
setStartAngle(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "end_angle") {
setEndAngle(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "thickness") {
setThickness(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "x") {
center.x = value;
position = center;
vertices_dirty = true;
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "y") {
center.y = value;
position = center;
vertices_dirty = true;
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
return false;
@ -238,6 +244,7 @@ bool UIArc::setProperty(const std::string& name, float value) {
bool UIArc::setProperty(const std::string& name, const sf::Color& value) {
if (name == "color") {
setColor(value);
markDirty(); // #144 - Content change
return true;
}
return false;
@ -246,6 +253,7 @@ bool UIArc::setProperty(const std::string& name, const sf::Color& value) {
bool UIArc::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "center") {
setCenter(value);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
return false;

View File

@ -471,71 +471,84 @@ bool UICaption::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
text.setPosition(position); // Keep text in sync
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "y") {
position.y = value;
text.setPosition(position); // Keep text in sync
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
text.setCharacterSize(static_cast<unsigned int>(value));
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline") {
text.setOutlineThickness(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.r") {
auto color = text.getFillColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.g") {
auto color = text.getFillColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.b") {
auto color = text.getFillColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.a") {
auto color = text.getFillColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline_color.r") {
auto color = text.getOutlineColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline_color.g") {
auto color = text.getOutlineColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline_color.b") {
auto color = text.getOutlineColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline_color.a") {
auto color = text.getOutlineColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
markDirty(); // #144 - Content change
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
markDirty(); // #144 - Z-order change affects parent
return true;
}
return false;
@ -544,10 +557,12 @@ bool UICaption::setProperty(const std::string& name, float value) {
bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
text.setFillColor(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "outline_color") {
text.setOutlineColor(value);
markDirty(); // #144 - Content change
return true;
}
return false;
@ -556,6 +571,7 @@ bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
bool UICaption::setProperty(const std::string& name, const std::string& value) {
if (name == "text") {
text.setString(value);
markDirty(); // #144 - Content change
return true;
}
return false;

View File

@ -172,15 +172,19 @@ void UICircle::resize(float w, float h) {
bool UICircle::setProperty(const std::string& name, float value) {
if (name == "radius") {
setRadius(value);
markDirty(); // #144 - Content change
return true;
} else if (name == "outline") {
setOutline(value);
markDirty(); // #144 - Content change
return true;
} else if (name == "x") {
position.x = value;
markDirty(); // #144 - Propagate to parent for texture caching
return true;
} else if (name == "y") {
position.y = value;
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
return false;
@ -189,9 +193,11 @@ bool UICircle::setProperty(const std::string& name, float value) {
bool UICircle::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
setFillColor(value);
markDirty(); // #144 - Content change
return true;
} else if (name == "outline_color") {
setOutlineColor(value);
markDirty(); // #144 - Content change
return true;
}
return false;
@ -200,6 +206,7 @@ bool UICircle::setProperty(const std::string& name, const sf::Color& value) {
bool UICircle::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "center" || name == "position") {
position = value;
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
return false;

View File

@ -674,15 +674,18 @@ bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
return true;
}
else if (name == "y") {
position.y = value;
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
return true;
}
else if (name == "sprite_scale") {
sprite.setScale(sf::Vector2f(value, value));
if (grid) grid->markDirty(); // #144 - Content change
return true;
}
return false;
@ -691,6 +694,7 @@ bool UIEntity::setProperty(const std::string& name, float value) {
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_index" || name == "sprite_number") {
sprite.setSpriteIndex(value);
if (grid) grid->markDirty(); // #144 - Content change
return true;
}
return false;

View File

@ -2725,54 +2725,66 @@ bool UIGrid::setProperty(const std::string& name, float value) {
position.x = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "y") {
position.y = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "w" || name == "width") {
box.setSize(sf::Vector2f(value, box.getSize().y));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
markDirty(); // #144 - Size change
return true;
}
else if (name == "h" || name == "height") {
box.setSize(sf::Vector2f(box.getSize().x, value));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
markDirty(); // #144 - Size change
return true;
}
else if (name == "center_x") {
center_x = value;
markDirty(); // #144 - View change affects content
return true;
}
else if (name == "center_y") {
center_y = value;
markDirty(); // #144 - View change affects content
return true;
}
else if (name == "zoom") {
zoom = value;
markDirty(); // #144 - View change affects content
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
markDirty(); // #144 - Z-order change affects parent
return true;
}
else if (name == "fill_color.r") {
fill_color.r = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.g") {
fill_color.g = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.b") {
fill_color.b = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
markDirty(); // #144 - Content change
return true;
}
else if (name == "fill_color.a") {
fill_color.a = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
markDirty(); // #144 - Content change
return true;
}
return false;
@ -2783,16 +2795,19 @@ bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
position = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "size") {
box.setSize(value);
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
markDirty(); // #144 - Size change
return true;
}
else if (name == "center") {
center_x = value.x;
center_y = value.y;
markDirty(); // #144 - View change affects content
return true;
}
return false;

View File

@ -207,36 +207,43 @@ bool UILine::setProperty(const std::string& name, float value) {
if (name == "thickness") {
thickness = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
else if (name == "x") {
float dx = value - position.x;
move(dx, 0);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "y") {
float dy = value - position.y;
move(0, dy);
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "start_x") {
start_pos.x = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
else if (name == "start_y") {
start_pos.y = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
else if (name == "end_x") {
end_pos.x = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
else if (name == "end_y") {
end_pos.y = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
return false;
@ -246,6 +253,7 @@ bool UILine::setProperty(const std::string& name, const sf::Color& value) {
if (name == "color") {
color = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
return false;
@ -255,11 +263,13 @@ bool UILine::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "start") {
start_pos = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
else if (name == "end") {
end_pos = value;
vertices_dirty = true;
markDirty(); // #144 - Content change
return true;
}
return false;

View File

@ -499,27 +499,33 @@ bool UISprite::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
sprite.setPosition(position); // Keep sprite in sync
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "y") {
position.y = value;
sprite.setPosition(position); // Keep sprite in sync
markDirty(); // #144 - Propagate to parent for texture caching
return true;
}
else if (name == "scale") {
sprite.setScale(sf::Vector2f(value, value));
markDirty(); // #144 - Content change
return true;
}
else if (name == "scale_x") {
sprite.setScale(sf::Vector2f(value, sprite.getScale().y));
markDirty(); // #144 - Content change
return true;
}
else if (name == "scale_y") {
sprite.setScale(sf::Vector2f(sprite.getScale().x, value));
markDirty(); // #144 - Content change
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
markDirty(); // #144 - Z-order change affects parent
return true;
}
return false;
@ -528,10 +534,12 @@ bool UISprite::setProperty(const std::string& name, float value) {
bool UISprite::setProperty(const std::string& name, int value) {
if (name == "sprite_index" || name == "sprite_number") {
setSpriteIndex(value);
markDirty(); // #144 - Content change
return true;
}
else if (name == "z_index") {
z_index = value;
markDirty(); // #144 - Z-order change affects parent
return true;
}
return false;

View File

@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""Comprehensive Performance Benchmark Suite for McRogueFace (#104, #144)
Runs 6 benchmark scenarios to establish baseline performance metrics:
1. Empty scene - Pure engine overhead
2. Static UI - 100 frames, no animation (best case for caching)
3. Animated UI - 100 frames, all animating (worst case for caching)
4. Mixed UI - 100 frames, 10 animating (realistic case)
5. Deep hierarchy - 5 levels of nesting (propagation cost)
6. Grid stress - Large grid with entities (known bottleneck)
Usage:
./mcrogueface --headless --exec tests/benchmarks/benchmark_suite.py
Results are printed to stdout in a format suitable for tracking over time.
"""
import mcrfpy
import sys
import random
# Benchmark configuration
WARMUP_FRAMES = 30 # Frames to skip before measuring
MEASURE_FRAMES = 120 # Frames to measure (2 seconds at 60fps)
FRAME_BUDGET_MS = 16.67 # Target: 60 FPS
# Storage for results
results = {}
current_scenario = None
frame_count = 0
metrics_samples = []
def collect_metrics(runtime):
"""Timer callback to collect metrics each frame."""
global frame_count, metrics_samples
frame_count += 1
# Skip warmup frames
if frame_count <= WARMUP_FRAMES:
return
# Collect sample
m = mcrfpy.getMetrics()
metrics_samples.append({
'frame_time': m['frame_time'],
'avg_frame_time': m['avg_frame_time'],
'fps': m['fps'],
'draw_calls': m['draw_calls'],
'ui_elements': m['ui_elements'],
'visible_elements': m['visible_elements'],
'grid_render_time': m['grid_render_time'],
'entity_render_time': m['entity_render_time'],
'python_time': m['python_time'],
'animation_time': m['animation_time'],
'grid_cells_rendered': m['grid_cells_rendered'],
'entities_rendered': m['entities_rendered'],
})
# Check if we have enough samples
if len(metrics_samples) >= MEASURE_FRAMES:
finish_scenario()
def finish_scenario():
"""Calculate statistics and store results for current scenario."""
global results, current_scenario, metrics_samples
mcrfpy.delTimer("benchmark_collect")
if not metrics_samples:
print(f" WARNING: No samples collected for {current_scenario}")
return
# Calculate averages
n = len(metrics_samples)
avg = lambda key: sum(s[key] for s in metrics_samples) / n
results[current_scenario] = {
'samples': n,
'avg_frame_time': avg('frame_time'),
'avg_fps': avg('fps'),
'avg_draw_calls': avg('draw_calls'),
'avg_ui_elements': avg('ui_elements'),
'avg_grid_render_time': avg('grid_render_time'),
'avg_entity_render_time': avg('entity_render_time'),
'avg_python_time': avg('python_time'),
'avg_animation_time': avg('animation_time'),
'avg_grid_cells': avg('grid_cells_rendered'),
'avg_entities': avg('entities_rendered'),
'max_frame_time': max(s['frame_time'] for s in metrics_samples),
'min_frame_time': min(s['frame_time'] for s in metrics_samples),
}
# Calculate percentage breakdown
r = results[current_scenario]
total = r['avg_frame_time']
if total > 0:
r['pct_grid'] = (r['avg_grid_render_time'] / total) * 100
r['pct_entity'] = (r['avg_entity_render_time'] / total) * 100
r['pct_python'] = (r['avg_python_time'] / total) * 100
r['pct_animation'] = (r['avg_animation_time'] / total) * 100
r['pct_other'] = 100 - r['pct_grid'] - r['pct_entity'] - r['pct_python'] - r['pct_animation']
print(f" Completed: {n} samples, avg {r['avg_frame_time']:.2f}ms ({r['avg_fps']:.0f} FPS)")
# Run next scenario
run_next_scenario()
def run_next_scenario():
"""Run the next benchmark scenario in sequence."""
global current_scenario, frame_count, metrics_samples
scenarios = [
('1_empty', setup_empty_scene),
('2_static_100', setup_static_100),
('3_animated_100', setup_animated_100),
('4_mixed_100', setup_mixed_100),
('5_deep_hierarchy', setup_deep_hierarchy),
('6_grid_stress', setup_grid_stress),
]
# Find current index
current_idx = -1
if current_scenario:
for i, (name, _) in enumerate(scenarios):
if name == current_scenario:
current_idx = i
break
# Move to next
next_idx = current_idx + 1
if next_idx >= len(scenarios):
# All done
print_results()
return
# Setup next scenario
current_scenario = scenarios[next_idx][0]
frame_count = 0
metrics_samples = []
print(f"\n[{next_idx + 1}/{len(scenarios)}] Running: {current_scenario}")
# Run setup function
scenarios[next_idx][1]()
# Start collection timer (runs every frame)
mcrfpy.setTimer("benchmark_collect", collect_metrics, 1)
# ============================================================================
# Scenario Setup Functions
# ============================================================================
def setup_empty_scene():
"""Scenario 1: Empty scene - pure engine overhead."""
mcrfpy.createScene("bench_empty")
mcrfpy.setScene("bench_empty")
def setup_static_100():
"""Scenario 2: 100 static frames - best case for caching."""
mcrfpy.createScene("bench_static")
ui = mcrfpy.sceneUI("bench_static")
# Create 100 frames in a 10x10 grid
for i in range(100):
x = (i % 10) * 100 + 12
y = (i // 10) * 70 + 12
frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
frame.outline = 2
frame.outline_color = mcrfpy.Color(255, 255, 255)
# Add a caption child
cap = mcrfpy.Caption(text=f"F{i}", pos=(5, 5))
cap.fill_color = mcrfpy.Color(255, 255, 255)
frame.children.append(cap)
ui.append(frame)
mcrfpy.setScene("bench_static")
def setup_animated_100():
"""Scenario 3: 100 frames all animating - worst case for caching."""
mcrfpy.createScene("bench_animated")
ui = mcrfpy.sceneUI("bench_animated")
frames = []
for i in range(100):
x = (i % 10) * 100 + 12
y = (i // 10) * 70 + 12
frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
frames.append(frame)
ui.append(frame)
mcrfpy.setScene("bench_animated")
# Start animations on all frames (color animation = content change)
for i, frame in enumerate(frames):
# Animate fill color - this dirties the frame
target_r = (i * 17) % 256
anim = mcrfpy.Animation("fill_color.r", float(target_r), 2.0, "linear")
anim.start(frame)
def setup_mixed_100():
"""Scenario 4: 100 frames, only 10 animating - realistic case."""
mcrfpy.createScene("bench_mixed")
ui = mcrfpy.sceneUI("bench_mixed")
frames = []
for i in range(100):
x = (i % 10) * 100 + 12
y = (i // 10) * 70 + 12
frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
frames.append(frame)
ui.append(frame)
mcrfpy.setScene("bench_mixed")
# Animate only 10 frames (every 10th)
for i in range(0, 100, 10):
frame = frames[i]
anim = mcrfpy.Animation("fill_color.r", 255.0, 2.0, "easeInOut")
anim.start(frame)
def setup_deep_hierarchy():
"""Scenario 5: 5 levels of nesting - test dirty flag propagation cost."""
mcrfpy.createScene("bench_deep")
ui = mcrfpy.sceneUI("bench_deep")
# Create 10 trees, each with 5 levels of nesting
deepest_frames = []
for tree in range(10):
x_offset = tree * 100 + 12
current_parent = None
for level in range(5):
frame = mcrfpy.Frame(
pos=(10, 10) if level > 0 else (x_offset, 100),
size=(80 - level * 10, 500 - level * 80)
)
frame.fill_color = mcrfpy.Color(50 + level * 40, 100, 200 - level * 30)
frame.outline = 1
if current_parent is None:
ui.append(frame)
else:
current_parent.children.append(frame)
current_parent = frame
if level == 4: # Deepest level
deepest_frames.append(frame)
mcrfpy.setScene("bench_deep")
# Animate the deepest frames - tests propagation up the hierarchy
for frame in deepest_frames:
anim = mcrfpy.Animation("fill_color.g", 255.0, 2.0, "linear")
anim.start(frame)
def setup_grid_stress():
"""Scenario 6: Large grid with entities - known performance bottleneck."""
mcrfpy.createScene("bench_grid")
ui = mcrfpy.sceneUI("bench_grid")
# Create a 50x50 grid (2500 cells)
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(50, 50), size=(700, 700))
grid.zoom = 0.75
grid.center = (400, 400) # Center view
ui.append(grid)
# Fill with alternating colors
for y in range(50):
for x in range(50):
cell = grid.at(x, y)
if (x + y) % 2 == 0:
cell.color = mcrfpy.Color(60, 60, 80)
else:
cell.color = mcrfpy.Color(40, 40, 60)
# Add 50 entities
try:
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
for i in range(50):
# Entity takes positional args: (position, texture, sprite_index, grid)
pos = mcrfpy.Vector(random.randint(5, 45), random.randint(5, 45))
entity = mcrfpy.Entity(pos, texture, random.randint(0, 100), grid)
grid.entities.append(entity)
except Exception as e:
print(f" Note: Could not create entities: {e}")
mcrfpy.setScene("bench_grid")
# ============================================================================
# Results Output
# ============================================================================
def print_results():
"""Print final benchmark results."""
print("\n" + "=" * 70)
print("BENCHMARK RESULTS")
print("=" * 70)
print(f"\n{'Scenario':<20} {'Avg FPS':>8} {'Avg ms':>8} {'Max ms':>8} {'Draw Calls':>10}")
print("-" * 70)
for name, r in results.items():
print(f"{name:<20} {r['avg_fps']:>8.1f} {r['avg_frame_time']:>8.2f} {r['max_frame_time']:>8.2f} {r['avg_draw_calls']:>10.0f}")
print("\n" + "-" * 70)
print("TIMING BREAKDOWN (% of frame time)")
print("-" * 70)
print(f"{'Scenario':<20} {'Grid':>8} {'Entity':>8} {'Python':>8} {'Anim':>8} {'Other':>8}")
print("-" * 70)
for name, r in results.items():
if 'pct_grid' in r:
print(f"{name:<20} {r['pct_grid']:>7.1f}% {r['pct_entity']:>7.1f}% {r['pct_python']:>7.1f}% {r['pct_animation']:>7.1f}% {r['pct_other']:>7.1f}%")
print("\n" + "=" * 70)
# Performance assessment
print("\nPERFORMANCE ASSESSMENT:")
for name, r in results.items():
status = "PASS" if r['avg_frame_time'] < FRAME_BUDGET_MS else "OVER BUDGET"
print(f" {name}: {status} ({r['avg_frame_time']:.2f}ms vs {FRAME_BUDGET_MS:.2f}ms budget)")
print("\nBenchmark complete.")
sys.exit(0)
# ============================================================================
# Main Entry Point
# ============================================================================
if __name__ == "__main__":
print("=" * 70)
print("McRogueFace Performance Benchmark Suite")
print("=" * 70)
print(f"Configuration: {WARMUP_FRAMES} warmup frames, {MEASURE_FRAMES} measurement frames")
print(f"Target: {FRAME_BUDGET_MS:.2f}ms per frame (60 FPS)")
# Start the benchmark sequence
run_next_scenario()