#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 #include #include #include 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(SDL_GetPerformanceFrequency()); return static_cast(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 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 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(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(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