sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzsrc/utils/frame_profiler.hpp raw
#pragma once
// ──────────────────────────────────────────────────────────────────────────
// FrameProfiler — lightweight scoped timing for the game loop.
//
// Usage:
// void some_function() {
// FP_ZONE("physics_substep"); // string literal!
// ... work ...
// }
//
// In the main loop:
// FRAME_BEGIN();
// ... work ...
// FRAME_END(); // may print a report (~1x / second)
//
// Design notes:
// * Zones are identified by a const char* string-literal pointer. We do
// pointer-equality lookup (no hashing, no allocations).
// * Accumulator is a tiny fixed-size array. Adding a new zone is O(N)
// first time, O(1) after — N is small (< 32 in practice).
// * Single-threaded. Do not call from worker threads (yet).
// * Compile-time disable: define FRAME_PROFILER_DISABLED to make all
// macros no-ops with zero runtime cost.
// ──────────────────────────────────────────────────────────────────────────
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cstdio>
namespace
Nimbin
{
#ifdef FRAME_PROFILER_DISABLED
# define FP_ZONE(name) ((void)0)
# define FRAME_BEGIN() ((void)0)
# define FRAME_END() ((void)0)
# define FRAME_SORT_PEAK ((void)0)
# define FRAME_SORT_TOTAL ((void)0)
# define FRAME_SORT_AVERAGE ((void)0)
#else
namespace fp
{
enum class SortMode { Total, Peak, Avg };
inline double
perf_to_ms(Uint64 ticks)
{
static const double inv_freq_ms =
1000.0 / static_cast<double>(SDL_GetPerformanceFrequency());
return static_cast<double>(ticks) * inv_freq_ms;
}
struct Zone
{
const char* name {nullptr};
uint32_t calls {0}; // calls across the report window
Uint64 total {0}; // accumulated ticks across the report window
Uint64 frame_sum {0}; // ticks this frame (so we can also track peak)
Uint64 peak {0}; // peak per-frame ticks in window
};
struct Frame
{
static constexpr size_t MAX_ZONES = 32;
bool started {false};
size_t num_zones {0};
uint32_t frames_in_win {0};
Uint64 window_start {0}; // perf-counter ticks
Uint64 report_every_ns{1'000'000'000ull}; // 1 second
SortMode sort_mode {SortMode::Total};
std::array<Zone, MAX_ZONES> zones{};
Zone* find_OrAdd(const char* name) {
// Pointer-equality lookup — string literals dedupe naturally.
for (size_t i = 0; i < num_zones; i++)
if (zones[i].name == name) return &zones[i];
if (num_zones >= MAX_ZONES) return nullptr; // silently drop
zones[num_zones].name = name;
return &zones[num_zones++];
}
void begin_Frame() {
if (!started) {
window_start = SDL_GetPerformanceCounter();
started = true;
}
for (size_t i = 0; i < num_zones; i++) zones[i].frame_sum = 0;
}
void end_Frame() {
++frames_in_win;
for (size_t i = 0; i < num_zones; i++) {
Zone& z = zones[i];
z.total += z.frame_sum;
if (z.frame_sum > z.peak) z.peak = z.frame_sum;
}
Uint64 now = SDL_GetPerformanceCounter();
Uint64 elapsed_ticks = now - window_start;
double elapsed_ms = perf_to_ms(elapsed_ticks);
if (elapsed_ms >= 1000.0) {
print_Report(elapsed_ms);
// reset window
for (size_t i = 0; i < num_zones; i++) {
zones[i].total = 0;
zones[i].peak = 0;
zones[i].calls = 0;
}
frames_in_win = 0;
window_start = now;
}
}
void print_Report(double elapsed_ms) const {
// Sort indices by total descending — don't reorder the zone array
// itself, names are stable pointers and we rely on order persistence.
std::array<size_t, MAX_ZONES> order{};
for (size_t i = 0; i < num_zones; i++) order[i] = i;
std::sort(order.begin(), order.begin() + num_zones,
[this](size_t a, size_t b) {
switch (sort_mode) {
case SortMode::Peak: return zones[a].peak > zones[b].peak;
case SortMode::Avg: return zones[a].total > zones[b].total;
case SortMode::Total:
default: return zones[a].total > zones[b].total;
}
});
double frame_avg_ms = elapsed_ms / static_cast<double>(frames_in_win);
double fps = 1000.0 / frame_avg_ms;
std::printf(
"\n── FrameProfiler %6.1f fps (%.2f ms/frame avg over %u frames) ──\n",
fps, frame_avg_ms, frames_in_win);
std::printf("%-22s %10s %10s %8s %8s\n",
"zone", "tot ms", "avg ms", "peak ms", "%frame");
for (size_t i = 0; i < num_zones; i++) {
const Zone& z = zones[order[i]];
if (z.total == 0) continue;
double tot_ms = perf_to_ms(z.total);
double avg_ms = tot_ms / static_cast<double>(frames_in_win);
double peak_ms = perf_to_ms(z.peak);
double pct = (avg_ms / frame_avg_ms) * 100.0;
std::printf("%-22s %10.2f %10.3f %8.3f %7.1f%%\n",
z.name, tot_ms, avg_ms, peak_ms, pct);
}
std::fflush(stdout);
}
};
inline Frame& frame() {
static Frame f;
return f;
}
// RAII zone timer.
struct ScopedZone {
Zone* zone;
Uint64 t0;
ScopedZone(const char* name)
: zone(frame().find_OrAdd(name))
, t0(zone ? SDL_GetPerformanceCounter() : 0) {}
~ScopedZone() {
if (!zone) return;
Uint64 t1 = SDL_GetPerformanceCounter();
zone->frame_sum += (t1 - t0);
zone->calls += 1;
}
};
} // namespace fp
// Token::Data-pasting line counter so two FP_ZONEs in the same scope don't collide.
#define FP_CONCAT_(a,b) a##b
#define FP_CONCAT(a,b) FP_CONCAT_(a,b)
#define FP_ZONE(name) ::fp::ScopedZone FP_CONCAT(_fp_zone_, __LINE__){name}
#define FRAME_BEGIN() ::fp::frame().begin_Frame()
#define FRAME_END() ::fp::frame().end_Frame()
#define FRAME_SORT_PEAK ::fp::frame().sort_mode = fp::SortMode::Peak;
#define FRAME_SORT_TOTAL ::fp::frame().sort_mode = fp::SortMode::Total;
#define FRAME_SORT_AVERAGE ::fp::frame().sort_mode = fp::SortMode::Avg;
#endif // !FRAME_PROFILER_DISABLED
} // namespace Nimbin