source-engine/game/client/vgui_netgraphpanel.cpp

1514 lines
39 KiB
C++
Raw Permalink Normal View History

2020-04-23 00:56:21 +08:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $Workfile: $
// $Date: $
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "hud.h"
#include "inetgraphpanel.h"
#include "kbutton.h"
#include <inetchannelinfo.h>
#include "input.h"
#include <vgui/IVGui.h>
#include "VGuiMatSurface/IMatSystemSurface.h"
#include <vgui_controls/Panel.h>
#include <vgui_controls/Controls.h>
#include <vgui/ISurface.h>
#include <vgui/IScheme.h>
#include <vgui/ILocalize.h>
#include "tier0/vprof.h"
#include "cdll_bounded_cvars.h"
#include "materialsystem/imaterialsystem.h"
#include "materialsystem/imesh.h"
#include "materialsystem/imaterial.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
using namespace vgui;
static ConVar net_scale ( "net_scale", "5", FCVAR_ARCHIVE );
static ConVar net_graphpos ( "net_graphpos", "1", FCVAR_ARCHIVE );
static ConVar net_graphsolid ( "net_graphsolid", "1", FCVAR_ARCHIVE );
static ConVar net_graphtext ( "net_graphtext", "1", FCVAR_ARCHIVE, "Draw text fields" );
static ConVar net_graphmsecs ( "net_graphmsecs", "400", FCVAR_ARCHIVE, "The latency graph represents this many milliseconds." );
static ConVar net_graphshowlatency( "net_graphshowlatency", "1", FCVAR_ARCHIVE, "Draw the ping/packet loss graph." );
static ConVar net_graphshowinterp ( "net_graphshowinterp", "1", FCVAR_ARCHIVE, "Draw the interpolation graph." );
void NetgraphFontChangeCallback( IConVar *var, const char *pOldValue, float flOldValue );
2022-03-02 04:00:42 +08:00
static ConVar net_graph ( "net_graph","0", FCVAR_ARCHIVE, "Draw the network usage graph, = 2 draws data on payload, = 3 draws payload legend.", NetgraphFontChangeCallback );
2020-04-23 00:56:21 +08:00
static ConVar net_graphheight ( "net_graphheight", "64", FCVAR_ARCHIVE, "Height of netgraph panel", NetgraphFontChangeCallback );
static ConVar net_graphproportionalfont( "net_graphproportionalfont", "1", FCVAR_ARCHIVE, "Determines whether netgraph font is proportional or not", NetgraphFontChangeCallback );
#define TIMINGS 1024 // Number of values to track (must be power of 2) b/c of masking
#define FRAMERATE_AVG_FRAC 0.9
#define PACKETLOSS_AVG_FRAC 0.5
#define PACKETCHOKE_AVG_FRAC 0.5
#define NUM_LATENCY_SAMPLES 8
#define GRAPH_RED (0.9 * 255)
#define GRAPH_GREEN (0.9 * 255)
#define GRAPH_BLUE (0.7 * 255)
#define LERP_HEIGHT 24
#define COLOR_DROPPED 0
#define COLOR_INVALID 1
#define COLOR_SKIPPED 2
#define COLOR_CHOKED 3
#define COLOR_NORMAL 4
//-----------------------------------------------------------------------------
// Purpose: Displays the NetGraph
//-----------------------------------------------------------------------------
class CNetGraphPanel : public Panel
{
typedef Panel BaseClass;
private:
typedef struct
{
int latency;
int choked;
} packet_latency_t;
typedef struct
{
unsigned short msgbytes[INetChannelInfo::TOTAL+1];
int sampleY;
int sampleHeight;
} netbandwidthgraph_t;
typedef struct
{
float cmd_lerp;
int size;
bool sent;
} cmdinfo_t;
typedef struct
{
byte color[3];
byte alpha;
} netcolor_t;
byte colors[ LERP_HEIGHT ][3];
byte sendcolor[ 3 ];
byte holdcolor[ 3 ];
byte extrap_base_color[ 3 ];
packet_latency_t m_PacketLatency[ TIMINGS ];
cmdinfo_t m_Cmdinfo[ TIMINGS ];
netbandwidthgraph_t m_Graph[ TIMINGS ];
float m_Framerate;
float m_AvgLatency;
float m_AvgPacketLoss;
float m_AvgPacketChoke;
int m_IncomingSequence;
int m_OutgoingSequence;
int m_UpdateWindowSize;
float m_IncomingData;
float m_OutgoingData;
float m_AvgPacketIn;
float m_AvgPacketOut;
int m_StreamRecv[MAX_FLOWS];
int m_StreamTotal[MAX_FLOWS];
netcolor_t netcolors[5];
HFont m_hFontProportional;
HFont m_hFont;
HFont m_hFontSmall;
const ConVar *cl_updaterate;
const ConVar *cl_cmdrate;
public:
CNetGraphPanel( VPANEL parent );
virtual ~CNetGraphPanel( void );
virtual void ApplySchemeSettings(IScheme *pScheme);
virtual void Paint();
virtual void OnTick( void );
virtual bool ShouldDraw( void );
void InitColors( void );
int GraphValue( void );
struct CLineSegment
{
int x1, y1, x2, y2;
byte color[4];
byte color2[4];
};
CUtlVector< CLineSegment > m_Rects;
inline void DrawLine( vrect_t *rect, unsigned char *color, unsigned char alpha );
inline void DrawLine2( vrect_t *rect, unsigned char *color, unsigned char *color2, unsigned char alpha, unsigned char alpha2 );
void ResetLineSegments();
void DrawLineSegments();
int DrawDataSegment( vrect_t *rcFill, int bytes, byte r, byte g, byte b, byte alpha = 255);
void DrawUpdateRate( int xright, int y );
void DrawCmdRate( int xright, int y );
void DrawHatches( int x, int y, int maxmsgbytes );
void DrawStreamProgress( int x, int y, int width );
void DrawTimes( vrect_t vrect, cmdinfo_t *cmdinfo, int x, int w, int graphtype );
void DrawTextFields( int graphvalue, int x, int y, int w, netbandwidthgraph_t *graph, cmdinfo_t *cmdinfo );
void GraphGetXY( vrect_t *rect, int width, int *x, int *y );
void GetCommandInfo( INetChannelInfo *netchannel, cmdinfo_t *cmdinfo );
void GetFrameData( INetChannelInfo *netchannel, int *biggest_message, float *avg_message, float *f95thpercentile );
void ColorForHeight( packet_latency_t *packet, byte *color, int *ping, byte *alpha );
void GetColorValues( int color, byte *cv, byte *alpha );
void OnFontChanged();
private:
void PaintLineArt( int x, int y, int w, int graphtype, int maxmsgbytes );
void DrawLargePacketSizes( int x, int w, int graphtype, float warning_threshold );
HFont GetNetgraphFont()
{
return net_graphproportionalfont.GetBool() ? m_hFontProportional : m_hFont;
}
void ComputeNetgraphHeight();
void UpdateEstimatedServerFramerate( INetChannelInfo *netchannel );
CMaterialReference m_WhiteMaterial;
int m_EstimatedWidth;
int m_nNetGraphHeight;
float m_flServerFramerate;
float m_flServerFramerateStdDeviation;
};
CNetGraphPanel *g_pNetGraphPanel = NULL;
//-----------------------------------------------------------------------------
// Purpose:
// Input : *parent -
//-----------------------------------------------------------------------------
CNetGraphPanel::CNetGraphPanel( VPANEL parent )
: BaseClass( NULL, "CNetGraphPanel" )
{
int w, h;
surface()->GetScreenSize( w, h );
SetParent( parent );
SetSize( w, h );
SetPos( 0, 0 );
SetVisible( false );
SetCursor( null );
m_hFont = 0;
m_hFontProportional = 0;
m_hFontSmall = 0;
m_EstimatedWidth = 1;
m_nNetGraphHeight = 100;
SetFgColor( Color( 0, 0, 0, 255 ) );
SetPaintBackgroundEnabled( false );
InitColors();
cl_updaterate = cvar->FindVar( "cl_updaterate" );
cl_cmdrate = cvar->FindVar( "cl_cmdrate" );
assert( cl_updaterate && cl_cmdrate );
memset( sendcolor, 0, 3 );
memset( holdcolor, 0, 3 );
sendcolor[ 0 ] = sendcolor[ 1 ] = 255;
memset( extrap_base_color, 255, 3 );
memset( m_PacketLatency, 0, TIMINGS * sizeof( packet_latency_t ) );
memset( m_Cmdinfo, 0, TIMINGS * sizeof( cmdinfo_t ) );
memset( m_Graph, 0, TIMINGS * sizeof( netbandwidthgraph_t ) );
m_Framerate = 0.0f;
m_AvgLatency = 0.0f;
m_AvgPacketLoss = 0.0f;
m_AvgPacketChoke = 0.0f;
m_IncomingSequence = 0;
m_OutgoingSequence = 0;
m_UpdateWindowSize = 0;
m_IncomingData = 0;
m_OutgoingData = 0;
m_AvgPacketIn = 0.0f;
m_AvgPacketOut = 0.0f;
m_flServerFramerate = 0;
m_flServerFramerateStdDeviation = 0;
netcolors[COLOR_DROPPED].color[0] = 255;
netcolors[COLOR_DROPPED].color[1] = 0;
netcolors[COLOR_DROPPED].color[2] = 0;
netcolors[COLOR_DROPPED].alpha = 255;
netcolors[COLOR_INVALID].color[0] = 0;
netcolors[COLOR_INVALID].color[1] = 0;
netcolors[COLOR_INVALID].color[2] = 255;
netcolors[COLOR_INVALID].alpha = 255;
netcolors[COLOR_SKIPPED].color[0] = 240;
netcolors[COLOR_SKIPPED].color[1] = 127;
netcolors[COLOR_SKIPPED].color[2] = 63;
netcolors[COLOR_SKIPPED].alpha = 255;
netcolors[COLOR_CHOKED].color[0] = 225;
netcolors[COLOR_CHOKED].color[1] = 225;
netcolors[COLOR_CHOKED].color[2] = 0;
netcolors[COLOR_CHOKED].alpha = 255;
netcolors[COLOR_NORMAL].color[0] = 63;
netcolors[COLOR_NORMAL].color[1] = 255;
netcolors[COLOR_NORMAL].color[2] = 63;
netcolors[COLOR_NORMAL].alpha = 232;
ivgui()->AddTickSignal( GetVPanel(), 500 );
m_WhiteMaterial.Init( "vgui/white", TEXTURE_GROUP_OTHER );
g_pNetGraphPanel = this;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CNetGraphPanel::~CNetGraphPanel( void )
{
g_pNetGraphPanel = NULL;
}
void NetgraphFontChangeCallback( IConVar *var, const char *pOldValue, float flOldValue )
{
if ( g_pNetGraphPanel )
{
g_pNetGraphPanel->OnFontChanged();
}
}
void CNetGraphPanel::OnFontChanged()
{
// Estimate the width of our panel.
char str[512];
wchar_t ustr[512];
Q_snprintf( str, sizeof( str ), "fps: 435 ping: 533 ms lerp 112.3 ms 0/0" );
g_pVGuiLocalize->ConvertANSIToUnicode( str, ustr, sizeof( ustr ) );
int textTall;
if ( m_hFontProportional == vgui::INVALID_FONT )
{
m_EstimatedWidth = textTall = 0;
}
else
{
g_pMatSystemSurface->GetTextSize( m_hFontProportional, ustr, m_EstimatedWidth, textTall );
}
int w, h;
surface()->GetScreenSize( w, h );
SetSize( w, h );
SetPos( 0, 0 );
ComputeNetgraphHeight();
}
void CNetGraphPanel::ApplySchemeSettings(IScheme *pScheme)
{
BaseClass::ApplySchemeSettings(pScheme);
m_hFont = pScheme->GetFont( "DefaultFixedOutline", false );
m_hFontProportional = pScheme->GetFont( "DefaultFixedOutline", true );
m_hFontSmall = pScheme->GetFont( "DefaultVerySmall", false );
OnFontChanged();
}
void CNetGraphPanel::ComputeNetgraphHeight()
{
m_nNetGraphHeight = net_graphheight.GetInt();
HFont fnt = GetNetgraphFont();
int tall = surface()->GetFontTall( fnt );
int lines = 3;
if ( net_graph.GetInt() > 3 )
{
lines = 5;
}
else if ( net_graph.GetInt() > 2 )
{
lines = 4;
}
m_nNetGraphHeight = MAX( lines * tall, m_nNetGraphHeight );
}
//-----------------------------------------------------------------------------
// Purpose: Copies data from netcolor_t array into fields passed in
// Input : color -
// *cv -
// *alpha -
//-----------------------------------------------------------------------------
void CNetGraphPanel::GetColorValues( int color, byte *cv, byte *alpha )
{
int i;
netcolor_t *pc = &netcolors[ color ];
for ( i = 0; i < 3; i++ )
{
cv[ i ] = pc->color[ i ];
}
*alpha = pc->alpha;
}
//-----------------------------------------------------------------------------
// Purpose: Sets appropriate color values
// Input : *packet -
// *color -
// *ping -
// *alpha -
//-----------------------------------------------------------------------------
void CNetGraphPanel::ColorForHeight( packet_latency_t *packet, byte *color, int *ping, byte *alpha )
{
int h = packet->latency;
*ping = 0;
switch ( h )
{
case 9999:
GetColorValues( COLOR_DROPPED, color, alpha );
break;
case 9998:
GetColorValues( COLOR_INVALID, color, alpha );
break;
case 9997:
GetColorValues( COLOR_SKIPPED, color, alpha );
break;
default:
*ping = 1;
if (packet->choked )
{
GetColorValues( COLOR_CHOKED, color, alpha );
}
else
{
GetColorValues( COLOR_NORMAL, color, alpha );
}
break;
}
}
//-----------------------------------------------------------------------------
// Purpose: Set up blend colors for comman/client-frame/interpolation graph
//-----------------------------------------------------------------------------
void CNetGraphPanel::InitColors( void )
{
int i, j;
byte mincolor[2][3];
byte maxcolor[2][3];
float dc[2][3];
int hfrac;
float f;
mincolor[0][0] = 63;
mincolor[0][1] = 0;
mincolor[0][2] = 100;
maxcolor[0][0] = 0;
maxcolor[0][1] = 63;
maxcolor[0][2] = 255;
mincolor[1][0] = 255;
mincolor[1][1] = 127;
mincolor[1][2] = 0;
maxcolor[1][0] = 250;
maxcolor[1][1] = 0;
maxcolor[1][2] = 0;
for ( i = 0; i < 3; i++ )
{
dc[0][i] = (float)(maxcolor[0][i] - mincolor[0][i]);
dc[1][i] = (float)(maxcolor[1][i] - mincolor[1][i]);
}
hfrac = LERP_HEIGHT / 3;
for ( i = 0; i < LERP_HEIGHT; i++ )
{
if ( i < hfrac )
{
f = (float)i / (float)hfrac;
for ( j = 0; j < 3; j++ )
{
colors[ i ][j] = mincolor[0][j] + f * dc[0][j];
}
}
else
{
f = (float)(i-hfrac) / (float)(LERP_HEIGHT - hfrac );
for ( j = 0; j < 3; j++ )
{
colors[ i ][j] = mincolor[1][j] + f * dc[1][j];
}
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Draw client framerate / usercommand graph
// Input : vrect -
// *cmdinfo -
// x -
// w -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawTimes( vrect_t vrect, cmdinfo_t *cmdinfo, int x, int w, int graphtype )
{
if ( !net_graphshowinterp.GetBool() || graphtype <= 1 )
return;
int i;
int j;
int extrap_point;
int a, h;
vrect_t rcFill;
ResetLineSegments();
extrap_point = LERP_HEIGHT / 3;
for (a=0 ; a<w ; a++)
{
i = ( m_OutgoingSequence - a ) & ( TIMINGS - 1 );
h = MIN( ( cmdinfo[i].cmd_lerp / 3.0 ) * LERP_HEIGHT, LERP_HEIGHT );
rcFill.x = x + w -a - 1;
rcFill.width = 1;
rcFill.height = 1;
rcFill.y = vrect.y + vrect.height - 4;
if ( h >= extrap_point )
{
int start = 0;
h -= extrap_point;
rcFill.y -= extrap_point;
if ( !net_graphsolid.GetInt() )
{
rcFill.y -= (h - 1);
start = (h - 1);
}
for ( j = start; j < h; j++ )
{
2022-03-02 04:00:42 +08:00
DrawLine(&rcFill, colors[j + extrap_point], 255 );
2020-04-23 00:56:21 +08:00
rcFill.y--;
}
}
else
{
int oldh;
oldh = h;
rcFill.y -= h;
h = extrap_point - h;
if ( !net_graphsolid.GetInt() )
{
h = 1;
}
for ( j = 0; j < h; j++ )
{
2022-03-02 04:00:42 +08:00
DrawLine(&rcFill, colors[j + oldh], 255 );
2020-04-23 00:56:21 +08:00
rcFill.y--;
}
}
rcFill.y = vrect.y + vrect.height - 4 - extrap_point;
DrawLine( &rcFill, extrap_base_color, 255 );
rcFill.y = vrect.y + vrect.height - 3;
if ( cmdinfo[ i ].sent )
{
DrawLine( &rcFill, sendcolor, 255 );
}
else
{
DrawLine( &rcFill, holdcolor, 200 );
}
}
DrawLineSegments();
}
//-----------------------------------------------------------------------------
// Purpose: Compute frame database for rendering m_NetChannel computes choked, and lost packets, too.
// Also computes latency data and sets max packet size
// Input : *packet_latency -
// *graph -
// *choke_count -
// *loss_count -
// *biggest_message -
// 1 -
//-----------------------------------------------------------------------------
void CNetGraphPanel::GetFrameData( INetChannelInfo *netchannel, int *biggest_message, float *avg_message, float *f95thpercentile )
{
float frame_received_time;
// float frame_latency;
*biggest_message = 0;
*avg_message = 0.0f;
*f95thpercentile = 0.0f;
int msg_count = 0;
m_IncomingSequence = netchannel->GetSequenceNr( FLOW_INCOMING );
m_OutgoingSequence = netchannel->GetSequenceNr( FLOW_OUTGOING );
m_UpdateWindowSize = netchannel->GetBufferSize();
m_AvgPacketLoss = netchannel->GetAvgLoss( FLOW_INCOMING );
m_AvgPacketChoke = netchannel->GetAvgChoke( FLOW_INCOMING );
m_AvgLatency = netchannel->GetAvgLatency( FLOW_OUTGOING );
m_IncomingData = netchannel->GetAvgData( FLOW_INCOMING ) / 1024.0f;
m_OutgoingData = netchannel->GetAvgData( FLOW_OUTGOING ) / 1024.0f;
m_AvgPacketIn = netchannel->GetAvgPackets( FLOW_INCOMING );
m_AvgPacketOut = netchannel->GetAvgPackets( FLOW_OUTGOING );
for ( int i=0; i<MAX_FLOWS; i++ )
netchannel->GetStreamProgress( i, &m_StreamRecv[i], &m_StreamTotal[i] );
float flAdjust = 0.0f;
if ( cl_updaterate->GetFloat() > 0.001f )
{
flAdjust = -0.5f / cl_updaterate->GetFloat();
m_AvgLatency += flAdjust;
}
// Can't be below zero
m_AvgLatency = MAX( 0.0, m_AvgLatency );
flAdjust *= 1000.0f;
// Fill in frame data
for ( int seqnr =m_IncomingSequence - m_UpdateWindowSize + 1
; seqnr <= m_IncomingSequence
; seqnr++)
{
frame_received_time = netchannel->GetPacketTime( FLOW_INCOMING, seqnr );
netbandwidthgraph_t *nbwg = &m_Graph[ seqnr & ( TIMINGS - 1 )];
packet_latency_t *lat = &m_PacketLatency[ seqnr & ( TIMINGS - 1 ) ];
netchannel->GetPacketResponseLatency( FLOW_INCOMING, seqnr, &lat->latency, &lat->choked );
if ( lat->latency < 9995 )
{
lat->latency += flAdjust;
lat->latency = MAX( lat->latency, 0 );
}
for ( int i=0; i<=INetChannelInfo::TOTAL; i++ )
{
nbwg->msgbytes[i] = netchannel->GetPacketBytes( FLOW_INCOMING, seqnr, i );
}
// Assert ( nbwg->msgbytes[INetChannelInfo::TOTAL] > 0 );
if ( nbwg->msgbytes[INetChannelInfo::TOTAL] > *biggest_message )
{
*biggest_message = nbwg->msgbytes[INetChannelInfo::TOTAL];
}
*avg_message += (float)( nbwg->msgbytes[INetChannelInfo::TOTAL] );
msg_count++;
}
if ( *biggest_message > 1000 )
{
*biggest_message = 1000;
}
if ( msg_count >= 1 )
{
*avg_message /= msg_count;
int deviationsquared = 0;
// Compute std deviation
// Fill in frame data
for (int seqnr=m_IncomingSequence - m_UpdateWindowSize + 1
; seqnr <= m_IncomingSequence
; seqnr++)
{
int bytes = m_Graph[ seqnr & ( TIMINGS - 1 )].msgbytes[INetChannelInfo::TOTAL] - ( *avg_message );
deviationsquared += ( bytes * bytes );
}
float var = ( float )( deviationsquared ) / (float)( msg_count - 1 );
float stddev = sqrt( var );
*f95thpercentile = *avg_message + 2.0f * stddev;
}
}
//-----------------------------------------------------------------------------
// Purpose: Fills in command interpolation/holdback & message size data
// Input : *cmdinfo -
//-----------------------------------------------------------------------------
void CNetGraphPanel::GetCommandInfo( INetChannelInfo *netchannel, cmdinfo_t *cmdinfo )
{
for ( int seqnr = m_OutgoingSequence - m_UpdateWindowSize + 1
; seqnr <= m_OutgoingSequence
; seqnr++)
{
// Also set up the lerp point.
cmdinfo_t *ci = &cmdinfo[ seqnr & ( TIMINGS - 1 ) ];
ci->cmd_lerp = netchannel->GetCommandInterpolationAmount( FLOW_OUTGOING, seqnr );
ci->sent = netchannel->IsValidPacket( FLOW_OUTGOING, seqnr );
ci->size = netchannel->GetPacketBytes( FLOW_OUTGOING, seqnr, INetChannelInfo::TOTAL);
}
}
//-----------------------------------------------------------------------------
// Purpose: Draws overlay text fields showing framerate, latency, bandwidth breakdowns,
// and, optionally, packet loss and choked packet percentages
// Input : graphvalue -
// x -
// y -
// *graph -
// *cmdinfo -
// count -
// avg -
// *framerate -
// 0.0 -
// avg -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawTextFields( int graphvalue, int x, int y, int w, netbandwidthgraph_t *graph, cmdinfo_t *cmdinfo )
{
if ( !net_graphtext.GetBool() )
return;
static int lastout;
char sz[ 256 ];
int out;
HFont font = GetNetgraphFont();
// Move rolling average
m_Framerate = FRAMERATE_AVG_FRAC * m_Framerate + ( 1.0 - FRAMERATE_AVG_FRAC ) * gpGlobals->absoluteframetime;
// Print it out
y -= m_nNetGraphHeight;
int saveY = y;
if ( m_Framerate <= 0.0f )
m_Framerate = 1.0f;
if ( engine->IsPlayingDemo() )
m_AvgLatency = 0.0f;
int textTall = surface()->GetFontTall( font );
Q_snprintf( sz, sizeof( sz ), "fps:%4i ping: %i ms", (int)(1.0f / m_Framerate), (int)(m_AvgLatency*1000.0f) );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
// Draw update rate
DrawUpdateRate( x + w, y );
y += textTall;
out = cmdinfo[ ( ( m_OutgoingSequence - 1 ) & ( TIMINGS - 1 ) ) ].size;
if ( !out )
{
out = lastout;
}
else
{
lastout = out;
}
int totalsize = graph[ ( m_IncomingSequence & ( TIMINGS - 1 ) ) ].msgbytes[INetChannelInfo::TOTAL];
2022-11-04 09:03:52 +08:00
2020-04-23 00:56:21 +08:00
Q_snprintf( sz, sizeof( sz ), "in :%4i %2.2f k/s ", totalsize, m_IncomingData );
int textWidth = g_pMatSystemSurface->DrawTextLen( font, "%s", sz );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
Q_snprintf( sz, sizeof( sz ), "lerp: %5.1f ms", GetClientInterpAmount() * 1000.0f );
int interpcolor[ 3 ] = { (int)GRAPH_RED, (int)GRAPH_GREEN, (int)GRAPH_BLUE };
float flInterp = GetClientInterpAmount();
if ( flInterp > 0.001f )
{
// Server framerate is lower than interp can possibly deal with
if ( m_flServerFramerate < ( 1.0f / flInterp ) )
{
interpcolor[ 0 ] = 255;
interpcolor[ 1 ] = 255;
interpcolor[ 2 ] = 31;
}
// flInterp is below recommended setting!!!
else if ( flInterp < ( 2.0f / cl_updaterate->GetFloat() ) )
{
interpcolor[ 0 ] = 255;
interpcolor[ 1 ] = 125;
interpcolor[ 2 ] = 31;
}
}
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x + textWidth, y, interpcolor[ 0 ], interpcolor[ 1 ], interpcolor[ 2 ], 255, "%s", sz );
2020-04-23 00:56:21 +08:00
Q_snprintf( sz, sizeof( sz ), "%3.1f/s", m_AvgPacketIn );
textWidth = g_pMatSystemSurface->DrawTextLen( font, "%s", sz );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x + w - textWidth - 1, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
y += textTall;
Q_snprintf( sz, sizeof( sz ), "out:%4i %2.2f k/s", out, m_OutgoingData );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
Q_snprintf( sz, sizeof( sz ), "%3.1f/s", m_AvgPacketOut );
textWidth = g_pMatSystemSurface->DrawTextLen( font, "%s", sz );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x + w - textWidth - 1, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
y += textTall;
DrawCmdRate( x + w, y );
if ( graphvalue > 2 )
{
Q_snprintf( sz, sizeof( sz ), "loss:%3i choke: %2i ", (int)(m_AvgPacketLoss*100.0f), (int)(m_AvgPacketChoke*100.0f) );
textWidth = g_pMatSystemSurface->DrawTextLen( font, "%s", sz );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
y += textTall;
if ( graphvalue > 3 )
{
Q_snprintf( sz, sizeof( sz ), "sv : %5.1f var: %4.2f msec", m_flServerFramerate, m_flServerFramerateStdDeviation * 1000.0f );
int servercolor[ 3 ] = { (int)GRAPH_RED, (int)GRAPH_GREEN, (int)GRAPH_BLUE };
if ( m_flServerFramerate < 10.0f )
{
servercolor[ 0 ] = 255;
servercolor[ 1 ] = 31;
servercolor[ 2 ] = 31;
}
else if ( m_flServerFramerate < 20.0f )
{
servercolor[ 0 ] = 255;
servercolor[ 1 ] = 255;
servercolor[ 2 ] = 0;
}
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( font, x, y, servercolor[ 0 ], servercolor[ 1 ], servercolor[ 2 ], 255, "%s", sz );
2020-04-23 00:56:21 +08:00
y += textTall;
}
}
// Draw legend
if ( graphvalue >= 3 )
{
2022-03-02 04:00:42 +08:00
int textTall = g_pMatSystemSurface->GetFontTall( m_hFontSmall );
2020-04-23 00:56:21 +08:00
y = saveY - textTall - 5;
int cw, ch;
g_pMatSystemSurface->GetTextSize( m_hFontSmall, L"otherplayersWWW", cw, ch );
if ( x - cw < 0 )
{
x += w + 5;
}
else
{
x -= cw;
}
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 0, 255, 255, "localplayer" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 255, 0, 255, "otherplayers" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 255, 0, 0, 255, "entities" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 255, 255, 0, 255, "sounds" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 255, 255, 255, "events" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 128, 128, 0, 255, "usermessages" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 128, 128, 255, "entmessages" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 128, 0, 0, 255, "stringcmds" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 128, 0, 255, "stringtables" );
y -= textTall;
g_pMatSystemSurface->DrawColoredText( m_hFontSmall, x, y, 0, 0, 128, 255, "voice" );
y -= textTall;
}
}
//-----------------------------------------------------------------------------
// Purpose: Determine type of graph to show, or if +graph key is held down, use detailed graph
// Output : int
//-----------------------------------------------------------------------------
int CNetGraphPanel::GraphValue( void )
{
int graphtype;
graphtype = net_graph.GetInt();
if ( !graphtype && !( in_graph.state & 1 ) )
return 0;
// With +graph key, use max area
if ( !graphtype )
{
graphtype = 2;
}
return graphtype;
}
//-----------------------------------------------------------------------------
// Purpose: Figure out x and y position for graph based on net_graphpos
// value.
// Input : *rect -
// width -
// *x -
// *y -
//-----------------------------------------------------------------------------
void CNetGraphPanel::GraphGetXY( vrect_t *rect, int width, int *x, int *y )
{
*x = rect->x + 5;
switch ( net_graphpos.GetInt() )
{
case 0:
break;
case 1:
*x = rect->x + rect->width - 5 - width;
break;
case 2:
*x = rect->x + ( rect->width - 10 - width ) / 2;
break;
default:
*x = rect->x + clamp( (int) XRES( net_graphpos.GetInt() ), 5, rect->width - width - 5 );
}
*y = rect->y+rect->height - LERP_HEIGHT - 5;
}
//-----------------------------------------------------------------------------
// Purpose: drawing stream progess (file download etc) as green bars ( under in/out)
// Input : x -
// y -
// maxmsgbytes -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawStreamProgress( int x, int y, int width )
{
vrect_t rcLine;
rcLine.height = 1;
rcLine.x = x;
byte color[3]; color[0] = 0; color[1] = 200; color[2] = 0;
if ( m_StreamTotal[FLOW_INCOMING] > 0 )
{
rcLine.y = y - m_nNetGraphHeight + 15 + 14;
rcLine.width = (m_StreamRecv[FLOW_INCOMING]*width)/m_StreamTotal[FLOW_INCOMING];
DrawLine( &rcLine, color, 255 );
}
if ( m_StreamTotal[FLOW_OUTGOING] > 0 )
{
rcLine.y = y - m_nNetGraphHeight + 2*15 + 14;
rcLine.width = (m_StreamRecv[FLOW_OUTGOING]*width)/m_StreamTotal[FLOW_OUTGOING];
DrawLine( &rcLine, color, 255 );
}
}
//-----------------------------------------------------------------------------
// Purpose: If showing bandwidth data, draw hatches big enough for largest message
// Input : x -
// y -
// maxmsgbytes -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawHatches( int x, int y, int maxmsgbytes )
{
int starty;
int ystep;
vrect_t rcHatch;
byte colorminor[3];
byte color[3];
ystep = (int)( 10.0 / net_scale.GetFloat() );
ystep = MAX( ystep, 1 );
rcHatch.y = y;
rcHatch.height = 1;
rcHatch.x = x;
rcHatch.width = 4;
color[0] = 0;
color[1] = 200;
color[2] = 0;
colorminor[0] = 63;
colorminor[1] = 63;
colorminor[2] = 0;
for ( starty = rcHatch.y; rcHatch.y > 0 && ((starty - rcHatch.y)*net_scale.GetFloat() < ( maxmsgbytes + 50 ) ); rcHatch.y -= ystep )
{
if ( !((int)((starty - rcHatch.y)*net_scale.GetFloat() ) % 50 ) )
{
DrawLine( &rcHatch, color, 255 );
}
else if ( ystep > 5 )
{
DrawLine( &rcHatch, colorminor, 200 );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: State how many updates a second are being requested
// Input : x -
// y -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawUpdateRate( int xright, int y )
{
char sz[ 32 ];
Q_snprintf( sz, sizeof( sz ), "%i/s", cl_updaterate->GetInt() );
wchar_t unicode[ 32 ];
g_pVGuiLocalize->ConvertANSIToUnicode( sz, unicode, sizeof( unicode ) );
// Last one
int textWide, textTall;
g_pMatSystemSurface->GetTextSize( GetNetgraphFont(), unicode, textWide, textTall );
g_pMatSystemSurface->DrawColoredText( GetNetgraphFont(), xright - textWide - 1, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
}
//-----------------------------------------------------------------------------
// Purpose: State how many updates a second are being requested
// Input : x -
// y -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawCmdRate( int xright, int y )
{
char sz[ 32 ];
Q_snprintf( sz, sizeof( sz ), "%i/s", cl_cmdrate->GetInt() );
wchar_t unicode[ 32 ];
g_pVGuiLocalize->ConvertANSIToUnicode( sz, unicode, sizeof( unicode ) );
// Last one
int textWide, textTall;
g_pMatSystemSurface->GetTextSize( GetNetgraphFont(), unicode, textWide, textTall );
g_pMatSystemSurface->DrawColoredText( GetNetgraphFont(), xright - textWide - 1, y, GRAPH_RED, GRAPH_GREEN, GRAPH_BLUE, 255, "%s", sz );
}
//-----------------------------------------------------------------------------
// Purpose: Draws bandwidth breakdown data
// Input : *rcFill -
// bytes -
// r -
// g -
// b -
// alpha -
// Output : int
//-----------------------------------------------------------------------------
int CNetGraphPanel::DrawDataSegment( vrect_t *rcFill, int bytes, byte r, byte g, byte b, byte alpha )
{
int h;
byte color[3];
h = bytes / net_scale.GetFloat();
color[0] = r;
color[1] = g;
color[2] = b;
rcFill->height = h;
rcFill->y -= h;
if ( rcFill->y < 2 )
return 0;
DrawLine( rcFill, color, alpha );
return 1;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNetGraphPanel::OnTick( void )
{
bool bVisible = ShouldDraw();
if ( IsVisible() != bVisible )
{
SetVisible( bVisible );
}
}
bool CNetGraphPanel::ShouldDraw( void )
{
if ( GraphValue() != 0 )
return true;
return false;
}
void CNetGraphPanel::DrawLargePacketSizes( int x, int w, int graphtype, float warning_threshold )
{
vrect_t rcFill = {0,0,0,0};
int a, i;
for (a=0 ; a<w ; a++)
{
i = (m_IncomingSequence-a) & ( TIMINGS - 1 );
rcFill.x = x + w -a -1;
rcFill.width = 1;
rcFill.y = m_Graph[i].sampleY;
rcFill.height = m_Graph[i].sampleHeight;
int nTotalBytes = m_Graph[ i ].msgbytes[ INetChannelInfo::TOTAL ];
if ( warning_threshold != 0.0f &&
nTotalBytes > MAX( 300, warning_threshold ) )
{
char sz[ 32 ];
Q_snprintf( sz, sizeof( sz ), "%i", nTotalBytes );
2022-11-04 09:03:52 +08:00
int len = g_pMatSystemSurface->DrawTextLen( m_hFont, "%s", sz );
2020-04-23 00:56:21 +08:00
int textx, texty;
textx = rcFill.x - len / 2;
texty = MAX( 0, rcFill.y - 11 );
2022-11-04 09:03:52 +08:00
g_pMatSystemSurface->DrawColoredText( m_hFont, textx, texty, 255, 255, 255, 255, "%s", sz );
2020-04-23 00:56:21 +08:00
}
}
}
//-----------------------------------------------------------------------------
// Purpose: A basic version (doesn't taken into account the "holding after
// screenshot" bit like TF does, but is good enough for hud_freezecamhide.
//-----------------------------------------------------------------------------
static bool IsTakingAFreezecamScreenshot()
{
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
bool bInFreezeCam = ( pPlayer && pPlayer->GetObserverMode() == OBS_MODE_FREEZECAM );
return ( bInFreezeCam && engine->IsTakingScreenshot() );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNetGraphPanel::Paint()
{
VPROF( "CNetGraphPanel::Paint" );
// Don't display net_graph if taking freezecam screenshot and hud_freezecamhide is enabled
extern ConVar hud_freezecamhide;
if ( hud_freezecamhide.GetBool() && IsTakingAFreezecamScreenshot() )
return;
int graphtype;
int x, y;
int w;
vrect_t vrect;
int maxmsgbytes = 0;
float avg_message = 0.0f;
float warning_threshold = 0.0f;
if ( ( graphtype = GraphValue() ) == 0 )
return;
// Since we divide by scale, make sure it's sensible
if ( net_scale.GetFloat() <= 0 )
{
net_scale.SetValue( 0.1f );
}
int sw, sh;
surface()->GetScreenSize( sw, sh );
// Get screen rectangle
vrect.x = 0;
vrect.y = 0;
vrect.width = sw;
vrect.height = sh;
w = MIN( (int)TIMINGS, m_EstimatedWidth );
if ( vrect.width < w + 10 )
{
w = vrect.width - 10;
}
// get current client netchannel INetChannelInfo interface
INetChannelInfo *nci = engine->GetNetChannelInfo();
if ( nci )
{
// update incoming data
GetFrameData( nci, &maxmsgbytes, &avg_message, &warning_threshold );
// update outgoing data
GetCommandInfo( nci, m_Cmdinfo );
UpdateEstimatedServerFramerate( nci );
}
GraphGetXY( &vrect, w, &x, &y );
if ( graphtype > 1 )
{
PaintLineArt( x, y, w, graphtype, maxmsgbytes );
DrawLargePacketSizes( x, w, graphtype, warning_threshold );
}
// Draw client frame timing info
DrawTimes( vrect, m_Cmdinfo, x, w, graphtype );
DrawTextFields( graphtype, x, y, w, m_Graph, m_Cmdinfo );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNetGraphPanel::PaintLineArt( int x, int y, int w, int graphtype, int maxmsgbytes )
{
VPROF( "CNetGraphPanel::PaintLineArt" );
ResetLineSegments();
int lastvalidh = 0;
byte color[3];
int ping;
byte alpha;
vrect_t rcFill = {0,0,0,0};
int pingheight = m_nNetGraphHeight - LERP_HEIGHT - 2;
if (net_graphmsecs.GetInt() < 50 )
{
net_graphmsecs.SetValue( 50 );
}
bool bShowLatency = net_graphshowlatency.GetBool() && graphtype >= 2;
for (int a=0 ; a<w ; a++)
{
int i = (m_IncomingSequence-a) & ( TIMINGS - 1 );
int h = bShowLatency ? m_PacketLatency[i].latency : 0;
packet_latency_t *pl = &m_PacketLatency[ i ];
ColorForHeight( pl, color, &ping, &alpha );
// Skipped
if ( !ping )
{
// Re-use the last latency
h = lastvalidh;
}
else
{
h = pingheight * (float)h/net_graphmsecs.GetFloat();
lastvalidh = h;
}
if ( h > pingheight )
{
h = pingheight;
}
rcFill.x = x + w -a -1;
rcFill.y = y - h;
rcFill.width = 1;
rcFill.height = h;
if ( ping )
{
rcFill.height = pl->choked ? 2 : 1;
}
if ( !ping )
{
DrawLine2(&rcFill, color, color, alpha, 31 );
}
else
{
DrawLine(&rcFill, color, alpha );
}
rcFill.y = y;
rcFill.height = 1;
color[0] = 0;
color[1] = 255;
color[2] = 0;
DrawLine( &rcFill, color, 160 );
if ( graphtype < 2 )
continue;
// Draw a separator.
rcFill.y = y - m_nNetGraphHeight - 1;
rcFill.height = 1;
color[0] = 255;
color[1] = 255;
color[2] = 255;
DrawLine(&rcFill, color, 255 );
// Move up for begining of data
rcFill.y -= 1;
// Packet didn't have any real data...
if ( m_PacketLatency[i].latency > 9995 )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::LOCALPLAYER], 0, 0, 255 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::OTHERPLAYERS], 0, 255, 0 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::ENTITIES], 255, 0, 0 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::SOUNDS], 255, 255, 0) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::EVENTS], 0, 255, 255 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::USERMESSAGES], 128, 128, 0 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::ENTMESSAGES], 0, 128, 128 ) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::STRINGCMD], 128, 0, 0) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::STRINGTABLE], 0, 128, 0) )
continue;
if ( !DrawDataSegment( &rcFill, m_Graph[ i ].msgbytes[INetChannelInfo::VOICE], 0, 0, 128 ) )
continue;
// Final data chunk is total size, don't use solid line routine for this
h = m_Graph[i].msgbytes[INetChannelInfo::TOTAL] / net_scale.GetFloat();
color[ 0 ] = color[ 1 ] = color[ 2 ] = 240;
rcFill.height = 1;
rcFill.y = y - m_nNetGraphHeight - 1 - h;
if ( rcFill.y < 2 )
continue;
DrawLine(&rcFill, color, 128 );
// Cache off height
m_Graph[i].sampleY = rcFill.y;
m_Graph[i].sampleHeight = rcFill.height;
}
if ( graphtype >= 2 )
{
// Draw hatches for first one:
// on the far right side
DrawHatches( x, y - m_nNetGraphHeight - 1, maxmsgbytes );
DrawStreamProgress( x, y, w );
}
DrawLineSegments();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNetGraphPanel::ResetLineSegments()
{
m_Rects.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawLineSegments()
{
int c = m_Rects.Count();
if ( c <= 0 )
return;
CMatRenderContextPtr pRenderContext( materials );
IMesh* m_pMesh = pRenderContext->GetDynamicMesh( true, NULL, NULL, m_WhiteMaterial );
CMeshBuilder meshBuilder;
meshBuilder.Begin( m_pMesh, MATERIAL_LINES, c );
int i;
for ( i = 0 ; i < c; i++ )
{
CLineSegment *seg = &m_Rects[ i ];
meshBuilder.Color4ubv( seg->color );
meshBuilder.TexCoord2f( 0, 0.0f, 0.0f );
meshBuilder.Position3f( seg->x1, seg->y1, 0 );
meshBuilder.AdvanceVertex();
meshBuilder.Color4ubv( seg->color2 );
meshBuilder.TexCoord2f( 0, 0.0f, 0.0f );
meshBuilder.Position3f( seg->x2, seg->y2, 0 );
meshBuilder.AdvanceVertex();
}
meshBuilder.End();
m_pMesh->Draw();
}
//-----------------------------------------------------------------------------
// Purpose: Draws a colored, filled rectangle
// Input : *rect -
// *color -
// alpha -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawLine( vrect_t *rect, unsigned char *color, unsigned char alpha )
{
DrawLine2( rect, color, color, alpha, alpha );
}
//-----------------------------------------------------------------------------
// Purpose: Draws a colored, filled rectangle
// Input : *rect -
// *color -
// alpha -
//-----------------------------------------------------------------------------
void CNetGraphPanel::DrawLine2( vrect_t *rect, unsigned char *color, unsigned char *color2, unsigned char alpha, unsigned char alpha2 )
{
VPROF( "CNetGraphPanel::DrawLine2" );
int idx = m_Rects.AddToTail();
CLineSegment *seg = &m_Rects[ idx ];
seg->color[0] = color[0];
seg->color[1] = color[1];
seg->color[2] = color[2];
seg->color[3] = alpha;
seg->color2[0] = color2[0];
seg->color2[1] = color2[1];
seg->color2[2] = color2[2];
seg->color2[3] = alpha2;
if ( rect->width == 1 )
{
seg->x1 = rect->x;
seg->y1 = rect->y;
seg->x2 = rect->x;
seg->y2 = rect->y + rect->height;
}
else if ( rect->height == 1 )
{
seg->x1 = rect->x;
seg->y1 = rect->y;
seg->x2 = rect->x + rect->width;
seg->y2 = rect->y;
}
else
{
Assert( 0 );
m_Rects.Remove( idx );
}
}
void CNetGraphPanel::UpdateEstimatedServerFramerate( INetChannelInfo *netchannel )
{
float flFrameTime;
netchannel->GetRemoteFramerate( &flFrameTime, &m_flServerFramerateStdDeviation );
if ( flFrameTime > FLT_EPSILON )
{
m_flServerFramerate = 1.0f / flFrameTime;
}
}
class CNetGraphPanelInterface : public INetGraphPanel
{
private:
CNetGraphPanel *netGraphPanel;
public:
CNetGraphPanelInterface( void )
{
netGraphPanel = NULL;
}
void Create( VPANEL parent )
{
netGraphPanel = new CNetGraphPanel( parent );
}
void Destroy( void )
{
if ( netGraphPanel )
{
netGraphPanel->SetParent( (Panel *)NULL );
2022-03-02 04:00:42 +08:00
delete netGraphPanel;
2020-04-23 00:56:21 +08:00
netGraphPanel = NULL;
}
}
};
static CNetGraphPanelInterface g_NetGraphPanel;
INetGraphPanel *netgraphpanel = ( INetGraphPanel * )&g_NetGraphPanel;