// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2020-2023 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file m_perfstats.c
/// \brief Performance measurement tools.

#include "m_perfstats.h"
#include "v_video.h"
#include "i_video.h"
#include "netcode/d_netcmd.h"
#include "r_main.h"
#include "i_system.h"
#include "z_zone.h"
#include "p_local.h"
#include "r_fps.h"

#ifdef HWRENDER
#include "hardware/hw_main.h"
#endif

struct perfstatrow;

typedef struct perfstatrow perfstatrow_t;

struct perfstatrow {
	const char  * lores_label;
	const char  * hires_label;
	ps_metric_t * metric;
	UINT8         flags;
};

// perfstatrow_t flags

#define PS_TIME      1  // metric measures time (uses precise_t instead of INT32)
#define PS_LEVEL     2  // metric is valid only when a level is active
#define PS_SW        4  // metric is valid only in software mode
#define PS_HW        8  // metric is valid only in opengl mode
#define PS_BATCHING  16 // metric is valid only when opengl batching is active
#define PS_HIDE_ZERO 32 // hide metric if its value is zero

static ps_metric_t ps_frametime = {0};

ps_metric_t ps_tictime = {0};

ps_metric_t ps_playerthink_time = {0};
ps_metric_t ps_thinkertime = {0};

ps_metric_t ps_thlist_times[NUM_THINKERLISTS];

static ps_metric_t ps_thinkercount = {0};
static ps_metric_t ps_polythcount = {0};
static ps_metric_t ps_mainthcount = {0};
static ps_metric_t ps_mobjcount = {0};
static ps_metric_t ps_regularcount = {0};
static ps_metric_t ps_scenerycount = {0};
static ps_metric_t ps_nothinkcount = {0};
static ps_metric_t ps_dynslopethcount = {0};
static ps_metric_t ps_precipcount = {0};
static ps_metric_t ps_removecount = {0};

ps_metric_t ps_checkposition_calls = {0};

ps_metric_t ps_lua_prethinkframe_time = {0};
ps_metric_t ps_lua_thinkframe_time = {0};
ps_metric_t ps_lua_postthinkframe_time = {0};

ps_metric_t ps_lua_mobjhooks = {0};

ps_metric_t ps_otherlogictime = {0};

// Columns for perfstats pages.

// Position on screen is determined separately in the drawing functions.

// New columns must also be added to the drawing and update functions.
// Drawing functions: PS_DrawRenderStats, PS_DrawGameLogicStats, etc.
// Update functions:
//  - PS_UpdateFrameStats for frame-dependent values
//  - PS_UpdateTickStats for tick-dependent values

// Rendering stats columns

perfstatrow_t rendertime_rows[] = {
	{"frmtime", "Frame time:    ", &ps_frametime, PS_TIME},
	{"drwtime", "3d rendering:  ", &ps_rendercalltime, PS_TIME|PS_LEVEL},

#ifdef HWRENDER
	{" skybox ", " Skybox render: ", &ps_hw_skyboxtime, PS_TIME|PS_LEVEL|PS_HW},
	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_HW},
	{" batsort", " Batch sort:    ", &ps_hw_batchsorttime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
	{" batdraw", " Batch render:  ", &ps_hw_batchdrawtime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
	{" sprsort", " Sprite sort:   ", &ps_hw_spritesorttime, PS_TIME|PS_LEVEL|PS_HW},
	{" sprdraw", " Sprite render: ", &ps_hw_spritedrawtime, PS_TIME|PS_LEVEL|PS_HW},
	{" nodesrt", " Drwnode sort:  ", &ps_hw_nodesorttime, PS_TIME|PS_LEVEL|PS_HW},
	{" nodedrw", " Drwnode render:", &ps_hw_nodedrawtime, PS_TIME|PS_LEVEL|PS_HW},
	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_HW},
#endif

	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_SW},
	{" sprclip", " R_ClipSprites: ", &ps_sw_spritecliptime, PS_TIME|PS_LEVEL|PS_SW},
	{" portals", " Portals+Skybox:", &ps_sw_portaltime, PS_TIME|PS_LEVEL|PS_SW},
	{" planes ", " R_DrawPlanes:  ", &ps_sw_planetime, PS_TIME|PS_LEVEL|PS_SW},
	{" masked ", " R_DrawMasked:  ", &ps_sw_maskedtime, PS_TIME|PS_LEVEL|PS_SW},
	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_SW},

	{"ui     ", "UI render:     ", &ps_uitime, PS_TIME},
	{"finupdt", "I_FinishUpdate:", &ps_swaptime, PS_TIME},
	{0}
};

perfstatrow_t gamelogicbrief_row[] = {
	{"logic  ", "Game logic:    ", &ps_tictime, PS_TIME},
	{0}
};

perfstatrow_t commoncounter_rows[] = {
	{"bspcall", "BSP calls:   ", &ps_numbspcalls, 0},
	{"sprites", "Sprites:     ", &ps_numsprites, 0},
	{"drwnode", "Drawnodes:   ", &ps_numdrawnodes, 0},
	{"plyobjs", "Polyobjects: ", &ps_numpolyobjects, 0},
	{0}
};

perfstatrow_t interpolation_rows[] = {
	{"intpfrc", "Interp frac: ", &ps_interp_frac, PS_TIME},
	{"intplag", "Interp lag:  ", &ps_interp_lag, PS_TIME},
	{0}
};

#ifdef HWRENDER
perfstatrow_t batchcount_rows[] = {
	{"polygon", "Polygons:  ", &ps_hw_numpolys, 0},
	{"vertex ", "Vertices:  ", &ps_hw_numverts, 0},
	{0}
};

perfstatrow_t batchcalls_rows[] = {
	{"drwcall", "Draw calls:", &ps_hw_numcalls, 0},
	{"shaders", "Shaders:   ", &ps_hw_numshaders, 0},
	{"texture", "Textures:  ", &ps_hw_numtextures, 0},
	{"polyflg", "Polyflags: ", &ps_hw_numpolyflags, 0},
	{"colors ", "Colors:    ", &ps_hw_numcolors, 0},
	{0}
};
#endif

// Game logic stats columns

perfstatrow_t gamelogic_rows[] = {
	{"logic  ", "Game logic:     ", &ps_tictime, PS_TIME},
	{" plrthnk", " P_PlayerThink:  ", &ps_playerthink_time, PS_TIME|PS_LEVEL},
	{" thnkers", " P_RunThinkers:  ", &ps_thinkertime, PS_TIME|PS_LEVEL},
	{"  plyobjs", "  Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ], PS_TIME|PS_LEVEL},
	{"  main   ", "  Main:           ", &ps_thlist_times[THINK_MAIN], PS_TIME|PS_LEVEL},
	{"  mobjs  ", "  Mobjs:          ", &ps_thlist_times[THINK_MOBJ], PS_TIME|PS_LEVEL},
	{"  dynslop", "  Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE], PS_TIME|PS_LEVEL},
	{"  precip ", "  Precipitation:  ", &ps_thlist_times[THINK_PRECIP], PS_TIME|PS_LEVEL},
	{" lprethinkf", " LUAh_PreThinkFrame:", &ps_lua_prethinkframe_time, PS_TIME|PS_LEVEL},
	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
	{" lpostthinkf", " LUAh_PostThinkFrame:", &ps_lua_postthinkframe_time, PS_TIME|PS_LEVEL},
	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
	{0}
};

perfstatrow_t thinkercount_rows[] = {
	{"thnkers", "Thinkers:       ", &ps_thinkercount, PS_LEVEL},
	{" plyobjs", " Polyobjects:    ", &ps_polythcount, PS_LEVEL},
	{" main   ", " Main:           ", &ps_mainthcount, PS_LEVEL},
	{" mobjs  ", " Mobjs:          ", &ps_mobjcount, PS_LEVEL},
	{"  regular", "  Regular:        ", &ps_regularcount, PS_LEVEL},
	{"  scenery", "  Scenery:        ", &ps_scenerycount, PS_LEVEL},
	{"  nothink", "  Nothink:        ", &ps_nothinkcount, PS_HIDE_ZERO|PS_LEVEL},
	{" dynslop", " Dynamic slopes: ", &ps_dynslopethcount, PS_LEVEL},
	{" precip ", " Precipitation:  ", &ps_precipcount, PS_LEVEL},
	{" remove ", " Pending removal:", &ps_removecount, PS_LEVEL},
	{0}
};

perfstatrow_t misc_calls_rows[] = {
	{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks, PS_LEVEL},
	{"chkpos", "P_CheckPosition:", &ps_checkposition_calls, PS_LEVEL},
	{0}
};

// Sample collection status for averaging.
// Maximum of these two is shown to user if nonzero to tell that
// the reported averages are not correct yet.
int ps_frame_samples_left = 0;
int ps_tick_samples_left = 0;
// History writing positions for frame and tick based metrics
int ps_frame_index = 0;
int ps_tick_index = 0;

// dynamically allocated resizeable array for thinkframe hook stats
ps_hookinfo_t *prethinkframe_hooks = NULL;
int prethinkframe_hooks_length = 0;
int prethinkframe_hooks_capacity = 16;

ps_hookinfo_t *thinkframe_hooks = NULL;
int thinkframe_hooks_length = 0;
int thinkframe_hooks_capacity = 16;

ps_hookinfo_t *postthinkframe_hooks = NULL;
int postthinkframe_hooks_length = 0;
int postthinkframe_hooks_capacity = 16;

void PS_SetPreThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
{
	if (!prethinkframe_hooks)
	{
		// array needs to be initialized
		prethinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * prethinkframe_hooks_capacity, PU_STATIC, NULL);
	}
	if (index >= prethinkframe_hooks_capacity)
	{
		// array needs more space, realloc with double size
		int new_capacity = prethinkframe_hooks_capacity * 2;
		prethinkframe_hooks = Z_Realloc(prethinkframe_hooks,
			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
		// initialize new memory with zeros so the pointers in the structs are null
		memset(&prethinkframe_hooks[prethinkframe_hooks_capacity], 0,
			sizeof(ps_hookinfo_t) * prethinkframe_hooks_capacity);
		prethinkframe_hooks_capacity = new_capacity;
	}
	prethinkframe_hooks[index].time_taken.value.p = time_taken;
	memcpy(prethinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
	// since the values are set sequentially from begin to end, the last call should leave
	// the correct value to this variable
	prethinkframe_hooks_length = index + 1;
}

void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
{
	if (!thinkframe_hooks)
	{
		// array needs to be initialized
		thinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
	}
	if (index >= thinkframe_hooks_capacity)
	{
		// array needs more space, realloc with double size
		int new_capacity = thinkframe_hooks_capacity * 2;
		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
		// initialize new memory with zeros so the pointers in the structs are null
		memset(&thinkframe_hooks[thinkframe_hooks_capacity], 0,
			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity);
		thinkframe_hooks_capacity = new_capacity;
	}
	thinkframe_hooks[index].time_taken.value.p = time_taken;
	memcpy(thinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
	// since the values are set sequentially from begin to end, the last call should leave
	// the correct value to this variable
	thinkframe_hooks_length = index + 1;
}

void PS_SetPostThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
{
	if (!postthinkframe_hooks)
	{
		// array needs to be initialized
		postthinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * postthinkframe_hooks_capacity, PU_STATIC, NULL);
	}
	if (index >= postthinkframe_hooks_capacity)
	{
		// array needs more space, realloc with double size
		int new_capacity = postthinkframe_hooks_capacity * 2;
		postthinkframe_hooks = Z_Realloc(postthinkframe_hooks,
			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
		// initialize new memory with zeros so the pointers in the structs are null
		memset(&postthinkframe_hooks[postthinkframe_hooks_capacity], 0,
			sizeof(ps_hookinfo_t) * postthinkframe_hooks_capacity);
		postthinkframe_hooks_capacity = new_capacity;
	}
	postthinkframe_hooks[index].time_taken.value.p = time_taken;
	memcpy(postthinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
	// since the values are set sequentially from begin to end, the last call should leave
	// the correct value to this variable
	postthinkframe_hooks_length = index + 1;
}

static boolean PS_HighResolution(void)
{
	return (vid.width >= 640 && vid.height >= 400);
}

static boolean PS_IsLevelActive(void)
{
	return gamestate == GS_LEVEL ||
			(gamestate == GS_TITLESCREEN && titlemapinaction);
}

// Is the row valid in the current context?
static boolean PS_IsRowValid(perfstatrow_t *row)
{
	return !((row->flags & PS_LEVEL && !PS_IsLevelActive())
		|| (row->flags & PS_SW && rendermode != render_soft)
		|| (row->flags & PS_HW && rendermode != render_opengl)
#ifdef HWRENDER
		|| (row->flags & PS_BATCHING && !cv_glbatching.value)
#endif
		);
}

// Should the row be visible on the screen?
static boolean PS_IsRowVisible(perfstatrow_t *row)
{
	boolean value_is_zero;

	if (row->flags & PS_TIME)
		value_is_zero = row->metric->value.p == 0;
	else
		value_is_zero = row->metric->value.i == 0;

	return !(!PS_IsRowValid(row) ||
		(row->flags & PS_HIDE_ZERO && value_is_zero));
}

static INT32 PS_GetMetricAverage(ps_metric_t *metric, boolean time_metric)
{
	char* history_read_pos = metric->history; // char* used for pointer arithmetic
	INT64 sum = 0;
	int i;
	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);

	for (i = 0; i < cv_ps_samplesize.value; i++)
	{
		if (time_metric)
			sum += (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
		else
			sum += *((INT32*)history_read_pos);
		history_read_pos += value_size;
	}

	return sum / cv_ps_samplesize.value;
}

static INT32 PS_GetMetricMinOrMax(ps_metric_t *metric, boolean time_metric, boolean get_max)
{
	char* history_read_pos = metric->history; // char* used for pointer arithmetic
	INT32 found_value = get_max ? INT32_MIN : INT32_MAX;
	int i;
	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);

	for (i = 0; i < cv_ps_samplesize.value; i++)
	{
		INT32 value;
		if (time_metric)
			value = (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
		else
			value = *((INT32*)history_read_pos);

		if ((get_max && value > found_value) ||
			(!get_max && value < found_value))
		{
			found_value = value;
		}
		history_read_pos += value_size;
	}

	return found_value;
}

// Calculates the standard deviation for metric.
static INT32 PS_GetMetricSD(ps_metric_t *metric, boolean time_metric)
{
	char* history_read_pos = metric->history; // char* used for pointer arithmetic
	INT64 sum = 0;
	int i;
	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
	INT32 avg = PS_GetMetricAverage(metric, time_metric);

	for (i = 0; i < cv_ps_samplesize.value; i++)
	{
		INT64 value;
		if (time_metric)
			value = (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
		else
			value = *((INT32*)history_read_pos);

		value -= avg;
		sum += value * value;

		history_read_pos += value_size;
	}

	return round(sqrt(sum / cv_ps_samplesize.value));
}

// Returns the value to show on screen for metric.
static INT32 PS_GetMetricScreenValue(ps_metric_t *metric, boolean time_metric)
{
	if (cv_ps_samplesize.value > 1 && metric->history)
	{
		if (cv_ps_descriptor.value == 1)
			return PS_GetMetricAverage(metric, time_metric);
		else if (cv_ps_descriptor.value == 2)
			return PS_GetMetricSD(metric, time_metric);
		else if (cv_ps_descriptor.value == 3)
			return PS_GetMetricMinOrMax(metric, time_metric, false);
		else
			return PS_GetMetricMinOrMax(metric, time_metric, true);
	}
	else
	{
		if (time_metric)
			return (metric->value.p) / (I_GetPrecisePrecision() / 1000000);
		else
			return metric->value.i;
	}
}

static int PS_DrawPerfRows(int x, int y, int color, perfstatrow_t *rows)
{
	const boolean hires = PS_HighResolution();
	INT32 draw_flags = V_MONOSPACE | color;
	perfstatrow_t * row;
	int draw_y = y;

	if (hires)
		draw_flags |= V_ALLOWLOWERCASE;

	for (row = rows; row->lores_label; ++row)
	{
		const char *label;
		INT32 value;
		char *final_str;

		if (!PS_IsRowVisible(row))
			continue;

		label = hires ? row->hires_label : row->lores_label;
		value = PS_GetMetricScreenValue(row->metric, !!(row->flags & PS_TIME));
		final_str = va("%s %d", label, value);

		if (hires)
		{
			V_DrawSmallString(x, draw_y, draw_flags, final_str);
			draw_y += 5;
		}
		else
		{
			V_DrawThinString(x, draw_y, draw_flags, final_str);
			draw_y += 8;
		}
	}

	return draw_y;
}

static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric)
{
	int index = frame_metric ? ps_frame_index : ps_tick_index;

	if (!metric->history)
	{
		// allocate history table
		int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
		void** memory_user = &metric->history;

		metric->history = Z_Calloc(value_size * cv_ps_samplesize.value, PU_PERFSTATS,
				memory_user);

		// reset "samples left" counter since this history table needs to be filled
		if (frame_metric)
			ps_frame_samples_left = cv_ps_samplesize.value;
		else
			ps_tick_samples_left = cv_ps_samplesize.value;
	}

	if (time_metric)
	{
		precise_t *history = (precise_t*)metric->history;
		history[index] = metric->value.p;
	}
	else
	{
		INT32 *history = (INT32*)metric->history;
		history[index] = metric->value.i;
	}
}

static void PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
{
	perfstatrow_t *row;
	for (row = rows; row->lores_label; row++)
	{
		if (PS_IsRowValid(row))
			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric);
	}
}

// Update all metrics that are calculated on every frame.
static void PS_UpdateFrameStats(void)
{
	// update frame time
	precise_t currenttime = I_GetPreciseTime();
	ps_frametime.value.p = currenttime - ps_prevframetime;
	ps_prevframetime = currenttime;

	// update 3d rendering stats
	if (PS_IsLevelActive())
	{
		// Remember to update this calculation when adding more 3d rendering stats!
		ps_otherrendertime.value.p = ps_rendercalltime.value.p - ps_bsptime.value.p;

#ifdef HWRENDER
		if (rendermode == render_opengl)
		{
			ps_otherrendertime.value.p -=
				ps_hw_skyboxtime.value.p +
				ps_hw_nodesorttime.value.p +
				ps_hw_nodedrawtime.value.p +
				ps_hw_spritesorttime.value.p +
				ps_hw_spritedrawtime.value.p;

			if (cv_glbatching.value)
			{
				ps_otherrendertime.value.p -=
					ps_hw_batchsorttime.value.p +
					ps_hw_batchdrawtime.value.p;
			}
		}
		else
#endif
		{
			ps_otherrendertime.value.p -=
				ps_sw_spritecliptime.value.p +
				ps_sw_portaltime.value.p +
				ps_sw_planetime.value.p +
				ps_sw_maskedtime.value.p;
		}
	}

	if (cv_ps_samplesize.value > 1)
	{
		PS_UpdateRowHistories(rendertime_rows, true);
		if (PS_IsLevelActive())
			PS_UpdateRowHistories(commoncounter_rows, true);

		if (R_UsingFrameInterpolation())
			PS_UpdateRowHistories(interpolation_rows, true);

#ifdef HWRENDER
		if (rendermode == render_opengl && cv_glbatching.value)
		{
			PS_UpdateRowHistories(batchcount_rows, true);
			PS_UpdateRowHistories(batchcalls_rows, true);
		}
#endif

		ps_frame_index++;
		if (ps_frame_index >= cv_ps_samplesize.value)
			ps_frame_index = 0;
		if (ps_frame_samples_left)
			ps_frame_samples_left--;
	}
}

// Update thinker counters by iterating the thinker lists.
static void PS_CountThinkers(void)
{
	int i;
	thinker_t *thinker;

	ps_thinkercount.value.i = 0;
	ps_polythcount.value.i = 0;
	ps_mainthcount.value.i = 0;
	ps_mobjcount.value.i = 0;
	ps_regularcount.value.i = 0;
	ps_scenerycount.value.i = 0;
	ps_nothinkcount.value.i = 0;
	ps_dynslopethcount.value.i = 0;
	ps_precipcount.value.i = 0;
	ps_removecount.value.i = 0;

	for (i = 0; i < NUM_THINKERLISTS; i++)
	{
		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
		{
			ps_thinkercount.value.i++;
			if (thinker->removing)
				ps_removecount.value.i++;
			else if (i == THINK_POLYOBJ)
				ps_polythcount.value.i++;
			else if (i == THINK_MAIN)
				ps_mainthcount.value.i++;
			else if (i == THINK_MOBJ)
			{
				if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
				{
					mobj_t *mobj = (mobj_t*)thinker;
					ps_mobjcount.value.i++;
					if (mobj->flags & MF_NOTHINK)
						ps_nothinkcount.value.i++;
					else if (mobj->flags & MF_SCENERY)
						ps_scenerycount.value.i++;
					else
						ps_regularcount.value.i++;
				}
			}
			else if (i == THINK_DYNSLOPE)
				ps_dynslopethcount.value.i++;
			else if (i == THINK_PRECIP)
				ps_precipcount.value.i++;
		}
	}
}

// Update all metrics that are calculated on every tick.
void PS_UpdateTickStats(void)
{
	if (cv_perfstats.value == 1 && cv_ps_samplesize.value > 1)
	{
		PS_UpdateRowHistories(gamelogicbrief_row, false);
	}
	if (cv_perfstats.value == 2)
	{
		if (PS_IsLevelActive())
		{
			ps_otherlogictime.value.p =
				ps_tictime.value.p -
				ps_playerthink_time.value.p -
				ps_thinkertime.value.p -
				ps_lua_prethinkframe_time.value.p -
				ps_lua_thinkframe_time.value.p -
				ps_lua_postthinkframe_time.value.p;

			PS_CountThinkers();
		}

		if (cv_ps_samplesize.value > 1)
		{
			PS_UpdateRowHistories(gamelogic_rows, false);
			PS_UpdateRowHistories(thinkercount_rows, false);
			PS_UpdateRowHistories(misc_calls_rows, false);
		}
	}
	if (cv_ps_samplesize.value > 1)
	{
		if(cv_perfstats.value >= 3 && PS_IsLevelActive())
		{
			int i;
			if (cv_perfstats.value == 3)
			{
				for (i = 0; i < thinkframe_hooks_length; i++)
					PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false);
			}
			else if (cv_perfstats.value == 4)
			{
				for (i = 0; i < prethinkframe_hooks_length; i++)
					PS_UpdateMetricHistory(&prethinkframe_hooks[i].time_taken, true, false);
			}
			else if (cv_perfstats.value == 5)
			{
				for (i = 0; i < postthinkframe_hooks_length; i++)
					PS_UpdateMetricHistory(&postthinkframe_hooks[i].time_taken, true, false);
			}
		}
		if (cv_perfstats.value)
		{
			ps_tick_index++;
			if (ps_tick_index >= cv_ps_samplesize.value)
				ps_tick_index = 0;
			if (ps_tick_samples_left)
				ps_tick_samples_left--;
		}
	}
}

static void PS_DrawDescriptorHeader(void)
{
	if (cv_ps_samplesize.value > 1)
	{
		const char* descriptor_names[] = {
			"average",
			"standard deviation",
			"minimum",
			"maximum"
		};
		const boolean hires = PS_HighResolution();
		char* str;
		INT32 flags = V_MONOSPACE | V_ALLOWLOWERCASE;
		int samples_left = max(ps_frame_samples_left, ps_tick_samples_left);
		int x, y;

		if (cv_perfstats.value >= 3)
		{
			x = 2;
			y = 0;
		}
		else
		{
			x = 20;
			y = hires ? 5 : 2;
		}

		if (samples_left)
		{
			str = va("Samples needed for correct results: %d", samples_left);
			flags |= V_REDMAP;
		}
		else
		{
			str = va("Showing the %s of %d samples.",
					descriptor_names[cv_ps_descriptor.value - 1], cv_ps_samplesize.value);
			flags |= V_GREENMAP;
		}

		if (hires)
			V_DrawSmallString(x, y, flags, str);
		else
			V_DrawThinString(x, y, flags, str);
	}
}

static void PS_DrawRenderStats(void)
{
	const boolean hires = PS_HighResolution();
	const int half_row = hires ? 5 : 4;
	int x, y, cy = 10;

	PS_DrawDescriptorHeader();

	y = PS_DrawPerfRows(20, 10, V_YELLOWMAP, rendertime_rows);

	PS_DrawPerfRows(20, y + half_row, V_GRAYMAP, gamelogicbrief_row);

	if (PS_IsLevelActive())
	{
		x = hires ? 115 : 90;
		cy = PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows) + half_row;

#ifdef HWRENDER
		if (rendermode == render_opengl && cv_glbatching.value)
		{
			x = hires ? 200 : 155;
			y = PS_DrawPerfRows(x, 10, V_PURPLEMAP, batchcount_rows);

			x = hires ? 200 : 220;
			y = hires ? y + half_row : 10;
			PS_DrawPerfRows(x, y, V_PURPLEMAP, batchcalls_rows);
		}
#endif
	}

	if (R_UsingFrameInterpolation())
	{
		x = hires ? 115 : 90;
		PS_DrawPerfRows(x, cy, V_ROSYMAP, interpolation_rows);
	}
}

static void PS_DrawGameLogicStats(void)
{
	const boolean hires = PS_HighResolution();
	int x, y;

	PS_DrawDescriptorHeader();

	PS_DrawPerfRows(20, 10, V_YELLOWMAP, gamelogic_rows);

	x = hires ? 115 : 90;
	PS_DrawPerfRows(x, 10, V_BLUEMAP, thinkercount_rows);

	if (hires)
		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");

	x = hires ? 216 : 170;
	y = hires ? 15 : 10;
	PS_DrawPerfRows(x, y, V_PURPLEMAP, misc_calls_rows);
}

static void draw_think_frame_stats(int hook_length, ps_hookinfo_t *hook)
{
	char s[100];
	int i;
	// text writing position
	int x = 2;
	int y = 4;
	UINT32 text_color;
	char tempbuffer[LUA_IDSIZE];
	char last_mod_name[LUA_IDSIZE];
	last_mod_name[0] = '\0';

	PS_DrawDescriptorHeader();

	for (i = 0; i < hook_length; i++)
	{

#define NEXT_ROW() \
y += 4; \
if (y > 192) \
{ \
	y = 4; \
	x += 106; \
	if (x > 214) \
		break; \
}

		char* str = hook[i].short_src;
		char* tempstr = tempbuffer;
		int len = (int)strlen(str);
		char* str_ptr;
		if (strcmp(".lua", str + len - 4) == 0)
		{
			str[len-4] = '\0'; // remove .lua at end
			len -= 4;
		}
		// we locate the wad/pk3 name in the string and compare it to
		// what we found on the previous iteration.
		// if the name has changed, print it out on the screen
		strcpy(tempstr, str);
		str_ptr = strrchr(tempstr, '|');
		if (str_ptr)
		{
			*str_ptr = '\0';
			str = str_ptr + 1; // this is the name of the hook without the mod file name
			str_ptr = strrchr(tempstr, PATHSEP[0]);
			if (str_ptr)
				tempstr = str_ptr + 1;
			// tempstr should now point to the mod name, (wad/pk3) possibly truncated
			if (strcmp(tempstr, last_mod_name) != 0)
			{
				strcpy(last_mod_name, tempstr);
				len = (int)strlen(tempstr);
				if (len > 25)
					tempstr += len - 25;
				snprintf(s, sizeof s - 1, "%s", tempstr);
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
				NEXT_ROW()
			}
			text_color = V_YELLOWMAP;
		}
		else
		{
			// probably a standalone lua file
			// cut off the folder if it's there
			str_ptr = strrchr(tempstr, PATHSEP[0]);
			if (str_ptr)
				str = str_ptr + 1;
			text_color = 0; // white
		}
		len = (int)strlen(str);
		if (len > 20)
			str += len - 20;
		snprintf(s, sizeof s - 1, "%20s: %d", str,
				PS_GetMetricScreenValue(&hook[i].time_taken, true));
		V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
		NEXT_ROW()

#undef NEXT_ROW

	}
}

static void PS_DrawPreThinkFrameStats(void)
{
	draw_think_frame_stats(prethinkframe_hooks_length, prethinkframe_hooks);
}

static void PS_DrawThinkFrameStats(void)
{
	draw_think_frame_stats(thinkframe_hooks_length, thinkframe_hooks);
}

static void PS_DrawPostThinkFrameStats(void)
{
	draw_think_frame_stats(postthinkframe_hooks_length, postthinkframe_hooks);
}

void M_DrawPerfStats(void)
{
	if (cv_perfstats.value == 1) // rendering
	{
		PS_UpdateFrameStats();
		PS_DrawRenderStats();
	}
	else if (cv_perfstats.value == 2) // logic
	{
		// PS_UpdateTickStats is called in TryRunTics, since otherwise it would miss
		// tics when frame skips happen
		PS_DrawGameLogicStats();
	}
	else if (cv_perfstats.value >= 3) // lua thinkframe
	{
		if (!PS_IsLevelActive())
			return;
		if (!PS_HighResolution())
		{
			// Low resolutions can't really use V_DrawSmallString that is used by thinkframe stats.
			// A low-res version using V_DrawThinString could be implemented,
			// but it would have much less space for information.
			V_DrawThinString(80, 92, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Lua Perfstats is not available");
			V_DrawThinString(80, 100, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "for resolutions below 640x400.");
			return;
		}
		if (cv_perfstats.value == 3)
		{
			PS_DrawThinkFrameStats();
		}
		else if (cv_perfstats.value == 4)
		{
			PS_DrawPreThinkFrameStats();
		}
		else if (cv_perfstats.value == 5)
		{
			PS_DrawPostThinkFrameStats();
		}
	}
}

// remove and unallocate history from all metrics
static void PS_ClearHistory(void)
{
	int i;

	Z_FreeTag(PU_PERFSTATS);
	// thinkframe hook metric history pointers need to be cleared manually
	for (i = 0; i < thinkframe_hooks_length; i++)
	{
		thinkframe_hooks[i].time_taken.history = NULL;
	}

	ps_frame_index = ps_tick_index = 0;
	// PS_UpdateMetricHistory will set these correctly when it runs
	ps_frame_samples_left = ps_tick_samples_left = 0;
}

void PS_PerfStats_OnChange(void)
{
	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
		PS_ClearHistory();
}

void PS_SampleSize_OnChange(void)
{
	if (cv_ps_samplesize.value > 1)
		PS_ClearHistory();
}
