Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <value>`
* 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.
Expand Down
247 changes: 234 additions & 13 deletions src/source/Scenes/SceneManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "stdafx.h"
#include <vector>
#include <algorithm>
#include <numeric>
#include "SceneManager.h"

//=============================================================================
Expand Down Expand Up @@ -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<float>(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<float>());

// 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())
Expand Down Expand Up @@ -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);
}
Comment on lines +408 to +474

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of immediate mode OpenGL (glBegin, glEnd) is deprecated and can be inefficient. The pull request description mentions optimizing OpenGL calls with vertex arrays, so it's surprising to see new code using this legacy approach. For better performance and modern practice, this should be refactored to use Vertex Buffer Objects (VBOs) and shaders, if the rendering engine supports it. Even for a simple overlay, using modern OpenGL is preferable for consistency and performance.


/**
* @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)
}

/**
Expand Down Expand Up @@ -697,6 +917,7 @@ void MainScene(HDC hDC)
{
Success = RenderCurrentScene(hDC);
RenderDebugInfo();
RenderFpsCounter();

if (Success)
{
Expand Down
5 changes: 5 additions & 0 deletions src/source/Scenes/SceneManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
34 changes: 27 additions & 7 deletions src/source/Utilities/Log/muConsoleDebug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}
Comment on lines +83 to 122

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The if-else if chain for command parsing is becoming long and can be difficult to maintain. A more scalable approach would be to use a std::map to associate command strings with their handler functions. This would make the code cleaner and more extensible.


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);
Expand Down