15#define WIN32_LEAN_AND_MEAN
19#pragma comment(lib, "dxgi.lib")
30 m_renderer = renderer;
34 for (
auto& m : m_customMetrics) {
36 m.Getter = std::move(getter);
40 m_customMetrics.push_back({name, std::move(getter)});
44 m_customMetrics.erase(
45 std::remove_if(m_customMetrics.begin(), m_customMetrics.end(),
47 m_customMetrics.end());
51 if (!m_recording)
return;
52 WriteFrameData(deltaTime);
62void Benchmark::StartRecording() {
63 if (m_recording)
return;
65 std::string filename = GenerateFilename();
66 std::filesystem::create_directories(
"benchmarks");
68 m_file.open(
"benchmarks/" + filename);
69 if (!m_file.is_open()) {
70 SLEAK_WARN(
"Benchmark: Failed to open file: benchmarks/{}", filename);
76 m_sessionTimer.
Reset();
82 m_minFrameTime = FLT_MAX;
83 m_maxFrameTime = 0.0f;
92 m_customSums.assign(m_customMetrics.size(), 0.0);
95 m_file <<
"Frame,Time_s,FrameTime_ms,FPS,Triangles,CPU_%,RAM_MB";
96 for (
auto& metric : m_customMetrics)
97 m_file <<
"," << metric.Name;
100 SLEAK_INFO(
"Benchmark: Recording started -> benchmarks/{}", filename);
103void Benchmark::StopRecording() {
104 if (!m_recording)
return;
112 SLEAK_INFO(
"Benchmark: Recording stopped ({} frames captured)", m_frameIndex);
115void Benchmark::WriteFrameData(
float deltaTime) {
116 if (!m_file.is_open() || !m_renderer)
return;
118 float elapsed = m_sessionTimer.Elapsed();
119 float frameTimeMs = deltaTime * 1000.0f;
120 int fps = m_renderer->GetFrameRate();
121 int triangles = m_renderer->GetTriangles();
124 float cpuPct = sysMetrics.CpuUsagePercent;
125 float ramMB = sysMetrics.RamUsageMB;
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;
135 m_sumFrameTime += frameTimeMs;
136 m_sumTriangles += triangles;
140 m_frameTimes.push_back(frameTimeMs);
142 if (frameTimeMs > 16.67f) ++m_spikes16;
143 if (frameTimeMs > 33.33f) ++m_spikes33;
144 if (frameTimeMs > 50.0f) ++m_spikes50;
147 m_file << m_frameIndex
148 <<
"," << std::fixed << std::setprecision(4) << elapsed
149 <<
"," << std::setprecision(3) << frameTimeMs
152 <<
"," << std::setprecision(1) << cpuPct
153 <<
"," << std::setprecision(1) << ramMB;
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;
166void Benchmark::WriteSummary() {
167 if (m_frameIndex <= 1)
return;
168 int n = m_frameIndex - 1;
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)];
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()));
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";
192 m_file <<
"# VSync," << (m_renderer->GetVSync() ?
"On" :
"Off") <<
"\n";
193 m_file <<
"# MSAA," << m_renderer->GetMSAASampleCount() <<
"x\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";
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";
211 m_file <<
"# Spikes_16ms," << m_spikes16 <<
"\n";
212 m_file <<
"# Spikes_33ms," << m_spikes33 <<
"\n";
213 m_file <<
"# Spikes_50ms," << m_spikes50 <<
"\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";
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";
226void Benchmark::WriteHardwareInfo() {
228 m_file <<
"# --- System Info ---\n";
232 std::string gpuName =
"Unknown";
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))) {
242 WideCharToMultiByte(CP_UTF8, 0, desc.Description, -1,
243 narrow,
sizeof(narrow),
nullptr,
nullptr);
245 vramGB =
static_cast<float>(desc.DedicatedVideoMemory)
246 / (1024.0f * 1024.0f * 1024.0f);
255 std::string cpuName =
"Unknown";
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) {
265 while (!cpuName.empty() && cpuName.back() ==
' ')
273 int cpuCores =
static_cast<int>(si.dwNumberOfProcessors);
277 ms.dwLength =
sizeof(ms);
278 GlobalMemoryStatusEx(&ms);
279 float totalRamGB =
static_cast<float>(ms.ullTotalPhys) / (1024.0f * 1024.0f * 1024.0f);
282 std::string osVersion =
"Windows";
284 using RtlGetVersionFn = LONG(WINAPI*)(PRTL_OSVERSIONINFOW);
285 HMODULE ntdll = GetModuleHandleA(
"ntdll.dll");
287 auto fn =
reinterpret_cast<RtlGetVersionFn
>(
288 GetProcAddress(ntdll,
"RtlGetVersion"));
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();
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";
310 m_file <<
"# CPU,N/A\n";
311 m_file <<
"# GPU,N/A\n";
315std::string Benchmark::GetRendererTag()
const {
316 if (!m_renderer)
return "unknown";
317 switch (m_renderer->GetType()) {
322 default:
return "unknown";
326std::string Benchmark::GenerateFilename()
const {
327 auto now = std::chrono::system_clock::now();
328 auto time = std::chrono::system_clock::to_time_t(now);
331 localtime_s(&tm, &time);
333 localtime_r(&time, &tm);
336 std::ostringstream oss;
337 oss <<
"benchmark_" << GetRendererTag() <<
"_"
338 << std::put_time(&tm,
"%Y%m%d_%H%M%S") <<
".csv";
void ToggleRecording()
Toggle recording on/off (F12).
void RegisterMetric(const std::string &name, std::function< float()> getter)
Register a custom metric (e.g. render distance from Game).
void Initialize(RenderEngine::Renderer *renderer)
void UnregisterMetric(const std::string &name)
void Tick(float deltaTime)
Call once per frame from the main loop.
Abstract render backend: swapchain, frame lifecycle, and post-effect toggles.
static SystemMetricsData Query()
Reads the current CPU/RAM/GPU usage from the OS.
void Reset()
Restarts the clock at zero.
Root namespace for everything the engine exposes.