diff --git a/README.md b/README.md index b4ad985e3..45145880b 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ What I have done so far: * The options menu includes a checkbox to reduce effects to achieve higher frame rates. * Chat commands: * Change FPS-Limit: `$fps ` - * Enable V-Sync: `$vsync on` - * Disable V-Sync: `$vsync off` + * V-Sync: `$vsync on` / `$vsync off` + * Show simple FPS counter: `$fpscounter on` / `$fpscounter off` + * Show detailed performance overlay (FPS stats, percentiles, frame graph): `$details on` / `$details off` * 🔥 Optimized some OpenGL calls by using vertex arrays. This should result in a better frame rate when many players and objects are visible. * 🔥 Added inventory and vault extensions. diff --git a/src/source/Scenes/SceneManager.cpp b/src/source/Scenes/SceneManager.cpp index 43e64a85c..22d065b68 100644 --- a/src/source/Scenes/SceneManager.cpp +++ b/src/source/Scenes/SceneManager.cpp @@ -4,6 +4,8 @@ #include "stdafx.h" #include +#include +#include #include "SceneManager.h" //============================================================================= @@ -58,6 +60,112 @@ extern bool Destroy; extern double WorldTime; extern float FPS_ANIMATION_FACTOR; +static bool g_bShowDebugInfo = +#ifdef _DEBUG + true; +#else + false; +#endif + +static bool g_bShowFpsCounter = false; + +void SetShowDebugInfo(bool enabled) +{ + g_bShowDebugInfo = enabled; + if (enabled) g_bShowFpsCounter = false; +} + +void SetShowFpsCounter(bool enabled) +{ + g_bShowFpsCounter = enabled; + if (enabled) g_bShowDebugInfo = false; +} + +//============================================================================= +// Frame Statistics Tracker +//============================================================================= + +static constexpr int FRAME_HISTORY_SIZE = 300; // ~5 seconds at 60fps +static constexpr float MIN_FRAME_TIME_MS = 0.5f; // clamp to 2000fps max +static constexpr double STATS_UPDATE_INTERVAL = 500.0; // ms between percentile recalculations +static constexpr int MIN_FRAMES_FOR_STATS = 10; +static constexpr float GRAPH_MAX_MS = 33.3f; // graph Y-axis scale (30fps) +static constexpr float THRESHOLD_60FPS_MS = 16.67f; // 60 FPS threshold +static constexpr float THRESHOLD_40FPS_MS = 25.0f; // 40 FPS threshold +static constexpr float DEBUG_TEXT_X = 10.0f; // debug overlay X position +static constexpr int DEBUG_TEXT_Y_START = 26; // debug overlay Y start +static constexpr int DEBUG_TEXT_LINE_HEIGHT = 10; // line spacing +static constexpr float DEBUG_GRAPH_WIDTH = 200.0f; // frame graph width +static constexpr float DEBUG_GRAPH_HEIGHT = 40.0f; // frame graph height +static constexpr float DEBUG_GRAPH_Y_OFFSET = 2.0f; // gap between text and graph + +static float s_frameTimesMs[FRAME_HISTORY_SIZE] = {}; +static int s_frameIndex = 0; +static int s_frameCount = 0; +static double s_lastFrameTime = 0.0; +static double s_highestFps = 0.0; + +// Percentile stats (updated periodically) +static float s_avgFps = 0.0f; +static float s_onePercentLow = 0.0f; +static float s_slowestFrameFps = 0.0f; +static double s_lastStatsUpdate = 0.0; + +void ResetFrameStats() +{ + memset(s_frameTimesMs, 0, sizeof(s_frameTimesMs)); + s_frameIndex = 0; + s_frameCount = 0; + s_lastFrameTime = 0.0; + s_highestFps = 0.0; + s_avgFps = 0.0f; + s_onePercentLow = 0.0f; + s_slowestFrameFps = 0.0f; + s_lastStatsUpdate = 0.0; +} + +static void UpdateFrameStats() +{ + double now = WorldTime; + if (s_lastFrameTime > 0.0) + { + double dt = now - s_lastFrameTime; + if (dt < MIN_FRAME_TIME_MS) dt = MIN_FRAME_TIME_MS; + s_frameTimesMs[s_frameIndex] = static_cast(dt); + s_frameIndex = (s_frameIndex + 1) % FRAME_HISTORY_SIZE; + if (s_frameCount < FRAME_HISTORY_SIZE) s_frameCount++; + + double instantaneousFps = 1000.0 / dt; + if (instantaneousFps > s_highestFps) s_highestFps = instantaneousFps; + } + s_lastFrameTime = now; + + // Update percentile stats periodically + if (now - s_lastStatsUpdate > STATS_UPDATE_INTERVAL && s_frameCount > MIN_FRAMES_FOR_STATS) + { + s_lastStatsUpdate = now; + + // Copy and sort frame times (descending = slowest first) + static float sorted[FRAME_HISTORY_SIZE]; + memcpy(sorted, s_frameTimesMs, sizeof(float) * s_frameCount); + std::sort(sorted, sorted + s_frameCount, std::greater()); + + // Average + float sum = std::accumulate(sorted, sorted + s_frameCount, 0.0f); + float avgMs = sum / s_frameCount; + s_avgFps = (avgMs > 0.0f) ? 1000.0f / avgMs : 0.0f; + + // 1% low: average of the slowest 1% of frames + int onePercCount = std::max(1, s_frameCount / 100); + float onePercSum = std::accumulate(sorted, sorted + onePercCount, 0.0f); + float onePercAvgMs = onePercSum / onePercCount; + s_onePercentLow = (onePercAvgMs > 0.0f) ? 1000.0f / onePercAvgMs : 0.0f; + + // Slowest frame: the single slowest frame in the window + s_slowestFrameFps = (sorted[0] > 0.0f) ? 1000.0f / sorted[0] : 0.0f; + } +} + void SetTargetFps(double targetFps) { if (IsVSyncEnabled() && targetFps >= GetFPSLimit()) @@ -292,29 +400,141 @@ static bool RenderCurrentScene(HDC hDC) } /** - * @brief Renders debug information overlay in development builds. + * @brief Renders a frame time graph using raw OpenGL quads. + * + * Draws a bar chart of recent frame times inside BeginBitmap's 2D ortho projection. + * Coordinates are in virtual 640x480 space, converted to window pixels. + */ +static void RenderFrameGraph(float graphX, float graphY, float graphW, float graphH) +{ + if (s_frameCount < 2) + return; + + // Convert virtual 640x480 coords to actual window pixels + float gx = graphX * (float)WindowWidth / 640.f; + float gy = graphY * (float)WindowHeight / 480.f; + float gw = graphW * (float)WindowWidth / 640.f; + float gh = graphH * (float)WindowHeight / 480.f; + + // Flip Y for OpenGL (origin bottom-left) + float glBottom = (float)WindowHeight - gy - gh; + float glTop = (float)WindowHeight - gy; + + // Background + glDisable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glColor4f(0.0f, 0.0f, 0.0f, 0.5f); + glBegin(GL_QUADS); + glVertex2f(gx, glBottom); + glVertex2f(gx + gw, glBottom); + glVertex2f(gx + gw, glTop); + glVertex2f(gx, glTop); + glEnd(); + + // Target line at 16.67ms (60fps) + float target60 = THRESHOLD_60FPS_MS / GRAPH_MAX_MS; + float lineY = glBottom + target60 * gh; + glColor4f(0.3f, 0.8f, 0.3f, 0.5f); + glBegin(GL_LINES); + glVertex2f(gx, lineY); + glVertex2f(gx + gw, lineY); + glEnd(); + + // Frame bars + float barW = gw / FRAME_HISTORY_SIZE; + int oldest = (s_frameCount < FRAME_HISTORY_SIZE) ? 0 : s_frameIndex; + + glBegin(GL_QUADS); + for (int i = 0; i < s_frameCount; i++) + { + int idx = (oldest + i) % FRAME_HISTORY_SIZE; + float ms = s_frameTimesMs[idx]; + float norm = std::min(ms / GRAPH_MAX_MS, 1.0f); + float barH = norm * gh; + + // Color: green < 16.67ms, yellow < 25ms, red >= 25ms + if (ms < THRESHOLD_60FPS_MS) + glColor4f(0.2f, 0.9f, 0.2f, 0.8f); + else if (ms < THRESHOLD_40FPS_MS) + glColor4f(0.9f, 0.9f, 0.2f, 0.8f); + else + glColor4f(0.9f, 0.2f, 0.2f, 0.8f); + + float bx = gx + i * barW; + glVertex2f(bx, glBottom); + glVertex2f(bx + barW, glBottom); + glVertex2f(bx + barW, glBottom + barH); + glVertex2f(bx, glBottom + barH); + } + glEnd(); + + glEnable(GL_TEXTURE_2D); +} + +/** + * @brief Renders debug information overlay. * - * Shows FPS, mouse position, and camera info on screen. + * Shows FPS stats, percentile lows, mouse position, camera info, and frame time graph. */ static void RenderDebugInfo() { -#if defined(_DEBUG) || defined(LDS_FOR_DEVELOPMENT_TESTMODE) || defined(LDS_UNFIXED_FIXEDFRAME_FORDEBUG) + if (!g_bShowDebugInfo) + return; + + UpdateFrameStats(); + BeginBitmap(); - wchar_t szDebugText[128]; - swprintf(szDebugText, L"FPS: %.1f Vsync: %d CPU: %.1f%%", FPS_AVG, IsVSyncEnabled(), CPU_AVG); - wchar_t szMousePos[128]; - swprintf(szMousePos, L"MousePos : %d %d %d", MouseX, MouseY, MouseLButtonPush); - wchar_t szCamera3D[128]; - swprintf(szCamera3D, L"Camera3D : %.1f %.1f:%.1f:%.1f", CameraFOV, CameraAngle[0], CameraAngle[1], CameraAngle[2]); + + wchar_t szLine[128]; g_pRenderText->SetFont(g_hFontBold); g_pRenderText->SetBgColor(0, 0, 0, 100); g_pRenderText->SetTextColor(255, 255, 255, 200); - g_pRenderText->RenderText(10, 26, szDebugText); - g_pRenderText->RenderText(10, 36, szMousePos); - g_pRenderText->RenderText(10, 46, szCamera3D); + + int y = DEBUG_TEXT_Y_START; + swprintf(szLine, L"FPS: %.1f Avg: %.1f Max: %.1f Vsync: %d CPU: %.1f%%", + FPS_AVG, s_avgFps, s_highestFps, IsVSyncEnabled(), CPU_AVG); + g_pRenderText->RenderText((int)DEBUG_TEXT_X, y, szLine); y += DEBUG_TEXT_LINE_HEIGHT; + + swprintf(szLine, L"1%% Low: %.1f Slowest: %.1f Frame: %.2fms", + s_onePercentLow, s_slowestFrameFps, + (s_avgFps > 0.0f) ? 1000.0f / s_avgFps : 0.0f); + g_pRenderText->RenderText((int)DEBUG_TEXT_X, y, szLine); y += DEBUG_TEXT_LINE_HEIGHT; + + swprintf(szLine, L"MousePos: %d %d %d", MouseX, MouseY, MouseLButtonPush); + g_pRenderText->RenderText((int)DEBUG_TEXT_X, y, szLine); y += DEBUG_TEXT_LINE_HEIGHT; + + swprintf(szLine, L"Camera3D: %.1f %.1f:%.1f:%.1f", CameraFOV, CameraAngle[0], CameraAngle[1], CameraAngle[2]); + g_pRenderText->RenderText((int)DEBUG_TEXT_X, y, szLine); y += DEBUG_TEXT_LINE_HEIGHT; + + // Frame time graph below text + RenderFrameGraph(DEBUG_TEXT_X, (float)y + DEBUG_GRAPH_Y_OFFSET, DEBUG_GRAPH_WIDTH, DEBUG_GRAPH_HEIGHT); + + g_pRenderText->SetFont(g_hFont); + EndBitmap(); +} + +/** + * @brief Renders a simple FPS counter overlay showing only current FPS. + */ +static void RenderFpsCounter() +{ + if (!g_bShowFpsCounter) + return; + + BeginBitmap(); + + wchar_t szLine[64]; + g_pRenderText->SetFont(g_hFontBold); + g_pRenderText->SetBgColor(0, 0, 0, 100); + g_pRenderText->SetTextColor(255, 255, 255, 200); + + swprintf(szLine, L"FPS: %.1f", FPS_AVG); + g_pRenderText->RenderText((int)DEBUG_TEXT_X, DEBUG_TEXT_Y_START, szLine); + g_pRenderText->SetFont(g_hFont); EndBitmap(); -#endif // defined(_DEBUG) || defined(LDS_FOR_DEVELOPMENT_TESTMODE) || defined(LDS_UNFIXED_FIXEDFRAME_FORDEBUG) } /** @@ -697,6 +917,7 @@ void MainScene(HDC hDC) { Success = RenderCurrentScene(hDC); RenderDebugInfo(); + RenderFpsCounter(); if (Success) { diff --git a/src/source/Scenes/SceneManager.h b/src/source/Scenes/SceneManager.h index 1f9cc3199..742a6b351 100644 --- a/src/source/Scenes/SceneManager.h +++ b/src/source/Scenes/SceneManager.h @@ -113,3 +113,8 @@ void MainScene(HDC hDC); // FPS management (legacy - use g_frameTiming instead) void SetTargetFps(double targetFps); double GetTargetFps(); + +// Debug overlay controls +void SetShowDebugInfo(bool enabled); +void SetShowFpsCounter(bool enabled); +void ResetFrameStats(); diff --git a/src/source/Utilities/Log/muConsoleDebug.cpp b/src/source/Utilities/Log/muConsoleDebug.cpp index 4582d3a24..13cb9a65b 100644 --- a/src/source/Utilities/Log/muConsoleDebug.cpp +++ b/src/source/Utilities/Log/muConsoleDebug.cpp @@ -15,6 +15,7 @@ #include "GlobalBitmap.h" #include "ZzzTexture.h" #include "Scenes/SceneCore.h" +#include "Scenes/SceneManager.h" #ifdef _EDITOR #include "../MuEditor/UI/Console/MuEditorConsoleUI.h" @@ -79,28 +80,47 @@ void CmuConsoleDebug::UpdateMainScene() bool CmuConsoleDebug::CheckCommand(const std::wstring& strCommand) { - if (strCommand.compare(0, 4, L"$fps") == 0) + if (strCommand.compare(L"$fpscounter on") == 0) + { + SetShowFpsCounter(true); + return true; + } + else if (strCommand.compare(L"$fpscounter off") == 0) + { + SetShowFpsCounter(false); + return true; + } + else if (strCommand.compare(L"$details on") == 0) + { + SetShowDebugInfo(true); + return true; + } + else if (strCommand.compare(L"$details off") == 0) + { + SetShowDebugInfo(false); + return true; + } + else if (strCommand.compare(0, 4, L"$fps") == 0) { auto fps_str = strCommand.substr(5); auto target_fps = std::stof(fps_str); SetTargetFps(target_fps); return true; } - - if (strCommand.compare(L"$vsync on") == 0) + else if (strCommand.compare(L"$vsync on") == 0) { EnableVSync(); SetTargetFps(-1); // unlimited + ResetFrameStats(); return true; } - - if (strCommand.compare(L"$vsync off") == 0) + else if (strCommand.compare(L"$vsync off") == 0) { DisableVSync(); + ResetFrameStats(); return true; } - - if (strCommand.compare(0, 7, L"$winmsg") == 0) + else if (strCommand.compare(0, 7, L"$winmsg") == 0) { auto str_limit = strCommand.substr(8); auto message_limit = std::stof(str_limit);