diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6446ff109b12e4925afa773dbb7c3afcb735f633..6c0e20e8ec15934b02b8db21a66a6a8f2fee91a3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -32,6 +32,7 @@ set(SRB2_CORE_SOURCES
 	m_fixed.c
 	m_menu.c
 	m_misc.c
+	m_perfstats.c
 	m_queue.c
 	m_random.c
 	md5.c
@@ -97,6 +98,7 @@ set(SRB2_CORE_HEADERS
 	m_fixed.h
 	m_menu.h
 	m_misc.h
+	m_perfstats.h
 	m_queue.h
 	m_random.h
 	m_swap.h
diff --git a/src/Makefile b/src/Makefile
index 2fe0b26cd55b643dca0f6a23d72ec16f26ca9991..8ad7ecf5a53de9868877a03d9d50b6985621bdb8 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -486,6 +486,7 @@ OBJS:=$(i_main_o) \
 		$(OBJDIR)/m_fixed.o  \
 		$(OBJDIR)/m_menu.o   \
 		$(OBJDIR)/m_misc.o   \
+		$(OBJDIR)/m_perfstats.o \
 		$(OBJDIR)/m_random.o \
 		$(OBJDIR)/m_queue.o  \
 		$(OBJDIR)/info.o     \
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index b6974b6cd66ddb5d46cf3de66951662c1c53eeda..51a3bb24b60f0595aa045dcc6beb0eaa0a119f1e 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -44,6 +44,7 @@
 #include "lua_script.h"
 #include "lua_hook.h"
 #include "md5.h"
+#include "m_perfstats.h"
 
 #ifndef NONET
 // cl loading screen
@@ -5444,14 +5445,14 @@ void TryRunTics(tic_t realtics)
 			{
 				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
 
-				rs_tictime = I_GetTimeMicros();
+				ps_tictime = I_GetTimeMicros();
 
 				G_Ticker((gametic % NEWTICRATERATIO) == 0);
 				ExtraDataTicker();
 				gametic++;
 				consistancy[gametic%BACKUPTICS] = Consistancy();
 
-				rs_tictime = I_GetTimeMicros() - rs_tictime;
+				ps_tictime = I_GetTimeMicros() - ps_tictime;
 
 				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
 				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
diff --git a/src/d_main.c b/src/d_main.c
index c4cde87a30e14527700a116dca7b0bd6a67c8f2e..ce1331fe3abf12eb1f0a9dfb01d01777ecaf7959 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -67,6 +67,7 @@
 #include "keys.h"
 #include "filesrch.h" // refreshdirmenu, mainwadstally
 #include "g_input.h" // tutorial mode control scheming
+#include "m_perfstats.h"
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -435,7 +436,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				rs_rendercalltime = I_GetTimeMicros();
+				ps_rendercalltime = I_GetTimeMicros();
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
 					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
@@ -482,7 +483,7 @@ static void D_Display(void)
 					if (postimgtype2)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
-				rs_rendercalltime = I_GetTimeMicros() - rs_rendercalltime;
+				ps_rendercalltime = I_GetTimeMicros() - ps_rendercalltime;
 			}
 
 			if (lastdraw)
@@ -496,7 +497,7 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			rs_uitime = I_GetTimeMicros();
+			ps_uitime = I_GetTimeMicros();
 
 			if (gamestate == GS_LEVEL)
 			{
@@ -509,7 +510,7 @@ static void D_Display(void)
 		}
 		else
 		{
-			rs_uitime = I_GetTimeMicros();
+			ps_uitime = I_GetTimeMicros();
 		}
 	}
 
@@ -551,7 +552,7 @@ static void D_Display(void)
 
 	CON_Drawer();
 
-	rs_uitime = I_GetTimeMicros() - rs_uitime;
+	ps_uitime = I_GetTimeMicros() - ps_uitime;
 
 	//
 	// wipe update
@@ -632,90 +633,14 @@ static void D_Display(void)
 			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-10, V_YELLOWMAP, s);
 		}
 
-		if (cv_renderstats.value)
+		if (cv_perfstats.value)
 		{
-			char s[50];
-			int frametime = I_GetTimeMicros() - rs_prevframetime;
-			int divisor = 1;
-			rs_prevframetime = I_GetTimeMicros();
-
-			if (rs_rendercalltime > 10000) divisor = 1000;
-
-			snprintf(s, sizeof s - 1, "ft   %d", frametime / divisor);
-			V_DrawThinString(30, 10, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "rtot %d", rs_rendercalltime / divisor);
-			V_DrawThinString(30, 20, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "bsp  %d", rs_bsptime / divisor);
-			V_DrawThinString(30, 30, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "nbsp %d", rs_numbspcalls);
-			V_DrawThinString(80, 10, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "nspr %d", rs_numsprites);
-			V_DrawThinString(80, 20, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "nnod %d", rs_numdrawnodes);
-			V_DrawThinString(80, 30, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "npob %d", rs_numpolyobjects);
-			V_DrawThinString(80, 40, V_MONOSPACE | V_BLUEMAP, s);
-			if (rendermode == render_opengl) // OpenGL specific stats
-			{
-#ifdef HWRENDER
-				snprintf(s, sizeof s - 1, "nsrt %d", rs_hw_nodesorttime / divisor);
-				V_DrawThinString(30, 40, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ndrw %d", rs_hw_nodedrawtime / divisor);
-				V_DrawThinString(30, 50, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ssrt %d", rs_hw_spritesorttime / divisor);
-				V_DrawThinString(30, 60, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "sdrw %d", rs_hw_spritedrawtime / divisor);
-				V_DrawThinString(30, 70, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ui   %d", rs_uitime / divisor);
-				V_DrawThinString(30, 80, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "fin  %d", rs_swaptime / divisor);
-				V_DrawThinString(30, 90, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "tic  %d", rs_tictime / divisor);
-				V_DrawThinString(30, 105, V_MONOSPACE | V_GRAYMAP, s);
-				if (cv_glbatching.value)
-				{
-					snprintf(s, sizeof s - 1, "bsrt %d", rs_hw_batchsorttime / divisor);
-					V_DrawThinString(80, 55, V_MONOSPACE | V_REDMAP, s);
-					snprintf(s, sizeof s - 1, "bdrw %d", rs_hw_batchdrawtime / divisor);
-					V_DrawThinString(80, 65, V_MONOSPACE | V_REDMAP, s);
-
-					snprintf(s, sizeof s - 1, "npol %d", rs_hw_numpolys);
-					V_DrawThinString(130, 10, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "ndc  %d", rs_hw_numcalls);
-					V_DrawThinString(130, 20, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "nshd %d", rs_hw_numshaders);
-					V_DrawThinString(130, 30, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "nvrt %d", rs_hw_numverts);
-					V_DrawThinString(130, 40, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "ntex %d", rs_hw_numtextures);
-					V_DrawThinString(185, 10, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "npf  %d", rs_hw_numpolyflags);
-					V_DrawThinString(185, 20, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "ncol %d", rs_hw_numcolors);
-					V_DrawThinString(185, 30, V_MONOSPACE | V_PURPLEMAP, s);
-				}
-#endif
-			}
-			else // software specific stats
-			{
-				snprintf(s, sizeof s - 1, "prtl %d", rs_sw_portaltime / divisor);
-				V_DrawThinString(30, 40, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "plns %d", rs_sw_planetime / divisor);
-				V_DrawThinString(30, 50, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "mskd %d", rs_sw_maskedtime / divisor);
-				V_DrawThinString(30, 60, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ui   %d", rs_uitime / divisor);
-				V_DrawThinString(30, 70, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "fin  %d", rs_swaptime / divisor);
-				V_DrawThinString(30, 80, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "tic  %d", rs_tictime / divisor);
-				V_DrawThinString(30, 95, V_MONOSPACE | V_GRAYMAP, s);
-			}
+			M_DrawPerfStats();
 		}
 
-		rs_swaptime = I_GetTimeMicros();
+		ps_swaptime = I_GetTimeMicros();
 		I_FinishUpdate(); // page flip or blit buffer
-		rs_swaptime = I_GetTimeMicros() - rs_swaptime;
+		ps_swaptime = I_GetTimeMicros() - ps_swaptime;
 	}
 
 	needpatchflush = false;
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 9082a818a4adca93d0fb9ff310319b85d2b2de9f..87abd596a1520b1b6088eacb476af290abcefa84 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -371,6 +371,10 @@ consvar_t cv_mute = CVAR_INIT ("mute", "Off", CV_NETVAR|CV_CALL, CV_OnOff, Mute_
 
 consvar_t cv_sleep = CVAR_INIT ("cpusleep", "1", CV_SAVE, sleeping_cons_t, NULL);
 
+static CV_PossibleValue_t perfstats_cons_t[] = {
+	{0, "Off"}, {1, "Rendering"}, {2, "Logic"}, {3, "ThinkFrame"}, {0, NULL}};
+consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL);
+
 char timedemo_name[256];
 boolean timedemo_csv;
 char timedemo_csv_id[256];
@@ -864,6 +868,8 @@ void D_RegisterClientCommands(void)
 
 	CV_RegisterVar(&cv_soundtest);
 
+	CV_RegisterVar(&cv_perfstats);
+
 	// ingame object placing
 	COM_AddCommand("objectplace", Command_ObjectPlace_f);
 	COM_AddCommand("writethings", Command_Writethings_f);
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 897c28968e7a42662ad04b533942005fb70f91d2..841f71acd6692341af29928b117b36699e295080 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -114,6 +114,8 @@ extern consvar_t cv_skipmapcheck;
 
 extern consvar_t cv_sleep;
 
+extern consvar_t cv_perfstats;
+
 extern char timedemo_name[256];
 extern boolean timedemo_csv;
 extern char timedemo_csv_id[256];
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index 492cea5fa13653163bfef64c6581ceb1b55280df..a63be3a729b00d042f2cd20eb7058721bda590b7 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -235,13 +235,13 @@ void HWR_RenderBatches(void)
 	currently_batching = false;// no longer collecting batches
 	if (!polygonArraySize)
 	{
-		rs_hw_numpolys = rs_hw_numcalls = rs_hw_numshaders = rs_hw_numtextures = rs_hw_numpolyflags = rs_hw_numcolors = 0;
+		ps_hw_numpolys = ps_hw_numcalls = ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 0;
 		return;// nothing to draw
 	}
 	// init stats vars
-	rs_hw_numpolys = polygonArraySize;
-	rs_hw_numcalls = rs_hw_numverts = 0;
-	rs_hw_numshaders = rs_hw_numtextures = rs_hw_numpolyflags = rs_hw_numcolors = 1;
+	ps_hw_numpolys = polygonArraySize;
+	ps_hw_numcalls = ps_hw_numverts = 0;
+	ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 1;
 	// init polygonIndexArray
 	for (i = 0; i < polygonArraySize; i++)
 	{
@@ -249,12 +249,12 @@ void HWR_RenderBatches(void)
 	}
 
 	// sort polygons
-	rs_hw_batchsorttime = I_GetTimeMicros();
+	ps_hw_batchsorttime = I_GetTimeMicros();
 	if (cv_glshaders.value && gl_shadersavailable)
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygons);
 	else
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygonsNoShaders);
-	rs_hw_batchsorttime = I_GetTimeMicros() - rs_hw_batchsorttime;
+	ps_hw_batchsorttime = I_GetTimeMicros() - ps_hw_batchsorttime;
 	// sort order
 	// 1. shader
 	// 2. texture
@@ -262,7 +262,7 @@ void HWR_RenderBatches(void)
 	// 4. colors + light level
 	// not sure about what order of the last 2 should be, or if it even matters
 
-	rs_hw_batchdrawtime = I_GetTimeMicros();
+	ps_hw_batchdrawtime = I_GetTimeMicros();
 
 	currentShader = polygonArray[polygonIndexArray[0]].shader;
 	currentTexture = polygonArray[polygonIndexArray[0]].texture;
@@ -398,8 +398,8 @@ void HWR_RenderBatches(void)
 			// execute draw call
             HWD.pfnDrawIndexedTriangles(&currentSurfaceInfo, finalVertexArray, finalIndexWritePos, currentPolyFlags, finalVertexIndexArray);
 			// update stats
-			rs_hw_numcalls++;
-			rs_hw_numverts += finalIndexWritePos;
+			ps_hw_numcalls++;
+			ps_hw_numverts += finalIndexWritePos;
 			// reset write positions
 			finalVertexWritePos = 0;
 			finalIndexWritePos = 0;
@@ -416,7 +416,7 @@ void HWR_RenderBatches(void)
 			currentShader = nextShader;
 			changeShader = false;
 
-			rs_hw_numshaders++;
+			ps_hw_numshaders++;
 		}
 		if (changeTexture)
 		{
@@ -425,21 +425,21 @@ void HWR_RenderBatches(void)
 			currentTexture = nextTexture;
 			changeTexture = false;
 
-			rs_hw_numtextures++;
+			ps_hw_numtextures++;
 		}
 		if (changePolyFlags)
 		{
 			currentPolyFlags = nextPolyFlags;
 			changePolyFlags = false;
 
-			rs_hw_numpolyflags++;
+			ps_hw_numpolyflags++;
 		}
 		if (changeSurfaceInfo)
 		{
 			currentSurfaceInfo = nextSurfaceInfo;
 			changeSurfaceInfo = false;
 
-			rs_hw_numcolors++;
+			ps_hw_numcolors++;
 		}
 		// and that should be it?
 	}
@@ -447,7 +447,7 @@ void HWR_RenderBatches(void)
 	polygonArraySize = 0;
 	unsortedVertexArraySize = 0;
 
-	rs_hw_batchdrawtime = I_GetTimeMicros() - rs_hw_batchdrawtime;
+	ps_hw_batchdrawtime = I_GetTimeMicros() - ps_hw_batchdrawtime;
 }
 
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index e7a0ac3ff954327365e912320c1bed73b2b7ae10..4268556e3b02508e8ee25ce04caad0ac78824e7a 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -146,21 +146,22 @@ static angle_t gl_aimingangle;
 static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean skybox);
 
 // Render stats
-int rs_hw_nodesorttime = 0;
-int rs_hw_nodedrawtime = 0;
-int rs_hw_spritesorttime = 0;
-int rs_hw_spritedrawtime = 0;
+int ps_hw_skyboxtime = 0;
+int ps_hw_nodesorttime = 0;
+int ps_hw_nodedrawtime = 0;
+int ps_hw_spritesorttime = 0;
+int ps_hw_spritedrawtime = 0;
 
 // Render stats for batching
-int rs_hw_numpolys = 0;
-int rs_hw_numverts = 0;
-int rs_hw_numcalls = 0;
-int rs_hw_numshaders = 0;
-int rs_hw_numtextures = 0;
-int rs_hw_numpolyflags = 0;
-int rs_hw_numcolors = 0;
-int rs_hw_batchsorttime = 0;
-int rs_hw_batchdrawtime = 0;
+int ps_hw_numpolys = 0;
+int ps_hw_numverts = 0;
+int ps_hw_numcalls = 0;
+int ps_hw_numshaders = 0;
+int ps_hw_numtextures = 0;
+int ps_hw_numpolyflags = 0;
+int ps_hw_numcolors = 0;
+int ps_hw_batchsorttime = 0;
+int ps_hw_batchdrawtime = 0;
 
 boolean gl_shadersavailable = true;
 
@@ -3188,7 +3189,7 @@ static void HWR_Subsector(size_t num)
 		}
 
 		// for render stats
-		rs_numpolyobjects += numpolys;
+		ps_numpolyobjects += numpolys;
 
 		// Sort polyobjects
 		R_SortPolyObjects(sub);
@@ -3296,7 +3297,7 @@ static void HWR_RenderBSPNode(INT32 bspnum)
 	// Decide which side the view point is on
 	INT32 side;
 
-	rs_numbspcalls++;
+	ps_numbspcalls++;
 
 	// Found a subsector?
 	if (bspnum & NF_SUBSECTOR)
@@ -4502,7 +4503,7 @@ static void HWR_CreateDrawNodes(void)
 	// that is already lying around. This should all be in some sort of linked list or lists.
 	sortindex = Z_Calloc(sizeof(size_t) * (numplanes + numpolyplanes + numwalls), PU_STATIC, NULL);
 
-	rs_hw_nodesorttime = I_GetTimeMicros();
+	ps_hw_nodesorttime = I_GetTimeMicros();
 
 	for (i = 0; i < numplanes; i++, p++)
 	{
@@ -4522,7 +4523,7 @@ static void HWR_CreateDrawNodes(void)
 		sortindex[p] = p;
 	}
 
-	rs_numdrawnodes = p;
+	ps_numdrawnodes = p;
 
 	// p is the number of stuff to sort
 
@@ -4557,9 +4558,9 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	rs_hw_nodesorttime = I_GetTimeMicros() - rs_hw_nodesorttime;
+	ps_hw_nodesorttime = I_GetTimeMicros() - ps_hw_nodesorttime;
 
-	rs_hw_nodedrawtime = I_GetTimeMicros();
+	ps_hw_nodedrawtime = I_GetTimeMicros();
 
 	// Okay! Let's draw it all! Woo!
 	HWD.pfnSetTransform(&atransform);
@@ -4596,7 +4597,7 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	rs_hw_nodedrawtime = I_GetTimeMicros() - rs_hw_nodedrawtime;
+	ps_hw_nodedrawtime = I_GetTimeMicros() - ps_hw_nodedrawtime;
 
 	numwalls = 0;
 	numplanes = 0;
@@ -5777,8 +5778,10 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	if (viewnumber == 0) // Only do it if it's the first screen being rendered
 		HWD.pfnClearBuffer(true, false, &ClearColor); // Clear the Color Buffer, stops HOMs. Also seems to fix the skybox issue on Intel GPUs.
 
+	ps_hw_skyboxtime = I_GetTimeMicros();
 	if (skybox && drawsky) // If there's a skybox and we should be drawing the sky, draw the skybox
 		HWR_RenderSkyboxView(viewnumber, player); // This is drawn before everything else so it is placed behind
+	ps_hw_skyboxtime = I_GetTimeMicros() - ps_hw_skyboxtime;
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -5889,9 +5892,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	HWD.pfnSetSpecialState(HWD_SET_SHADERS, cv_glshaders.value);
 	HWD.pfnSetShader(SHADER_DEFAULT);
 
-	rs_numbspcalls = 0;
-	rs_numpolyobjects = 0;
-	rs_bsptime = I_GetTimeMicros();
+	ps_numbspcalls = 0;
+	ps_numpolyobjects = 0;
+	ps_bsptime = I_GetTimeMicros();
 
 	validcount++;
 
@@ -5929,7 +5932,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
-	rs_bsptime = I_GetTimeMicros() - rs_bsptime;
+	ps_bsptime = I_GetTimeMicros() - ps_bsptime;
 
 	if (cv_glbatching.value)
 		HWR_RenderBatches();
@@ -5944,22 +5947,22 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 #endif
 
 	// Draw MD2 and sprites
-	rs_numsprites = gl_visspritecount;
-	rs_hw_spritesorttime = I_GetTimeMicros();
+	ps_numsprites = gl_visspritecount;
+	ps_hw_spritesorttime = I_GetTimeMicros();
 	HWR_SortVisSprites();
-	rs_hw_spritesorttime = I_GetTimeMicros() - rs_hw_spritesorttime;
-	rs_hw_spritedrawtime = I_GetTimeMicros();
+	ps_hw_spritesorttime = I_GetTimeMicros() - ps_hw_spritesorttime;
+	ps_hw_spritedrawtime = I_GetTimeMicros();
 	HWR_DrawSprites();
-	rs_hw_spritedrawtime = I_GetTimeMicros() - rs_hw_spritedrawtime;
+	ps_hw_spritedrawtime = I_GetTimeMicros() - ps_hw_spritedrawtime;
 
 #ifdef NEWCORONAS
 	//Hurdler: they must be drawn before translucent planes, what about gl fog?
 	HWR_DrawCoronas();
 #endif
 
-	rs_numdrawnodes = 0;
-	rs_hw_nodesorttime = 0;
-	rs_hw_nodedrawtime = 0;
+	ps_numdrawnodes = 0;
+	ps_hw_nodesorttime = 0;
+	ps_hw_nodedrawtime = 0;
 	if (numplanes || numpolyplanes || numwalls) //Hurdler: render 3D water and transparent walls after everything
 	{
 		HWR_CreateDrawNodes();
@@ -6061,7 +6064,6 @@ void HWR_AddCommands(void)
 	CV_RegisterVar(&cv_glfiltermode);
 	CV_RegisterVar(&cv_glsolvetjoin);
 
-	CV_RegisterVar(&cv_renderstats);
 	CV_RegisterVar(&cv_glbatching);
 
 #ifndef NEWCLIP
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 9bce49b251a31259ce781eb1c1434037c84fb800..85072dfd9c483456206d0315f248d606cd18f380 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -113,21 +113,22 @@ extern FTransform atransform;
 
 
 // Render stats
-extern int rs_hw_nodesorttime;
-extern int rs_hw_nodedrawtime;
-extern int rs_hw_spritesorttime;
-extern int rs_hw_spritedrawtime;
+extern int ps_hw_skyboxtime;
+extern int ps_hw_nodesorttime;
+extern int ps_hw_nodedrawtime;
+extern int ps_hw_spritesorttime;
+extern int ps_hw_spritedrawtime;
 
 // Render stats for batching
-extern int rs_hw_numpolys;
-extern int rs_hw_numverts;
-extern int rs_hw_numcalls;
-extern int rs_hw_numshaders;
-extern int rs_hw_numtextures;
-extern int rs_hw_numpolyflags;
-extern int rs_hw_numcolors;
-extern int rs_hw_batchsorttime;
-extern int rs_hw_batchdrawtime;
+extern int ps_hw_numpolys;
+extern int ps_hw_numverts;
+extern int ps_hw_numcalls;
+extern int ps_hw_numshaders;
+extern int ps_hw_numtextures;
+extern int ps_hw_numpolyflags;
+extern int ps_hw_numcolors;
+extern int ps_hw_batchsorttime;
+extern int ps_hw_batchdrawtime;
 
 extern boolean gl_shadersavailable;
 
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 8840c81a008def5c46fc9a6a0944392b7fa07f18..a5d4af412634a4c1faf22480137312c69ae7df17 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -23,6 +23,10 @@
 #include "lua_hook.h"
 #include "lua_hud.h" // hud_running errors
 
+#include "m_perfstats.h"
+#include "d_netcmd.h" // for cv_perfstats
+#include "i_system.h" // I_GetTimeMicros
+
 static UINT8 hooksAvailable[(hook_MAX/8)+1];
 
 const char *const hookNames[hook_MAX+1] = {
@@ -269,6 +273,7 @@ boolean LUAh_MobjHook(mobj_t *mo, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 			LUA_PushUserdata(gL, mo, META_MOBJ);
 		PushHook(gL, hookp);
@@ -290,6 +295,7 @@ boolean LUAh_MobjHook(mobj_t *mo, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 			LUA_PushUserdata(gL, mo, META_MOBJ);
 		PushHook(gL, hookp);
@@ -325,6 +331,7 @@ boolean LUAh_PlayerHook(player_t *plr, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 			LUA_PushUserdata(gL, plr, META_PLAYER);
 		PushHook(gL, hookp);
@@ -456,6 +463,9 @@ void LUAh_PreThinkFrame(void)
 void LUAh_ThinkFrame(void)
 {
 	hook_p hookp;
+	// variables used by perf stats
+	int hook_index = 0;
+	int time_taken = 0;
 	if (!gL || !(hooksAvailable[hook_ThinkFrame/8] & (1<<(hook_ThinkFrame%8))))
 		return;
 
@@ -466,6 +476,8 @@ void LUAh_ThinkFrame(void)
 		if (hookp->type != hook_ThinkFrame)
 			continue;
 
+		if (cv_perfstats.value == 3)
+			time_taken = I_GetTimeMicros();
 		PushHook(gL, hookp);
 		if (lua_pcall(gL, 0, 0, 1)) {
 			if (!hookp->error || cv_debug & DBG_LUA)
@@ -473,6 +485,16 @@ void LUAh_ThinkFrame(void)
 			lua_pop(gL, 1);
 			hookp->error = true;
 		}
+		if (cv_perfstats.value == 3)
+		{
+			lua_Debug ar;
+			time_taken = I_GetTimeMicros() - time_taken;
+			// we need the function, let's just retrieve it again
+			PushHook(gL, hookp);
+			lua_getinfo(gL, ">S", &ar);
+			PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+			hook_index++;
+		}
 	}
 
 	lua_pop(gL, 1); // Pop error handler
@@ -523,6 +545,7 @@ UINT8 LUAh_MobjCollideHook(mobj_t *thing1, mobj_t *thing2, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, thing1, META_MOBJ);
@@ -553,6 +576,7 @@ UINT8 LUAh_MobjCollideHook(mobj_t *thing1, mobj_t *thing2, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, thing1, META_MOBJ);
@@ -600,6 +624,7 @@ UINT8 LUAh_MobjLineCollideHook(mobj_t *thing, line_t *line, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, thing, META_MOBJ);
@@ -630,6 +655,7 @@ UINT8 LUAh_MobjLineCollideHook(mobj_t *thing, line_t *line, enum hook which)
 		if (hookp->type != which)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, thing, META_MOBJ);
@@ -675,6 +701,7 @@ boolean LUAh_MobjThinker(mobj_t *mo)
 	// Look for all generic mobj thinker hooks
 	for (hookp = mobjthinkerhooks[MT_NULL]; hookp; hookp = hookp->next)
 	{
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 			LUA_PushUserdata(gL, mo, META_MOBJ);
 		PushHook(gL, hookp);
@@ -693,6 +720,7 @@ boolean LUAh_MobjThinker(mobj_t *mo)
 
 	for (hookp = mobjthinkerhooks[mo->type]; hookp; hookp = hookp->next)
 	{
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 			LUA_PushUserdata(gL, mo, META_MOBJ);
 		PushHook(gL, hookp);
@@ -732,6 +760,7 @@ boolean LUAh_TouchSpecial(mobj_t *special, mobj_t *toucher)
 		if (hookp->type != hook_TouchSpecial)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, special, META_MOBJ);
@@ -757,6 +786,7 @@ boolean LUAh_TouchSpecial(mobj_t *special, mobj_t *toucher)
 		if (hookp->type != hook_TouchSpecial)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, special, META_MOBJ);
@@ -800,6 +830,7 @@ UINT8 LUAh_ShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32
 		if (hookp->type != hook_ShouldDamage)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -835,6 +866,7 @@ UINT8 LUAh_ShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32
 	{
 		if (hookp->type != hook_ShouldDamage)
 			continue;
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -889,6 +921,7 @@ boolean LUAh_MobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32
 		if (hookp->type != hook_MobjDamage)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -920,6 +953,7 @@ boolean LUAh_MobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32
 		if (hookp->type != hook_MobjDamage)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -969,6 +1003,7 @@ boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8
 		if (hookp->type != hook_MobjDeath)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -998,6 +1033,7 @@ boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8
 		if (hookp->type != hook_MobjDeath)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, target, META_MOBJ);
@@ -1190,6 +1226,7 @@ boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 		if (strcmp(hookp->s.str, line->stringargs[0]))
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, line, META_LINE);
@@ -1364,6 +1401,7 @@ boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing)
 		if (hookp->type != hook_MapThingSpawn)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, mo, META_MOBJ);
@@ -1389,6 +1427,7 @@ boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing)
 		if (hookp->type != hook_MapThingSpawn)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, mo, META_MOBJ);
@@ -1430,6 +1469,7 @@ boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj)
 		if (hookp->type != hook_FollowMobj)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, player, META_PLAYER);
@@ -1455,6 +1495,7 @@ boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj)
 		if (hookp->type != hook_FollowMobj)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, player, META_PLAYER);
@@ -1495,6 +1536,7 @@ UINT8 LUAh_PlayerCanDamage(player_t *player, mobj_t *mobj)
 		if (hookp->type != hook_PlayerCanDamage)
 			continue;
 
+		ps_lua_mobjhooks++;
 		if (lua_gettop(gL) == 1)
 		{
 			LUA_PushUserdata(gL, player, META_PLAYER);
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
new file mode 100644
index 0000000000000000000000000000000000000000..df1e31b5e20ab2770f20ec00d2f1679caf00c140
--- /dev/null
+++ b/src/m_perfstats.c
@@ -0,0 +1,541 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 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 "d_netcmd.h"
+#include "r_main.h"
+#include "i_system.h"
+#include "z_zone.h"
+#include "p_local.h"
+
+#ifdef HWRENDER
+#include "hardware/hw_main.h"
+#endif
+
+int ps_tictime = 0;
+
+int ps_playerthink_time = 0;
+int ps_thinkertime = 0;
+
+int ps_thlist_times[NUM_THINKERLISTS];
+static const char* thlist_names[] = {
+	"Polyobjects:     %d",
+	"Main:            %d",
+	"Mobjs:           %d",
+	"Dynamic slopes:  %d",
+	"Precipitation:   %d",
+	NULL
+};
+static const char* thlist_shortnames[] = {
+	"plyobjs %d",
+	"main    %d",
+	"mobjs   %d",
+	"dynslop %d",
+	"precip  %d",
+	NULL
+};
+
+int ps_checkposition_calls = 0;
+
+int ps_lua_thinkframe_time = 0;
+int ps_lua_mobjhooks = 0;
+
+// dynamically allocated resizeable array for thinkframe hook stats
+ps_hookinfo_t *thinkframe_hooks = NULL;
+int thinkframe_hooks_length = 0;
+int thinkframe_hooks_capacity = 16;
+
+void PS_SetThinkFrameHookInfo(int index, UINT32 time_taken, char* short_src)
+{
+	if (!thinkframe_hooks)
+	{
+		// array needs to be initialized
+		thinkframe_hooks = Z_Malloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+	}
+	if (index >= thinkframe_hooks_capacity)
+	{
+		// array needs more space, realloc with double size
+		thinkframe_hooks_capacity *= 2;
+		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
+			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+	}
+	thinkframe_hooks[index].time_taken = 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 M_DrawPerfStats(void)
+{
+	char s[100];
+	int currenttime = I_GetTimeMicros();
+	int frametime = currenttime - ps_prevframetime;
+	ps_prevframetime = currenttime;
+
+	if (cv_perfstats.value == 1) // rendering
+	{
+		if (vid.width < 640 || vid.height < 400) // low resolution
+		{
+			snprintf(s, sizeof s - 1, "frmtime %d", frametime);
+			V_DrawThinString(20, 10, V_MONOSPACE | V_YELLOWMAP, s);
+			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+			{
+				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
+				V_DrawThinString(20, 18, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
+				V_DrawThinString(20, 26, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
+				V_DrawThinString(20, 38, V_MONOSPACE | V_GRAYMAP, s);
+				return;
+			}
+			snprintf(s, sizeof s - 1, "drwtime %d", ps_rendercalltime);
+			V_DrawThinString(20, 18, V_MONOSPACE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "bspcall %d", ps_numbspcalls);
+			V_DrawThinString(90, 10, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "sprites %d", ps_numsprites);
+			V_DrawThinString(90, 18, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "drwnode %d", ps_numdrawnodes);
+			V_DrawThinString(90, 26, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "plyobjs %d", ps_numpolyobjects);
+			V_DrawThinString(90, 34, V_MONOSPACE | V_BLUEMAP, s);
+#ifdef HWRENDER
+			if (rendermode == render_opengl) // OpenGL specific stats
+			{
+				snprintf(s, sizeof s - 1, "skybox  %d", ps_hw_skyboxtime);
+				V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "bsptime %d", ps_bsptime);
+				V_DrawThinString(24, 34, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "nodesrt %d", ps_hw_nodesorttime);
+				V_DrawThinString(24, 42, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "nodedrw %d", ps_hw_nodedrawtime);
+				V_DrawThinString(24, 50, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "sprsort %d", ps_hw_spritesorttime);
+				V_DrawThinString(24, 58, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "sprdraw %d", ps_hw_spritedrawtime);
+				V_DrawThinString(24, 66, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "other   %d",
+					ps_rendercalltime - ps_hw_skyboxtime - ps_bsptime - ps_hw_nodesorttime
+					- ps_hw_nodedrawtime - ps_hw_spritesorttime - ps_hw_spritedrawtime
+					- ps_hw_batchsorttime - ps_hw_batchdrawtime);
+				V_DrawThinString(24, 74, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
+				V_DrawThinString(20, 82, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
+				V_DrawThinString(20, 90, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
+				V_DrawThinString(20, 102, V_MONOSPACE | V_GRAYMAP, s);
+				if (cv_glbatching.value)
+				{
+					snprintf(s, sizeof s - 1, "batsort %d", ps_hw_batchsorttime);
+					V_DrawThinString(90, 46, V_MONOSPACE | V_REDMAP, s);
+					snprintf(s, sizeof s - 1, "batdraw %d", ps_hw_batchdrawtime);
+					V_DrawThinString(90, 54, V_MONOSPACE | V_REDMAP, s);
+
+					snprintf(s, sizeof s - 1, "polygon %d", ps_hw_numpolys);
+					V_DrawThinString(155, 10, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "drwcall %d", ps_hw_numcalls);
+					V_DrawThinString(155, 18, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "shaders %d", ps_hw_numshaders);
+					V_DrawThinString(155, 26, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "vertex  %d", ps_hw_numverts);
+					V_DrawThinString(155, 34, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "texture %d", ps_hw_numtextures);
+					V_DrawThinString(220, 10, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "polyflg %d", ps_hw_numpolyflags);
+					V_DrawThinString(220, 18, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "colors  %d", ps_hw_numcolors);
+					V_DrawThinString(220, 26, V_MONOSPACE | V_PURPLEMAP, s);
+				}
+				else
+				{
+					// reset these vars so the "other" measurement isn't off
+					ps_hw_batchsorttime = 0;
+					ps_hw_batchdrawtime = 0;
+				}
+			}
+			else // software specific stats
+#endif
+			{
+				snprintf(s, sizeof s - 1, "bsptime %d", ps_bsptime);
+				V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "sprclip %d", ps_sw_spritecliptime);
+				V_DrawThinString(24, 34, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "portals %d", ps_sw_portaltime);
+				V_DrawThinString(24, 42, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "planes  %d", ps_sw_planetime);
+				V_DrawThinString(24, 50, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "masked  %d", ps_sw_maskedtime);
+				V_DrawThinString(24, 58, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "other   %d",
+					ps_rendercalltime - ps_bsptime - ps_sw_spritecliptime
+					- ps_sw_portaltime - ps_sw_planetime - ps_sw_maskedtime);
+				V_DrawThinString(24, 66, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
+				V_DrawThinString(20, 74, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
+				V_DrawThinString(20, 82, V_MONOSPACE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
+				V_DrawThinString(20, 94, V_MONOSPACE | V_GRAYMAP, s);
+			}
+		}
+		else // high resolution
+		{
+			snprintf(s, sizeof s - 1, "Frame time:     %d", frametime);
+			V_DrawSmallString(20, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+			{
+				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
+				V_DrawSmallString(20, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
+				V_DrawSmallString(20, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
+				V_DrawSmallString(20, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+				return;
+			}
+			snprintf(s, sizeof s - 1, "3d rendering:   %d", ps_rendercalltime);
+			V_DrawSmallString(20, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "BSP calls:    %d", ps_numbspcalls);
+			V_DrawSmallString(115, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Sprites:      %d", ps_numsprites);
+			V_DrawSmallString(115, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Drawnodes:    %d", ps_numdrawnodes);
+			V_DrawSmallString(115, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Polyobjects:  %d", ps_numpolyobjects);
+			V_DrawSmallString(115, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+#ifdef HWRENDER
+			if (rendermode == render_opengl) // OpenGL specific stats
+			{
+				snprintf(s, sizeof s - 1, "Skybox render:  %d", ps_hw_skyboxtime);
+				V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "RenderBSPNode:  %d", ps_bsptime);
+				V_DrawSmallString(24, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Drwnode sort:   %d", ps_hw_nodesorttime);
+				V_DrawSmallString(24, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Drwnode render: %d", ps_hw_nodedrawtime);
+				V_DrawSmallString(24, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Sprite sort:    %d", ps_hw_spritesorttime);
+				V_DrawSmallString(24, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Sprite render:  %d", ps_hw_spritedrawtime);
+				V_DrawSmallString(24, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				// Remember to update this calculation when adding more 3d rendering stats!
+				snprintf(s, sizeof s - 1, "Other:          %d",
+					ps_rendercalltime - ps_hw_skyboxtime - ps_bsptime - ps_hw_nodesorttime
+					- ps_hw_nodedrawtime - ps_hw_spritesorttime - ps_hw_spritedrawtime
+					- ps_hw_batchsorttime - ps_hw_batchdrawtime);
+				V_DrawSmallString(24, 50, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
+				V_DrawSmallString(20, 55, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
+				V_DrawSmallString(20, 60, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
+				V_DrawSmallString(20, 70, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+				if (cv_glbatching.value)
+				{
+					snprintf(s, sizeof s - 1, "Batch sort:   %d", ps_hw_batchsorttime);
+					V_DrawSmallString(115, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_REDMAP, s);
+					snprintf(s, sizeof s - 1, "Batch render: %d", ps_hw_batchdrawtime);
+					V_DrawSmallString(115, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_REDMAP, s);
+
+					snprintf(s, sizeof s - 1, "Polygons:   %d", ps_hw_numpolys);
+					V_DrawSmallString(200, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Vertices:   %d", ps_hw_numverts);
+					V_DrawSmallString(200, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Draw calls: %d", ps_hw_numcalls);
+					V_DrawSmallString(200, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Shaders:    %d", ps_hw_numshaders);
+					V_DrawSmallString(200, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Textures:   %d", ps_hw_numtextures);
+					V_DrawSmallString(200, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Polyflags:  %d", ps_hw_numpolyflags);
+					V_DrawSmallString(200, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "Colors:     %d", ps_hw_numcolors);
+					V_DrawSmallString(200, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+				}
+				else
+				{
+					// reset these vars so the "other" measurement isn't off
+					ps_hw_batchsorttime = 0;
+					ps_hw_batchdrawtime = 0;
+				}
+			}
+			else // software specific stats
+#endif
+			{
+				snprintf(s, sizeof s - 1, "RenderBSPNode:  %d", ps_bsptime);
+				V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "R_ClipSprites:  %d", ps_sw_spritecliptime);
+				V_DrawSmallString(24, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Portals+Skybox: %d", ps_sw_portaltime);
+				V_DrawSmallString(24, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "R_DrawPlanes:   %d", ps_sw_planetime);
+				V_DrawSmallString(24, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "R_DrawMasked:   %d", ps_sw_maskedtime);
+				V_DrawSmallString(24, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				// Remember to update this calculation when adding more 3d rendering stats!
+				snprintf(s, sizeof s - 1, "Other:          %d",
+					ps_rendercalltime - ps_bsptime - ps_sw_spritecliptime
+					- ps_sw_portaltime - ps_sw_planetime - ps_sw_maskedtime);
+				V_DrawSmallString(24, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
+				V_DrawSmallString(20, 50, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
+				V_DrawSmallString(20, 55, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
+				V_DrawSmallString(20, 65, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+			}
+		}
+	}
+	else if (cv_perfstats.value == 2) // logic
+	{
+		int i = 0;
+		thinker_t *thinker;
+		int thinkercount = 0;
+		int polythcount = 0;
+		int mainthcount = 0;
+		int mobjcount = 0;
+		int nothinkcount = 0;
+		int scenerycount = 0;
+		int dynslopethcount = 0;
+		int precipcount = 0;
+		int removecount = 0;
+		// y offset for drawing columns
+		int yoffset1 = 0;
+		int yoffset2 = 0;
+
+		for (i = 0; i < NUM_THINKERLISTS; i++)
+		{
+			for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
+			{
+				thinkercount++;
+				if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					removecount++;
+				else if (i == THINK_POLYOBJ)
+					polythcount++;
+				else if (i == THINK_MAIN)
+					mainthcount++;
+				else if (i == THINK_MOBJ)
+				{
+					if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
+					{
+						mobj_t *mobj = (mobj_t*)thinker;
+						mobjcount++;
+						if (mobj->flags & MF_NOTHINK)
+							nothinkcount++;
+						else if (mobj->flags & MF_SCENERY)
+							scenerycount++;
+					}
+				}
+				else if (i == THINK_DYNSLOPE)
+					dynslopethcount++;
+				else if (i == THINK_PRECIP)
+					precipcount++;
+			}
+		}
+
+		if (vid.width < 640 || vid.height < 400) // low resolution
+		{
+			snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
+			V_DrawThinString(20, 10, V_MONOSPACE | V_YELLOWMAP, s);
+			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+				return;
+			snprintf(s, sizeof s - 1, "plrthnk %d", ps_playerthink_time);
+			V_DrawThinString(24, 18, V_MONOSPACE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "thnkers %d", ps_thinkertime);
+			V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
+			for (i = 0; i < NUM_THINKERLISTS; i++)
+			{
+				yoffset1 += 8;
+				snprintf(s, sizeof s - 1, thlist_shortnames[i], ps_thlist_times[i]);
+				V_DrawThinString(28, 26+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
+			}
+			snprintf(s, sizeof s - 1, "lthinkf %d", ps_lua_thinkframe_time);
+			V_DrawThinString(24, 34+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "other   %d",
+				ps_tictime - ps_playerthink_time - ps_thinkertime - ps_lua_thinkframe_time);
+			V_DrawThinString(24, 42+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
+
+			snprintf(s, sizeof s - 1, "thnkers %d", thinkercount);
+			V_DrawThinString(90, 10, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "plyobjs %d", polythcount);
+			V_DrawThinString(94, 18, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "main    %d", mainthcount);
+			V_DrawThinString(94, 26, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "mobjs   %d", mobjcount);
+			V_DrawThinString(94, 34, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "regular %d", mobjcount - scenerycount - nothinkcount);
+			V_DrawThinString(98, 42, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "scenery %d", scenerycount);
+			V_DrawThinString(98, 50, V_MONOSPACE | V_BLUEMAP, s);
+			if (nothinkcount)
+			{
+				snprintf(s, sizeof s - 1, "nothink %d", nothinkcount);
+				V_DrawThinString(98, 58, V_MONOSPACE | V_BLUEMAP, s);
+				yoffset2 += 8;
+			}
+			snprintf(s, sizeof s - 1, "dynslop %d", dynslopethcount);
+			V_DrawThinString(94, 58+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "precip  %d", precipcount);
+			V_DrawThinString(94, 66+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "remove  %d", removecount);
+			V_DrawThinString(94, 74+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
+
+			snprintf(s, sizeof s - 1, "lmhooks %d", ps_lua_mobjhooks);
+			V_DrawThinString(170, 10, V_MONOSPACE | V_PURPLEMAP, s);
+			snprintf(s, sizeof s - 1, "chkpos  %d", ps_checkposition_calls);
+			V_DrawThinString(170, 18, V_MONOSPACE | V_PURPLEMAP, s);
+		}
+		else // high resolution
+		{
+			snprintf(s, sizeof s - 1, "Game logic:      %d", ps_tictime);
+			V_DrawSmallString(20, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+				return;
+			snprintf(s, sizeof s - 1, "P_PlayerThink:   %d", ps_playerthink_time);
+			V_DrawSmallString(24, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "P_RunThinkers:   %d", ps_thinkertime);
+			V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			for (i = 0; i < NUM_THINKERLISTS; i++)
+			{
+				yoffset1 += 5;
+				snprintf(s, sizeof s - 1, thlist_names[i], ps_thlist_times[i]);
+				V_DrawSmallString(28, 20+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			}
+			snprintf(s, sizeof s - 1, "LUAh_ThinkFrame: %d", ps_lua_thinkframe_time);
+			V_DrawSmallString(24, 25+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+			snprintf(s, sizeof s - 1, "Other:           %d",
+				ps_tictime - ps_playerthink_time - ps_thinkertime - ps_lua_thinkframe_time);
+			V_DrawSmallString(24, 30+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
+
+			snprintf(s, sizeof s - 1, "Thinkers:        %d", thinkercount);
+			V_DrawSmallString(115, 10+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Polyobjects:     %d", polythcount);
+			V_DrawSmallString(119, 15+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Main:            %d", mainthcount);
+			V_DrawSmallString(119, 20+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Mobjs:           %d", mobjcount);
+			V_DrawSmallString(119, 25+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Regular:         %d", mobjcount - scenerycount - nothinkcount);
+			V_DrawSmallString(123, 30+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Scenery:         %d", scenerycount);
+			V_DrawSmallString(123, 35+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			if (nothinkcount)
+			{
+				snprintf(s, sizeof s - 1, "Nothink:         %d", nothinkcount);
+				V_DrawSmallString(123, 40+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+				yoffset2 += 5;
+			}
+			snprintf(s, sizeof s - 1, "Dynamic slopes:  %d", dynslopethcount);
+			V_DrawSmallString(119, 40+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Precipitation:   %d", precipcount);
+			V_DrawSmallString(119, 45+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			snprintf(s, sizeof s - 1, "Pending removal: %d", removecount);
+			V_DrawSmallString(119, 50+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+
+			snprintf(s, sizeof s - 1, "Calls:");
+			V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+			snprintf(s, sizeof s - 1, "Lua mobj hooks:  %d", ps_lua_mobjhooks);
+			V_DrawSmallString(216, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+			snprintf(s, sizeof s - 1, "P_CheckPosition: %d", ps_checkposition_calls);
+			V_DrawSmallString(216, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+		}
+	}
+	else if (cv_perfstats.value == 3) // lua thinkframe
+	{
+		if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+			return;
+		if (vid.width < 640 || vid.height < 400) // low resolution
+		{
+			// it's not gonna fit very well..
+			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
+		}
+		else // high resolution
+		{
+			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';
+			for (i = 0; i < thinkframe_hooks_length; i++)
+			{
+				char* str = thinkframe_hooks[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);
+						y += 4; // repeated code!
+						if (y > 192)
+						{
+							y = 4;
+							x += 106;
+							if (x > 214)
+								break;
+						}
+					}
+					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: %u", str, thinkframe_hooks[i].time_taken);
+				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
+				y += 4; // repeated code!
+				if (y > 192)
+				{
+					y = 4;
+					x += 106;
+					if (x > 214)
+						break;
+				}
+			}
+		}
+	}
+}
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
new file mode 100644
index 0000000000000000000000000000000000000000..1db46025e43c2a1baea59ef176d8682ae91e02f0
--- /dev/null
+++ b/src/m_perfstats.h
@@ -0,0 +1,42 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 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.h
+/// \brief Performance measurement tools.
+
+#ifndef __M_PERFSTATS_H__
+#define __M_PERFSTATS_H__
+
+#include "doomdef.h"
+#include "lua_script.h"
+#include "p_local.h"
+
+extern int ps_tictime;
+
+extern int ps_playerthink_time;
+extern int ps_thinkertime;
+
+extern int ps_thlist_times[];
+
+extern int ps_checkposition_calls;
+
+extern int ps_lua_thinkframe_time;
+extern int ps_lua_mobjhooks;
+
+typedef struct
+{
+	UINT32 time_taken;
+	char short_src[LUA_IDSIZE];
+} ps_hookinfo_t;
+
+void PS_SetThinkFrameHookInfo(int index, UINT32 time_taken, char* short_src);
+
+void M_DrawPerfStats(void);
+
+#endif
diff --git a/src/p_map.c b/src/p_map.c
index f2faf29b6958d50355c615e42d0a5e6dc6b54fcb..544df66d747b5940a87789d042fef834aca7b772 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -33,6 +33,8 @@
 
 #include "lua_hook.h"
 
+#include "m_perfstats.h" // ps_checkposition_calls
+
 fixed_t tmbbox[4];
 mobj_t *tmthing;
 static INT32 tmflags;
@@ -2019,6 +2021,8 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 	subsector_t *newsubsec;
 	boolean blockval = true;
 
+	ps_checkposition_calls++;
+
 	I_Assert(thing != NULL);
 #ifdef PARANOIA
 	if (P_MobjWasRemoved(thing))
diff --git a/src/p_tick.c b/src/p_tick.c
index f84ae96c0b5d860dfe26447c0403676536453fec..451e5e6266d9084586e21f0211e9a4ff3b15210d 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -21,6 +21,8 @@
 #include "m_random.h"
 #include "lua_script.h"
 #include "lua_hook.h"
+#include "m_perfstats.h"
+#include "i_system.h" // I_GetTimeMicros
 
 // Object place
 #include "m_cheat.h"
@@ -321,6 +323,7 @@ static inline void P_RunThinkers(void)
 	size_t i;
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
+		ps_thlist_times[i] = I_GetTimeMicros();
 		for (currentthinker = thlist[i].next; currentthinker != &thlist[i]; currentthinker = currentthinker->next)
 		{
 #ifdef PARANOIA
@@ -328,6 +331,7 @@ static inline void P_RunThinkers(void)
 #endif
 			currentthinker->function.acp1(currentthinker);
 		}
+		ps_thlist_times[i] = I_GetTimeMicros() - ps_thlist_times[i];
 	}
 
 }
@@ -641,11 +645,16 @@ void P_Ticker(boolean run)
 		if (demoplayback)
 			G_ReadDemoTiccmd(&players[consoleplayer].cmd, 0);
 
+		ps_lua_mobjhooks = 0;
+		ps_checkposition_calls = 0;
+
 		LUAh_PreThinkFrame();
 
+		ps_playerthink_time = I_GetTimeMicros();
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerThink(&players[i]);
+		ps_playerthink_time = I_GetTimeMicros() - ps_playerthink_time;
 	}
 
 	// Keep track of how long they've been playing!
@@ -660,14 +669,18 @@ void P_Ticker(boolean run)
 
 	if (run)
 	{
+		ps_thinkertime = I_GetTimeMicros();
 		P_RunThinkers();
+		ps_thinkertime = I_GetTimeMicros() - ps_thinkertime;
 
 		// Run any "after all the other thinkers" stuff
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerAfterThink(&players[i]);
 
+		ps_lua_thinkframe_time = I_GetTimeMicros();
 		LUAh_ThinkFrame();
+		ps_lua_thinkframe_time = I_GetTimeMicros() - ps_lua_thinkframe_time;
 	}
 
 	// Run shield positioning
diff --git a/src/r_bsp.c b/src/r_bsp.c
index a430ef04069446eb0aee9452e6acb737ae4332f5..56d038b44183a49ee7a5571251fce4af8fe888c8 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -799,7 +799,7 @@ static void R_AddPolyObjects(subsector_t *sub)
 	}
 
 	// for render stats
-	rs_numpolyobjects += numpolys;
+	ps_numpolyobjects += numpolys;
 
 	// sort polyobjects
 	R_SortPolyObjects(sub);
@@ -1239,7 +1239,7 @@ void R_RenderBSPNode(INT32 bspnum)
 	node_t *bsp;
 	INT32 side;
 
-	rs_numbspcalls++;
+	ps_numbspcalls++;
 
 	while (!(bspnum & NF_SUBSECTOR))  // Found a subsector?
 	{
diff --git a/src/r_main.c b/src/r_main.c
index 0c13e3423f03b6df43b14cd4f9524fd4df4a6718..5165b3c87b3fa51a1505fb9033715c2f713d6a46 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -100,22 +100,22 @@ lighttable_t *zlight[LIGHTLEVELS][MAXLIGHTZ];
 extracolormap_t *extra_colormaps = NULL;
 
 // Render stats
-int rs_prevframetime = 0;
-int rs_rendercalltime = 0;
-int rs_uitime = 0;
-int rs_swaptime = 0;
-int rs_tictime = 0;
+int ps_prevframetime = 0;
+int ps_rendercalltime = 0;
+int ps_uitime = 0;
+int ps_swaptime = 0;
 
-int rs_bsptime = 0;
+int ps_bsptime = 0;
 
-int rs_sw_portaltime = 0;
-int rs_sw_planetime = 0;
-int rs_sw_maskedtime = 0;
+int ps_sw_spritecliptime = 0;
+int ps_sw_portaltime = 0;
+int ps_sw_planetime = 0;
+int ps_sw_maskedtime = 0;
 
-int rs_numbspcalls = 0;
-int rs_numsprites = 0;
-int rs_numdrawnodes = 0;
-int rs_numpolyobjects = 0;
+int ps_numbspcalls = 0;
+int ps_numsprites = 0;
+int ps_numdrawnodes = 0;
+int ps_numpolyobjects = 0;
 
 static CV_PossibleValue_t drawdist_cons_t[] = {
 	{256, "256"},	{512, "512"},	{768, "768"},
@@ -1490,11 +1490,11 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	rs_numbspcalls = rs_numpolyobjects = rs_numdrawnodes = 0;
-	rs_bsptime = I_GetTimeMicros();
+	ps_numbspcalls = ps_numpolyobjects = ps_numdrawnodes = 0;
+	ps_bsptime = I_GetTimeMicros();
 	R_RenderBSPNode((INT32)numnodes - 1);
-	rs_bsptime = I_GetTimeMicros() - rs_bsptime;
-	rs_numsprites = visspritecount;
+	ps_bsptime = I_GetTimeMicros() - ps_bsptime;
+	ps_numsprites = visspritecount;
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1504,7 +1504,9 @@ void R_RenderPlayerView(player_t *player)
 //profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
+	ps_sw_spritecliptime = I_GetTimeMicros();
 	R_ClipSprites(drawsegs, NULL);
+	ps_sw_spritecliptime = I_GetTimeMicros() - ps_sw_spritecliptime;
 
 
 	// Add skybox portals caused by sky visplanes.
@@ -1512,7 +1514,7 @@ void R_RenderPlayerView(player_t *player)
 		Portal_AddSkyboxPortals();
 
 	// Portal rendering. Hijacks the BSP traversal.
-	rs_sw_portaltime = I_GetTimeMicros();
+	ps_sw_portaltime = I_GetTimeMicros();
 	if (portal_base)
 	{
 		portal_t *portal;
@@ -1552,20 +1554,20 @@ void R_RenderPlayerView(player_t *player)
 			Portal_Remove(portal);
 		}
 	}
-	rs_sw_portaltime = I_GetTimeMicros() - rs_sw_portaltime;
+	ps_sw_portaltime = I_GetTimeMicros() - ps_sw_portaltime;
 
-	rs_sw_planetime = I_GetTimeMicros();
+	ps_sw_planetime = I_GetTimeMicros();
 	R_DrawPlanes();
 #ifdef FLOORSPLATS
 	R_DrawVisibleFloorSplats();
 #endif
-	rs_sw_planetime = I_GetTimeMicros() - rs_sw_planetime;
+	ps_sw_planetime = I_GetTimeMicros() - ps_sw_planetime;
 
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	rs_sw_maskedtime = I_GetTimeMicros();
+	ps_sw_maskedtime = I_GetTimeMicros();
 	R_DrawMasked(masks, nummasks);
-	rs_sw_maskedtime = I_GetTimeMicros() - rs_sw_maskedtime;
+	ps_sw_maskedtime = I_GetTimeMicros() - ps_sw_maskedtime;
 
 	free(masks);
 }
diff --git a/src/r_main.h b/src/r_main.h
index 5466d2a25204fa6170b721fcfc06982d2a833734..379b5b8df1f5d306e50b2eed802c058749cb3928 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -78,24 +78,22 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 
 // Render stats
 
-extern consvar_t cv_renderstats;
-
-extern int rs_prevframetime;// time when previous frame was rendered
-extern int rs_rendercalltime;
-extern int rs_uitime;
-extern int rs_swaptime;
-extern int rs_tictime;
-
-extern int rs_bsptime;
-
-extern int rs_sw_portaltime;
-extern int rs_sw_planetime;
-extern int rs_sw_maskedtime;
-
-extern int rs_numbspcalls;
-extern int rs_numsprites;
-extern int rs_numdrawnodes;
-extern int rs_numpolyobjects;
+extern int ps_prevframetime;// time when previous frame was rendered
+extern int ps_rendercalltime;
+extern int ps_uitime;
+extern int ps_swaptime;
+
+extern int ps_bsptime;
+
+extern int ps_sw_spritecliptime;
+extern int ps_sw_portaltime;
+extern int ps_sw_planetime;
+extern int ps_sw_maskedtime;
+
+extern int ps_numbspcalls;
+extern int ps_numsprites;
+extern int ps_numdrawnodes;
+extern int ps_numpolyobjects;
 
 //
 // REFRESH - the actual rendering functions.
diff --git a/src/r_things.c b/src/r_things.c
index 382eed5608b372d7a2d3c83191d20fa9f0626f66..cc205f9eab51aa97d7030f7262cff0b3c2059cc5 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -2531,7 +2531,7 @@ static drawnode_t *R_CreateDrawNode(drawnode_t *link)
 	node->ffloor = NULL;
 	node->sprite = NULL;
 
-	rs_numdrawnodes++;
+	ps_numdrawnodes++;
 	return node;
 }
 
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 5b0ff7425f3337e4342ffa33d441dce294389078..c2d6456e462bbf67bca399703e5b30b63d773d0a 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -262,6 +262,7 @@
     <ClInclude Include="..\m_fixed.h" />
     <ClInclude Include="..\m_menu.h" />
     <ClInclude Include="..\m_misc.h" />
+    <ClInclude Include="..\m_perfstats.h" />
     <ClInclude Include="..\m_queue.h" />
     <ClInclude Include="..\m_random.h" />
     <ClInclude Include="..\m_swap.h" />
@@ -418,6 +419,7 @@
     <ClCompile Include="..\m_fixed.c" />
     <ClCompile Include="..\m_menu.c" />
     <ClCompile Include="..\m_misc.c" />
+    <ClCompile Include="..\m_perfstats.c" />
     <ClCompile Include="..\m_queue.c" />
     <ClCompile Include="..\m_random.c" />
     <ClCompile Include="..\p_ceilng.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 5053843136a6146dbc6267bb54bd8f09ecc4b44f..438746ac7f02a01745f3785c9bf60b1e969e8de1 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -348,6 +348,9 @@
     <ClInclude Include="..\m_misc.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
+    <ClInclude Include="..\m_perfstats.h">
+      <Filter>M_Misc</Filter>
+    </ClInclude>
     <ClInclude Include="..\m_queue.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
@@ -765,6 +768,9 @@
     <ClCompile Include="..\m_misc.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
+    <ClCompile Include="..\m_perfstats.c">
+      <Filter>M_Misc</Filter>
+    </ClCompile>
     <ClCompile Include="..\m_queue.c">
       <Filter>M_Misc</Filter>
     </ClCompile>