Skip to content
Open
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
1 change: 1 addition & 0 deletions Core/GameEngine/Include/GameClient/Display.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class Display : public SubsystemInterface
virtual void setCinematicTextFrames( Int frames ) { m_cinematicTextFrames = frames; }

virtual Real getAverageFPS() = 0; ///< returns the average FPS.
virtual Real getLow1PercentFPS() = 0; ///< returns the 1% low FPS.
virtual Real getCurrentFPS() = 0; ///< returns the current FPS.
virtual Int getLastFrameDrawCalls() = 0; ///< returns the number of draw calls issued in the previous frame

Expand Down
3 changes: 3 additions & 0 deletions Generals/Code/GameEngine/Include/GameClient/InGameUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,16 +747,19 @@ friend class Drawable; // for selection/deselection transactions

// Render FPS Counter
DisplayString * m_renderFpsString;
DisplayString * m_renderFpsLowString;
DisplayString * m_renderFpsLimitString;
AsciiString m_renderFpsFont;
Int m_renderFpsPointSize;
Bool m_renderFpsBold;
Coord2D m_renderFpsPosition;
Color m_renderFpsColor;
Color m_renderFpsLowColor;
Color m_renderFpsLimitColor;
Color m_renderFpsDropColor;
UnsignedInt m_renderFpsRefreshMs;
UnsignedInt m_lastRenderFps;
UnsignedInt m_lastRenderFpsLow;
UnsignedInt m_lastRenderFpsLimit;
UnsignedInt m_lastRenderFpsUpdateMs;

Expand Down
34 changes: 31 additions & 3 deletions Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ const FieldParse InGameUI::s_fieldParseTable[] =
{ "RenderFpsBold", INI::parseBool, nullptr, offsetof( InGameUI, m_renderFpsBold ) },
{ "RenderFpsPosition", INI::parseCoord2D, nullptr, offsetof( InGameUI, m_renderFpsPosition ) },
{ "RenderFpsColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsColor ) },
{ "RenderFpsLowColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsLowColor ) },
{ "RenderFpsLimitColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsLimitColor ) },
{ "RenderFpsDropColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsDropColor ) },
{ "RenderFpsRefreshMs", INI::parseUnsignedInt, nullptr, offsetof( InGameUI, m_renderFpsRefreshMs ) },
Expand Down Expand Up @@ -1133,17 +1134,20 @@ InGameUI::InGameUI()
m_lastNetworkLatencyFrames = ~0u;

m_renderFpsString = nullptr;
m_renderFpsLowString = nullptr;
m_renderFpsLimitString = nullptr;
m_renderFpsFont = "Tahoma";
m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize;
m_renderFpsBold = TRUE;
m_renderFpsPosition.x = kHudAnchorX;
m_renderFpsPosition.y = kHudAnchorY;
m_renderFpsColor = GameMakeColor( 255, 255, 0, 255 );
m_renderFpsLowColor = GameMakeColor( 180, 170, 120, 255 );
m_renderFpsLimitColor = GameMakeColor(119, 119, 119, 255);
m_renderFpsDropColor = GameMakeColor( 0, 0, 0, 255 );
m_renderFpsRefreshMs = 1000;
m_lastRenderFps = ~0u;
m_lastRenderFpsLow = ~0u;
m_lastRenderFpsLimit = ~0u;
m_lastRenderFpsUpdateMs = 0u;

Expand Down Expand Up @@ -2215,6 +2219,8 @@ void InGameUI::freeCustomUiResources()
m_networkLatencyString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsString);
m_renderFpsString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsLowString);
m_renderFpsLowString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsLimitString);
m_renderFpsLimitString = nullptr;
TheDisplayStringManager->freeDisplayString(m_systemTimeString);
Expand Down Expand Up @@ -5868,6 +5874,12 @@ void InGameUI::refreshRenderFpsResources()
m_lastRenderFpsUpdateMs = 0u;
}

if (!m_renderFpsLowString)
{
m_renderFpsLowString = TheDisplayStringManager->newDisplayString();
m_lastRenderFpsLow = ~0u;
}

if (!m_renderFpsLimitString)
{
m_renderFpsLimitString = TheDisplayStringManager->newDisplayString();
Expand All @@ -5878,6 +5890,7 @@ void InGameUI::refreshRenderFpsResources()
Int adjustedRenderFpsFontSize = TheGlobalLanguageData->adjustFontSize(m_renderFpsPointSize);
GameFont *fpsFont = TheWindowManager->winFindFont(m_renderFpsFont, adjustedRenderFpsFontSize, m_renderFpsBold);
m_renderFpsString->setFont(fpsFont);
m_renderFpsLowString->setFont(fpsFont);
m_renderFpsLimitString->setFont(fpsFont);

if (m_renderFpsPointSize > 0)
Expand Down Expand Up @@ -5990,6 +6003,15 @@ void InGameUI::updateRenderFpsString()
m_renderFpsString->setText(fpsStr);
m_lastRenderFps = renderFps;
}

const UnsignedInt renderFpsLow = (UnsignedInt)(TheDisplay->getLow1PercentFPS() + 0.5f);
if (renderFpsLow != m_lastRenderFpsLow)
{
UnicodeString lowStr;
lowStr.format(L"(%u)", renderFpsLow);
m_renderFpsLowString->setText(lowStr);
m_lastRenderFpsLow = renderFpsLow;
}
}

void InGameUI::drawNetworkLatency(Int &x, Int &y)
Expand Down Expand Up @@ -6056,14 +6078,20 @@ void InGameUI::drawRenderFps(Int &x, Int &y)
const Int drawY = kHudAnchorY + y;

m_renderFpsString->draw(kHudAnchorX + x, drawY, m_renderFpsColor, m_renderFpsDropColor);
x += m_renderFpsString->getWidth();
x += m_renderFpsString->getWidth() + kHudGapPx / 2;
m_renderFpsLowString->draw(kHudAnchorX + x, drawY, m_renderFpsLowColor, m_renderFpsDropColor);
x += m_renderFpsLowString->getWidth() + kHudGapPx / 2;
m_renderFpsLimitString->draw(kHudAnchorX + x, drawY, m_renderFpsLimitColor, m_renderFpsDropColor);
x += m_renderFpsLimitString->getWidth() + kHudGapPx;
}
else
{
m_renderFpsString->draw(m_renderFpsPosition.x, m_renderFpsPosition.y, m_renderFpsColor, m_renderFpsDropColor);
m_renderFpsLimitString->draw(m_renderFpsPosition.x + m_renderFpsString->getWidth(), m_renderFpsPosition.y, m_renderFpsLimitColor, m_renderFpsDropColor);
Int currentX = m_renderFpsPosition.x;
m_renderFpsString->draw(currentX, m_renderFpsPosition.y, m_renderFpsColor, m_renderFpsDropColor);
currentX += m_renderFpsString->getWidth() + kHudGapPx / 2;
m_renderFpsLowString->draw(currentX, m_renderFpsPosition.y, m_renderFpsLowColor, m_renderFpsDropColor);
currentX += m_renderFpsLowString->getWidth() + kHudGapPx / 2;
m_renderFpsLimitString->draw(currentX, m_renderFpsPosition.y, m_renderFpsLimitColor, m_renderFpsDropColor);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class W3DDisplay : public Display

void drawFPSStats(); ///< draw the fps on the screen
virtual Real getAverageFPS() override; ///< return the average FPS.
virtual Real getLow1PercentFPS() override; ///< return the 1% low FPS.
virtual Real getCurrentFPS() override; ///< return the current FPS.
virtual Int getLastFrameDrawCalls() override; ///< returns the number of draw calls issued in the previous frame

Expand All @@ -161,7 +162,10 @@ class W3DDisplay : public Display
void drawCurrentDebugDisplay(); ///< draws current debug display
void calculateTerrainLOD(); ///< Calculate terrain LOD.
void renderLetterBox(UnsignedInt time); ///< draw letter box border
void updateAverageFPS(); ///< calculate the average fps over the last 30 frames.
void updatePerformanceMetrics(); ///< update the average and 1% low fps metrics.
void addFpsSample(Real elapsedSeconds); ///< add a new sample to the history buffer.
Real calculateAverageFPS(Real windowSeconds); ///< calculate average FPS over a time window.
Real calculateLow1PercentFPS(Real windowSeconds); ///< calculate 1% low FPS over a time window.
void setup2DRenderState(TextureClass *tex, DrawImageMode mode, Bool grayscale);
virtual void onBeginBatch() override;
virtual void onEndBatch() override;
Expand All @@ -172,9 +176,18 @@ class W3DDisplay : public Display
Render2DClass *m_2DRender; ///< interface for common 2D functions
IRegion2D m_clipRegion; ///< the clipping region for images
Bool m_isClippedEnabled; ///<used by 2D drawing operations to define clip re
Real m_averageFPS; ///<average fps over the last 30 frames.
Real m_averageFPS; ///< average fps over the last 0.5s.
Real m_low1PercentFPS; ///<1% low fps.
Real m_currentFPS; ///<current fps value.

enum { FPS_HISTORY_SIZE = 5000 }; // covers 5s at 1000 FPS, degrades gracefully beyond
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think this number needs evaluation but I would like input from others.

As it stands, 5000 samples in three Real arrays require 60 kB in memory - just for the FPS trackers.
I think the question should be: given 3 seconds timeframe for the 1% low FPS, At what 3-second average FPS is the low fps no longer relevant. Say you average 300 fps, what is the chance that the 1% is so low that it is still relevant as a performance metric. If 300 fps is the upper bound, only 900 samples need to be stored. That's only 18% of the memory needed compared to the current setting.

Copy link
Copy Markdown
Author

@githubawn githubawn May 1, 2026

Choose a reason for hiding this comment

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

Say you average 300 fps, what is the chance that the 1% is so low that it is still relevant as a performance metric.

It's pretty common to see 300 average fps and 30 fps lows in this game in large skirmish matches even on vs2022 non-retail.

But definitely would like to hear multiple inputs for this number.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The implementation approach is fine, but the data size requirements for the fps counters are outrageous.

I inquired a bit with Chat Gippy and it would be possible to reduce size requirements by quantizing samples and moving intermediate samples into timed buckets.

Here is a sample FrameBucket with avg and min frame time, stored in 2 bytes each, with a resolution of 16 micro seconds.

// Compact frametime statistics bucket.
//
// Stores average and maximum frametime values using 16-bit integers
// with fixed-point quantization.
//
// ------------------------------------------------------------------
// Encoding
// ------------------------------------------------------------------
//
// Values are stored in units of 16 microseconds:
//
//     stored_value = frametime_us / 16
//
// This allows a uint16_t to represent:
//
//     65535 * 16 us = 1,048,560 us
//                   = ~1048 ms
//
// which comfortably covers extremely slow frames (~1 FPS).
//
// ------------------------------------------------------------------
// Precision
// ------------------------------------------------------------------
//
// Quantization step:
//
//     16 us = 0.016 ms
//
// Maximum quantization error:
//
//     +/- 8 us
//
// This is effectively negligible for FPS telemetry.
//
// Example:
//
//     16.667 ms frame (60 FPS)
//
// Encoded as:
//
//     16667 us / 16 = 1042
//
// Decoded:
//
//     1042 * 16 = 16672 us
//
// Error:
//
//     +5 us = 0.005 ms
//
// ------------------------------------------------------------------
// Why fixed-point quantization?
// ------------------------------------------------------------------
//
// Advantages:
//
// - extremely compact (4 bytes total per bucket)
// - deterministic runtime cost
// - cache friendly
// - no floating-point storage
// - sufficient precision for telemetry
// - supports >1 second frametimes
//
// ------------------------------------------------------------------
// Intended usage
// ------------------------------------------------------------------
//
// Typical bucket generation:
//
//     avgFrameUs = accumulatedFrameUs / frameCount
//     maxFrameUs = worstFrameUsSeen
//
// Then:
//
//     bucket.SetAvgUs(avgFrameUs);
//     bucket.SetMaxUs(maxFrameUs);
//
// ------------------------------------------------------------------
// Recommended usage pattern
// ------------------------------------------------------------------
//
// Build buckets over fixed time windows:
//
//     e.g. 50 ms or 100 ms
//
// rather than fixed frame counts.
//
// This keeps statistical resolution stable across varying FPS.
//
// ------------------------------------------------------------------

struct FrameBucket
{
    uint16_t avg16us;
    uint16_t max16us;

    static constexpr uint32_t QUANTUM_US    = 16;
    static constexpr uint32_t QUANTUM_SHIFT = 4;

    static constexpr uint32_t MAX_US =
        0xFFFFu * QUANTUM_US;

    // Encode microseconds into 16 us fixed-point units.
    //
    // Rounds to nearest unit.
    //
    // Input is clamped to representable range.
    static uint16_t EncodeUs(uint32_t us)
    {
        if (us > MAX_US)
            us = MAX_US;

        return static_cast<uint16_t>(
            (us + (QUANTUM_US / 2)) >> QUANTUM_SHIFT);
    }

    // Decode fixed-point units back into microseconds.
    static uint32_t DecodeUs(uint16_t v)
    {
        return static_cast<uint32_t>(v)
            << QUANTUM_SHIFT;
    }

    void SetAvgUs(uint32_t us)
    {
        avg16us = EncodeUs(us);
    }

    void SetMaxUs(uint32_t us)
    {
        max16us = EncodeUs(us);
    }

    uint32_t GetAvgUs() const
    {
        return DecodeUs(avg16us);
    }

    uint32_t GetMaxUs() const
    {
        return DecodeUs(max16us);
    }

    float GetAvgMs() const
    {
        return static_cast<float>(GetAvgUs()) * 0.001f;
    }

    float GetMaxMs() const
    {
        return static_cast<float>(GetMaxUs()) * 0.001f;
    }

    float GetAvgFPS() const
    {
        const uint32_t us = GetAvgUs();

        return (us > 0)
            ? (1000000.0f / static_cast<float>(us))
            : 0.0f;
    }

    float GetMinFPS() const
    {
        const uint32_t us = GetMaxUs();

        return (us > 0)
            ? (1000000.0f / static_cast<float>(us))
            : 0.0f;
    }
};

And then we move these FrameBuckets into a time based array.

Sample implementation:

// ============================================================================
// Rolling 3-second FPS statistics
// ============================================================================
//
// Stores:
//
//     3 seconds @ 10 ms resolution
//
// = 300 buckets
//
// Each bucket summarizes:
//
//     - average frametime
//     - worst frametime
//
// over a 10 ms interval.
//
// ============================================================================

class FpsHistory
{
public:

    static constexpr uint32_t BUCKET_INTERVAL_US = 10000; // 10 ms
    static constexpr uint32_t HISTORY_SECONDS    = 3;

    static constexpr uint32_t BUCKET_COUNT =
        (HISTORY_SECONDS * 1000000) / BUCKET_INTERVAL_US;

    // ------------------------------------------------------------------------

    void Reset()
    {
        m_writeIndex = 0;
        m_bucketCount = 0;

        m_accumulatedUs = 0;
        m_accumulatedFrames = 0;
        m_maxFrameUs = 0;
        m_bucketElapsedUs = 0;

        std::fill(
            std::begin(m_buckets),
            std::end(m_buckets),
            FrameBucket{});
    }

    // ------------------------------------------------------------------------
    // Add a frame
    //
    // frameUs:
    //     frametime in microseconds
    //
    // Example:
    //
    //     16.667 ms = 16667 us
    //
    // ------------------------------------------------------------------------

    void AddFrame(uint32_t frameUs)
    {
        // Accumulate stats for current bucket.

        m_accumulatedUs += frameUs;
        m_accumulatedFrames++;

        if (frameUs > m_maxFrameUs)
            m_maxFrameUs = frameUs;

        m_bucketElapsedUs += frameUs;

        // Emit one or more buckets if enough time elapsed.

        while (m_bucketElapsedUs >= BUCKET_INTERVAL_US)
        {
            EmitBucket();

            m_bucketElapsedUs -= BUCKET_INTERVAL_US;
        }
    }

    // ------------------------------------------------------------------------

    float GetAverageFPS() const
    {
        if (m_bucketCount == 0)
            return 0.0f;

        uint64_t totalUs = 0;

        for (uint32_t i = 0; i < m_bucketCount; ++i)
        {
            totalUs += m_buckets[i].GetAvgUs();
        }

        const float avgUs =
            static_cast<float>(totalUs)
            / static_cast<float>(m_bucketCount);

        return avgUs > 0.0f
            ? (1000000.0f / avgUs)
            : 0.0f;
    }

    // ------------------------------------------------------------------------
    // Approximate 1% low FPS
    //
    // Uses the worst bucket frametimes.
    //
    // This is intentionally approximate and designed for:
    //
    // - low memory use
    // - deterministic runtime
    // - good stutter detection
    //
    // ------------------------------------------------------------------------

    float GetOnePercentLowFPS() const
    {
        if (m_bucketCount == 0)
            return 0.0f;

        uint32_t worstUs = 0;

        // Find worst bucket maximum frametime.

        for (uint32_t i = 0; i < m_bucketCount; ++i)
        {
            worstUs = std::max(
                worstUs,
                m_buckets[i].GetMaxUs());
        }

        return worstUs > 0
            ? (1000000.0f / static_cast<float>(worstUs))
            : 0.0f;
    }

private:

    // ------------------------------------------------------------------------

    void EmitBucket()
    {
        FrameBucket& bucket =
            m_buckets[m_writeIndex];

        // Compute average frametime for bucket.

        const uint32_t avgUs =
            (m_accumulatedFrames > 0)
            ? static_cast<uint32_t>(
                m_accumulatedUs / m_accumulatedFrames)
            : 0;

        bucket.SetAvgUs(avgUs);
        bucket.SetMaxUs(m_maxFrameUs);

        // Advance ring buffer.

        m_writeIndex =
            (m_writeIndex + 1) % BUCKET_COUNT;

        if (m_bucketCount < BUCKET_COUNT)
            ++m_bucketCount;

        // Reset accumulators.

        m_accumulatedUs = 0;
        m_accumulatedFrames = 0;
        m_maxFrameUs = 0;
    }

private:

    FrameBucket m_buckets[BUCKET_COUNT];

    uint32_t m_writeIndex = 0;
    uint32_t m_bucketCount = 0;

    uint64_t m_accumulatedUs = 0;
    uint32_t m_accumulatedFrames = 0;
    uint32_t m_maxFrameUs = 0;

    uint32_t m_bucketElapsedUs = 0;
};

// ============================================================================
// Example usage
// ============================================================================

int main()
{
    FpsHistory history;

    history.Reset();

    // Simulate ~60 FPS.

    for (int i = 0; i < 500; ++i)
    {
        uint32_t frameUs = 16667;

        // Simulate occasional stutter.

        if ((i % 120) == 0)
        {
            frameUs = 50000; // 50 ms hitch
        }

        history.AddFrame(frameUs);
    }

    printf(
        "Average FPS: %.2f\n",
        history.GetAverageFPS());

    printf(
        "Approx 1%% Low FPS: %.2f\n",
        history.GetOnePercentLowFPS());

    return 0;
}

This data organization approach uses just 1200 bytes at most. Bucket intervals could also be longer for even less size.

It loses value accuracy. It optimizes for speed over accuracy.

However, your current implemention would perhaps be more efficient at low frame rates, because less values need reading at small frame counts. I do like that aspect, because low fps runtimes need to do less.

I suggest to look into this more how to find a good balance between size,speed and accuracy. There are many options here.

Real m_fpsHistory[FPS_HISTORY_SIZE];
Real m_durationHistory[FPS_HISTORY_SIZE];
Real m_sortBuffer[FPS_HISTORY_SIZE];
Int m_historyOffset;
Int m_historyCount;
Int64 m_lastUpdateTime64;

TextureClass *m_batchTexture;
DrawImageMode m_batchMode;
Bool m_batchGrayscale;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ W3DDisplay::W3DDisplay()
m_2DScene = nullptr;
m_3DInterfaceScene = nullptr;
m_averageFPS = TheGlobalData->m_framesPerSecondLimit;
m_low1PercentFPS = TheGlobalData->m_framesPerSecondLimit;
#if defined(RTS_DEBUG)
m_timerAtCumuFPSStart = 0;
#endif
Expand All @@ -360,6 +361,15 @@ W3DDisplay::W3DDisplay()
m_batchGrayscale = FALSE;
m_batchNeedsInit = FALSE;

m_historyOffset = 0;
m_historyCount = 0;
m_lastUpdateTime64 = 0;
for (Int h = 0; h < FPS_HISTORY_SIZE; ++h)
{
m_fpsHistory[h] = 0.0f;
m_durationHistory[h] = 0.0f;
}

#ifdef PROFILER_ENABLED
m_profilerFrameCapture = NEW W3DProfilerFrameCapture();
#endif
Expand Down Expand Up @@ -958,14 +968,92 @@ void W3DDisplay::reset()

const UnsignedInt START_CUMU_FRAME = LOGICFRAMES_PER_SECOND / 2; // skip first half-sec

void W3DDisplay::updateAverageFPS()
void W3DDisplay::addFpsSample(Real elapsedSeconds)
{
constexpr const Int FPS_HISTORY_SIZE = 30;
if (elapsedSeconds <= 0.0f)
{
return;
}

m_currentFPS = 1.0f / elapsedSeconds;
m_fpsHistory[m_historyOffset] = m_currentFPS;
m_durationHistory[m_historyOffset] = elapsedSeconds;

m_historyOffset = (m_historyOffset + 1) % FPS_HISTORY_SIZE;
if (m_historyCount < FPS_HISTORY_SIZE)
{
m_historyCount++;
}
}

Real W3DDisplay::calculateAverageFPS(Real windowSeconds)
{
if (m_historyCount == 0)
{
return m_currentFPS;
}

Real timeSum = 0;
Int samples = 0;

for (Int i = 0; i < m_historyCount; ++i)
{
Int idx = (m_historyOffset - 1 - i + FPS_HISTORY_SIZE) % FPS_HISTORY_SIZE;
timeSum += m_durationHistory[idx];
samples++;

if (timeSum >= windowSeconds)
{
break;
}
}

return (timeSum > 0) ? ((Real)samples / timeSum) : m_currentFPS;
}

Real W3DDisplay::calculateLow1PercentFPS(Real windowSeconds)
{
if (m_historyCount == 0)
{
return m_currentFPS;
}

Real timeSum = 0;
Int sampleCount = 0;
Int i;

static Int64 lastUpdateTime64 = 0;
static Int historyOffset = 0;
static Real fpsHistory[FPS_HISTORY_SIZE] = {0};
for (i = 0; i < m_historyCount; ++i)
{
Int idx = (m_historyOffset - 1 - i + FPS_HISTORY_SIZE) % FPS_HISTORY_SIZE;
timeSum += m_durationHistory[idx];
m_sortBuffer[sampleCount++] = m_fpsHistory[idx];

if (timeSum >= windowSeconds)
{
break;
}
}

if (sampleCount == 0)
{
return m_currentFPS;
}

const Int bottomSampleCount = std::max((sampleCount + 50) / 100, 1);

std::nth_element(m_sortBuffer, m_sortBuffer + bottomSampleCount, m_sortBuffer + sampleCount);

Real lowSum = 0;
for (i = 0; i < bottomSampleCount; ++i)
{
lowSum += m_sortBuffer[i];
}

return lowSum / (Real)bottomSampleCount;
}

void W3DDisplay::updatePerformanceMetrics()
{
const Int64 freq64 = getPerformanceCounterFrequency();
const Int64 time64 = getPerformanceCounter();

Expand All @@ -976,23 +1064,27 @@ void W3DDisplay::updateAverageFPS()
}
#endif

const Int64 timeDiff = time64 - lastUpdateTime64;

// convert elapsed time to seconds
Real elapsedSeconds = (Real)timeDiff/(Real)freq64;
if (m_lastUpdateTime64 == 0)
{
m_lastUpdateTime64 = time64;
return;
}

// append new sample to fps history.
if (historyOffset >= FPS_HISTORY_SIZE)
historyOffset = 0;
const Int64 timeDiff = time64 - m_lastUpdateTime64;
Real elapsedSeconds = (Real)timeDiff / (Real)freq64;

m_currentFPS = 1.0f/elapsedSeconds;
fpsHistory[historyOffset++] = m_currentFPS;
addFpsSample(elapsedSeconds);
m_averageFPS = calculateAverageFPS(0.5f);

// determine average frame rate over our past history.
const Real sum = std::accumulate(fpsHistory, fpsHistory + FPS_HISTORY_SIZE, 0.0f);
m_averageFPS = sum / FPS_HISTORY_SIZE;
static UnsignedInt lastLowUpdate = 0;
UnsignedInt now = timeGetTime();
if (now - lastLowUpdate >= 1000)
{
lastLowUpdate = now;
m_low1PercentFPS = calculateLow1PercentFPS(3.0f);
}

lastUpdateTime64 = time64;
m_lastUpdateTime64 = time64;
}

#if defined(RTS_DEBUG) //debug hack to view object under mouse stats
Expand Down Expand Up @@ -1693,6 +1785,11 @@ Real W3DDisplay::getAverageFPS()
return m_averageFPS;
}

Real W3DDisplay::getLow1PercentFPS()
{
return m_low1PercentFPS;
}

Real W3DDisplay::getCurrentFPS()
{
return m_currentFPS;
Expand Down Expand Up @@ -1727,7 +1824,7 @@ void W3DDisplay::draw()
if (TheGlobalData->m_headless)
return;

updateAverageFPS();
updatePerformanceMetrics();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
DynamicGameLODLevel lod=TheGameLODManager->findDynamicLODLevel(m_averageFPS);
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class GUIEditDisplay : public Display
#endif

virtual Real getAverageFPS() override { return 0; }
virtual Real getLow1PercentFPS() override { return 0; }
virtual Real getCurrentFPS() override { return 0; }
virtual Int getLastFrameDrawCalls() override { return 0; }

Expand Down
3 changes: 3 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -769,16 +769,19 @@ friend class Drawable; // for selection/deselection transactions

// Render FPS Counter
DisplayString * m_renderFpsString;
DisplayString * m_renderFpsLowString;
DisplayString * m_renderFpsLimitString;
AsciiString m_renderFpsFont;
Int m_renderFpsPointSize;
Bool m_renderFpsBold;
Coord2D m_renderFpsPosition;
Color m_renderFpsColor;
Color m_renderFpsLowColor;
Color m_renderFpsLimitColor;
Color m_renderFpsDropColor;
UnsignedInt m_renderFpsRefreshMs;
UnsignedInt m_lastRenderFps;
UnsignedInt m_lastRenderFpsLow;
UnsignedInt m_lastRenderFpsLimit;
UnsignedInt m_lastRenderFpsUpdateMs;

Expand Down
Loading
Loading