Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/utils/frame_profiler.hpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz
src/utils/frame_profiler.hpp 6.7 KB · 197 lines 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