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:
parent
8153fd2503
commit
e9e9cd2f81
|
|
@ -45,6 +45,9 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
|
|
||||||
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() &&
|
||||||
config.python_command.empty() &&
|
config.python_command.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
|
||||||
|
{
|
||||||
|
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
||||||
McRFPy_API::updatePythonScenes(frameTime);
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -52,6 +57,11 @@ private:
|
||||||
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();
|
||||||
|
|
@ -70,6 +80,18 @@ public:
|
||||||
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};
|
||||||
|
|
@ -93,8 +115,21 @@ public:
|
||||||
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();
|
||||||
|
|
@ -146,3 +181,28 @@ public:
|
||||||
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);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(); }
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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,6 +96,9 @@ 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;
|
||||||
|
|
||||||
|
|
@ -135,6 +139,7 @@ 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)
|
||||||
|
|
@ -163,11 +168,21 @@ 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)
|
||||||
|
{
|
||||||
|
ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
|
||||||
|
int entitiesRendered = 0;
|
||||||
|
int totalEntities = entities->size();
|
||||||
|
|
||||||
for (auto e : *entities) {
|
for (auto e : *entities) {
|
||||||
// Skip out-of-bounds entities for performance
|
// Skip out-of-bounds entities for performance
|
||||||
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
||||||
|
|
@ -186,12 +201,20 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
//drawent.setPosition(pixel_pos);
|
//drawent.setPosition(pixel_pos);
|
||||||
//renderTexture.draw(drawent);
|
//renderTexture.draw(drawent);
|
||||||
drawent.render(pixel_pos, renderTexture);
|
drawent.render(pixel_pos, renderTexture);
|
||||||
|
|
||||||
|
entitiesRendered++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record entity rendering stats
|
||||||
|
Resources::game->metrics.entitiesRendered += entitiesRendered;
|
||||||
|
Resources::game->metrics.totalEntities += totalEntities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue