diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 2082c39..50b838c 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -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())); diff --git a/src/UIArc.cpp b/src/UIArc.cpp index ce8d450..120d59d 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -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; diff --git a/src/UICaption.cpp b/src/UICaption.cpp index aa3810f..9014ef7 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -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(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(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(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(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(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(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(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(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(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(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; diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 6236e38..d141b95 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -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; diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 88f1563..bc6eb88 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -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; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 3cdbf56..e646178 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -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(value); + markDirty(); // #144 - Z-order change affects parent return true; } else if (name == "fill_color.r") { fill_color.r = static_cast(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(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(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(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; diff --git a/src/UILine.cpp b/src/UILine.cpp index a504c24..f874281 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -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; diff --git a/src/UISprite.cpp b/src/UISprite.cpp index a9c0ac5..56bd4de 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -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(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; diff --git a/tests/benchmarks/benchmark_suite.py b/tests/benchmarks/benchmark_suite.py new file mode 100644 index 0000000..18806d4 --- /dev/null +++ b/tests/benchmarks/benchmark_suite.py @@ -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()