ddnet/src/engine/client/graph.cpp
Robert Müller 4be76f0d08 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.
2024-01-17 20:43:19 +01:00

210 lines
5.6 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <engine/graphics.h>
#include <engine/textrender.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)
{
SetMin(Min);
SetMax(Max);
}
void CGraph::SetMin(float Min)
{
m_MinRange = m_Min = Min;
}
void CGraph::SetMax(float Max)
{
m_MaxRange = m_Max = Max;
}
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_Max = m_MaxRange;
for(SEntry *pEntry = m_pFirstScaled; pEntry != nullptr; pEntry = m_Entries.Next(pEntry))
{
if(pEntry->m_Value > m_Max)
m_Max = pEntry->m_Value;
else if(pEntry->m_Value < m_Min)
m_Min = pEntry->m_Value;
}
}
void CGraph::Add(float Value, ColorRGBA Color)
{
InsertAt(time_get(), Value, Color);
}
void CGraph::InsertAt(int64_t Time, float Value, ColorRGBA Color)
{
SEntry *pEntry = m_Entries.Allocate(sizeof(SEntry));
pEntry->m_Time = Time;
pEntry->m_Value = Value;
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)
{
pGraphics->TextureClear();
pGraphics->QuadsBegin();
pGraphics->SetColor(0.0f, 0.0f, 0.0f, 0.75f);
IGraphics::CQuadItem QuadItem(x, y, w, h);
pGraphics->QuadsDrawTL(&QuadItem, 1);
pGraphics->QuadsEnd();
pGraphics->LinesBegin();
pGraphics->SetColor(0.95f, 0.95f, 0.95f, 1.0f);
IGraphics::CLineItem LineItem(x, y + h / 2, x + w, y + h / 2);
pGraphics->LinesDraw(&LineItem, 1);
pGraphics->SetColor(0.5f, 0.5f, 0.5f, 0.75f);
IGraphics::CLineItem aLineItems[2] = {
IGraphics::CLineItem(x, y + (h * 3) / 4, x + w, y + (h * 3) / 4),
IGraphics::CLineItem(x, y + h / 4, x + w, y + h / 4)};
pGraphics->LinesDraw(aLineItems, std::size(aLineItems));
if(m_pFirstScaled != nullptr)
{
IGraphics::CLineItem aValueLineItems[128];
size_t NumValueLineItems = 0;
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();
const float FontSize = 12.0f;
const float Spacing = 2.0f;
pTextRender->Text(x + Spacing, y + h - FontSize - Spacing, FontSize, pDescription);
char aBuf[32];
str_format(aBuf, sizeof(aBuf), "%.2f", m_Max);
pTextRender->Text(x + w - pTextRender->TextWidth(FontSize, aBuf) - Spacing, y + Spacing, FontSize, aBuf);
str_format(aBuf, sizeof(aBuf), "%.2f", m_Min);
pTextRender->Text(x + w - pTextRender->TextWidth(FontSize, aBuf) - Spacing, y + h - FontSize - Spacing, FontSize, aBuf);
}