feat: Add comprehensive profiling system with F3 overlay

Add real-time performance profiling infrastructure to monitor frame times,
render performance, and identify bottlenecks.

Features:
- Profiler.h: ScopedTimer RAII helper for automatic timing measurements
- ProfilerOverlay: F3-togglable overlay displaying real-time metrics
- Detailed timing breakdowns: grid rendering, entity rendering, FOV,
  Python callbacks, and animation updates
- Per-frame counters: cells rendered, entities rendered, draw calls
- Performance color coding: green (<16ms), yellow (<33ms), red (>33ms)
- Benchmark suite: static grid and moving entities performance tests

Integration:
- GameEngine: Integrated profiler overlay with F3 toggle
- UIGrid: Added timing instrumentation for grid and entity rendering
- Metrics tracked in ProfilingMetrics struct with 60-frame averaging

Usage:
- Press F3 in-game to toggle profiler overlay
- Run benchmarks with tests/benchmark_*.py scripts
- ScopedTimer automatically measures code block execution time

This addresses issue #104 (Basic profiling/metrics).

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-10-25 00:45:44 -04:00
parent 8153fd2503
commit e9e9cd2f81
8 changed files with 715 additions and 29 deletions

View File

@ -42,8 +42,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
updateViewport(); updateViewport();
scene = "uitest"; scene = "uitest";
scenes["uitest"] = new UITestScene(this); scenes["uitest"] = new UITestScene(this);
McRFPy_API::game = this; McRFPy_API::game = this;
// Initialize profiler overlay
profilerOverlay = new ProfilerOverlay(Resources::font);
// Only load game.py if no custom script/command/module/exec is specified // Only load game.py if no custom script/command/module/exec is specified
bool should_load_game = config.script_path.empty() && bool should_load_game = config.script_path.empty() &&
@ -85,6 +88,7 @@ GameEngine::~GameEngine()
for (auto& [name, scene] : scenes) { for (auto& [name, scene] : scenes) {
delete scene; delete scene;
} }
delete profilerOverlay;
} }
void GameEngine::cleanup() void GameEngine::cleanup()
@ -199,10 +203,14 @@ void GameEngine::run()
testTimers(); testTimers();
// Update Python scenes // Update Python scenes
McRFPy_API::updatePythonScenes(frameTime); {
ScopedTimer pyTimer(metrics.pythonScriptTime);
McRFPy_API::updatePythonScenes(frameTime);
}
// Update animations (only if frameTime is valid) // Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) { if (frameTime > 0.0f && frameTime < 1.0f) {
ScopedTimer animTimer(metrics.animationTime);
AnimationManager::getInstance().update(frameTime); AnimationManager::getInstance().update(frameTime);
} }
@ -240,6 +248,12 @@ void GameEngine::run()
currentScene()->render(); currentScene()->render();
} }
// Update and render profiler overlay (if enabled)
if (profilerOverlay && !headless) {
profilerOverlay->update(metrics);
profilerOverlay->render(*render_target);
}
// Display the frame // Display the frame
if (headless) { if (headless) {
headless_renderer->display(); headless_renderer->display();
@ -330,6 +344,14 @@ void GameEngine::processEvent(const sf::Event& event)
int actionCode = 0; int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; return; } if (event.type == sf::Event::Closed) { running = false; return; }
// Handle F3 for profiler overlay toggle
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) {
if (profilerOverlay) {
profilerOverlay->toggle();
}
return;
}
// Handle window resize events // Handle window resize events
else if (event.type == sf::Event::Resized) { else if (event.type == sf::Event::Resized) {
// Update the viewport to handle the new window size // Update the viewport to handle the new window size

View File

@ -9,11 +9,16 @@
#include "McRogueFaceConfig.h" #include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h" #include "HeadlessRenderer.h"
#include "SceneTransition.h" #include "SceneTransition.h"
#include "Profiler.h"
#include <memory> #include <memory>
#include <sstream>
class GameEngine class GameEngine
{ {
public: public:
// Forward declare nested class so private section can use it
class ProfilerOverlay;
// Viewport modes (moved here so private section can use it) // Viewport modes (moved here so private section can use it)
enum class ViewportMode { enum class ViewportMode {
Center, // 1:1 pixels, viewport centered in window Center, // 1:1 pixels, viewport centered in window
@ -51,7 +56,12 @@ private:
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
sf::View gameView; // View for the game content sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit; ViewportMode viewportMode = ViewportMode::Fit;
// Profiling overlay
bool showProfilerOverlay = false; // F3 key toggles this
int overlayUpdateCounter = 0; // Only update overlay every N frames
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
void updateViewport(); void updateViewport();
void testTimers(); void testTimers();
@ -69,17 +79,29 @@ public:
int drawCalls = 0; // Draw calls per frame int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements int visibleElements = 0; // Number of visible elements
// Detailed timing breakdowns (added for profiling system)
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
float animationTime = 0.0f; // Time spent updating animations (ms)
// Grid-specific metrics
int gridCellsRendered = 0; // Number of grid cells drawn this frame
int entitiesRendered = 0; // Number of entities drawn this frame
int totalEntities = 0; // Total entities in scene
// Frame time history for averaging // Frame time history for averaging
static constexpr int HISTORY_SIZE = 60; static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0}; float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0; int historyIndex = 0;
void updateFrameTime(float deltaMs) { void updateFrameTime(float deltaMs) {
frameTime = deltaMs; frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs; frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE; historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average // Calculate average
float sum = 0.0f; float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) { for (int i = 0; i < HISTORY_SIZE; ++i) {
@ -88,13 +110,26 @@ public:
avgFrameTime = sum / HISTORY_SIZE; avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0; fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
} }
void resetPerFrame() { void resetPerFrame() {
drawCalls = 0; drawCalls = 0;
uiElements = 0; uiElements = 0;
visibleElements = 0; visibleElements = 0;
// Reset per-frame timing metrics
gridRenderTime = 0.0f;
entityRenderTime = 0.0f;
fovOverlayTime = 0.0f;
pythonScriptTime = 0.0f;
animationTime = 0.0f;
// Reset per-frame counters
gridCellsRendered = 0;
entitiesRendered = 0;
totalEntities = 0;
} }
} metrics; } metrics;
GameEngine(); GameEngine();
GameEngine(const McRogueFaceConfig& cfg); GameEngine(const McRogueFaceConfig& cfg);
~GameEngine(); ~GameEngine();
@ -144,5 +179,30 @@ public:
sf::Music music; sf::Music music;
sf::Sound sfx; sf::Sound sfx;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene); std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
};
/**
* @brief Visual overlay that displays real-time profiling metrics
*/
class GameEngine::ProfilerOverlay {
private:
sf::Font& font;
sf::Text text;
sf::RectangleShape background;
bool visible;
int updateInterval;
int frameCounter;
sf::Color getPerformanceColor(float frameTimeMs);
std::string formatFloat(float value, int precision = 1);
std::string formatPercentage(float part, float total);
public:
ProfilerOverlay(sf::Font& fontRef);
void toggle();
void setVisible(bool vis);
bool isVisible() const;
void update(const ProfilingMetrics& metrics);
void render(sf::RenderTarget& target);
}; };

61
src/Profiler.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "Profiler.h"
#include <iostream>
ProfilingLogger::ProfilingLogger()
: headers_written(false)
{
}
ProfilingLogger::~ProfilingLogger() {
close();
}
bool ProfilingLogger::open(const std::string& filename, const std::vector<std::string>& columns) {
column_names = columns;
file.open(filename);
if (!file.is_open()) {
std::cerr << "Failed to open profiling log file: " << filename << std::endl;
return false;
}
// Write CSV header
for (size_t i = 0; i < columns.size(); ++i) {
file << columns[i];
if (i < columns.size() - 1) {
file << ",";
}
}
file << "\n";
file.flush();
headers_written = true;
return true;
}
void ProfilingLogger::writeRow(const std::vector<float>& values) {
if (!file.is_open()) {
return;
}
if (values.size() != column_names.size()) {
std::cerr << "ProfilingLogger: value count (" << values.size()
<< ") doesn't match column count (" << column_names.size() << ")" << std::endl;
return;
}
for (size_t i = 0; i < values.size(); ++i) {
file << values[i];
if (i < values.size() - 1) {
file << ",";
}
}
file << "\n";
}
void ProfilingLogger::close() {
if (file.is_open()) {
file.flush();
file.close();
}
}

111
src/Profiler.h Normal file
View File

@ -0,0 +1,111 @@
#pragma once
#include <chrono>
#include <string>
#include <vector>
#include <fstream>
/**
* @brief Simple RAII-based profiling timer for measuring code execution time
*
* Usage:
* float timing = 0.0f;
* {
* ScopedTimer timer(timing);
* // ... code to profile ...
* } // timing now contains elapsed milliseconds
*/
class ScopedTimer {
private:
std::chrono::high_resolution_clock::time_point start;
float& target_ms;
public:
/**
* @brief Construct a new Scoped Timer and start timing
* @param target Reference to float that will receive elapsed time in milliseconds
*/
explicit ScopedTimer(float& target)
: target_ms(target)
{
start = std::chrono::high_resolution_clock::now();
}
/**
* @brief Destructor automatically records elapsed time
*/
~ScopedTimer() {
auto end = std::chrono::high_resolution_clock::now();
target_ms = std::chrono::duration<float, std::milli>(end - start).count();
}
// Prevent copying
ScopedTimer(const ScopedTimer&) = delete;
ScopedTimer& operator=(const ScopedTimer&) = delete;
};
/**
* @brief Accumulating timer that adds elapsed time to existing value
*
* Useful for measuring total time across multiple calls in a single frame
*/
class AccumulatingTimer {
private:
std::chrono::high_resolution_clock::time_point start;
float& target_ms;
public:
explicit AccumulatingTimer(float& target)
: target_ms(target)
{
start = std::chrono::high_resolution_clock::now();
}
~AccumulatingTimer() {
auto end = std::chrono::high_resolution_clock::now();
target_ms += std::chrono::duration<float, std::milli>(end - start).count();
}
AccumulatingTimer(const AccumulatingTimer&) = delete;
AccumulatingTimer& operator=(const AccumulatingTimer&) = delete;
};
/**
* @brief CSV profiling data logger for batch analysis
*
* Writes profiling data to CSV file for later analysis with Python/pandas/Excel
*/
class ProfilingLogger {
private:
std::ofstream file;
bool headers_written;
std::vector<std::string> column_names;
public:
ProfilingLogger();
~ProfilingLogger();
/**
* @brief Open a CSV file for writing profiling data
* @param filename Path to CSV file
* @param columns Column names for the CSV header
* @return true if file opened successfully
*/
bool open(const std::string& filename, const std::vector<std::string>& columns);
/**
* @brief Write a row of profiling data
* @param values Data values (must match column count)
*/
void writeRow(const std::vector<float>& values);
/**
* @brief Close the file and flush data
*/
void close();
/**
* @brief Check if logger is ready to write
*/
bool isOpen() const { return file.is_open(); }
};

135
src/ProfilerOverlay.cpp Normal file
View File

@ -0,0 +1,135 @@
#include "GameEngine.h"
#include <sstream>
#include <iomanip>
GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef)
: font(fontRef), visible(false), updateInterval(10), frameCounter(0)
{
text.setFont(font);
text.setCharacterSize(14);
text.setFillColor(sf::Color::White);
text.setPosition(10.0f, 10.0f);
// Semi-transparent dark background
background.setFillColor(sf::Color(0, 0, 0, 180));
background.setPosition(5.0f, 5.0f);
}
void GameEngine::ProfilerOverlay::toggle() {
visible = !visible;
}
void GameEngine::ProfilerOverlay::setVisible(bool vis) {
visible = vis;
}
bool GameEngine::ProfilerOverlay::isVisible() const {
return visible;
}
sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) {
if (frameTimeMs < 16.6f) {
return sf::Color::Green; // 60+ FPS
} else if (frameTimeMs < 33.3f) {
return sf::Color::Yellow; // 30-60 FPS
} else {
return sf::Color::Red; // <30 FPS
}
}
std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) {
std::stringstream ss;
ss << std::fixed << std::setprecision(precision) << value;
return ss.str();
}
std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) {
if (total <= 0.0f) return "0%";
float pct = (part / total) * 100.0f;
return formatFloat(pct, 0) + "%";
}
void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) {
if (!visible) return;
// Only update text every N frames to reduce overhead
frameCounter++;
if (frameCounter < updateInterval) {
return;
}
frameCounter = 0;
std::stringstream ss;
ss << "McRogueFace Performance Monitor\n";
ss << "================================\n";
// Frame time and FPS
float frameMs = metrics.avgFrameTime;
ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n";
// Performance warning
if (frameMs > 33.3f) {
ss << "WARNING: Frame time exceeds 30 FPS target!\n";
}
ss << "\n";
// Timing breakdown
ss << "Frame Time Breakdown:\n";
ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms ("
<< formatPercentage(metrics.gridRenderTime, frameMs) << ")\n";
ss << " Cells: " << metrics.gridCellsRendered << " rendered\n";
ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n";
if (metrics.fovOverlayTime > 0.01f) {
ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n";
}
if (metrics.entityRenderTime > 0.01f) {
ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms ("
<< formatPercentage(metrics.entityRenderTime, frameMs) << ")\n";
}
if (metrics.pythonScriptTime > 0.01f) {
ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms ("
<< formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n";
}
if (metrics.animationTime > 0.01f) {
ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms ("
<< formatPercentage(metrics.animationTime, frameMs) << ")\n";
}
ss << "\n";
// Other metrics
ss << "Draw Calls: " << metrics.drawCalls << "\n";
ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n";
// Calculate unaccounted time
float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime +
metrics.pythonScriptTime + metrics.animationTime;
float unaccountedTime = frameMs - accountedTime;
if (unaccountedTime > 1.0f) {
ss << "\n";
ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms ("
<< formatPercentage(unaccountedTime, frameMs) << ")\n";
}
ss << "\n";
ss << "Press F3 to hide this overlay";
text.setString(ss.str());
// Update background size to fit text
sf::FloatRect textBounds = text.getLocalBounds();
background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f));
}
void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) {
if (!visible) return;
target.draw(background);
target.draw(text);
}

View File

@ -3,6 +3,7 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "UIEntity.h" #include "UIEntity.h"
#include "Profiler.h"
#include <algorithm> #include <algorithm>
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
@ -95,11 +96,14 @@ void UIGrid::update() {}
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
{ {
// Profile total grid rendering time
ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime);
// Check visibility // Check visibility
if (!visible) return; if (!visible) return;
// TODO: Apply opacity to output sprite // TODO: Apply opacity to output sprite
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
// output size can change; update size when drawing // output size can change; update size when drawing
output.setTextureRect( output.setTextureRect(
@ -135,11 +139,12 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
if (y_limit > grid_y) y_limit = grid_y; if (y_limit > grid_y) y_limit = grid_y;
// base layer - bottom color, tile sprite ("ground") // base layer - bottom color, tile sprite ("ground")
int cellsRendered = 0;
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
x < x_limit; //x < view_width; x < x_limit; //x < view_width;
x+=1) x+=1)
{ {
//for (float y = (top_edge >= 0 ? top_edge : 0); //for (float y = (top_edge >= 0 ? top_edge : 0);
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
y < y_limit; //y < view_height; y < y_limit; //y < view_height;
y+=1) y+=1)
@ -163,35 +168,53 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);; sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
renderTexture.draw(sprite); renderTexture.draw(sprite);
} }
cellsRendered++;
} }
} }
// Record how many cells were rendered
Resources::game->metrics.gridCellsRendered += cellsRendered;
// middle layer - entities // middle layer - entities
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window) // disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
for (auto e : *entities) { {
// Skip out-of-bounds entities for performance ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities) int entitiesRendered = 0;
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || int totalEntities = entities->size();
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
continue; // Skip this entity as it's not visible for (auto e : *entities) {
// Skip out-of-bounds entities for performance
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
continue; // Skip this entity as it's not visible
}
//auto drawent = e->cGrid->indexsprite.drawable();
auto& drawent = e->sprite;
//drawent.setScale(zoom, zoom);
drawent.setScale(sf::Vector2f(zoom, zoom));
auto pixel_pos = sf::Vector2f(
(e->position.x*cell_width - left_spritepixels) * zoom,
(e->position.y*cell_height - top_spritepixels) * zoom );
//drawent.setPosition(pixel_pos);
//renderTexture.draw(drawent);
drawent.render(pixel_pos, renderTexture);
entitiesRendered++;
} }
//auto drawent = e->cGrid->indexsprite.drawable(); // Record entity rendering stats
auto& drawent = e->sprite; Resources::game->metrics.entitiesRendered += entitiesRendered;
//drawent.setScale(zoom, zoom); Resources::game->metrics.totalEntities += totalEntities;
drawent.setScale(sf::Vector2f(zoom, zoom));
auto pixel_pos = sf::Vector2f(
(e->position.x*cell_width - left_spritepixels) * zoom,
(e->position.y*cell_height - top_spritepixels) * zoom );
//drawent.setPosition(pixel_pos);
//renderTexture.draw(drawent);
drawent.render(pixel_pos, renderTexture);
} }
// top layer - opacity for discovered / visible status based on perspective // top layer - opacity for discovered / visible status based on perspective
// Only render visibility overlay if perspective is enabled // Only render visibility overlay if perspective is enabled
if (perspective_enabled) { if (perspective_enabled) {
ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime);
auto entity = perspective_entity.lock(); auto entity = perspective_entity.lock();
// Create rectangle for overlays // Create rectangle for overlays

View File

@ -0,0 +1,152 @@
"""
Benchmark: Moving Entities Performance Test
This benchmark measures McRogueFace's performance with 50 randomly moving
entities on a 100x100 grid.
Expected results:
- Should maintain 60 FPS
- Entity render time should be <3ms
- Grid render time will be higher due to constant updates (no dirty flag benefit)
Usage:
./build/mcrogueface --exec tests/benchmark_moving_entities.py
Press F3 to toggle performance overlay
Press ESC to exit
"""
import mcrfpy
import sys
import random
# Create the benchmark scene
mcrfpy.createScene("benchmark")
mcrfpy.setScene("benchmark")
# Get scene UI
ui = mcrfpy.sceneUI("benchmark")
# Create a 100x100 grid
grid = mcrfpy.Grid(
grid_size=(100, 100),
pos=(0, 0),
size=(1024, 768)
)
# Simple floor pattern
for x in range(100):
for y in range(100):
cell = grid.at((x, y))
cell.tilesprite = 0
cell.color = (40, 40, 40, 255)
# Create 50 entities with random positions and velocities
entities = []
ENTITY_COUNT = 50
for i in range(ENTITY_COUNT):
entity = mcrfpy.Entity(
grid_pos=(random.randint(0, 99), random.randint(0, 99)),
sprite_index=random.randint(10, 20) # Use varied sprites
)
# Give each entity a random velocity
entity.velocity_x = random.uniform(-0.5, 0.5)
entity.velocity_y = random.uniform(-0.5, 0.5)
grid.entities.append(entity)
entities.append(entity)
ui.append(grid)
# Instructions caption
instructions = mcrfpy.Caption(
text=f"Moving Entities Benchmark ({ENTITY_COUNT} entities)\n"
"Press F3 for performance overlay\n"
"Press ESC to exit\n"
"Goal: 60 FPS with entities moving",
pos=(10, 10),
fill_color=(255, 255, 0, 255)
)
ui.append(instructions)
# Benchmark info
print("=" * 60)
print("MOVING ENTITIES BENCHMARK")
print("=" * 60)
print(f"Entity count: {ENTITY_COUNT}")
print("Grid size: 100x100 cells")
print("Expected FPS: 60")
print("")
print("Entities move randomly and bounce off walls.")
print("This tests entity rendering performance and position updates.")
print("")
print("Press F3 in-game to see real-time performance metrics.")
print("=" * 60)
# Exit handler
def handle_key(key, state):
if key == "Escape" and state:
print("\nBenchmark ended by user")
sys.exit(0)
mcrfpy.keypressScene(handle_key)
# Update entity positions
def update_entities(ms):
dt = ms / 1000.0 # Convert to seconds
for entity in entities:
# Update position
new_x = entity.x + entity.velocity_x
new_y = entity.y + entity.velocity_y
# Bounce off walls
if new_x < 0 or new_x >= 100:
entity.velocity_x = -entity.velocity_x
new_x = max(0, min(99, new_x))
if new_y < 0 or new_y >= 100:
entity.velocity_y = -entity.velocity_y
new_y = max(0, min(99, new_y))
# Update entity position
entity.x = new_x
entity.y = new_y
# Run movement update every frame (16ms)
mcrfpy.setTimer("movement", update_entities, 16)
# Benchmark statistics
frame_count = 0
start_time = None
def benchmark_timer(ms):
global frame_count, start_time
if start_time is None:
import time
start_time = time.time()
frame_count += 1
# After 10 seconds, print summary
import time
elapsed = time.time() - start_time
if elapsed >= 10.0:
print("\n" + "=" * 60)
print("BENCHMARK COMPLETE")
print("=" * 60)
print(f"Frames rendered: {frame_count}")
print(f"Time elapsed: {elapsed:.2f}s")
print(f"Average FPS: {frame_count / elapsed:.1f}")
print(f"Entities: {ENTITY_COUNT}")
print("")
print("Check profiler overlay (F3) for detailed timing breakdown.")
print("Entity render time and total frame time are key metrics.")
print("=" * 60)
# Don't exit - let user review
mcrfpy.setTimer("benchmark", benchmark_timer, 100)

View File

@ -0,0 +1,122 @@
"""
Benchmark: Static Grid Performance Test
This benchmark measures McRogueFace's grid rendering performance with a static
100x100 grid. The goal is 60 FPS with minimal CPU usage.
Expected results:
- 60 FPS (16.6ms per frame)
- Grid render time should be <2ms after dirty flag optimization
- Currently will be higher (likely 8-12ms) - this establishes baseline
Usage:
./build/mcrogueface --exec tests/benchmark_static_grid.py
Press F3 to toggle performance overlay
Press ESC to exit
"""
import mcrfpy
import sys
# Create the benchmark scene
mcrfpy.createScene("benchmark")
mcrfpy.setScene("benchmark")
# Get scene UI
ui = mcrfpy.sceneUI("benchmark")
# Create a 100x100 grid with default texture
grid = mcrfpy.Grid(
grid_size=(100, 100),
pos=(0, 0),
size=(1024, 768)
)
# Fill grid with varied tile patterns to ensure realistic rendering
for x in range(100):
for y in range(100):
cell = grid.at((x, y))
# Checkerboard pattern with different sprites
if (x + y) % 2 == 0:
cell.tilesprite = 0
cell.color = (50, 50, 50, 255)
else:
cell.tilesprite = 1
cell.color = (70, 70, 70, 255)
# Add some variation
if x % 10 == 0 or y % 10 == 0:
cell.tilesprite = 2
cell.color = (100, 100, 100, 255)
# Add grid to scene
ui.append(grid)
# Instructions caption
instructions = mcrfpy.Caption(
text="Static Grid Benchmark (100x100)\n"
"Press F3 for performance overlay\n"
"Press ESC to exit\n"
"Goal: 60 FPS with low grid render time",
pos=(10, 10),
fill_color=(255, 255, 0, 255)
)
ui.append(instructions)
# Benchmark info
print("=" * 60)
print("STATIC GRID BENCHMARK")
print("=" * 60)
print("Grid size: 100x100 cells")
print("Expected FPS: 60")
print("Tiles rendered: ~1024 visible cells per frame")
print("")
print("This benchmark establishes baseline grid rendering performance.")
print("After dirty flag optimization, grid render time should drop")
print("significantly for static content.")
print("")
print("Press F3 in-game to see real-time performance metrics.")
print("=" * 60)
# Exit handler
def handle_key(key, state):
if key == "Escape" and state:
print("\nBenchmark ended by user")
sys.exit(0)
mcrfpy.keypressScene(handle_key)
# Run for 10 seconds then provide summary
frame_count = 0
start_time = None
def benchmark_timer(ms):
global frame_count, start_time
if start_time is None:
import time
start_time = time.time()
frame_count += 1
# After 10 seconds, print summary and exit
import time
elapsed = time.time() - start_time
if elapsed >= 10.0:
print("\n" + "=" * 60)
print("BENCHMARK COMPLETE")
print("=" * 60)
print(f"Frames rendered: {frame_count}")
print(f"Time elapsed: {elapsed:.2f}s")
print(f"Average FPS: {frame_count / elapsed:.1f}")
print("")
print("Check profiler overlay (F3) for detailed timing breakdown.")
print("Grid render time is the key metric for optimization.")
print("=" * 60)
# Don't exit automatically - let user review with F3
# sys.exit(0)
# Update every 100ms
mcrfpy.setTimer("benchmark", benchmark_timer, 100)