Add time scale to debug graphs for constant scrolling speed

Store X value (time) for all graph entries in addition to the Y value (FPS, prediction margin etc.). The `CGraph::Add` function adds values to the graph at the current time. The `CGraph::InsertAt` function allows specifying arbitrary X values, as long as the values are inserted in increasing order.

The entries are kept in a ringbuffer and old entries are recycled when it's full. The size of the ringbuffer is configurable for each graph, as the FPS graph needs significantly more buffer because values are added more often.

The scrolling speed of the graphs is fixed by specifying the maximum size of the window of values which should be displayed. For this purpose, a parameter is added to the `CGraph::Scale` function to specify the size of the window which should be rendered in the `CGraph::Render` function. For the FPS graph only the last second is rendered, so small spikes are still noticeable. For prediction and gametime margin graphs the last five seconds are rendered, which should result in a similar scrolling speed as before this change. The debug tuning graph is a special case, where the X values set manually and fixed to 0-127, same as before, instead of being based on the current time.

The graph rendering is made much more efficient by precalculating when the vertex colors need to be updated, to avoid all unnecessary calls to `SetColorVertex`. Additionally, line items are bundled together in an array to avoid calling `LinesDraw` for every individual line item.
This commit is contained in:
Robert Müller 2024-01-02 23:46:52 +01:00
parent c90a52f51c
commit 4be76f0d08
5 changed files with 169 additions and 53 deletions

View file

@ -75,7 +75,10 @@ static const ColorRGBA gs_ClientNetworkPrintColor{0.7f, 1, 0.7f, 1.0f};
static const ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f}; static const ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f};
CClient::CClient() : CClient::CClient() :
m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }) m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }),
m_InputtimeMarginGraph(128),
m_GametimeMarginGraph(128),
m_FpsGraph(4096)
{ {
m_StateStartTime = time_get(); m_StateStartTime = time_get();
for(auto &DemoRecorder : m_aDemoRecorder) for(auto &DemoRecorder : m_aDemoRecorder)
@ -821,11 +824,11 @@ void CClient::DebugRender()
float sp = Graphics()->ScreenWidth() / 100.0f; float sp = Graphics()->ScreenWidth() / 100.0f;
float x = Graphics()->ScreenWidth() - w - sp; float x = Graphics()->ScreenWidth() - w - sp;
m_FpsGraph.Scale(); m_FpsGraph.Scale(time_freq());
m_FpsGraph.Render(Graphics(), TextRender(), x, sp * 5, w, h, "FPS"); m_FpsGraph.Render(Graphics(), TextRender(), x, sp * 5, w, h, "FPS");
m_InputtimeMarginGraph.Scale(); m_InputtimeMarginGraph.Scale(5 * time_freq());
m_InputtimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 6 + h, w, h, "Prediction Margin"); m_InputtimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 6 + h, w, h, "Prediction Margin");
m_GametimeMarginGraph.Scale(); m_GametimeMarginGraph.Scale(5 * time_freq());
m_GametimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin"); m_GametimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin");
} }
} }

View file

@ -6,13 +6,15 @@
#include "graph.h" #include "graph.h"
CGraph::CGraph(int MaxEntries) :
m_Entries(MaxEntries * (sizeof(SEntry) + 2 * CRingBufferBase::ITEM_SIZE), CRingBufferBase::FLAG_RECYCLE)
{
}
void CGraph::Init(float Min, float Max) void CGraph::Init(float Min, float Max)
{ {
SetMin(Min); SetMin(Min);
SetMax(Max); SetMax(Max);
m_Index = 0;
for(auto &Entry : m_aEntries)
Entry.m_Initialized = false;
} }
void CGraph::SetMin(float Min) void CGraph::SetMin(float Min)
@ -25,34 +27,103 @@ void CGraph::SetMax(float Max)
m_MaxRange = m_Max = Max; m_MaxRange = m_Max = Max;
} }
void CGraph::Scale() void CGraph::Scale(int64_t WantedTotalTime)
{ {
// Scale X axis for wanted total time
if(m_Entries.First() != nullptr)
{
const int64_t EndTime = m_Entries.Last()->m_Time;
bool ScaleTotalTime = false;
m_pFirstScaled = nullptr;
if(m_Entries.First()->m_Time >= EndTime - WantedTotalTime)
{
m_pFirstScaled = m_Entries.First();
}
else
{
m_pFirstScaled = m_Entries.Last();
while(m_pFirstScaled)
{
SEntry *pPrev = m_Entries.Prev(m_pFirstScaled);
if(pPrev == nullptr)
break;
if(pPrev->m_Time < EndTime - WantedTotalTime)
{
// Scale based on actual total time instead of based on wanted total time,
// to avoid flickering last segment due to rounding errors.
ScaleTotalTime = true;
break;
}
m_pFirstScaled = pPrev;
}
}
m_RenderedTotalTime = ScaleTotalTime ? (EndTime - m_pFirstScaled->m_Time) : WantedTotalTime;
// Ensure that color is applied to first line segment
if(m_pFirstScaled)
{
m_pFirstScaled->m_ApplyColor = true;
SEntry *pNext = m_Entries.Next(m_pFirstScaled);
if(pNext != nullptr)
{
pNext->m_ApplyColor = true;
}
}
}
else
{
m_pFirstScaled = nullptr;
m_RenderedTotalTime = 0;
}
// Scale Y axis
m_Min = m_MinRange; m_Min = m_MinRange;
m_Max = m_MaxRange; m_Max = m_MaxRange;
for(auto &Entry : m_aEntries) for(SEntry *pEntry = m_pFirstScaled; pEntry != nullptr; pEntry = m_Entries.Next(pEntry))
{ {
if(Entry.m_Value > m_Max) if(pEntry->m_Value > m_Max)
m_Max = Entry.m_Value; m_Max = pEntry->m_Value;
else if(Entry.m_Value < m_Min) else if(pEntry->m_Value < m_Min)
m_Min = Entry.m_Value; m_Min = pEntry->m_Value;
} }
} }
void CGraph::Add(float Value, ColorRGBA Color) void CGraph::Add(float Value, ColorRGBA Color)
{ {
InsertAt(m_Index, Value, Color); InsertAt(time_get(), Value, Color);
m_Index = (m_Index + 1) % MAX_VALUES;
} }
void CGraph::InsertAt(size_t Index, float Value, ColorRGBA Color) void CGraph::InsertAt(int64_t Time, float Value, ColorRGBA Color)
{ {
dbg_assert(Index < MAX_VALUES, "Index out of bounds"); SEntry *pEntry = m_Entries.Allocate(sizeof(SEntry));
m_aEntries[Index].m_Initialized = true; pEntry->m_Time = Time;
m_aEntries[Index].m_Value = Value; pEntry->m_Value = Value;
m_aEntries[Index].m_Color = Color; pEntry->m_Color = Color;
// Determine whether the line (pPrev, pEntry) has different
// vertex colors than the line (pPrevPrev, pPrev).
SEntry *pPrev = m_Entries.Prev(pEntry);
if(pPrev == nullptr)
{
pEntry->m_ApplyColor = true;
}
else
{
SEntry *pPrevPrev = m_Entries.Prev(pPrev);
if(pPrevPrev == nullptr)
{
pEntry->m_ApplyColor = true;
}
else
{
pEntry->m_ApplyColor = Color != pPrev->m_Color || pPrev->m_Color != pPrevPrev->m_Color;
}
}
} }
void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) const void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription)
{ {
pGraphics->TextureClear(); pGraphics->TextureClear();
@ -66,28 +137,61 @@ void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, flo
pGraphics->SetColor(0.95f, 0.95f, 0.95f, 1.0f); pGraphics->SetColor(0.95f, 0.95f, 0.95f, 1.0f);
IGraphics::CLineItem LineItem(x, y + h / 2, x + w, y + h / 2); IGraphics::CLineItem LineItem(x, y + h / 2, x + w, y + h / 2);
pGraphics->LinesDraw(&LineItem, 1); pGraphics->LinesDraw(&LineItem, 1);
pGraphics->SetColor(0.5f, 0.5f, 0.5f, 0.75f); pGraphics->SetColor(0.5f, 0.5f, 0.5f, 0.75f);
IGraphics::CLineItem aLineItems[2] = { IGraphics::CLineItem aLineItems[2] = {
IGraphics::CLineItem(x, y + (h * 3) / 4, x + w, y + (h * 3) / 4), IGraphics::CLineItem(x, y + (h * 3) / 4, x + w, y + (h * 3) / 4),
IGraphics::CLineItem(x, y + h / 4, x + w, y + h / 4)}; IGraphics::CLineItem(x, y + h / 4, x + w, y + h / 4)};
pGraphics->LinesDraw(aLineItems, std::size(aLineItems)); pGraphics->LinesDraw(aLineItems, std::size(aLineItems));
for(int i = 1; i < MAX_VALUES; i++)
{
const auto &Entry0 = m_aEntries[(m_Index + i - 1) % MAX_VALUES];
const auto &Entry1 = m_aEntries[(m_Index + i) % MAX_VALUES];
if(!Entry0.m_Initialized || !Entry1.m_Initialized)
continue;
float a0 = (i - 1) / (float)(MAX_VALUES - 1);
float a1 = i / (float)(MAX_VALUES - 1);
float v0 = (Entry0.m_Value - m_Min) / (m_Max - m_Min);
float v1 = (Entry1.m_Value - m_Min) / (m_Max - m_Min);
IGraphics::CColorVertex aColorVertices[2] = { if(m_pFirstScaled != nullptr)
IGraphics::CColorVertex(0, Entry0.m_Color.r, Entry0.m_Color.g, Entry0.m_Color.b, Entry0.m_Color.a), {
IGraphics::CColorVertex(1, Entry1.m_Color.r, Entry1.m_Color.g, Entry1.m_Color.b, Entry1.m_Color.a)}; IGraphics::CLineItem aValueLineItems[128];
pGraphics->SetColorVertex(aColorVertices, std::size(aColorVertices)); size_t NumValueLineItems = 0;
IGraphics::CLineItem LineItem2(x + a0 * w, y + h - v0 * h, x + a1 * w, y + h - v1 * h);
pGraphics->LinesDraw(&LineItem2, 1); const int64_t StartTime = m_pFirstScaled->m_Time;
SEntry *pEntry0 = m_pFirstScaled;
int a0 = round_to_int((pEntry0->m_Time - StartTime) * w / m_RenderedTotalTime);
int v0 = round_to_int((pEntry0->m_Value - m_Min) * h / (m_Max - m_Min));
while(pEntry0 != nullptr)
{
SEntry *pEntry1 = m_Entries.Next(pEntry0);
if(pEntry1 == nullptr)
break;
const int a1 = round_to_int((pEntry1->m_Time - StartTime) * w / m_RenderedTotalTime);
const int v1 = round_to_int((pEntry1->m_Value - m_Min) * h / (m_Max - m_Min));
if(pEntry1->m_ApplyColor)
{
if(NumValueLineItems)
{
pGraphics->LinesDraw(aValueLineItems, NumValueLineItems);
NumValueLineItems = 0;
}
IGraphics::CColorVertex aColorVertices[2] = {
IGraphics::CColorVertex(0, pEntry0->m_Color.r, pEntry0->m_Color.g, pEntry0->m_Color.b, pEntry0->m_Color.a),
IGraphics::CColorVertex(1, pEntry1->m_Color.r, pEntry1->m_Color.g, pEntry1->m_Color.b, pEntry1->m_Color.a)};
pGraphics->SetColorVertex(aColorVertices, std::size(aColorVertices));
}
if(NumValueLineItems == std::size(aValueLineItems))
{
pGraphics->LinesDraw(aValueLineItems, NumValueLineItems);
NumValueLineItems = 0;
}
aValueLineItems[NumValueLineItems] = IGraphics::CLineItem(x + a0, y + h - v0, x + a1, y + h - v1);
++NumValueLineItems;
pEntry0 = pEntry1;
a0 = a1;
v0 = v1;
}
if(NumValueLineItems)
{
pGraphics->LinesDraw(aValueLineItems, NumValueLineItems);
}
} }
pGraphics->LinesEnd(); pGraphics->LinesEnd();

View file

@ -6,6 +6,8 @@
#include <base/color.h> #include <base/color.h>
#include <engine/shared/ringbuffer.h>
#include <cstddef> #include <cstddef>
class IGraphics; class IGraphics;
@ -13,33 +15,31 @@ class ITextRender;
class CGraph class CGraph
{ {
public:
enum
{
MAX_VALUES = 128,
};
private: private:
struct SEntry struct SEntry
{ {
bool m_Initialized; int64_t m_Time;
float m_Value; float m_Value;
ColorRGBA m_Color; ColorRGBA m_Color;
bool m_ApplyColor;
}; };
SEntry *m_pFirstScaled = nullptr;
int64_t m_RenderedTotalTime = 0;
float m_Min, m_Max; float m_Min, m_Max;
float m_MinRange, m_MaxRange; float m_MinRange, m_MaxRange;
SEntry m_aEntries[MAX_VALUES]; CDynamicRingBuffer<SEntry> m_Entries;
size_t m_Index;
public: public:
CGraph(int MaxEntries);
void Init(float Min, float Max); void Init(float Min, float Max);
void SetMin(float Min); void SetMin(float Min);
void SetMax(float Max); void SetMax(float Max);
void Scale(); void Scale(int64_t WantedTotalTime);
void Add(float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f)); void Add(float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f));
void InsertAt(size_t Index, float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f)); void InsertAt(int64_t Time, float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f));
void Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) const; void Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription);
}; };
#endif #endif

View file

@ -12,6 +12,14 @@
#include "debughud.h" #include "debughud.h"
static constexpr int64_t GRAPH_MAX_VALUES = 128;
CDebugHud::CDebugHud() :
m_RampGraph(GRAPH_MAX_VALUES),
m_ZoomedInGraph(GRAPH_MAX_VALUES)
{
}
void CDebugHud::RenderNetCorrections() void CDebugHud::RenderNetCorrections()
{ {
if(!g_Config.m_Debug || g_Config.m_DbgGraphs || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter) if(!g_Config.m_Debug || g_Config.m_DbgGraphs || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
@ -179,7 +187,7 @@ void CDebugHud::RenderTuning()
m_RampGraph.Init(0.0f, 0.0f); m_RampGraph.Init(0.0f, 0.0f);
m_SpeedTurningPoint = 0; m_SpeedTurningPoint = 0;
float PreviousRampedSpeed = 1.0f; float PreviousRampedSpeed = 1.0f;
for(size_t i = 0; i < CGraph::MAX_VALUES; i++) for(int64_t i = 0; i < GRAPH_MAX_VALUES; i++)
{ {
// This is a calculation of the speed values per second on the X axis, from 270 to 34560 in steps of 270 // This is a calculation of the speed values per second on the X axis, from 270 to 34560 in steps of 270
const float Speed = (i + 1) * StepSizeRampGraph; const float Speed = (i + 1) * StepSizeRampGraph;
@ -196,12 +204,12 @@ void CDebugHud::RenderTuning()
} }
PreviousRampedSpeed = RampedSpeed; PreviousRampedSpeed = RampedSpeed;
} }
m_RampGraph.Scale(); m_RampGraph.Scale(GRAPH_MAX_VALUES - 1);
m_ZoomedInGraph.Init(0.0f, 0.0f); m_ZoomedInGraph.Init(0.0f, 0.0f);
PreviousRampedSpeed = 1.0f; PreviousRampedSpeed = 1.0f;
MiddleOfZoomedInGraph = m_SpeedTurningPoint; MiddleOfZoomedInGraph = m_SpeedTurningPoint;
for(size_t i = 0; i < CGraph::MAX_VALUES; i++) for(int64_t i = 0; i < GRAPH_MAX_VALUES; i++)
{ {
// This is a calculation of the speed values per second on the X axis, from (MiddleOfZoomedInGraph - 64 * StepSize) to (MiddleOfZoomedInGraph + 64 * StepSize) // This is a calculation of the speed values per second on the X axis, from (MiddleOfZoomedInGraph - 64 * StepSize) to (MiddleOfZoomedInGraph + 64 * StepSize)
const float Speed = MiddleOfZoomedInGraph - 64 * StepSizeZoomedInGraph + i * StepSizeZoomedInGraph; const float Speed = MiddleOfZoomedInGraph - 64 * StepSizeZoomedInGraph + i * StepSizeZoomedInGraph;
@ -222,7 +230,7 @@ void CDebugHud::RenderTuning()
} }
PreviousRampedSpeed = RampedSpeed; PreviousRampedSpeed = RampedSpeed;
} }
m_ZoomedInGraph.Scale(); m_ZoomedInGraph.Scale(GRAPH_MAX_VALUES - 1);
} }
const float GraphFontSize = 12.0f; const float GraphFontSize = 12.0f;

View file

@ -21,6 +21,7 @@ class CDebugHud : public CComponent
float m_OldVelrampCurvature; float m_OldVelrampCurvature;
public: public:
CDebugHud();
virtual int Sizeof() const override { return sizeof(*this); } virtual int Sizeof() const override { return sizeof(*this); }
virtual void OnRender() override; virtual void OnRender() override;
}; };