SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
Benchmark.cpp
Go to the documentation of this file.
1#include <Debug/Benchmark.hpp>
4#include <Core/Logger.hpp>
5#include <chrono>
6#include <iomanip>
7#include <sstream>
8#include <algorithm>
9#include <filesystem>
10#include <cmath>
11#include <cfloat>
12#include <numeric>
13
14#ifdef PLATFORM_WIN
15#define WIN32_LEAN_AND_MEAN
16#include <windows.h>
17#include <dxgi.h>
18#include <winreg.h>
19#pragma comment(lib, "dxgi.lib")
20#endif
21
22using namespace Sleak;
23
25 if (m_recording)
26 StopRecording();
27}
28
30 m_renderer = renderer;
31}
32
33void Benchmark::RegisterMetric(const std::string& name, std::function<float()> getter) {
34 for (auto& m : m_customMetrics) {
35 if (m.Name == name) {
36 m.Getter = std::move(getter);
37 return;
38 }
39 }
40 m_customMetrics.push_back({name, std::move(getter)});
41}
42
43void Benchmark::UnregisterMetric(const std::string& name) {
44 m_customMetrics.erase(
45 std::remove_if(m_customMetrics.begin(), m_customMetrics.end(),
46 [&](const BenchmarkMetric& m) { return m.Name == name; }),
47 m_customMetrics.end());
48}
49
50void Benchmark::Tick(float deltaTime) {
51 if (!m_recording) return;
52 WriteFrameData(deltaTime);
53}
54
56 if (m_recording)
57 StopRecording();
58 else
59 StartRecording();
60}
61
62void Benchmark::StartRecording() {
63 if (m_recording) return;
64
65 std::string filename = GenerateFilename();
66 std::filesystem::create_directories("benchmarks");
67
68 m_file.open("benchmarks/" + filename);
69 if (!m_file.is_open()) {
70 SLEAK_WARN("Benchmark: Failed to open file: benchmarks/{}", filename);
71 return;
72 }
73
74 m_recording = true;
75 m_frameIndex = 0;
76 m_sessionTimer.Reset();
77
78 // Reset accumulators
79 m_minFPS = INT_MAX;
80 m_maxFPS = 0;
81 m_sumFPS = 0.0;
82 m_minFrameTime = FLT_MAX;
83 m_maxFrameTime = 0.0f;
84 m_sumFrameTime = 0.0;
85 m_sumTriangles = 0.0;
86 m_sumCPU = 0.0;
87 m_sumRAM = 0.0;
88 m_spikes16 = 0;
89 m_spikes33 = 0;
90 m_spikes50 = 0;
91 m_frameTimes.clear();
92 m_customSums.assign(m_customMetrics.size(), 0.0);
93
94 // CSV header
95 m_file << "Frame,Time_s,FrameTime_ms,FPS,Triangles,CPU_%,RAM_MB";
96 for (auto& metric : m_customMetrics)
97 m_file << "," << metric.Name;
98 m_file << "\n";
99
100 SLEAK_INFO("Benchmark: Recording started -> benchmarks/{}", filename);
101}
102
103void Benchmark::StopRecording() {
104 if (!m_recording) return;
105
106 m_recording = false;
107
108 WriteSummary();
109 WriteHardwareInfo();
110 m_file.close();
111
112 SLEAK_INFO("Benchmark: Recording stopped ({} frames captured)", m_frameIndex);
113}
114
115void Benchmark::WriteFrameData(float deltaTime) {
116 if (!m_file.is_open() || !m_renderer) return;
117
118 float elapsed = m_sessionTimer.Elapsed();
119 float frameTimeMs = deltaTime * 1000.0f;
120 int fps = m_renderer->GetFrameRate();
121 int triangles = m_renderer->GetTriangles();
122
123 auto sysMetrics = SystemMetrics::Query();
124 float cpuPct = sysMetrics.CpuUsagePercent;
125 float ramMB = sysMetrics.RamUsageMB;
126
127 // Accumulate stats (skip frame 0 — may include setup cost)
128 if (m_frameIndex > 0) {
129 if (fps < m_minFPS) m_minFPS = fps;
130 if (fps > m_maxFPS) m_maxFPS = fps;
131 if (frameTimeMs < m_minFrameTime) m_minFrameTime = frameTimeMs;
132 if (frameTimeMs > m_maxFrameTime) m_maxFrameTime = frameTimeMs;
133
134 m_sumFPS += fps;
135 m_sumFrameTime += frameTimeMs;
136 m_sumTriangles += triangles;
137 m_sumCPU += cpuPct;
138 m_sumRAM += ramMB;
139
140 m_frameTimes.push_back(frameTimeMs);
141
142 if (frameTimeMs > 16.67f) ++m_spikes16;
143 if (frameTimeMs > 33.33f) ++m_spikes33;
144 if (frameTimeMs > 50.0f) ++m_spikes50;
145 }
146
147 m_file << m_frameIndex
148 << "," << std::fixed << std::setprecision(4) << elapsed
149 << "," << std::setprecision(3) << frameTimeMs
150 << "," << fps
151 << "," << triangles
152 << "," << std::setprecision(1) << cpuPct
153 << "," << std::setprecision(1) << ramMB;
154
155 for (size_t i = 0; i < m_customMetrics.size(); ++i) {
156 float val = m_customMetrics[i].Getter();
157 m_file << "," << std::setprecision(2) << val;
158 if (m_frameIndex > 0 && i < m_customSums.size())
159 m_customSums[i] += val;
160 }
161
162 m_file << "\n";
163 ++m_frameIndex;
164}
165
166void Benchmark::WriteSummary() {
167 if (m_frameIndex <= 1) return;
168 int n = m_frameIndex - 1; // exclude frame 0
169
170 // Percentiles — sort a copy of collected frame times
171 std::vector<float> sorted = m_frameTimes;
172 std::sort(sorted.begin(), sorted.end());
173 auto percentile = [&](float p) -> float {
174 if (sorted.empty()) return 0.0f;
175 size_t idx = static_cast<size_t>(p * 0.01f * (sorted.size() - 1));
176 return sorted[std::min(idx, sorted.size() - 1)];
177 };
178
179 // Frame time standard deviation (jitter metric)
180 double avgFT = m_sumFrameTime / n;
181 double variance = 0.0;
182 for (float ft : m_frameTimes)
183 variance += (ft - avgFT) * (ft - avgFT);
184 float stdev = static_cast<float>(std::sqrt(variance / m_frameTimes.size()));
185
186 m_file << "\n# Summary\n";
187 m_file << "# Frames," << m_frameIndex << "\n";
188 m_file << "# Duration_s," << std::fixed << std::setprecision(2) << m_sessionTimer.Elapsed() << "\n";
189 m_file << "# Renderer," << GetRendererTag() << "\n";
190
191 if (m_renderer) {
192 m_file << "# VSync," << (m_renderer->GetVSync() ? "On" : "Off") << "\n";
193 m_file << "# MSAA," << m_renderer->GetMSAASampleCount() << "x\n";
194 }
195
196 m_file << "#\n";
197 m_file << "# FPS_Min," << m_minFPS << "\n";
198 m_file << "# FPS_Max," << m_maxFPS << "\n";
199 m_file << "# FPS_Avg," << std::setprecision(1) << (m_sumFPS / n) << "\n";
200
201 m_file << "#\n";
202 m_file << "# FrameTime_Min_ms," << std::setprecision(3) << m_minFrameTime << "\n";
203 m_file << "# FrameTime_Max_ms," << std::setprecision(3) << m_maxFrameTime << "\n";
204 m_file << "# FrameTime_Avg_ms," << std::setprecision(3) << avgFT << "\n";
205 m_file << "# FrameTime_P50_ms," << std::setprecision(3) << percentile(50.0f) << "\n";
206 m_file << "# FrameTime_P95_ms," << std::setprecision(3) << percentile(95.0f) << "\n";
207 m_file << "# FrameTime_P99_ms," << std::setprecision(3) << percentile(99.0f) << "\n";
208 m_file << "# FrameTime_Stdev_ms," << std::setprecision(3) << stdev << "\n";
209
210 m_file << "#\n";
211 m_file << "# Spikes_16ms," << m_spikes16 << "\n";
212 m_file << "# Spikes_33ms," << m_spikes33 << "\n";
213 m_file << "# Spikes_50ms," << m_spikes50 << "\n";
214
215 m_file << "#\n";
216 m_file << "# Triangles_Avg," << std::setprecision(0) << (m_sumTriangles / n) << "\n";
217 m_file << "# CPU_Avg_%," << std::setprecision(1) << (m_sumCPU / n) << "\n";
218 m_file << "# RAM_Avg_MB," << std::setprecision(1) << (m_sumRAM / n) << "\n";
219
220 for (size_t i = 0; i < m_customMetrics.size() && i < m_customSums.size(); ++i) {
221 m_file << "# " << m_customMetrics[i].Name << "_Avg,"
222 << std::setprecision(2) << (m_customSums[i] / n) << "\n";
223 }
224}
225
226void Benchmark::WriteHardwareInfo() {
227 m_file << "#\n";
228 m_file << "# --- System Info ---\n";
229
230#ifdef PLATFORM_WIN
231 // GPU name + dedicated VRAM via DXGI
232 std::string gpuName = "Unknown";
233 float vramGB = 0.0f;
234 {
235 IDXGIFactory* factory = nullptr;
236 if (SUCCEEDED(CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory)))) {
237 IDXGIAdapter* adapter = nullptr;
238 if (SUCCEEDED(factory->EnumAdapters(0, &adapter))) {
239 DXGI_ADAPTER_DESC desc{};
240 if (SUCCEEDED(adapter->GetDesc(&desc))) {
241 char narrow[256]{};
242 WideCharToMultiByte(CP_UTF8, 0, desc.Description, -1,
243 narrow, sizeof(narrow), nullptr, nullptr);
244 gpuName = narrow;
245 vramGB = static_cast<float>(desc.DedicatedVideoMemory)
246 / (1024.0f * 1024.0f * 1024.0f);
247 }
248 adapter->Release();
249 }
250 factory->Release();
251 }
252 }
253
254 // CPU name from registry
255 std::string cpuName = "Unknown";
256 {
257 char buf[256]{};
258 DWORD size = sizeof(buf);
259 if (RegGetValueA(HKEY_LOCAL_MACHINE,
260 "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0",
261 "ProcessorNameString",
262 RRF_RT_REG_SZ, nullptr, buf, &size) == ERROR_SUCCESS) {
263 cpuName = buf;
264 // Trim trailing spaces
265 while (!cpuName.empty() && cpuName.back() == ' ')
266 cpuName.pop_back();
267 }
268 }
269
270 // Logical CPU count
271 SYSTEM_INFO si{};
272 GetSystemInfo(&si);
273 int cpuCores = static_cast<int>(si.dwNumberOfProcessors);
274
275 // Total installed RAM
276 MEMORYSTATUSEX ms{};
277 ms.dwLength = sizeof(ms);
278 GlobalMemoryStatusEx(&ms);
279 float totalRamGB = static_cast<float>(ms.ullTotalPhys) / (1024.0f * 1024.0f * 1024.0f);
280
281 // OS version via RtlGetVersion (avoids deprecated GetVersionEx)
282 std::string osVersion = "Windows";
283 {
284 using RtlGetVersionFn = LONG(WINAPI*)(PRTL_OSVERSIONINFOW);
285 HMODULE ntdll = GetModuleHandleA("ntdll.dll");
286 if (ntdll) {
287 auto fn = reinterpret_cast<RtlGetVersionFn>(
288 GetProcAddress(ntdll, "RtlGetVersion"));
289 if (fn) {
290 RTL_OSVERSIONINFOW rovi{};
291 rovi.dwOSVersionInfoSize = sizeof(rovi);
292 if (fn(&rovi) == 0) {
293 std::ostringstream oss;
294 oss << "Windows " << rovi.dwMajorVersion
295 << "." << rovi.dwMinorVersion
296 << " (Build " << rovi.dwBuildNumber << ")";
297 osVersion = oss.str();
298 }
299 }
300 }
301 }
302
303 m_file << "# CPU," << cpuName << "\n";
304 m_file << "# CPU_Cores," << cpuCores << "\n";
305 m_file << "# Total_RAM_GB," << std::fixed << std::setprecision(1) << totalRamGB << "\n";
306 m_file << "# GPU," << gpuName << "\n";
307 m_file << "# GPU_VRAM_GB," << std::setprecision(1) << vramGB << "\n";
308 m_file << "# OS," << osVersion << "\n";
309#else
310 m_file << "# CPU,N/A\n";
311 m_file << "# GPU,N/A\n";
312#endif
313}
314
315std::string Benchmark::GetRendererTag() const {
316 if (!m_renderer) return "unknown";
317 switch (m_renderer->GetType()) {
318 case RenderEngine::RendererType::DirectX11: return "dx11";
319 case RenderEngine::RendererType::DirectX12: return "dx12";
320 case RenderEngine::RendererType::OpenGL: return "opengl";
321 case RenderEngine::RendererType::Vulkan: return "vulkan";
322 default: return "unknown";
323 }
324}
325
326std::string Benchmark::GenerateFilename() const {
327 auto now = std::chrono::system_clock::now();
328 auto time = std::chrono::system_clock::to_time_t(now);
329 std::tm tm{};
330#ifdef PLATFORM_WIN
331 localtime_s(&tm, &time);
332#else
333 localtime_r(&time, &tm);
334#endif
335
336 std::ostringstream oss;
337 oss << "benchmark_" << GetRendererTag() << "_"
338 << std::put_time(&tm, "%Y%m%d_%H%M%S") << ".csv";
339 return oss.str();
340}
#define SLEAK_INFO(...)
Definition Logger.hpp:20
#define SLEAK_WARN(...)
Definition Logger.hpp:21
void ToggleRecording()
Toggle recording on/off (F12).
Definition Benchmark.cpp:55
void RegisterMetric(const std::string &name, std::function< float()> getter)
Register a custom metric (e.g. render distance from Game).
Definition Benchmark.cpp:33
void Initialize(RenderEngine::Renderer *renderer)
Definition Benchmark.cpp:29
void UnregisterMetric(const std::string &name)
Definition Benchmark.cpp:43
void Tick(float deltaTime)
Call once per frame from the main loop.
Definition Benchmark.cpp:50
Abstract render backend: swapchain, frame lifecycle, and post-effect toggles.
Definition Renderer.hpp:37
static SystemMetricsData Query()
Reads the current CPU/RAM/GPU usage from the OS.
void Reset()
Restarts the clock at zero.
Definition Timer.cpp:8
Root namespace for everything the engine exposes.
Definition Camera.hpp:10