diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
index f0b59658ee741e7d709b4d4037b1745a1e3bfefb..d2877b374346115621773c822af00a92dd4de72c 100644
--- a/src/Makefile.d/versions.mk
+++ b/src/Makefile.d/versions.mk
@@ -156,7 +156,7 @@ ifdef DEBUGMODE
 ifdef GCC48
 opts+=-Og
 else
-opts+=O0
+opts+=-O0
 endif
 endif
 
diff --git a/src/Sourcefile b/src/Sourcefile
index 983dadaf0cbea42079ce68f032323c6c03a4a595..de90bb60910286242ae4b62b41af5d9ad57706ad 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -18,6 +18,7 @@ am_map.c
 command.c
 console.c
 hu_stuff.c
+i_time.c
 y_inter.c
 st_stuff.c
 m_aatree.c
@@ -55,6 +56,7 @@ tables.c
 r_bsp.c
 r_data.c
 r_draw.c
+r_fps.c
 r_main.c
 r_plane.c
 r_segs.c
@@ -95,4 +97,5 @@ lua_taglib.c
 lua_polyobjlib.c
 lua_blockmaplib.c
 lua_hudlib.c
+lua_hudlib_drawlist.c
 lua_inputlib.c
diff --git a/src/android/i_system.c b/src/android/i_system.c
index 752e9f6c6bb4138caaf17004cbcc0fcaa6b66cee..1fba4d82536bc532cd8ee498afd105c35fd59d15 100644
--- a/src/android/i_system.c
+++ b/src/android/i_system.c
@@ -82,13 +82,17 @@ INT64 current_time_in_ps() {
   return (t.tv_sec * (INT64)1000000) + t.tv_usec;
 }
 
-tic_t I_GetTime(void)
+void I_Sleep(UINT32 ms){}
+
+precise_t I_GetPreciseTime(void)
 {
-  INT64 since_start = current_time_in_ps() - start_time;
-  return (since_start*TICRATE)/1000000;
+	return 0;
 }
 
-void I_Sleep(void){}
+UINT64 I_GetPrecisePrecision(void) 
+{
+	return 1000000;
+}
 
 void I_GetEvent(void){}
 
diff --git a/src/b_bot.c b/src/b_bot.c
index 775a13e294cf31e8070a9f6fc51894f91317496c..f1df5abbd445ee14a62135ddfe65934a35432a66 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -579,7 +579,7 @@ void B_RespawnBot(INT32 playernum)
 	player->powers[pw_nocontrol] = sonic->player->powers[pw_nocontrol];
 	player->pflags |= PF_AUTOBRAKE|(sonic->player->pflags & PF_DIRECTIONCHAR);
 
-	P_TeleportMove(tails, x, y, z);
+	P_SetOrigin(tails, x, y, z);
 	if (player->charability == CA_FLY)
 	{
 		P_SetPlayerMobjState(tails, S_PLAY_FLY);
diff --git a/src/command.c b/src/command.c
index dae4dc7b160e1399ccba0ae6f4874cf120e0524c..c849341ffcc02b3fa8ca0435c5640439b0fd0aa8 100644
--- a/src/command.c
+++ b/src/command.c
@@ -2076,9 +2076,10 @@ void CV_AddValue(consvar_t *var, INT32 increment)
 					{
 						increment = 0;
 						currentindice = max;
+						break; // The value we definitely want, stop here.
 					}
 					else if (var->PossibleValue[max].value == var->value)
-						currentindice = max;
+						currentindice = max; // The value we maybe want.
 				}
 
 				if (increment)
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index ac8bba608288063b7b34dff533f9d2ef0e9c84e2..4cd6333c5e6f1b8a831041d36927b0ee82bc4a1d 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -15,6 +15,7 @@
 #include <unistd.h> //for unlink
 #endif
 
+#include "i_time.h"
 #include "i_net.h"
 #include "i_system.h"
 #include "i_video.h"
@@ -113,6 +114,9 @@ static INT16 consistancy[BACKUPTICS];
 static UINT8 player_joining = false;
 UINT8 hu_redownloadinggamestate = 0;
 
+// true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks
+boolean hu_stopped = false;
+
 UINT8 adminpassmd5[16];
 boolean adminpasswordset = false;
 
@@ -2441,7 +2445,10 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 #endif
 	}
 	else
-		I_Sleep();
+	{
+		I_Sleep(cv_sleep.value);
+		I_UpdateTime(cv_timescale.value);
+	}
 
 	return true;
 }
@@ -5206,8 +5213,10 @@ static void SV_Maketic(void)
 	maketic++;
 }
 
-void TryRunTics(tic_t realtics)
+boolean TryRunTics(tic_t realtics)
 {
+	boolean ticking;
+
 	// the machine has lagged but it is not so bad
 	if (realtics > TICRATE/7) // FIXME: consistency failure!!
 	{
@@ -5231,7 +5240,7 @@ void TryRunTics(tic_t realtics)
 
 	if (demoplayback)
 	{
-		neededtic = gametic + (realtics * cv_playbackspeed.value);
+		neededtic = gametic + realtics;
 		// start a game after a demo
 		maketic += realtics;
 		firstticstosend = maketic;
@@ -5251,10 +5260,22 @@ void TryRunTics(tic_t realtics)
 	}
 #endif
 
+	ticking = neededtic > gametic;
+
+	if (ticking)
+	{
+		if (realtics)
+			hu_stopped = false;
+	}
+
 	if (player_joining)
-		return;
+	{
+		if (realtics)
+			hu_stopped = true;
+		return false;
+	}
 
-	if (neededtic > gametic)
+	if (ticking)
 	{
 		if (advancedemo)
 		{
@@ -5290,6 +5311,13 @@ void TryRunTics(tic_t realtics)
 					break;
 			}
 	}
+	else
+	{
+		if (realtics)
+			hu_stopped = true;
+	}
+
+	return ticking;
 }
 
 /*
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index bf3f0b64f5126eca92510a07686e10afbd07cba7..e07864122455bf0960e2299ba5ef7c3d9b2ac39f 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -430,7 +430,7 @@ boolean Playing(void);
 void D_QuitNetGame(void);
 
 //? How many ticks to run?
-void TryRunTics(tic_t realtic);
+boolean TryRunTics(tic_t realtic);
 
 // extra data for lmps
 // these functions scare me. they contain magic.
@@ -458,4 +458,7 @@ extern UINT8 hu_redownloadinggamestate;
 
 extern UINT8 adminpassmd5[16];
 extern boolean adminpasswordset;
+
+extern boolean hu_stopped;
+
 #endif
diff --git a/src/d_main.c b/src/d_main.c
index fa9e21337ced42286e27b75a7bd2987ee62538e5..91a42ff140dd60277b5b70edb1f82587753354cd 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -40,6 +40,7 @@
 #include "hu_stuff.h"
 #include "i_sound.h"
 #include "i_system.h"
+#include "i_time.h"
 #include "i_threads.h"
 #include "i_video.h"
 #include "m_argv.h"
@@ -64,6 +65,7 @@
 #include "deh_tables.h" // Dehacked list test
 #include "m_cond.h" // condition initialization
 #include "fastcmp.h"
+#include "r_fps.h" // Frame interpolation/uncapped
 #include "keys.h"
 #include "filesrch.h" // refreshdirmenu
 #include "g_input.h" // tutorial mode control scheming
@@ -476,6 +478,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
+				R_ApplyLevelInterpolators(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
@@ -524,6 +527,7 @@ static void D_Display(void)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
 				PS_STOP_TIMING(ps_rendercalltime);
+				R_RestoreLevelInterpolators();
 			}
 
 			if (lastdraw)
@@ -692,9 +696,14 @@ tic_t rendergametic;
 
 void D_SRB2Loop(void)
 {
-	tic_t oldentertics = 0, entertic = 0, realtics = 0, rendertimeout = INFTICS;
+	tic_t entertic = 0, oldentertics = 0, realtics = 0, rendertimeout = INFTICS;
+	double deltatics = 0.0;
+	double deltasecs = 0.0;
 	static lumpnum_t gstartuplumpnum;
 
+	boolean interp = false;
+	boolean doDisplay = false;
+
 	if (dedicated)
 		server = true;
 
@@ -705,6 +714,7 @@ void D_SRB2Loop(void)
 	I_DoStartupMouse();
 #endif
 
+	I_UpdateTime(cv_timescale.value);
 	oldentertics = I_GetTime();
 
 	// end of loading screen: CONS_Printf() will no more call FinishUpdate()
@@ -745,6 +755,19 @@ void D_SRB2Loop(void)
 
 	for (;;)
 	{
+		// capbudget is the minimum precise_t duration of a single loop iteration
+		precise_t capbudget;
+		precise_t enterprecise = I_GetPreciseTime();
+		precise_t finishprecise = enterprecise;
+
+		{
+			// Casting the return value of a function is bad practice (apparently)
+			double budget = round((1.0 / R_GetFramerateCap()) * I_GetPrecisePrecision());
+			capbudget = (precise_t) budget;
+		}
+
+		I_UpdateTime(cv_timescale.value);
+
 		if (lastwipetic)
 		{
 			oldentertics = lastwipetic;
@@ -756,7 +779,11 @@ void D_SRB2Loop(void)
 		realtics = entertic - oldentertics;
 		oldentertics = entertic;
 
-		refreshdirmenu = 0; // not sure where to put this, here as good as any?
+		if (demoplayback && gamestate == GS_LEVEL)
+		{
+			// Nicer place to put this.
+			realtics = realtics * cv_playbackspeed.value;
+		}
 
 #ifdef DEBUGFILE
 		if (!realtics)
@@ -764,64 +791,119 @@ void D_SRB2Loop(void)
 				debugload--;
 #endif
 
-		if (!realtics && !singletics)
-		{
-			I_Sleep();
-			continue;
-		}
+		interp = R_UsingFrameInterpolation() && !dedicated;
+		doDisplay = false;
 
 #ifdef HW3SOUND
 		HW3S_BeginFrameUpdate();
 #endif
 
-		// don't skip more than 10 frames at a time
-		// (fadein / fadeout cause massive frame skip!)
-		if (realtics > 8)
-			realtics = 1;
-
-		// process tics (but maybe not if realtic == 0)
-		TryRunTics(realtics);
+		refreshdirmenu = 0; // not sure where to put this, here as good as any?
 
-		if (lastdraw || singletics || gametic > rendergametic)
+		if (realtics > 0 || singletics)
 		{
-			rendergametic = gametic;
-			rendertimeout = entertic+TICRATE/17;
+			// don't skip more than 10 frames at a time
+			// (fadein / fadeout cause massive frame skip!)
+			if (realtics > 8)
+				realtics = 1;
 
-			// Update display, next frame, with current state.
-			D_Display();
+			// process tics (but maybe not if realtic == 0)
+			TryRunTics(realtics);
+
+			if (lastdraw || singletics || gametic > rendergametic)
+			{
+				rendergametic = gametic;
+				rendertimeout = entertic + TICRATE/17;
+
+				doDisplay = true;
+			}
+			else if (rendertimeout < entertic) // in case the server hang or netsplit
+			{
+				// Lagless camera! Yay!
+				if (gamestate == GS_LEVEL && netgame)
+				{
+					// Evaluate the chase cam once for every local realtic
+					// This might actually be better suited inside G_Ticker or TryRunTics
+					for (tic_t chasecamtics = 0; chasecamtics < realtics; chasecamtics++)
+					{
+						if (splitscreen && camera2.chase)
+							P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
+						if (camera.chase)
+							P_MoveChaseCamera(&players[displayplayer], &camera, false);
+					}
+					R_UpdateViewInterpolation();
+				}
 
-			if (moviemode)
-				M_SaveFrame();
-			if (takescreenshot) // Only take screenshots after drawing.
-				M_DoScreenShot();
+				doDisplay = true;
+			}
+
+			renderisnewtic = true;
+		}
+		else
+		{
+			renderisnewtic = false;
 		}
-		else if (rendertimeout < entertic) // in case the server hang or netsplit
+
+		if (interp)
 		{
-			// Lagless camera! Yay!
-			if (gamestate == GS_LEVEL && netgame)
+			// I looked at the possibility of putting in a float drawer for
+			// perfstats and it's very complicated, so we'll just do this instead...
+			ps_interp_frac.value.p = (precise_t)((FIXED_TO_FLOAT(g_time.timefrac)) * 1000.0f);
+			ps_interp_lag.value.p = (precise_t)((deltasecs) * 1000.0f);
+
+			renderdeltatics = FLOAT_TO_FIXED(deltatics);
+
+			if (!(paused || P_AutoPause()) && deltatics < 1.0 && !hu_stopped)
 			{
-				if (splitscreen && camera2.chase)
-					P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
-				if (camera.chase)
-					P_MoveChaseCamera(&players[displayplayer], &camera, false);
+				rendertimefrac = g_time.timefrac;
 			}
-			D_Display();
+			else
+			{
+				rendertimefrac = FRACUNIT;
+			}
+		}
+		else
+		{
+			renderdeltatics = realtics * FRACUNIT;
+			rendertimefrac = FRACUNIT;
+		}
 
-			if (moviemode)
-				M_SaveFrame();
-			if (takescreenshot) // Only take screenshots after drawing.
-				M_DoScreenShot();
+		if (interp || doDisplay)
+		{
+			D_Display();
 		}
 
-		// consoleplayer -> displayplayer (hear sounds from viewpoint)
+		// Only take screenshots after drawing.
+		if (moviemode)
+			M_SaveFrame();
+		if (takescreenshot)
+			M_DoScreenShot();
+
+		// consoleplayer -> displayplayers (hear sounds from viewpoint)
 		S_UpdateSounds(); // move positional sounds
-		S_UpdateClosedCaptions();
+		if (realtics > 0 || singletics)
+			S_UpdateClosedCaptions();
 
 #ifdef HW3SOUND
 		HW3S_EndFrameUpdate();
 #endif
 
 		LUA_Step();
+
+		// Fully completed frame made.
+		finishprecise = I_GetPreciseTime();
+		if (!singletics)
+		{
+			INT64 elapsed = (INT64)(finishprecise - enterprecise);
+			if (elapsed > 0 && (INT64)capbudget > elapsed)
+			{
+				I_SleepDuration(capbudget - (finishprecise - enterprecise));
+			}
+		}
+		// Capture the time once more to get the real delta time.
+		finishprecise = I_GetPreciseTime();
+		deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision();
+		deltatics = deltasecs * NEWTICRATE;
 	}
 }
 
@@ -1307,8 +1389,8 @@ void D_SRB2Main(void)
 	//---------------------------------------------------- READY TIME
 	// we need to check for dedicated before initialization of some subsystems
 
-	CONS_Printf("I_StartupTimer()...\n");
-	I_StartupTimer();
+	CONS_Printf("I_InitializeTime()...\n");
+	I_InitializeTime();
 
 	// Make backups of some SOCcable tables.
 	P_BackupTables();
@@ -1583,6 +1665,8 @@ void D_SRB2Main(void)
 		// as having been modified for the first game.
 		M_PushSpecialParameters(); // push all "+" parameter at the command buffer
 
+		COM_BufExecute(); // ensure the command buffer gets executed before the map starts (+skin)
+
 		if (M_CheckParm("-gametype") && M_IsNextParm())
 		{
 			// from Command_Map_f
diff --git a/src/d_net.c b/src/d_net.c
index 5e5c10889c2a3f2a0383f005107d0ce277e7082c..a7e1eb16d6d4fc53f118da9e45d8f8baf4f94590 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -18,6 +18,7 @@
 
 #include "doomdef.h"
 #include "g_game.h"
+#include "i_time.h"
 #include "i_net.h"
 #include "i_system.h"
 #include "m_argv.h"
@@ -614,7 +615,10 @@ void Net_WaitAllAckReceived(UINT32 timeout)
 	while (timeout > I_GetTime() && !Net_AllAcksReceived())
 	{
 		while (tictac == I_GetTime())
-			I_Sleep();
+		{
+			I_Sleep(cv_sleep.value);
+			I_UpdateTime(cv_timescale.value);
+		}
 		tictac = I_GetTime();
 		HGetPacket();
 		Net_AckTicker();
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 1733d33240753f269a7eabd1e86de449d3e81597..4e90db0dcc81c3bf45b03d8951f5465bdd2cabf6 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -16,6 +16,7 @@
 
 #include "console.h"
 #include "command.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "g_game.h"
 #include "hu_stuff.h"
@@ -191,7 +192,7 @@ static CV_PossibleValue_t joyport_cons_t[] = {{1, "/dev/js0"}, {2, "/dev/js1"},
 static CV_PossibleValue_t teamscramble_cons_t[] = {{0, "Off"}, {1, "Random"}, {2, "Points"}, {0, NULL}};
 
 static CV_PossibleValue_t startingliveslimit_cons_t[] = {{1, "MIN"}, {99, "MAX"}, {0, NULL}};
-static CV_PossibleValue_t sleeping_cons_t[] = {{-1, "MIN"}, {1000/TICRATE, "MAX"}, {0, NULL}};
+static CV_PossibleValue_t sleeping_cons_t[] = {{0, "MIN"}, {1000/TICRATE, "MAX"}, {0, NULL}};
 static CV_PossibleValue_t competitionboxes_cons_t[] = {{0, "Normal"}, {1, "Mystery"}, //{2, "Teleport"},
 	{3, "None"}, {0, NULL}};
 
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 37fb7265f8abe714d90dab7c0882f55d73bc1cb0..edbef30bbf92ba6156da44f3d30fb32d8ab0cccb 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -35,6 +35,7 @@
 #include "doomstat.h"
 #include "d_main.h"
 #include "g_game.h"
+#include "i_time.h"
 #include "i_net.h"
 #include "i_system.h"
 #include "m_argv.h"
diff --git a/src/d_player.h b/src/d_player.h
index 6df6689c5fcf4f1085511218d7f19047cd8a7c0f..1067c86814e1398c26f67301a8d9680d773a6fe9 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -383,6 +383,8 @@ typedef struct player_s
 
 	// fun thing for player sprite
 	angle_t drawangle;
+	angle_t old_drawangle;
+	angle_t old_drawangle2;
 
 	// player's ring count
 	INT16 rings;
diff --git a/src/dummy/i_system.c b/src/dummy/i_system.c
index a1d7572048d2f65144150530f6aa3dc52d9ae7ea..997115ad0c8da17a2aa66e727ab166b500a6dee4 100644
--- a/src/dummy/i_system.c
+++ b/src/dummy/i_system.c
@@ -14,24 +14,16 @@ UINT32 I_GetFreeMem(UINT32 *total)
 	return 0;
 }
 
-tic_t I_GetTime(void)
-{
-	return 0;
-}
+void I_Sleep(UINT32 ms){}
 
-precise_t I_GetPreciseTime(void)
-{
+precise_t I_GetPreciseTime(void) {
 	return 0;
 }
 
-int I_PreciseToMicros(precise_t d)
-{
-	(void)d;
-	return 0;
+UINT64 I_GetPrecisePrecision(void) {
+	return 1000000;
 }
 
-void I_Sleep(void){}
-
 void I_GetEvent(void){}
 
 void I_OsPolling(void){}
diff --git a/src/f_finale.c b/src/f_finale.c
index ec325206b426a4ffb986232915f5b3abc06172ce..ef5b5cbf49d546df245f0febdbd2a6711830c058 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -20,6 +20,7 @@
 #include "hu_stuff.h"
 #include "r_local.h"
 #include "s_sound.h"
+#include "i_time.h"
 #include "i_video.h"
 #include "v_video.h"
 #include "w_wad.h"
@@ -62,8 +63,6 @@ static tic_t stoptimer;
 static boolean keypressed = false;
 
 // (no longer) De-Demo'd Title Screen
-static tic_t xscrolltimer;
-static tic_t yscrolltimer;
 static INT32 menuanimtimer; // Title screen: background animation timing
 mobj_t *titlemapcameraref = NULL;
 
@@ -229,6 +228,8 @@ static tic_t cutscene_lasttextwrite = 0;
 // STJR Intro
 char stjrintro[9] = "STJRI000";
 
+static huddrawlist_h luahuddrawlist_title;
+
 //
 // This alters the text string cutscene_disptext.
 // Use the typical string drawing functions to display it.
@@ -517,9 +518,9 @@ void F_StartIntro(void)
 }
 
 //
-// F_IntroDrawScene
+// F_IntroDrawer
 //
-static void F_IntroDrawScene(void)
+void F_IntroDrawer(void)
 {
 	boolean highres = true;
 	INT32 cx = 8, cy = 128;
@@ -625,24 +626,22 @@ static void F_IntroDrawScene(void)
 		if (intro_curtime > 1 && intro_curtime < (INT32)introscenetime[intro_scenenum])
 		{
 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
 			if (intro_curtime < TICRATE-5) // Make the text shine!
+			{
 				sprintf(stjrintro, "STJRI%03u", intro_curtime-1);
+			}
 			else if (intro_curtime >= TICRATE-6 && intro_curtime < 2*TICRATE-20) // Pause on black screen for just a second
+			{
 				return;
+			}
 			else if (intro_curtime == 2*TICRATE-19)
 			{
 				// Fade in the text
 				// The text fade out is automatically handled when switching to a new intro scene
 				strncpy(stjrintro, "STJRI029", 9);
-				S_ChangeMusicInternal("_stjr", false);
-
 				background = W_CachePatchName(stjrintro, PU_PATCH_LOWPRIORITY);
-				wipestyleflags = WSF_FADEIN;
-				F_WipeStartScreen();
-				F_TryColormapFade(31);
 				V_DrawSmallScaledPatch(bgxoffs, 84, 0, background);
-				F_WipeEndScreen();
-				F_RunWipe(0,true);
 			}
 
 			if (!WipeInAction) // Draw the patch if not in a wipe
@@ -841,17 +840,27 @@ static void F_IntroDrawScene(void)
 		V_DrawRightAlignedString(BASEVIDWIDTH-4, BASEVIDHEIGHT-12, V_ALLOWLOWERCASE|(trans<<V_ALPHASHIFT), "\x86""Press ""\x82""ENTER""\x86"" to skip...");
 	}
 
-	if (animtimer)
-		animtimer--;
-
 	V_DrawString(cx, cy, V_ALLOWLOWERCASE, cutscene_disptext);
 }
 
 //
-// F_IntroDrawer
+// F_IntroTicker
 //
-void F_IntroDrawer(void)
+void F_IntroTicker(void)
 {
+	// advance animation
+	finalecount++;
+
+	timetonext--;
+
+	F_WriteText();
+
+	// check for skipping
+	if (keypressed)
+		keypressed = false;
+
+	wipestyleflags = WSF_CROSSFADE;
+
 	if (timetonext <= 0)
 	{
 		if (intro_scenenum == 0)
@@ -861,6 +870,9 @@ void F_IntroDrawer(void)
 				wipestyleflags = WSF_FADEOUT;
 				F_WipeStartScreen();
 				F_TryColormapFade(31);
+
+				F_IntroDrawer();
+
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -874,6 +886,9 @@ void F_IntroDrawer(void)
 				wipestyleflags = (WSF_FADEOUT|WSF_TOWHITE);
 				F_WipeStartScreen();
 				F_TryColormapFade(0);
+
+				F_IntroDrawer();
+
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -885,6 +900,9 @@ void F_IntroDrawer(void)
 				wipestyleflags = WSF_FADEOUT;
 				F_WipeStartScreen();
 				F_TryColormapFade(31);
+
+				F_IntroDrawer();
+
 				F_WipeEndScreen();
 				F_RunWipe(99,true);
 			}
@@ -897,7 +915,10 @@ void F_IntroDrawer(void)
 				while (quittime > nowtime)
 				{
 					while (!((nowtime = I_GetTime()) - lasttime))
-						I_Sleep();
+					{
+						I_Sleep(cv_sleep.value);
+						I_UpdateTime(cv_timescale.value);
+					}
 					lasttime = nowtime;
 
 					I_OsPolling();
@@ -920,12 +941,12 @@ void F_IntroDrawer(void)
 			wipegamestate = GS_INTRO;
 			return;
 		}
+
 		F_NewCutscene(introtext[++intro_scenenum]);
 		timetonext = introscenetime[intro_scenenum];
 
 		F_WipeStartScreen();
 		wipegamestate = -1;
-		wipestyleflags = WSF_CROSSFADE;
 		animtimer = stoptimer = 0;
 	}
 
@@ -933,78 +954,36 @@ void F_IntroDrawer(void)
 
 	if (rendermode != render_none)
 	{
-		if (intro_scenenum == 5 && intro_curtime == 5*TICRATE)
+		if (intro_scenenum == 0 && intro_curtime == 2*TICRATE-19)
 		{
-			patch_t *radar = W_CachePatchName("RADAR", PU_PATCH_LOWPRIORITY);
+			S_ChangeMusicInternal("_stjr", false);
 
+			wipestyleflags = WSF_FADEIN;
 			F_WipeStartScreen();
-			F_WipeColorFill(31);
-			V_DrawSmallScaledPatch(0, 0, 0, radar);
-			W_UnlockCachedPatch(radar);
-			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
+			F_TryColormapFade(31);
 
-			F_WipeEndScreen();
-			F_RunWipe(99,true);
-		}
-		else if (intro_scenenum == 7 && intro_curtime == 6*TICRATE) // Force a wipe here
-		{
-			patch_t *grass = W_CachePatchName("SGRASS2", PU_PATCH_LOWPRIORITY);
-
-			F_WipeStartScreen();
-			F_WipeColorFill(31);
-			V_DrawSmallScaledPatch(0, 0, 0, grass);
-			W_UnlockCachedPatch(grass);
-			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
+			F_IntroDrawer();
 
 			F_WipeEndScreen();
 			F_RunWipe(99,true);
 		}
-		/*else if (intro_scenenum == 11 && intro_curtime == 7*TICRATE)
+		else if ((intro_scenenum == 5 && intro_curtime == 5*TICRATE)
+			|| (intro_scenenum == 7 && intro_curtime == 6*TICRATE)
+			//|| (intro_scenenum == 11 && intro_curtime == 7*TICRATE)
+			|| (intro_scenenum == 15 && intro_curtime == 7*TICRATE))
 		{
-			patch_t *confront = W_CachePatchName("CONFRONT", PU_PATCH_LOWPRIORITY);
-
 			F_WipeStartScreen();
 			F_WipeColorFill(31);
-			V_DrawSmallScaledPatch(0, 0, 0, confront);
-			W_UnlockCachedPatch(confront);
-			V_DrawString(8, 128, V_ALLOWLOWERCASE, cutscene_disptext);
-
-			F_WipeEndScreen();
-			F_RunWipe(99,true);
-		}*/
-		if (intro_scenenum == 15 && intro_curtime == 7*TICRATE)
-		{
-			patch_t *sdo = W_CachePatchName("SONICDO2", PU_PATCH_LOWPRIORITY);
 
-			F_WipeStartScreen();
-			F_WipeColorFill(31);
-			V_DrawSmallScaledPatch(0, 0, 0, sdo);
-			W_UnlockCachedPatch(sdo);
-			V_DrawString(224, 8, V_ALLOWLOWERCASE, cutscene_disptext);
+			F_IntroDrawer();
 
 			F_WipeEndScreen();
 			F_RunWipe(99,true);
 		}
 	}
 
-	F_IntroDrawScene();
-}
-
-//
-// F_IntroTicker
-//
-void F_IntroTicker(void)
-{
-	// advance animation
-	finalecount++;
-
-	timetonext--;
-
-	F_WriteText();
-
-	// check for skipping
-	if (keypressed)
-		keypressed = false;
+	if (animtimer)
+		animtimer--;
 }
 
 //
@@ -2304,6 +2283,9 @@ void F_InitMenuPresValues(void)
 	M_SetMenuCurBackground((gamestate == GS_TIMEATTACK) ? "RECATTBG" : "TITLESKY");
 	M_SetMenuCurFadeValue(16);
 	M_SetMenuCurTitlePics();
+
+	LUA_HUD_DestroyDrawList(luahuddrawlist_title);
+	luahuddrawlist_title = LUA_HUD_CreateDrawList();
 }
 
 //
@@ -2319,6 +2301,7 @@ void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname)
 	INT32 pw, ph; // scaled by dupz
 	patch_t *pat;
 	INT32 i, j;
+	fixed_t fracmenuanimtimer, xscrolltimer, yscrolltimer;
 
 	if (rendermode == render_none)
 		return;
@@ -2345,12 +2328,13 @@ void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname)
 	tilex = max(FixedCeil(FixedDiv(vid.width, pw)) >> FRACBITS, 1)+2; // one tile on both sides of center
 	tiley = max(FixedCeil(FixedDiv(vid.height, ph)) >> FRACBITS, 1)+2;
 
-	xscrolltimer = ((menuanimtimer*scrollxspeed)/16 + patwidth*xneg) % (patwidth);
-	yscrolltimer = ((menuanimtimer*scrollyspeed)/16 + patheight*yneg) % (patheight);
+	fracmenuanimtimer = (menuanimtimer * FRACUNIT) - (FRACUNIT - rendertimefrac);
+	xscrolltimer = ((fracmenuanimtimer*scrollxspeed)/16 + patwidth*xneg*FRACUNIT) % (patwidth * FRACUNIT);
+	yscrolltimer = ((fracmenuanimtimer*scrollyspeed)/16 + patheight*yneg*FRACUNIT) % (patheight * FRACUNIT);
 
 	// coordinate offsets
-	xscrolled = xscrolltimer * dupz;
-	yscrolled = yscrolltimer * dupz;
+	xscrolled = FixedInt(xscrolltimer * dupz);
+	yscrolled = FixedInt(yscrolltimer * dupz);
 
 	for (x = (xispos) ? -pw*(tilex-1)+pw : 0, i = 0;
 		i < tilex;
@@ -3421,7 +3405,21 @@ void F_TitleScreenDrawer(void)
 	}
 
 luahook:
-	LUA_HUDHOOK(title);
+	// The title drawer is sometimes called without first being started
+	// In order to avoid use-before-initialization crashes, let's check and
+	// create the drawlist if it doesn't exist.
+	if (!LUA_HUD_IsDrawListValid(luahuddrawlist_title))
+	{
+		LUA_HUD_DestroyDrawList(luahuddrawlist_title);
+		luahuddrawlist_title = LUA_HUD_CreateDrawList();
+	}
+
+	if (renderisnewtic)
+	{
+		LUA_HUD_ClearDrawList(luahuddrawlist_title);
+		LUA_HUDHOOK(title, luahuddrawlist_title);
+	}
+	LUA_HUD_DrawList(luahuddrawlist_title);
 }
 
 // separate animation timer for backgrounds, since we also count
@@ -3858,11 +3856,27 @@ boolean F_ContinueResponder(event_t *event)
 static INT32 scenenum, cutnum;
 static INT32 picxpos, picypos, picnum, pictime, picmode, numpics, pictoloop;
 static INT32 textxpos, textypos;
-static boolean dofadenow = false, cutsceneover = false;
+static boolean cutsceneover = false;
 static boolean runningprecutscene = false, precutresetplayer = false;
 
 static void F_AdvanceToNextScene(void)
 {
+	if (rendermode != render_none)
+	{
+		F_WipeStartScreen();
+
+		// Fade to any palette color you want.
+		if (cutscenes[cutnum]->scene[scenenum].fadecolor)
+		{
+			V_DrawFill(0,0,BASEVIDWIDTH,BASEVIDHEIGHT,cutscenes[cutnum]->scene[scenenum].fadecolor);
+
+			F_WipeEndScreen();
+			F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeinid, true);
+
+			F_WipeStartScreen();
+		}
+	}
+
 	// Don't increment until after endcutscene check
 	// (possible overflow / bad patch names from the one tic drawn before the fade)
 	if (scenenum+1 >= cutscenes[cutnum]->numscenes)
@@ -3870,6 +3884,7 @@ static void F_AdvanceToNextScene(void)
 		F_EndCutScene();
 		return;
 	}
+
 	++scenenum;
 
 	timetonext = 0;
@@ -3885,7 +3900,6 @@ static void F_AdvanceToNextScene(void)
 			cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0);
 
 	// Fade to the next
-	dofadenow = true;
 	F_NewCutscene(cutscenes[cutnum]->scene[scenenum].text);
 
 	picnum = 0;
@@ -3895,6 +3909,14 @@ static void F_AdvanceToNextScene(void)
 	textypos = cutscenes[cutnum]->scene[scenenum].textypos;
 
 	animtimer = pictime = cutscenes[cutnum]->scene[scenenum].picduration[picnum];
+
+	if (rendermode != render_none)
+	{
+		F_CutsceneDrawer();
+
+		F_WipeEndScreen();
+		F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeoutid, true);
+	}
 }
 
 // See also G_AfterIntermission, the only other place which handles intra-map/ending transitions
@@ -3969,21 +3991,6 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 //
 void F_CutsceneDrawer(void)
 {
-	if (dofadenow && rendermode != render_none)
-	{
-		F_WipeStartScreen();
-
-		// Fade to any palette color you want.
-		if (cutscenes[cutnum]->scene[scenenum].fadecolor)
-		{
-			V_DrawFill(0,0,BASEVIDWIDTH,BASEVIDHEIGHT,cutscenes[cutnum]->scene[scenenum].fadecolor);
-
-			F_WipeEndScreen();
-			F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeinid, true);
-
-			F_WipeStartScreen();
-		}
-	}
 	V_DrawFill(0,0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
 
 	if (cutscenes[cutnum]->scene[scenenum].picname[picnum][0] != '\0')
@@ -3996,12 +4003,6 @@ void F_CutsceneDrawer(void)
 				W_CachePatchName(cutscenes[cutnum]->scene[scenenum].picname[picnum], PU_PATCH_LOWPRIORITY));
 	}
 
-	if (dofadenow && rendermode != render_none)
-	{
-		F_WipeEndScreen();
-		F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeoutid, true);
-	}
-
 	V_DrawString(textxpos, textypos, V_ALLOWLOWERCASE, cutscene_disptext);
 }
 
@@ -4018,8 +4019,6 @@ void F_CutsceneTicker(void)
 	finalecount++;
 	cutscene_boostspeed = 0;
 
-	dofadenow = false;
-
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (netgame && i != serverplayer && !IsPlayerAdmin(i))
diff --git a/src/f_wipe.c b/src/f_wipe.c
index 43b7180b754408faf39387ef259d54aae590bf9f..ab869ca605b4afaf7dac023fd45f0aaf4145184b 100644
--- a/src/f_wipe.c
+++ b/src/f_wipe.c
@@ -24,6 +24,7 @@
 #include "w_wad.h"
 #include "z_zone.h"
 
+#include "i_time.h"
 #include "i_system.h"
 #include "i_threads.h"
 #include "m_menu.h"
@@ -555,7 +556,10 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu)
 
 		// wait loop
 		while (!((nowtime = I_GetTime()) - lastwipetic))
-			I_Sleep();
+		{
+			I_Sleep(cv_sleep.value);
+			I_UpdateTime(cv_timescale.value);
+		}
 		lastwipetic = nowtime;
 
 		// Wipe styles
diff --git a/src/g_demo.c b/src/g_demo.c
index 77c9ab10b2f0ee4295d09c7d81e5e25b5cabc0fa..2da5a76ab2c764d0db0466f023f0cf20007f7c40 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -17,6 +17,7 @@
 #include "d_player.h"
 #include "d_clisrv.h"
 #include "p_setup.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "m_random.h"
 #include "p_local.h"
@@ -1008,7 +1009,7 @@ void G_ReadMetalTic(mobj_t *metal)
 		oldmetal.x = READFIXED(metal_p);
 		oldmetal.y = READFIXED(metal_p);
 		oldmetal.z = READFIXED(metal_p);
-		P_TeleportMove(metal, oldmetal.x, oldmetal.y, oldmetal.z);
+		P_MoveOrigin(metal, oldmetal.x, oldmetal.y, oldmetal.z);
 		oldmetal.x = metal->x;
 		oldmetal.y = metal->y;
 		oldmetal.z = metal->z;
diff --git a/src/g_game.c b/src/g_game.c
index 39d0030565c468823819c0a35d2653d2a773db01..abefac161fefce2f13d28fbc255f2b3500bd275e 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -19,6 +19,7 @@
 #include "f_finale.h"
 #include "p_setup.h"
 #include "p_saveg.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "am_map.h"
 #include "m_random.h"
@@ -46,6 +47,7 @@
 #include "b_bot.h"
 #include "m_cond.h" // condition sets
 #include "lua_script.h"
+#include "r_fps.h" // frame interpolation/uncapped
 
 #include "lua_hud.h"
 
@@ -1903,7 +1905,10 @@ void G_PreLevelTitleCard(void)
 	{
 		// draw loop
 		while (!((nowtime = I_GetTime()) - lasttime))
-			I_Sleep();
+		{
+			I_Sleep(cv_sleep.value);
+			I_UpdateTime(cv_timescale.value);
+		}
 		lasttime = nowtime;
 
 		ST_runTitleCard();
@@ -2362,6 +2367,7 @@ void G_Ticker(boolean run)
 			F_TextPromptTicker();
 			AM_Ticker();
 			HU_Ticker();
+
 			break;
 
 		case GS_INTERMISSION:
@@ -2414,7 +2420,9 @@ void G_Ticker(boolean run)
 			break;
 
 		case GS_TITLESCREEN:
-			if (titlemapinaction) P_Ticker(run); // then intentionally fall through
+			if (titlemapinaction)
+				P_Ticker(run);
+				// then intentionally fall through
 			/* FALLTHRU */
 		case GS_WAITINGPLAYERS:
 			F_MenuPresTicker(run);
@@ -2801,6 +2809,13 @@ void G_MovePlayerToSpawnOrStarpost(INT32 playernum)
 		P_MovePlayerToStarpost(playernum);
 	else
 		P_MovePlayerToSpawn(playernum, G_FindMapStart(playernum));
+	
+	R_ResetMobjInterpolationState(players[playernum].mo);
+	
+	if (playernum == consoleplayer)
+		P_ResetCamera(&players[playernum], &camera);
+	else if (playernum == secondarydisplayplayer)
+		P_ResetCamera(&players[playernum], &camera2);
 }
 
 mapthing_t *G_FindCTFStart(INT32 playernum)
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index 718774773c1dead0dd452617fbb8a5065433fbe7..3b9b8681c26eec0baf19cecafd1cc50fad6d3072 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -51,7 +51,7 @@ EXPORT void HWRAPI(ClearMipMapCache) (void);
 EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value);
 
 //Hurdler: added for new development
-EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface);
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface);
 EXPORT void HWRAPI(CreateModelVBOs) (model_t *model);
 EXPORT void HWRAPI(SetTransform) (FTransform *ptransform);
 EXPORT INT32 HWRAPI(GetTextureUsed) (void);
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 9793a5f690aef4c9f84a2417aadb3581062e9f19..e0d5eeb39f1dbd9f33210d4b6955fa2b369aa192 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -24,6 +24,7 @@
 #include "../v_video.h"
 #include "../p_local.h"
 #include "../p_setup.h"
+#include "../r_fps.h"
 #include "../r_local.h"
 #include "../r_patch.h"
 #include "../r_picformats.h"
@@ -38,6 +39,7 @@
 #include "../m_cheat.h"
 #include "../f_finale.h"
 #include "../r_things.h" // R_GetShadowZ
+#include "../d_main.h"
 #include "../p_slopes.h"
 #include "hw_md2.h"
 
@@ -2968,6 +2970,7 @@ static void HWR_Subsector(size_t num)
 	INT32 light = 0;
 	extracolormap_t *floorcolormap;
 	extracolormap_t *ceilingcolormap;
+	ffloor_t *rover;
 
 #ifdef PARANOIA //no risk while developing, enough debugging nights!
 	if (num >= addsubsector)
@@ -3025,7 +3028,22 @@ static void HWR_Subsector(size_t num)
 
 	if (gl_frontsector->ffloors)
 	{
-		if (gl_frontsector->moved)
+		boolean anyMoved = gl_frontsector->moved;
+
+		if (anyMoved == false)
+		{
+			for (rover = gl_frontsector->ffloors; rover; rover = rover->next)
+			{
+				sector_t *controlSec = &sectors[rover->secnum];
+				if (controlSec->moved == true)
+				{
+					anyMoved = true;
+					break;
+				}
+			}
+		}
+
+		if (anyMoved == true)
 		{
 			gl_frontsector->numlights = sub->sector->numlights = 0;
 			R_Prep3DFloors(gl_frontsector);
@@ -3104,7 +3122,6 @@ static void HWR_Subsector(size_t num)
 	if (gl_frontsector->ffloors)
 	{
 		/// \todo fix light, xoffs, yoffs, extracolormap ?
-		ffloor_t * rover;
 		for (rover = gl_frontsector->ffloors;
 			rover; rover = rover->next)
 		{
@@ -3616,6 +3633,18 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	fixed_t slopez;
 	pslope_t *groundslope;
 
+	// uncapped/interpolation
+	interpmobjstate_t interp = {0};
+
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
 	groundz = R_GetShadowZ(thing, &groundslope);
 
 	heightsec = thing->subsector->sector->heightsec;
@@ -3636,7 +3665,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 			return;
 	}
 
-	floordiff = abs((flip < 0 ? thing->height : 0) + thing->z - groundz);
+	floordiff = abs((flip < 0 ? thing->height : 0) + interp.z - groundz);
 
 	alpha = floordiff / (4*FRACUNIT) + 75;
 	if (alpha >= 255) return;
@@ -3650,8 +3679,8 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	scalemul = FixedMul(scalemul, (thing->radius*2) / gpatch->height);
 
 	fscale = FIXED_TO_FLOAT(scalemul);
-	fx = FIXED_TO_FLOAT(thing->x);
-	fy = FIXED_TO_FLOAT(thing->y);
+	fx = FIXED_TO_FLOAT(interp.x);
+	fy = FIXED_TO_FLOAT(interp.y);
 
 	//  3--2
 	//  | /|
@@ -3729,12 +3758,44 @@ static void HWR_RotateSpritePolyToAim(gl_vissprite_t *spr, FOutVector *wallVerts
 		&& spr && spr->mobj && !R_ThingIsPaperSprite(spr->mobj)
 		&& wallVerts)
 	{
-		float basey = FIXED_TO_FLOAT(spr->mobj->z);
-		float lowy = wallVerts[0].y;
-		if (!precip && P_MobjFlip(spr->mobj) == -1) // precip doesn't have eflags so they can't flip
+		// uncapped/interpolation
+		interpmobjstate_t interp = {0};
+		float basey, lowy;
+
+		// do interpolation
+		if (R_UsingFrameInterpolation() && !paused)
+		{
+			if (precip)
+			{
+				R_InterpolatePrecipMobjState((precipmobj_t *)spr->mobj, rendertimefrac, &interp);
+			}
+			else
+			{
+				R_InterpolateMobjState(spr->mobj, rendertimefrac, &interp);
+			}
+		}
+		else
+		{
+			if (precip)
+			{
+				R_InterpolatePrecipMobjState((precipmobj_t *)spr->mobj, FRACUNIT, &interp);
+			}
+			else
+			{
+				R_InterpolateMobjState(spr->mobj, FRACUNIT, &interp);
+			}
+		}
+
+		if (P_MobjFlip(spr->mobj) == -1)
+		{
+			basey = FIXED_TO_FLOAT(interp.z + spr->mobj->height);
+		}
+		else
 		{
-			basey = FIXED_TO_FLOAT(spr->mobj->z + spr->mobj->height);
+			basey = FIXED_TO_FLOAT(interp.z);
 		}
+		lowy = wallVerts[0].y;
+
 		// Rotate sprites to fully billboard with the camera
 		// X, Y, AND Z need to be manipulated for the polys to rotate around the
 		// origin, because of how the origin setting works I believe that should
@@ -5013,7 +5074,6 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	INT32 heightsec, phs;
 	const boolean splat = R_ThingIsFloorSprite(thing);
 	const boolean papersprite = (R_ThingIsPaperSprite(thing) && !splat);
-	angle_t mobjangle = (thing->player ? thing->player->drawangle : thing->angle);
 	float z1, z2;
 
 	fixed_t spr_width, spr_height;
@@ -5023,10 +5083,10 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	INT32 rollangle = 0;
 #endif
 
-	if (!thing)
-		return;
+	// uncapped/interpolation
+	interpmobjstate_t interp = {0};
 
-	if (thing->spritexscale < 1 || thing->spriteyscale < 1)
+	if (!thing)
 		return;
 
 	INT32 blendmode;
@@ -5044,13 +5104,26 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	dispoffset = thing->info->dispoffset;
 
-	this_scale = FIXED_TO_FLOAT(thing->scale);
-	spritexscale = FIXED_TO_FLOAT(thing->spritexscale);
-	spriteyscale = FIXED_TO_FLOAT(thing->spriteyscale);
+
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	if (interp.spritexscale < 1 || interp.spriteyscale < 1)
+		return;
+
+	this_scale = FIXED_TO_FLOAT(interp.scale);
+	spritexscale = FIXED_TO_FLOAT(interp.spritexscale);
+	spriteyscale = FIXED_TO_FLOAT(interp.spriteyscale);
 
 	// transform the origin point
-	tr_x = FIXED_TO_FLOAT(thing->x) - gl_viewx;
-	tr_y = FIXED_TO_FLOAT(thing->y) - gl_viewy;
+	tr_x = FIXED_TO_FLOAT(interp.x) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interp.y) - gl_viewy;
 
 	// rotation around vertical axis
 	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
@@ -5073,8 +5146,8 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	}
 
 	// The above can stay as it works for cutting sprites that are too close
-	tr_x = FIXED_TO_FLOAT(thing->x);
-	tr_y = FIXED_TO_FLOAT(thing->y);
+	tr_x = FIXED_TO_FLOAT(interp.x);
+	tr_y = FIXED_TO_FLOAT(interp.y);
 
 	// decide which patch to use for sprite relative to player
 #ifdef RANGECHECK
@@ -5122,7 +5195,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 		I_Error("sprframes NULL for sprite %d\n", thing->sprite);
 #endif
 
-	ang = R_PointToAngle (thing->x, thing->y) - mobjangle;
+	ang = R_PointToAngle (interp.x, interp.y) - interp.angle;
 	if (mirrored)
 		ang = InvAngle(ang);
 
@@ -5190,8 +5263,8 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	if (thing->renderflags & RF_ABSOLUTEOFFSETS)
 	{
-		spr_offset = thing->spritexoffset;
-		spr_topoffset = thing->spriteyoffset;
+		spr_offset = interp.spritexoffset;
+		spr_topoffset = interp.spriteyoffset;
 	}
 	else
 	{
@@ -5200,14 +5273,14 @@ static void HWR_ProjectSprite(mobj_t *thing)
 		if ((thing->renderflags & RF_FLIPOFFSETS) && flip)
 			flipoffset = -1;
 
-		spr_offset += thing->spritexoffset * flipoffset;
-		spr_topoffset += thing->spriteyoffset * flipoffset;
+		spr_offset += interp.spritexoffset * flipoffset;
+		spr_topoffset += interp.spriteyoffset * flipoffset;
 	}
 
 	if (papersprite)
 	{
-		rightsin = FIXED_TO_FLOAT(FINESINE((mobjangle)>>ANGLETOFINESHIFT));
-		rightcos = FIXED_TO_FLOAT(FINECOSINE((mobjangle)>>ANGLETOFINESHIFT));
+		rightsin = FIXED_TO_FLOAT(FINESINE(interp.angle >> ANGLETOFINESHIFT));
+		rightcos = FIXED_TO_FLOAT(FINECOSINE(interp.angle >> ANGLETOFINESHIFT));
 	}
 	else
 	{
@@ -5220,14 +5293,24 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	if (thing->renderflags & RF_SHADOWEFFECTS)
 	{
 		mobj_t *caster = thing->target;
+		interpmobjstate_t casterinterp = {};
+
+		if (R_UsingFrameInterpolation() && !paused)
+		{
+			R_InterpolateMobjState(caster, rendertimefrac, &casterinterp);
+		}
+		else
+		{
+			R_InterpolateMobjState(caster, FRACUNIT, &casterinterp);
+		}
 
 		if (caster && !P_MobjWasRemoved(caster))
 		{
 			fixed_t groundz = R_GetShadowZ(thing, NULL);
-			fixed_t floordiff = abs(((thing->eflags & MFE_VERTICALFLIP) ? caster->height : 0) + caster->z - groundz);
+			fixed_t floordiff = abs(((thing->eflags & MFE_VERTICALFLIP) ? caster->height : 0) + casterinterp.z - groundz);
 
 			shadowheight = FIXED_TO_FLOAT(floordiff);
-			shadowscale = FIXED_TO_FLOAT(FixedMul(FRACUNIT - floordiff/640, caster->scale));
+			shadowscale = FIXED_TO_FLOAT(FixedMul(FRACUNIT - floordiff/640, casterinterp.scale));
 
 			if (splat)
 				spritexscale *= shadowscale;
@@ -5268,12 +5351,12 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	if (vflip)
 	{
-		gz = FIXED_TO_FLOAT(thing->z + thing->height) - (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
+		gz = FIXED_TO_FLOAT(interp.z + thing->height) - (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
 		gzt = gz + (FIXED_TO_FLOAT(spr_height) * this_yscale);
 	}
 	else
 	{
-		gzt = FIXED_TO_FLOAT(thing->z) + (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
+		gzt = FIXED_TO_FLOAT(interp.z) + (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
 		gz = gzt - (FIXED_TO_FLOAT(spr_height) * this_yscale);
 	}
 
@@ -5292,7 +5375,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	if (heightsec != -1 && phs != -1) // only clip things which are in special sectors
 	{
 		float top = gzt;
-		float bottom = FIXED_TO_FLOAT(thing->z);
+		float bottom = FIXED_TO_FLOAT(interp.z);
 
 		if (R_ThingIsFloorSprite(thing))
 			top = bottom;
@@ -5309,13 +5392,24 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	if ((thing->flags2 & MF2_LINKDRAW) && thing->tracer)
 	{
+		interpmobjstate_t tracer_interp = {};
+
 		if (! R_ThingVisible(thing->tracer))
 			return;
 
+		if (R_UsingFrameInterpolation() && !paused)
+		{
+			R_InterpolateMobjState(thing->tracer, rendertimefrac, &tracer_interp);
+		}
+		else
+		{
+			R_InterpolateMobjState(thing->tracer, FRACUNIT, &tracer_interp);
+		}
+
 		// calculate tz for tracer, same way it is calculated for this sprite
 		// transform the origin point
-		tr_x = FIXED_TO_FLOAT(thing->tracer->x) - gl_viewx;
-		tr_y = FIXED_TO_FLOAT(thing->tracer->y) - gl_viewy;
+		tr_x = FIXED_TO_FLOAT(tracer_interp.x) - gl_viewx;
+		tr_y = FIXED_TO_FLOAT(tracer_interp.y) - gl_viewy;
 
 		// rotation around vertical axis
 		tracertz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
@@ -5438,6 +5532,9 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	unsigned rot = 0;
 	UINT8 flip;
 
+	if (!thing)
+		return;
+
 	// Visibility check by the blend mode.
 	if (thing->frame & FF_TRANSMASK)
 	{
@@ -5445,9 +5542,22 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 			return;
 	}
 
+	// uncapped/interpolation
+	interpmobjstate_t interp = {0};
+
+	// do interpolation
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolatePrecipMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolatePrecipMobjState(thing, FRACUNIT, &interp);
+	}
+
 	// transform the origin point
-	tr_x = FIXED_TO_FLOAT(thing->x) - gl_viewx;
-	tr_y = FIXED_TO_FLOAT(thing->y) - gl_viewy;
+	tr_x = FIXED_TO_FLOAT(interp.x) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interp.y) - gl_viewy;
 
 	// rotation around vertical axis
 	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
@@ -5456,8 +5566,8 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	if (tz < ZCLIP_PLANE)
 		return;
 
-	tr_x = FIXED_TO_FLOAT(thing->x);
-	tr_y = FIXED_TO_FLOAT(thing->y);
+	tr_x = FIXED_TO_FLOAT(interp.x);
+	tr_y = FIXED_TO_FLOAT(interp.y);
 
 	// decide which patch to use for sprite relative to player
 	if ((unsigned)thing->sprite >= numsprites)
@@ -5520,7 +5630,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis->colormap = NULL;
 
 	// set top/bottom coords
-	vis->gzt = FIXED_TO_FLOAT(thing->z + spritecachedinfo[lumpoff].topoffset);
+	vis->gzt = FIXED_TO_FLOAT(interp.z + spritecachedinfo[lumpoff].topoffset);
 	vis->gz = vis->gzt - FIXED_TO_FLOAT(spritecachedinfo[lumpoff].height);
 
 	vis->precip = true;
@@ -6656,6 +6766,7 @@ void HWR_DoPostProcessor(player_t *player)
 		// 10 by 10 grid. 2 coordinates (xy)
 		float v[SCREENVERTS][SCREENVERTS][2];
 		static double disStart = 0;
+
 		UINT8 x, y;
 		INT32 WAVELENGTH;
 		INT32 AMPLITUDE;
@@ -6664,15 +6775,15 @@ void HWR_DoPostProcessor(player_t *player)
 		// Modifies the wave.
 		if (*type == postimg_water)
 		{
-			WAVELENGTH = 20; // Lower is longer
-			AMPLITUDE = 20; // Lower is bigger
-			FREQUENCY = 16; // Lower is faster
+			WAVELENGTH = 5;
+			AMPLITUDE = 20;
+			FREQUENCY = 8;
 		}
 		else
 		{
-			WAVELENGTH = 10; // Lower is longer
-			AMPLITUDE = 30; // Lower is bigger
-			FREQUENCY = 4; // Lower is faster
+			WAVELENGTH = 10;
+			AMPLITUDE = 60;
+			FREQUENCY = 4;
 		}
 
 		for (x = 0; x < SCREENVERTS; x++)
@@ -6686,7 +6797,7 @@ void HWR_DoPostProcessor(player_t *player)
 		}
 		HWD.pfnPostImgRedraw(v);
 		if (!(paused || P_AutoPause()))
-			disStart += 1;
+			disStart += FIXED_TO_FLOAT(renderdeltatics);
 
 		// Capture the screen again for screen waving on the intermission
 		if(gamestate != GS_INTERMISSION)
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index a003163dbe3b4dc375e6ba0af788d0530a337ad1..f33d67bbbf015e2fb7777c4ad84ec50736e4e7f4 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -30,6 +30,7 @@
 #include "hw_md2.h"
 #include "../d_main.h"
 #include "../r_bsp.h"
+#include "../r_fps.h"
 #include "../r_main.h"
 #include "../m_misc.h"
 #include "../w_wad.h"
@@ -1338,8 +1339,8 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 	{
 		patch_t *gpatch, *blendgpatch;
 		GLPatch_t *hwrPatch = NULL, *hwrBlendPatch = NULL;
-		INT32 durs = spr->mobj->state->tics;
-		INT32 tics = spr->mobj->tics;
+		float durs = (float)spr->mobj->state->tics;
+		float tics = (float)spr->mobj->tics;
 		const boolean papersprite = (R_ThingIsPaperSprite(spr->mobj) && !R_ThingIsFloorSprite(spr->mobj));
 		const UINT8 flip = (UINT8)(!(spr->mobj->eflags & MFE_VERTICALFLIP) != !R_ThingVerticallyFlipped(spr->mobj));
 		const UINT8 hflip = (UINT8)(!(spr->mobj->mirrored) != !R_ThingHorizontallyFlipped(spr->mobj));
@@ -1349,6 +1350,16 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		angle_t ang;
 		INT32 mod;
 		float finalscale;
+		interpmobjstate_t interp;
+
+		if (R_UsingFrameInterpolation() && !paused)
+		{
+			R_InterpolateMobjState(spr->mobj, rendertimefrac, &interp);
+		}
+		else
+		{
+			R_InterpolateMobjState(spr->mobj, FRACUNIT, &interp);
+		}
 
 		// Apparently people don't like jump frames like that, so back it goes
 		//if (tics > durs)
@@ -1498,8 +1509,8 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		if (spr->mobj->frame & FF_ANIMATE)
 		{
 			// set duration and tics to be the correct values for FF_ANIMATE states
-			durs = spr->mobj->state->var2;
-			tics = spr->mobj->anim_duration;
+			durs = (float)spr->mobj->state->var2;
+			tics = (float)spr->mobj->anim_duration;
 		}
 
 		frame = (spr->mobj->frame & FF_FRAMEMASK);
@@ -1523,7 +1534,11 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		}
 
 #ifdef USE_MODEL_NEXTFRAME
-#define INTERPOLERATION_LIMIT TICRATE/4
+		// Interpolate the model interpolation. (lol)
+		tics -= FixedToFloat(rendertimefrac);
+
+#define INTERPOLERATION_LIMIT (TICRATE * 0.25f)
+
 		if (cv_glmodelinterpolation.value && tics <= durs && tics <= INTERPOLERATION_LIMIT)
 		{
 			if (durs > INTERPOLERATION_LIMIT)
@@ -1572,13 +1587,13 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 #endif
 
 		//Hurdler: it seems there is still a small problem with mobj angle
-		p.x = FIXED_TO_FLOAT(spr->mobj->x);
-		p.y = FIXED_TO_FLOAT(spr->mobj->y)+md2->offset;
+		p.x = FIXED_TO_FLOAT(interp.x);
+		p.y = FIXED_TO_FLOAT(interp.y)+md2->offset;
 
 		if (flip)
-			p.z = FIXED_TO_FLOAT(spr->mobj->z + spr->mobj->height);
+			p.z = FIXED_TO_FLOAT(interp.z + spr->mobj->height);
 		else
-			p.z = FIXED_TO_FLOAT(spr->mobj->z);
+			p.z = FIXED_TO_FLOAT(interp.z);
 
 		if (spr->mobj->skin && spr->mobj->sprite == SPR_PLAY)
 			sprdef = &((skin_t *)spr->mobj->skin)->sprites[spr->mobj->sprite2];
@@ -1589,16 +1604,13 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 
 		if (sprframe->rotate || papersprite)
 		{
-			fixed_t anglef = AngleFixed(spr->mobj->angle);
-
-			if (spr->mobj->player)
-				anglef = AngleFixed(spr->mobj->player->drawangle);
+			fixed_t anglef = AngleFixed(interp.angle);
 
 			p.angley = FIXED_TO_FLOAT(anglef);
 		}
 		else
 		{
-			const fixed_t anglef = AngleFixed((R_PointToAngle(spr->mobj->x, spr->mobj->y))-ANGLE_180);
+			const fixed_t anglef = AngleFixed((R_PointToAngle(interp.x, interp.y))-ANGLE_180);
 			p.angley = FIXED_TO_FLOAT(anglef);
 		}
 
@@ -1620,7 +1632,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 				p.rotaxis = (UINT8)(sprinfo->pivot[(spr->mobj->frame & FF_FRAMEMASK)].rotaxis);
 
 			// for NiGHTS specifically but should work everywhere else
-			ang = R_PointToAngle (spr->mobj->x, spr->mobj->y) - (spr->mobj->player ? spr->mobj->player->drawangle : spr->mobj->angle);
+			ang = R_PointToAngle (interp.x, interp.y) - interp.angle;
 			if ((sprframe->rotate & SRF_RIGHT) && (ang < ANGLE_180)) // See from right
 				p.rollflip = 1;
 			else if ((sprframe->rotate & SRF_LEFT) && (ang >= ANGLE_180)) // See from left
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 7ec7ee2702f4bdaadbef776a0b281b7c3506f6a9..a58097c91a878f96ecc45b65397c3208ad52add2 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -2672,7 +2672,7 @@ EXPORT void HWRAPI(CreateModelVBOs) (model_t *model)
 
 #define BUFFER_OFFSET(i) ((void*)(i))
 
-static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
+static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
 {
 	static GLRGBAFloat poly = {0,0,0,0};
 	static GLRGBAFloat tint = {0,0,0,0};
@@ -2701,11 +2701,11 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32
 	scaley = scale;
 	scalez = scale;
 
-	if (duration != 0 && duration != -1 && tics != -1) // don't interpolate if instantaneous or infinite in length
+	if (duration > 0.0 && tics >= 0.0) // don't interpolate if instantaneous or infinite in length
 	{
-		UINT32 newtime = (duration - tics); // + 1;
+		float newtime = (duration - tics); // + 1;
 
-		pol = (newtime)/(float)duration;
+		pol = newtime / duration;
 
 		if (pol > 1.0f)
 			pol = 1.0f;
@@ -2977,7 +2977,7 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32
 // -----------------+
 // HWRAPI DrawModel : Draw a model
 // -----------------+
-EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
 {
 	DrawModelEx(model, frameIndex, duration, tics, nextFrameIndex, pos, scale, flipped, hflipped, Surface);
 }
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 5d893a5515fcf21e82f72959b9aa0cc8773ad96c..b02eecb6c0d12fdd0d697cbedf76a530d5ab9810 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -48,6 +48,7 @@
 #endif
 
 #include "lua_hud.h"
+#include "lua_hudlib_drawlist.h"
 #include "lua_hook.h"
 
 // coords are scaled
@@ -166,10 +167,14 @@ static tic_t cechotimer = 0;
 static tic_t cechoduration = 5*TICRATE;
 static INT32 cechoflags = 0;
 
+static huddrawlist_h luahuddrawlist_scores;
+
 //======================================================================
 //                          HEADS UP INIT
 //======================================================================
 
+static tic_t resynch_ticker = 0;
+
 #ifndef NONET
 // just after
 static void Command_Say_f(void);
@@ -332,6 +337,8 @@ void HU_Init(void)
 
 	// set shift translation table
 	shiftxform = english_shiftxform;
+
+	luahuddrawlist_scores = LUA_HUD_CreateDrawList();
 }
 
 static inline void HU_Stop(void)
@@ -382,12 +389,12 @@ static INT16 addy = 0; // use this to make the messages scroll smoothly when one
 
 static void HU_removeChatText_Mini(void)
 {
-    // MPC: Don't create new arrays, just iterate through an existing one
+	// MPC: Don't create new arrays, just iterate through an existing one
 	size_t i;
-    for(i=0;i<chat_nummsg_min-1;i++) {
-        strcpy(chat_mini[i], chat_mini[i+1]);
-        chat_timers[i] = chat_timers[i+1];
-    }
+	for(i=0;i<chat_nummsg_min-1;i++) {
+		strcpy(chat_mini[i], chat_mini[i+1]);
+		chat_timers[i] = chat_timers[i+1];
+	}
 	chat_nummsg_min--; // lost 1 msg.
 
 	// use addy and make shit slide smoothly af.
@@ -400,10 +407,10 @@ static void HU_removeChatText_Log(void)
 {
 	// MPC: Don't create new arrays, just iterate through an existing one
 	size_t i;
-    for(i=0;i<chat_nummsg_log-1;i++) {
-        strcpy(chat_log[i], chat_log[i+1]);
-    }
-    chat_nummsg_log--; // lost 1 msg.
+	for(i=0;i<chat_nummsg_log-1;i++) {
+		strcpy(chat_log[i], chat_log[i+1]);
+	}
+	chat_nummsg_log--; // lost 1 msg.
 }
 #endif
 
@@ -874,6 +881,39 @@ void HU_Ticker(void)
 		hu_showscores = !chat_on;
 	else
 		hu_showscores = false;
+
+	if (chat_on)
+	{
+		// count down the scroll timer.
+		if (chat_scrolltime > 0)
+			chat_scrolltime--;
+	}
+
+	if (netgame)
+	{
+		size_t i = 0;
+
+		// handle spam while we're at it:
+		for(; (i<MAXPLAYERS); i++)
+		{
+			if (stop_spamming[i] > 0)
+				stop_spamming[i]--;
+		}
+
+		// handle chat timers
+		for (i=0; (i<chat_nummsg_min); i++)
+		{
+			if (chat_timers[i] > 0)
+				chat_timers[i]--;
+			else
+				HU_removeChatText_Mini();
+		}
+	}
+
+	if (cechotimer > 0) --cechotimer;
+
+	if (hu_redownloadinggamestate)
+		resynch_ticker++;
 }
 
 #ifndef NONET
@@ -1102,7 +1142,7 @@ boolean HU_Responder(event_t *ev)
 			if (chatlen+pastelen > HU_MAXMSGLEN)
 				return true; // we can't paste this!!
 
-			memmove(&w_chat[c_input + pastelen], &w_chat[c_input], pastelen);
+			memmove(&w_chat[c_input + pastelen], &w_chat[c_input], (chatlen - c_input) + 1); // +1 for '\0'
 			memcpy(&w_chat[c_input], paste, pastelen); // copy all of that.
 			c_input += pastelen;
 			return true;
@@ -1854,8 +1894,6 @@ static void HU_DrawCEcho(void)
 		echoptr = line;
 		echoptr++;
 	}
-
-	--cechotimer;
 }
 
 static void HU_drawGametype(void)
@@ -1917,9 +1955,6 @@ void HU_Drawer(void)
 	// draw chat string plus cursor
 	if (chat_on)
 	{
-		// count down the scroll timer.
-		if (chat_scrolltime > 0)
-			chat_scrolltime--;
 		if (!OLDCHAT)
 			HU_DrawChat();
 		else
@@ -1929,30 +1964,10 @@ void HU_Drawer(void)
 	{
 		typelines = 1;
 		chat_scrolltime = 0;
+
 		if (!OLDCHAT && cv_consolechat.value < 2 && netgame) // Don't display minimized chat if you set the mode to Window (Hidden)
 			HU_drawMiniChat(); // draw messages in a cool fashion.
 	}
-
-	if (netgame) // would handle that in hu_drawminichat, but it's actually kinda awkward when you're typing a lot of messages. (only handle that in netgames duh)
-	{
-		size_t i = 0;
-
-		// handle spam while we're at it:
-		for(; (i<MAXPLAYERS); i++)
-		{
-			if (stop_spamming[i] > 0)
-				stop_spamming[i]--;
-		}
-
-		// handle chat timers
-		for (i=0; (i<chat_nummsg_min); i++)
-		{
-			if (chat_timers[i] > 0)
-				chat_timers[i]--;
-			else
-				HU_removeChatText_Mini();
-		}
-	}
 #endif
 
 	if (cechotimer)
@@ -1979,7 +1994,13 @@ void HU_Drawer(void)
 		}
 		else
 			HU_DrawCoopOverlay();
-		LUA_HUDHOOK(scores);
+		
+		if (renderisnewtic)
+		{
+			LUA_HUD_ClearDrawList(luahuddrawlist_scores);
+			LUA_HUDHOOK(scores, luahuddrawlist_scores);
+		}
+		LUA_HUD_DrawList(luahuddrawlist_scores);
 	}
 
 	if (gamestate != GS_LEVEL)
@@ -1992,12 +2013,9 @@ void HU_Drawer(void)
 	// draw desynch text
 	if (hu_redownloadinggamestate)
 	{
-		static UINT32 resynch_ticker = 0;
 		char resynch_text[14];
 		UINT32 i;
 
-		// Animate the dots
-		resynch_ticker++;
 		strcpy(resynch_text, "Resynching");
 		for (i = 0; i < (resynch_ticker / 16) % 4; i++)
 			strcat(resynch_text, ".");
diff --git a/src/i_system.h b/src/i_system.h
index 27fcdeb3f21d247e1001d12d7e8b1bee028e62a8..7153aa7357ad0a7d3e1cbedc84c5c929e5fd5fbd 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -42,23 +42,32 @@ extern UINT8 keyboard_started;
 */
 UINT32 I_GetFreeMem(UINT32 *total);
 
-/**	\brief  Called by D_SRB2Loop, returns current time in tics.
-*/
-tic_t I_GetTime(void);
-
-/**	\brief	Returns precise time value for performance measurement.
+/**	\brief	Returns precise time value for performance measurement. The precise
+            time should be a monotonically increasing counter, and will wrap.
+			precise_t is internally represented as an unsigned integer and
+			integer arithmetic may be used directly between values of precise_t.
   */
 precise_t I_GetPreciseTime(void);
 
-/**	\brief	Converts a precise_t to microseconds and casts it to a 32 bit integer.
+/** \brief  Get the precision of precise_t in units per second. Invocations of
+            this function for the program's duration MUST return the same value.
   */
-int I_PreciseToMicros(precise_t);
+UINT64 I_GetPrecisePrecision(void);
+
+/** \brief  Get the current time in rendering tics, including fractions.
+*/
+double I_GetFrameTime(void);
 
-/**	\brief	The I_Sleep function
+/**	\brief	Sleeps for the given duration in milliseconds. Depending on the
+            operating system's scheduler, the calling thread may give up its
+			time slice for a longer duration. The implementation should give a
+			best effort to sleep for the given duration, without spin-locking.
+			Calling code should check the current precise time after sleeping
+			and not assume the thread has slept for the expected duration.
 
 	\return	void
 */
-void I_Sleep(void);
+void I_Sleep(UINT32 ms);
 
 /**	\brief Get events
 
diff --git a/src/i_time.c b/src/i_time.c
new file mode 100644
index 0000000000000000000000000000000000000000..c1cc9dfd44d1c20828c598b44826fdbb48fed919
--- /dev/null
+++ b/src/i_time.c
@@ -0,0 +1,123 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 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  i_time.c
+/// \brief Timing for the system layer.
+
+#include "i_time.h"
+
+#include <math.h>
+
+#include "command.h"
+#include "doomtype.h"
+#include "d_netcmd.h"
+#include "m_fixed.h"
+#include "i_system.h"
+
+timestate_t g_time;
+
+static CV_PossibleValue_t timescale_cons_t[] = {{FRACUNIT/20, "MIN"}, {20*FRACUNIT, "MAX"}, {0, NULL}};
+consvar_t cv_timescale = CVAR_INIT ("timescale", "1.0", CV_NETVAR|CV_CHEAT|CV_FLOAT, timescale_cons_t, NULL);
+
+static precise_t enterprecise, oldenterprecise;
+static fixed_t entertic, oldentertics;
+static double tictimer;
+
+// A little more than the minimum sleep duration on Windows.
+// May be incorrect for other platforms, but we don't currently have a way to
+// query the scheduler granularity. SDL will do what's needed to make this as
+// low as possible though.
+#define MIN_SLEEP_DURATION_MS 2.1
+
+tic_t I_GetTime(void)
+{
+	return g_time.time;
+}
+
+void I_InitializeTime(void)
+{
+	g_time.time = 0;
+	g_time.timefrac = 0;
+
+	enterprecise = 0;
+	oldenterprecise = 0;
+	tictimer = 0.0;
+
+	CV_RegisterVar(&cv_timescale);
+
+	// I_StartupTimer is preserved for potential subsystems that need to setup
+	// timing information for I_GetPreciseTime and sleeping
+	I_StartupTimer();
+}
+
+void I_UpdateTime(fixed_t timescale)
+{
+	double ticratescaled;
+	double elapsedseconds;
+	tic_t realtics;
+
+	// get real tics
+	ticratescaled = (double)TICRATE * FIXED_TO_FLOAT(timescale);
+
+	enterprecise = I_GetPreciseTime();
+	elapsedseconds = (double)(enterprecise - oldenterprecise) / I_GetPrecisePrecision();
+	tictimer += elapsedseconds;
+	while (tictimer > 1.0/ticratescaled)
+	{
+		entertic += 1;
+		tictimer -= 1.0/ticratescaled;
+	}
+	realtics = entertic - oldentertics;
+	oldentertics = entertic;
+	oldenterprecise = enterprecise;
+
+	// Update global time state
+	g_time.time += realtics;
+	{
+		double fractional, integral;
+		fractional = modf(tictimer * ticratescaled, &integral);
+		g_time.timefrac = FLOAT_TO_FIXED(fractional);
+	}
+}
+
+void I_SleepDuration(precise_t duration)
+{
+	UINT64 precision = I_GetPrecisePrecision();
+	INT32 sleepvalue = cv_sleep.value;
+	UINT64 delaygranularity;
+	precise_t cur;
+	precise_t dest;
+
+	{
+		double gran = round(((double)(precision / 1000) * sleepvalue * MIN_SLEEP_DURATION_MS));
+		delaygranularity = (UINT64)gran;
+	}
+
+	cur = I_GetPreciseTime();
+	dest = cur + duration;
+
+	// the reason this is not dest > cur is because the precise counter may wrap
+	// two's complement arithmetic is our friend here, though!
+	// e.g. cur 0xFFFFFFFFFFFFFFFE = -2, dest 0x0000000000000001 = 1
+	// 0x0000000000000001 - 0xFFFFFFFFFFFFFFFE = 3
+	while ((INT64)(dest - cur) > 0)
+	{
+		// If our cv_sleep value exceeds the remaining sleep duration, use the
+		// hard sleep function.
+		if (sleepvalue > 0 && (dest - cur) > delaygranularity)
+		{
+			I_Sleep(sleepvalue);
+		}
+
+		// Otherwise, this is a spinloop.
+
+		cur = I_GetPreciseTime();
+	}
+}
diff --git a/src/i_time.h b/src/i_time.h
new file mode 100644
index 0000000000000000000000000000000000000000..cab36133b6bf63ed43359f55f0c297d2a32b20c7
--- /dev/null
+++ b/src/i_time.h
@@ -0,0 +1,54 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 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  i_time.h
+/// \brief Timing for the system layer.
+
+#ifndef __I_TIME_H__
+#define __I_TIME_H__
+
+#include "command.h"
+#include "doomtype.h"
+#include "m_fixed.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct timestate_s {
+	tic_t time;
+	fixed_t timefrac;
+} timestate_t;
+
+extern timestate_t g_time;
+extern consvar_t cv_timescale;
+
+/**	\brief  Called by D_SRB2Loop, returns current time in game tics.
+*/
+tic_t I_GetTime(void);
+
+/**	\brief  Initializes timing system.
+*/
+void I_InitializeTime(void);
+
+void I_UpdateTime(fixed_t timescale);
+
+/** \brief  Block for at minimum the duration specified. This function makes a
+            best effort not to oversleep, and will spinloop if sleeping would
+			take too long. However, callers should still check the current time
+			after this returns.
+*/
+void I_SleepDuration(precise_t duration);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __I_TIME_H__
diff --git a/src/i_video.h b/src/i_video.h
index 638fcb6689f026722ffbb4e777111260ec54e97c..d66b2d95fcf8d91caf690ef2f76036fcf4e4b978 100644
--- a/src/i_video.h
+++ b/src/i_video.h
@@ -151,4 +151,6 @@ void I_BeginRead(void);
 */
 void I_EndRead(void);
 
+UINT32 I_GetRefreshRate(void);
+
 #endif
diff --git a/src/info.c b/src/info.c
index 179370ca4c989965bee2bc9e65ac33fbc8b35c82..c4c197027347f01fee9bf07799f829e228e21d7b 100644
--- a/src/info.c
+++ b/src/info.c
@@ -770,9 +770,9 @@ state_t states[NUMSTATES] =
 
 	// 1-Up box sprites (uses player sprite)
 	{SPR_PLAY, SPR2_LIFE,  2, {NULL}, 0, 18, S_PLAY_BOX2},  // S_PLAY_BOX1
-	{SPR_NULL,         0,  1, {NULL}, 0,  0, S_PLAY_BOX1},  // S_PLAY_BOX2
+	{SPR_NULL,         0,  1, {NULL}, 0, 18, S_PLAY_BOX1},  // S_PLAY_BOX2
 	{SPR_PLAY, SPR2_LIFE,  4, {NULL}, 0,  4, S_PLAY_ICON2}, // S_PLAY_ICON1
-	{SPR_NULL,         0, 12, {NULL}, 0,  0, S_PLAY_ICON3}, // S_PLAY_ICON2
+	{SPR_NULL,         0, 12, {NULL}, 0,  4, S_PLAY_ICON3}, // S_PLAY_ICON2
 	{SPR_PLAY, SPR2_LIFE, 20, {NULL}, 0,  4, S_NULL},       // S_PLAY_ICON3
 
 	// Level end sign (uses player sprite)
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index da36142716c722701b59bb20479ec8e07a6512c4..5af9669ca3ab14672da929a900fcb6c448386fec 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -31,7 +31,7 @@
 #include "m_misc.h" // M_MapNumber
 #include "b_bot.h" // B_UpdateBotleader
 #include "d_clisrv.h" // CL_RemovePlayer
-#include "i_system.h" // I_GetPreciseTime, I_PreciseToMicros
+#include "i_system.h" // I_GetPreciseTime, I_GetPrecisePrecision
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -1783,7 +1783,42 @@ static int lib_pTeleportMove(lua_State *L)
 	INLEVEL
 	if (!thing)
 		return LUA_ErrInvalid(L, "mobj_t");
-	lua_pushboolean(L, P_TeleportMove(thing, x, y, z));
+	LUA_Deprecated(L, "P_TeleportMove", "P_SetOrigin\" or \"P_MoveOrigin");
+	lua_pushboolean(L, P_MoveOrigin(thing, x, y, z));
+	LUA_PushUserdata(L, tmthing, META_MOBJ);
+	P_SetTarget(&tmthing, ptmthing);
+	return 2;
+}
+
+static int lib_pSetOrigin(lua_State *L)
+{
+	mobj_t *ptmthing = tmthing;
+	mobj_t *thing = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	fixed_t z = luaL_checkfixed(L, 4);
+	NOHUD
+	INLEVEL
+	if (!thing)
+		return LUA_ErrInvalid(L, "mobj_t");
+	lua_pushboolean(L, P_SetOrigin(thing, x, y, z));
+	LUA_PushUserdata(L, tmthing, META_MOBJ);
+	P_SetTarget(&tmthing, ptmthing);
+	return 2;
+}
+
+static int lib_pMoveOrigin(lua_State *L)
+{
+	mobj_t *ptmthing = tmthing;
+	mobj_t *thing = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	fixed_t z = luaL_checkfixed(L, 4);
+	NOHUD
+	INLEVEL
+	if (!thing)
+		return LUA_ErrInvalid(L, "mobj_t");
+	lua_pushboolean(L, P_MoveOrigin(thing, x, y, z));
 	LUA_PushUserdata(L, tmthing, META_MOBJ);
 	P_SetTarget(&tmthing, ptmthing);
 	return 2;
@@ -3909,7 +3944,7 @@ static int lib_gTicsToMilliseconds(lua_State *L)
 
 static int lib_getTimeMicros(lua_State *L)
 {
-	lua_pushinteger(L, I_PreciseToMicros(I_GetPreciseTime()));
+	lua_pushinteger(L, I_GetPreciseTime() / (I_GetPrecisePrecision() / 1000000));
 	return 1;
 }
 
@@ -4048,6 +4083,8 @@ static luaL_Reg lib[] = {
 	{"P_TryMove",lib_pTryMove},
 	{"P_Move",lib_pMove},
 	{"P_TeleportMove",lib_pTeleportMove},
+	{"P_SetOrigin",lib_pSetOrigin},
+	{"P_MoveOrigin",lib_pMoveOrigin},
 	{"P_SlideMove",lib_pSlideMove},
 	{"P_BounceMove",lib_pBounceMove},
 	{"P_CheckSight", lib_pCheckSight},
diff --git a/src/lua_hook.h b/src/lua_hook.h
index fc6a5f4ee4e4c4aa92683126c8b939c5ee36d2c3..5a14294c3b312f2cac04ce434e4af2d92c713b5d 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -14,6 +14,7 @@
 #include "d_player.h"
 #include "s_sound.h"
 #include "d_event.h"
+#include "lua_hudlib_drawlist.h"
 
 /*
 Do you know what an 'X Macro' is? Such a macro is called over each element of
@@ -110,12 +111,12 @@ ENUM (STRING_HOOK);
 
 /* dead simple, LUA_HOOK(GameQuit) */
 #define LUA_HOOK(type) LUA_HookVoid(HOOK(type))
-#define LUA_HUDHOOK(type) LUA_HookHUD(HUD_HOOK(type))
+#define LUA_HUDHOOK(type,drawlist) LUA_HookHUD(HUD_HOOK(type),(drawlist))
 
 extern boolean hook_cmd_running;
 
 void LUA_HookVoid(int hook);
-void LUA_HookHUD(int hook);
+void LUA_HookHUD(int hook, huddrawlist_h drawlist);
 
 int  LUA_HookMobj(mobj_t *, int hook);
 int  LUA_Hook2Mobj(mobj_t *, mobj_t *, int hook);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 48980f4a4c1f809b8bf296d48c0332ea682e5234..0b24b7b535b917fa095510db4564d2564688a508 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -641,7 +641,7 @@ int LUA_HookKey(event_t *event, int hook_type)
 	return hook.status;
 }
 
-void LUA_HookHUD(int hook_type)
+void LUA_HookHUD(int hook_type, huddrawlist_h list)
 {
 	const hook_t * map = &hudHookIds[hook_type];
 	Hook_State hook;
@@ -650,12 +650,15 @@ void LUA_HookHUD(int hook_type)
 		start_hook_stack();
 		begin_hook_values(&hook);
 
-		LUA_SetHudHook(hook_type);
+		LUA_SetHudHook(hook_type, list);
 
 		hud_running = true; // local hook
 		init_hook_call(&hook, 0, res_none);
 		call_mapped(&hook, map);
 		hud_running = false;
+
+		lua_pushnil(gL);
+		lua_setfield(gL, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
 	}
 }
 
diff --git a/src/lua_hud.h b/src/lua_hud.h
index ad2b51d3ef7d58b4df71ace0c18a9560962e947e..0d629d233d0eeb10d7a0591e8cd95ab357e4aecb 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -10,6 +10,11 @@
 /// \file  lua_hud.h
 /// \brief HUD enable/disable flags for Lua scripting
 
+#ifndef __LUA_HUD_H__
+#define __LUA_HUD_H__
+
+#include "lua_hudlib_drawlist.h"
+
 enum hud {
 	hud_stagetitle = 0,
 	hud_textspectator,
@@ -47,4 +52,6 @@ extern boolean hud_running;
 
 boolean LUA_HudEnabled(enum hud option);
 
-void LUA_SetHudHook(int hook);
+void LUA_SetHudHook(int hook, huddrawlist_h list);
+
+#endif // __LUA_HUD_H__
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index c7f2bbc28ec31d6266aa9eb435248663a4418232..40c6f6e1fc0e8ad901d31adfdbea28b559e3860b 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -645,7 +645,8 @@ static int libd_draw(lua_State *L)
 {
 	INT32 x, y, flags;
 	patch_t *patch;
-	const UINT8 *colormap = NULL;
+	UINT8 *colormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
@@ -659,7 +660,14 @@ static int libd_draw(lua_State *L)
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawFixedPatch(x<<FRACBITS, y<<FRACBITS, FRACUNIT, flags, patch, colormap);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDraw(list, x, y, patch, flags, colormap);
+	else
+		V_DrawFixedPatch(x<<FRACBITS, y<<FRACBITS, FRACUNIT, flags, patch, colormap);
 	return 0;
 }
 
@@ -668,7 +676,8 @@ static int libd_drawScaled(lua_State *L)
 	fixed_t x, y, scale;
 	INT32 flags;
 	patch_t *patch;
-	const UINT8 *colormap = NULL;
+	UINT8 *colormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
@@ -685,7 +694,14 @@ static int libd_drawScaled(lua_State *L)
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawFixedPatch(x, y, scale, flags, patch, colormap);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawScaled(list, x, y, scale, patch, flags, colormap);
+	else
+		V_DrawFixedPatch(x, y, scale, flags, patch, colormap);
 	return 0;
 }
 
@@ -694,7 +710,8 @@ static int libd_drawStretched(lua_State *L)
 	fixed_t x, y, hscale, vscale;
 	INT32 flags;
 	patch_t *patch;
-	const UINT8 *colormap = NULL;
+	UINT8 *colormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
@@ -712,7 +729,14 @@ static int libd_drawStretched(lua_State *L)
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawStretchyFixedPatch(x, y, hscale, vscale, flags, patch, colormap);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawStretched(list, x, y, hscale, vscale, patch, flags, colormap);
+	else
+		V_DrawStretchyFixedPatch(x, y, hscale, vscale, flags, patch, colormap);
 	return 0;
 }
 
@@ -721,7 +745,8 @@ static int libd_drawCropped(lua_State *L)
 	fixed_t x, y, hscale, vscale, sx, sy, w, h;
 	INT32 flags;
 	patch_t *patch;
-	const UINT8 *colormap = NULL;
+	UINT8 *colormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
@@ -751,13 +776,22 @@ static int libd_drawCropped(lua_State *L)
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawCroppedPatch(x, y, hscale, vscale, flags, patch, colormap, sx, sy, w, h);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawCropped(list, x, y, hscale, vscale, patch, flags, colormap, sx, sy, w, h);
+	else
+		V_DrawCroppedPatch(x, y, hscale, vscale, flags, patch, colormap, sx, sy, w, h);
 	return 0;
 }
 
 static int libd_drawNum(lua_State *L)
 {
 	INT32 x, y, flags, num;
+	huddrawlist_h list;
+
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
 	y = luaL_checkinteger(L, 2);
@@ -765,13 +799,22 @@ static int libd_drawNum(lua_State *L)
 	flags = luaL_optinteger(L, 4, 0);
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawTallNum(x, y, flags, num);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawNum(list, x, y, num, flags);
+	else
+		V_DrawTallNum(x, y, flags, num);
 	return 0;
 }
 
 static int libd_drawPaddedNum(lua_State *L)
 {
 	INT32 x, y, flags, num, digits;
+	huddrawlist_h list;
+
 	HUDONLY
 	x = luaL_checkinteger(L, 1);
 	y = luaL_checkinteger(L, 2);
@@ -780,12 +823,20 @@ static int libd_drawPaddedNum(lua_State *L)
 	flags = luaL_optinteger(L, 5, 0);
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawPaddedTallNum(x, y, flags, num, digits);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawPaddedNum(list, x, y, num, digits, flags);
+	else
+		V_DrawPaddedTallNum(x, y, flags, num, digits);
 	return 0;
 }
 
 static int libd_drawFill(lua_State *L)
 {
+	huddrawlist_h list;
 	INT32 x = luaL_optinteger(L, 1, 0);
 	INT32 y = luaL_optinteger(L, 2, 0);
 	INT32 w = luaL_optinteger(L, 3, BASEVIDWIDTH);
@@ -793,12 +844,21 @@ static int libd_drawFill(lua_State *L)
 	INT32 c = luaL_optinteger(L, 5, 31);
 
 	HUDONLY
-	V_DrawFill(x, y, w, h, c);
+
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawFill(list, x, y, w, h, c);
+	else
+		V_DrawFill(x, y, w, h, c);
 	return 0;
 }
 
 static int libd_drawString(lua_State *L)
 {
+	huddrawlist_h list;
 	fixed_t x = luaL_checkinteger(L, 1);
 	fixed_t y = luaL_checkinteger(L, 2);
 	const char *str = luaL_checkstring(L, 3);
@@ -808,6 +868,15 @@ static int libd_drawString(lua_State *L)
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
 	HUDONLY
+
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	// okay, sorry, this is kind of ugly
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawString(list, x, y, str, flags, align);
+	else
 	switch(align)
 	{
 	// hu_font
@@ -899,6 +968,7 @@ static int libd_drawNameTag(lua_State *L)
 	UINT16 outlinecolor;
 	UINT8 *basecolormap = NULL;
 	UINT8 *outlinecolormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 
@@ -914,7 +984,15 @@ static int libd_drawNameTag(lua_State *L)
 		outlinecolormap = R_GetTranslationColormap(TC_DEFAULT, outlinecolor, GTC_CACHE);
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
-	V_DrawNameTag(x, y, flags, FRACUNIT, basecolormap, outlinecolormap, str);
+
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawNameTag(list, x, y, str, flags, basecolor, outlinecolor, basecolormap, outlinecolormap);
+	else
+		V_DrawNameTag(x, y, flags, FRACUNIT, basecolormap, outlinecolormap, str);
 	return 0;
 }
 
@@ -929,6 +1007,7 @@ static int libd_drawScaledNameTag(lua_State *L)
 	UINT16 outlinecolor;
 	UINT8 *basecolormap = NULL;
 	UINT8 *outlinecolormap = NULL;
+	huddrawlist_h list;
 
 	HUDONLY
 
@@ -947,7 +1026,15 @@ static int libd_drawScaledNameTag(lua_State *L)
 		outlinecolormap = R_GetTranslationColormap(TC_DEFAULT, outlinecolor, GTC_CACHE);
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
-	V_DrawNameTag(FixedInt(x), FixedInt(y), flags, scale, basecolormap, outlinecolormap, str);
+
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawScaledNameTag(list, x, y, str, flags, scale, basecolor, outlinecolor, basecolormap, outlinecolormap);
+	else
+		V_DrawNameTag(FixedInt(x), FixedInt(y), flags, scale, basecolormap, outlinecolormap, str);
 	return 0;
 }
 
@@ -957,6 +1044,7 @@ static int libd_drawLevelTitle(lua_State *L)
 	INT32 y;
 	const char *str;
 	INT32 flags;
+	huddrawlist_h list;
 
 	HUDONLY
 
@@ -967,7 +1055,14 @@ static int libd_drawLevelTitle(lua_State *L)
 
 	flags &= ~V_PARAMMASK; // Don't let crashes happen.
 
-	V_DrawLevelTitle(x, y, flags, str);
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddDrawLevelTitle(list, x, y, str, flags);
+	else
+		V_DrawLevelTitle(x, y, flags, str);
 	return 0;
 }
 
@@ -1060,6 +1155,7 @@ static int libd_getStringColormap(lua_State *L)
 
 static int libd_fadeScreen(lua_State *L)
 {
+	huddrawlist_h list;
 	UINT16 color = luaL_checkinteger(L, 1);
 	UINT8 strength = luaL_checkinteger(L, 2);
 	const UINT8 maxstrength = ((color & 0xFF00) ? 32 : 10);
@@ -1072,13 +1168,24 @@ static int libd_fadeScreen(lua_State *L)
 	if (strength > maxstrength)
 		return luaL_error(L, "%s fade strength %d out of range (0 - %d)", ((color & 0xFF00) ? "COLORMAP" : "TRANSMAP"), strength, maxstrength);
 
+	lua_getfield(L, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+	list = (huddrawlist_h) lua_touserdata(L, -1);
+	lua_pop(L, 1);
+
 	if (strength == maxstrength) // Allow as a shortcut for drawfill...
 	{
-		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, ((color & 0xFF00) ? 31 : color));
+		if (LUA_HUD_IsDrawListValid(list))
+			LUA_HUD_AddDrawFill(list, 0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, ((color & 0xFF00) ? 31 : color));
+		else
+			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, ((color & 0xFF00) ? 31 : color));
 		return 0;
 	}
 
-	V_DrawFadeScreen(color, strength);
+	if (LUA_HUD_IsDrawListValid(list))
+		LUA_HUD_AddFadeScreen(list, color, strength);
+	else
+		V_DrawFadeScreen(color, strength);
+	
 	return 0;
 }
 
@@ -1356,10 +1463,13 @@ boolean LUA_HudEnabled(enum hud option)
 	return false;
 }
 
-void LUA_SetHudHook(int hook)
+void LUA_SetHudHook(int hook, huddrawlist_h list)
 {
 	lua_getref(gL, lib_draw_ref);
 
+	lua_pushlightuserdata(gL, list);
+	lua_setfield(gL, LUA_REGISTRYINDEX, "HUD_DRAW_LIST");
+
 	switch (hook)
 	{
 		case HUD_HOOK(game): {
diff --git a/src/lua_hudlib_drawlist.c b/src/lua_hudlib_drawlist.c
new file mode 100644
index 0000000000000000000000000000000000000000..bcf132ec69e7fd706f55cb19c30dd543616a1554
--- /dev/null
+++ b/src/lua_hudlib_drawlist.c
@@ -0,0 +1,572 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2014-2016 by John "JTE" Muniz.
+// Copyright (C) 2014-2022 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  lua_hudlib_drawlist.c
+/// \brief a data structure for managing cached drawlists for the Lua hud lib
+
+#include "lua_hudlib_drawlist.h"
+
+#include <string.h>
+
+#include "v_video.h"
+#include "z_zone.h"
+
+enum drawitem_e {
+	DI_Draw = 0,
+	DI_DrawScaled,
+	DI_DrawStretched,
+	DI_DrawCropped,
+	DI_DrawNum,
+	DI_DrawPaddedNum,
+	DI_DrawFill,
+	DI_DrawString,
+	DI_DrawNameTag,
+	DI_DrawScaledNameTag,
+	DI_DrawLevelTitle,
+	DI_FadeScreen,
+	DI_MAX,
+};
+
+// A single draw item with all possible arguments needed for a draw call.
+typedef struct drawitem_s {
+	enum drawitem_e type;
+	fixed_t x;
+	fixed_t y;
+	fixed_t w;
+	fixed_t h;
+	INT32 c;
+	fixed_t scale;
+	fixed_t hscale;
+	fixed_t vscale;
+	patch_t *patch;
+	INT32 flags;
+	UINT16 basecolor;
+	UINT16 outlinecolor;
+	UINT8 *colormap;
+	UINT8 *basecolormap;
+	UINT8 *outlinecolormap;
+	fixed_t sx;
+	fixed_t sy;
+	INT32 num;
+	INT32 digits;
+	const char *str;
+	UINT16 color;
+	UINT8 strength;
+	INT32 align;
+} drawitem_t;
+
+// The internal structure of a drawlist.
+struct huddrawlist_s {
+	drawitem_t *items;
+	size_t items_capacity;
+	size_t items_len;
+	char *strbuf;
+	size_t strbuf_capacity;
+	size_t strbuf_len;
+};
+
+// alignment types for v.drawString
+enum align {
+	align_left = 0,
+	align_center,
+	align_right,
+	align_fixed,
+	align_fixedcenter,
+	align_fixedright,
+	align_small,
+	align_smallfixed,
+	align_smallfixedcenter,
+	align_smallfixedright,
+	align_smallcenter,
+	align_smallright,
+	align_smallthin,
+	align_smallthincenter,
+	align_smallthinright,
+	align_smallthinfixed,
+	align_smallthinfixedcenter,
+	align_smallthinfixedright,
+	align_thin,
+	align_thinfixed,
+	align_thinfixedcenter,
+	align_thinfixedright,
+	align_thincenter,
+	align_thinright
+};
+
+huddrawlist_h LUA_HUD_CreateDrawList(void)
+{
+	huddrawlist_h drawlist;
+
+	drawlist = (huddrawlist_h) Z_CallocAlign(sizeof(struct huddrawlist_s), PU_STATIC, NULL, 64);
+	drawlist->items = NULL;
+	drawlist->items_capacity = 0;
+	drawlist->items_len = 0;
+	drawlist->strbuf = NULL;
+	drawlist->strbuf_capacity = 0;
+	drawlist->strbuf_len = 0;
+
+	return drawlist;
+}
+
+void LUA_HUD_ClearDrawList(huddrawlist_h list)
+{
+	// rather than deallocate, we'll just save the existing allocation and empty
+	// it out for reuse
+
+	// this memset probably isn't necessary
+	if (list->items)
+	{
+		memset(list->items, 0, sizeof(drawitem_t) * list->items_capacity);
+	}
+
+	list->items_len = 0;
+
+	if (list->strbuf)
+	{
+		list->strbuf[0] = 0;
+	}
+	list->strbuf_len = 0;
+}
+
+void LUA_HUD_DestroyDrawList(huddrawlist_h list)
+{
+	if (list == NULL) return;
+
+	if (list->items)
+	{
+		Z_Free(list->items);
+	}
+	Z_Free(list);
+}
+
+boolean LUA_HUD_IsDrawListValid(huddrawlist_h list)
+{
+	if (!list) return false;
+
+	// that's all we can really do to check the validity of the handle right now
+	return true;
+}
+
+static size_t AllocateDrawItem(huddrawlist_h list)
+{
+	if (!list) I_Error("can't allocate draw item: invalid list");
+	if (list->items_capacity <= list->items_len + 1)
+	{
+		if (list->items_capacity == 0) list->items_capacity = 128;
+		else list->items_capacity *= 2;
+		list->items = (drawitem_t *) Z_ReallocAlign(list->items, sizeof(struct drawitem_s) * list->items_capacity, PU_STATIC, NULL, 64);
+	}
+
+	return list->items_len++;
+}
+
+// copy string to list's internal string buffer
+// lua can deallocate the string before we get to use it, so it's important to
+// keep our own copy
+static const char *CopyString(huddrawlist_h list, const char* str)
+{
+	size_t lenstr;
+
+	if (!list) I_Error("can't allocate string; invalid list");
+	lenstr = strlen(str);
+	if (list->strbuf_capacity <= list->strbuf_len + lenstr + 1)
+	{
+		if (list->strbuf_capacity == 0) list->strbuf_capacity = 256;
+		else list->strbuf_capacity *= 2;
+		list->strbuf = (char*) Z_ReallocAlign(list->strbuf, sizeof(char) * list->strbuf_capacity, PU_STATIC, NULL, 8);
+	}
+	const char *result = (const char *) &list->strbuf[list->strbuf_len];
+	strncpy(&list->strbuf[list->strbuf_len], str, lenstr + 1);
+	list->strbuf_len += lenstr + 1;
+	return result;
+}
+
+void LUA_HUD_AddDraw(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_Draw;
+	item->x = x;
+	item->y = y;
+	item->patch = patch;
+	item->flags = flags;
+	item->colormap = colormap;
+}
+
+void LUA_HUD_AddDrawScaled(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t scale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawScaled;
+	item->x = x;
+	item->y = y;
+	item->scale = scale;
+	item->patch = patch;
+	item->flags = flags;
+	item->colormap = colormap;
+}
+
+void LUA_HUD_AddDrawStretched(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t hscale,
+	fixed_t vscale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawStretched;
+	item->x = x;
+	item->y = y;
+	item->hscale = hscale;
+	item->vscale = vscale;
+	item->patch = patch;
+	item->flags = flags;
+	item->colormap = colormap;
+}
+
+void LUA_HUD_AddDrawCropped(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t hscale,
+	fixed_t vscale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap,
+	fixed_t sx,
+	fixed_t sy,
+	fixed_t w,
+	fixed_t h
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawCropped;
+	item->x = x;
+	item->y = y;
+	item->hscale = hscale;
+	item->vscale = vscale;
+	item->patch = patch;
+	item->flags = flags;
+	item->colormap = colormap;
+	item->sx = sx;
+	item->sy = sy;
+	item->w = w;
+	item->h = h;
+}
+
+void LUA_HUD_AddDrawNum(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 num,
+	INT32 flags
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawNum;
+	item->x = x;
+	item->y = y;
+	item->num = num;
+	item->flags = flags;
+}
+
+void LUA_HUD_AddDrawPaddedNum(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 num,
+	INT32 digits,
+	INT32 flags
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawPaddedNum;
+	item->x = x;
+	item->y = y;
+	item->num = num;
+	item->digits = digits;
+	item->flags = flags;
+}
+
+void LUA_HUD_AddDrawFill(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 w,
+	INT32 h,
+	INT32 c
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawFill;
+	item->x = x;
+	item->y = y;
+	item->w = w;
+	item->h = h;
+	item->c = c;
+}
+
+void LUA_HUD_AddDrawString(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	const char *str,
+	INT32 flags,
+	INT32 align
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawString;
+	item->x = x;
+	item->y = y;
+	item->str = CopyString(list, str);
+	item->flags = flags;
+	item->align = align;
+}
+
+void LUA_HUD_AddDrawNameTag(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	const char *str,
+	INT32 flags,
+	UINT16 basecolor,
+	UINT16 outlinecolor,
+	UINT8 *basecolormap,
+	UINT8 *outlinecolormap
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawNameTag;
+	item->x = x;
+	item->y = y;
+	item->str = CopyString(list, str);
+	item->flags = flags;
+	item->basecolor = basecolor;
+	item->outlinecolor = outlinecolor;
+	item->basecolormap = basecolormap;
+	item->outlinecolormap = outlinecolormap;
+}
+
+void LUA_HUD_AddDrawScaledNameTag(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	const char *str,
+	INT32 flags,
+	fixed_t scale,
+	UINT16 basecolor,
+	UINT16 outlinecolor,
+	UINT8 *basecolormap,
+	UINT8 *outlinecolormap
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawScaledNameTag;
+	item->x = x;
+	item->y = y;
+	item->str = CopyString(list, str);
+	item->flags = flags;
+	item->scale = scale;
+	item->basecolor = basecolor;
+	item->outlinecolor = outlinecolor;
+	item->basecolormap = basecolormap;
+	item->outlinecolormap = outlinecolormap;
+}
+
+void LUA_HUD_AddDrawLevelTitle(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	const char *str,
+	INT32 flags
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_DrawLevelTitle;
+	item->x = x;
+	item->y = y;
+	item->str = CopyString(list, str);
+	item->flags = flags;
+}
+
+void LUA_HUD_AddFadeScreen(
+	huddrawlist_h list,
+	UINT16 color,
+	UINT8 strength
+)
+{
+	size_t i = AllocateDrawItem(list);
+	drawitem_t *item = &list->items[i];
+	item->type = DI_FadeScreen;
+	item->color = color;
+	item->strength = strength;
+}
+
+void LUA_HUD_DrawList(huddrawlist_h list)
+{
+	size_t i;
+
+	if (!list) I_Error("HUD drawlist invalid");
+	if (list->items_len <= 0) return;
+	if (!list->items) I_Error("HUD drawlist->items invalid");
+
+	for (i = 0; i < list->items_len; i++)
+	{
+		drawitem_t *item = &list->items[i];
+
+		switch (item->type)
+		{
+			case DI_Draw:
+				V_DrawFixedPatch(item->x<<FRACBITS, item->y<<FRACBITS, FRACUNIT, item->flags, item->patch, item->colormap);
+				break;
+			case DI_DrawScaled:
+				V_DrawFixedPatch(item->x, item->y, item->scale, item->flags, item->patch, item->colormap);
+				break;
+			case DI_DrawStretched:
+				V_DrawStretchyFixedPatch(item->x, item->y, item->hscale, item->vscale, item->flags, item->patch, item->colormap);
+				break;
+			case DI_DrawCropped:
+				V_DrawCroppedPatch(item->x, item->y, item->hscale, item->vscale, item->flags, item->patch, item->colormap, item->sx, item->sy, item->w, item->h);
+				break;
+			case DI_DrawNum:
+				V_DrawTallNum(item->x, item->y, item->flags, item->num);
+				break;
+			case DI_DrawPaddedNum:
+				V_DrawPaddedTallNum(item->x, item->y, item->flags, item->num, item->digits);
+				break;
+			case DI_DrawFill:
+				V_DrawFill(item->x, item->y, item->w, item->h, item->c);
+				break;
+			case DI_DrawString:
+				switch(item->align)
+				{
+				// hu_font
+				case align_left:
+					V_DrawString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_center:
+					V_DrawCenteredString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_right:
+					V_DrawRightAlignedString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_fixed:
+					V_DrawStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_fixedcenter:
+					V_DrawCenteredStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_fixedright:
+					V_DrawRightAlignedStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				// hu_font, 0.5x scale
+				case align_small:
+					V_DrawSmallString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallfixed:
+					V_DrawSmallStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallfixedcenter:
+					V_DrawCenteredSmallStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallfixedright:
+					V_DrawRightAlignedSmallStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallcenter:
+					V_DrawCenteredSmallString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallright:
+					V_DrawRightAlignedSmallString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthin:
+					V_DrawSmallThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthincenter:
+					V_DrawCenteredSmallThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthinright:
+					V_DrawRightAlignedSmallThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthinfixed:
+					V_DrawSmallThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthinfixedcenter:
+					V_DrawCenteredSmallThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_smallthinfixedright:
+					V_DrawRightAlignedSmallThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				// tny_font
+				case align_thin:
+					V_DrawThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_thincenter:
+					V_DrawCenteredThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_thinright:
+					V_DrawRightAlignedThinString(item->x, item->y, item->flags, item->str);
+					break;
+				case align_thinfixed:
+					V_DrawThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_thinfixedcenter:
+					V_DrawCenteredThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				case align_thinfixedright:
+					V_DrawRightAlignedThinStringAtFixed(item->x, item->y, item->flags, item->str);
+					break;
+				}
+				break;
+			case DI_DrawNameTag:
+				V_DrawNameTag(item->x, item->y, item->flags, FRACUNIT, item->basecolormap, item->outlinecolormap, item->str);
+				break;
+			case DI_DrawScaledNameTag:
+				V_DrawNameTag(FixedInt(item->x), FixedInt(item->y), item->flags, item->scale, item->basecolormap, item->outlinecolormap, item->str);
+				break;
+			case DI_DrawLevelTitle:
+				V_DrawLevelTitle(item->x, item->y, item->flags, item->str);
+				break;
+			case DI_FadeScreen:
+				V_DrawFadeScreen(item->color, item->strength);
+				break;
+			default:
+				I_Error("can't draw draw list item: invalid draw list item type");
+				continue;
+		}
+	}
+}
diff --git a/src/lua_hudlib_drawlist.h b/src/lua_hudlib_drawlist.h
new file mode 100644
index 0000000000000000000000000000000000000000..5e97b5c2653bd5dcded976fe5e274a4ec1d31cd7
--- /dev/null
+++ b/src/lua_hudlib_drawlist.h
@@ -0,0 +1,152 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2022-2022 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  lua_hudlib_drawlist.h
+/// \brief a data structure for managing cached drawlists for the Lua hud lib
+
+// The idea behinds this module is to cache drawcall information into an ordered
+// list to repeat the same draw operations in later frames. It's used to ensure
+// that the HUD hooks from Lua are called at precisely 35hz to avoid problems
+// with variable framerates in existing Lua addons.
+
+#ifndef __LUA_HUDLIB_DRAWLIST__
+#define __LUA_HUDLIB_DRAWLIST__
+
+#include "doomtype.h"
+#include "r_defs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct huddrawlist_s *huddrawlist_h;
+
+// Create a new drawlist. Returns a handle to it.
+huddrawlist_h LUA_HUD_CreateDrawList(void);
+// Clears the draw list.
+void LUA_HUD_ClearDrawList(huddrawlist_h list);
+// Destroys the drawlist, invalidating the given handle
+void LUA_HUD_DestroyDrawList(huddrawlist_h list);
+boolean LUA_HUD_IsDrawListValid(huddrawlist_h list);
+
+void LUA_HUD_AddDraw(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+);
+void LUA_HUD_AddDrawScaled(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t scale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+);
+void LUA_HUD_AddDrawStretched(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t hscale,
+	fixed_t vscale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap
+);
+void LUA_HUD_AddDrawCropped(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	fixed_t hscale,
+	fixed_t vscale,
+	patch_t *patch,
+	INT32 flags,
+	UINT8 *colormap,
+	fixed_t sx,
+	fixed_t sy,
+	fixed_t w,
+	fixed_t h
+);
+void LUA_HUD_AddDrawNum(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 num,
+	INT32 flags
+);
+void LUA_HUD_AddDrawPaddedNum(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 num,
+	INT32 digits,
+	INT32 flags
+);
+void LUA_HUD_AddDrawFill(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	INT32 w,
+	INT32 h,
+	INT32 c
+);
+void LUA_HUD_AddDrawString(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	const char *str,
+	INT32 flags,
+	INT32 align
+);
+void LUA_HUD_AddDrawNameTag(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	const char *str,
+	INT32 flags,
+	UINT16 basecolor,
+	UINT16 outlinecolor,
+	UINT8 *basecolormap,
+	UINT8 *outlinecolormap
+);
+void LUA_HUD_AddDrawScaledNameTag(
+	huddrawlist_h list,
+	fixed_t x,
+	fixed_t y,
+	const char *str,
+	INT32 flags,
+	fixed_t scale,
+	UINT16 basecolor,
+	UINT16 outlinecolor,
+	UINT8 *basecolormap,
+	UINT8 *outlinecolormap
+);
+void LUA_HUD_AddDrawLevelTitle(
+	huddrawlist_h list,
+	INT32 x,
+	INT32 y,
+	const char *str,
+	INT32 flags
+);
+void LUA_HUD_AddFadeScreen(
+	huddrawlist_h list,
+	UINT16 color,
+	UINT8 strength
+);
+
+// Draws the given draw list
+void LUA_HUD_DrawList(huddrawlist_h list);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif 
+
+#endif // __LUA_HUDLIB_DRAWLIST__
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index 953b390006c3419be9a63c2a925d112716568eb4..2aec48c907886a26dc8e22fe1ce58ee5f9df4a98 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -458,7 +458,7 @@ static int mobj_get(lua_State *L)
 }
 
 #define NOSET luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " should not be set directly.", mobj_opt[field])
-#define NOSETPOS luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " should not be set directly. Use " LUA_QL("P_Move") ", " LUA_QL("P_TryMove") ", or " LUA_QL("P_TeleportMove") " instead.", mobj_opt[field])
+#define NOSETPOS luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " should not be set directly. Use " LUA_QL("P_Move") ", " LUA_QL("P_TryMove") ", or " LUA_QL("P_SetOrigin") ", or " LUA_QL("P_MoveOrigin") " instead.", mobj_opt[field])
 static int mobj_set(lua_State *L)
 {
 	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -767,6 +767,7 @@ static int mobj_set(lua_State *L)
 			scale = FRACUNIT/100;
 		mo->destscale = scale;
 		P_SetScale(mo, scale);
+		mo->old_scale = scale;
 		break;
 	}
 	case mobj_destscale:
diff --git a/src/m_anigif.c b/src/m_anigif.c
index b3a1d0fe22304faea9f7b9656bd9dcd4e7a872a9..41765e6e1cc415e81c7a0ab9f9b8f893eb993df0 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -608,7 +608,7 @@ static void GIF_framewrite(void)
 		{
 			// golden's attempt at creating a "dynamic delay"
 			UINT16 mingifdelay = 10; // minimum gif delay in milliseconds (keep at 10 because gifs can't get more precise).
-			gif_delayus += I_PreciseToMicros(I_GetPreciseTime() - gif_prevframetime); // increase delay by how much time was spent between last measurement
+			gif_delayus += (I_GetPreciseTime() - gif_prevframetime) / (I_GetPrecisePrecision() / 1000000); // increase delay by how much time was spent between last measurement
 
 			if (gif_delayus/1000 >= mingifdelay) // delay is big enough to be able to effect gif frame delay?
 			{
@@ -621,7 +621,7 @@ static void GIF_framewrite(void)
 		{
 			float delayf = ceil(100.0f/NEWTICRATE);
 
-			delay = (UINT16)I_PreciseToMicros((I_GetPreciseTime() - gif_prevframetime))/10/1000;
+			delay = (UINT16)((I_GetPreciseTime() - gif_prevframetime)) / (I_GetPrecisePrecision() / 1000000) /10/1000;
 
 			if (delay < (UINT16)(delayf))
 				delay = (UINT16)(delayf);
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 40b9a1230eb439e5ef1afff546e0368e66514bbc..89c8009aed321a0535d3e9921432fe59b09a68ee 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -475,7 +475,7 @@ void Command_RTeleport_f(void)
 	CONS_Printf(M_GetText("Teleporting by %d, %d, %d...\n"), intx, inty, FixedInt((intz-p->mo->z)));
 
 	P_MapStart();
-	if (!P_TeleportMove(p->mo, p->mo->x+intx*FRACUNIT, p->mo->y+inty*FRACUNIT, intz))
+	if (!P_SetOrigin(p->mo, p->mo->x+intx*FRACUNIT, p->mo->y+inty*FRACUNIT, intz))
 		CONS_Alert(CONS_WARNING, M_GetText("Unable to teleport to that spot!\n"));
 	else
 		S_StartSound(p->mo, sfx_mixup);
@@ -696,7 +696,7 @@ void Command_Teleport_f(void)
 	}
 
 	P_MapStart();
-	if (!P_TeleportMove(p->mo, intx, inty, intz))
+	if (!P_SetOrigin(p->mo, intx, inty, intz))
 		CONS_Alert(CONS_WARNING, M_GetText("Unable to teleport to that spot!\n"));
 	else
 		S_StartSound(p->mo, sfx_mixup);
@@ -1315,13 +1315,13 @@ void OP_ObjectplaceMovement(player_t *player)
 	if (cmd->forwardmove != 0)
 	{
 		P_Thrust(player->mo, player->mo->angle, (cmd->forwardmove*player->mo->scale/MAXPLMOVE)*cv_speed.value);
-		P_TeleportMove(player->mo, player->mo->x+player->mo->momx, player->mo->y+player->mo->momy, player->mo->z);
+		P_MoveOrigin(player->mo, player->mo->x+player->mo->momx, player->mo->y+player->mo->momy, player->mo->z);
 		player->mo->momx = player->mo->momy = 0;
 	}
 	if (cmd->sidemove != 0)
 	{
 		P_Thrust(player->mo, player->mo->angle-ANGLE_90, (cmd->sidemove*player->mo->scale/MAXPLMOVE)*cv_speed.value);
-		P_TeleportMove(player->mo, player->mo->x+player->mo->momx, player->mo->y+player->mo->momy, player->mo->z);
+		P_MoveOrigin(player->mo, player->mo->x+player->mo->momx, player->mo->y+player->mo->momy, player->mo->z);
 		player->mo->momx = player->mo->momy = 0;
 	}
 
diff --git a/src/m_menu.c b/src/m_menu.c
index 9daab767f43e98f4c642fcd0700a217473d2e7c0..3a5a1a13a8bbc2972b21b74df1a11fc70faacbd9 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -22,6 +22,7 @@
 #include "d_main.h"
 #include "d_netcmd.h"
 #include "console.h"
+#include "r_fps.h"
 #include "r_local.h"
 #include "hu_stuff.h"
 #include "g_game.h"
@@ -31,6 +32,7 @@
 // Data.
 #include "sounds.h"
 #include "s_sound.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "i_threads.h"
 
@@ -171,10 +173,10 @@ static INT32 vidm_nummodes;
 static INT32 vidm_column_size;
 
 // new menus
-static tic_t recatkdrawtimer = 0;
-static tic_t ntsatkdrawtimer = 0;
+static fixed_t recatkdrawtimer = 0;
+static fixed_t ntsatkdrawtimer = 0;
 
-static tic_t charseltimer = 0;
+static fixed_t charseltimer = 0;
 static fixed_t char_scroll = 0;
 #define charscrollamt 128*FRACUNIT
 
@@ -207,17 +209,17 @@ menu_t SPauseDef;
 static levelselect_t levelselect = {0, NULL};
 static UINT8 levelselectselect[3];
 static patch_t *levselp[2][3];
-static INT32 lsoffs[2];
+static fixed_t lsoffs[2];
 
 #define lsrow levelselectselect[0]
 #define lscol levelselectselect[1]
 #define lshli levelselectselect[2]
 
 #define lshseperation 101
-#define lsbasevseperation (62*vid.height)/(BASEVIDHEIGHT*vid.dupy) //62
+#define lsbasevseperation ((62*vid.height)/(BASEVIDHEIGHT*vid.dupy)) //62
 #define lsheadingheight 16
 #define getheadingoffset(row) (levelselect.rows[row].header[0] ? lsheadingheight : 0)
-#define lsvseperation(row) lsbasevseperation + getheadingoffset(row)
+#define lsvseperation(row) (lsbasevseperation + getheadingoffset(row))
 #define lswide(row) levelselect.rows[row].mapavailable[3]
 
 #define lsbasex 19
@@ -1386,6 +1388,7 @@ static menuitem_t OP_VideoOptionsMenu[] =
 #ifdef HWRENDER
 	{IT_HEADER, NULL, "Renderer", NULL, 208},
 	{IT_CALL | IT_STRING, NULL, "OpenGL Options...",         M_OpenGLOptionsMenu, 214},
+	{IT_STRING | IT_CVAR, NULL, "FPS Cap",                   &cv_fpscap,          219},
 #endif
 };
 
@@ -5496,7 +5499,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				{
 					if (!lsoffs[0]) // prevent sound spam
 					{
-						lsoffs[0] = -8;
+						lsoffs[0] = -8 * FRACUNIT;
 						S_StartSound(NULL,sfx_s3kb7);
 					}
 					return;
@@ -5505,7 +5508,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 			}
 			lsrow++;
 
-			lsoffs[0] = lsvseperation(lsrow);
+			lsoffs[0] = lsvseperation(lsrow) * FRACUNIT;
 
 			if (levelselect.rows[lsrow].header[0])
 				lshli = lsrow;
@@ -5524,7 +5527,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				{
 					if (!lsoffs[0]) // prevent sound spam
 					{
-						lsoffs[0] = 8;
+						lsoffs[0] = 8 * FRACUNIT;
 						S_StartSound(NULL,sfx_s3kb7);
 					}
 					return;
@@ -5533,7 +5536,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 			}
 			lsrow--;
 
-			lsoffs[0] = -lsvseperation(iter);
+			lsoffs[0] = -lsvseperation(iter) * FRACUNIT;
 
 			if (levelselect.rows[lsrow].header[0])
 				lshli = lsrow;
@@ -5574,7 +5577,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				}
 				else if (!lsoffs[0]) // prevent sound spam
 				{
-					lsoffs[0] = -8;
+					lsoffs[0] = -8 * FRACUNIT;
 					S_StartSound(NULL,sfx_s3kb2);
 				}
 				break;
@@ -5600,14 +5603,14 @@ static void M_HandleLevelPlatter(INT32 choice)
 			{
 				lscol++;
 
-				lsoffs[1] = (lswide(lsrow) ? 8 : -lshseperation);
+				lsoffs[1] = (lswide(lsrow) ? 8 : -lshseperation) * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 
 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
 			}
 			else if (!lsoffs[1]) // prevent sound spam
 			{
-				lsoffs[1] = 8;
+				lsoffs[1] = 8 * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 			}
 			break;
@@ -5632,14 +5635,14 @@ static void M_HandleLevelPlatter(INT32 choice)
 			{
 				lscol--;
 
-				lsoffs[1] = (lswide(lsrow) ? -8 : lshseperation);
+				lsoffs[1] = (lswide(lsrow) ? -8 : lshseperation) * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 
 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
 			}
 			else if (!lsoffs[1]) // prevent sound spam
 			{
-				lsoffs[1] = -8;
+				lsoffs[1] = -8 * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 			}
 			break;
@@ -5802,7 +5805,7 @@ static void M_DrawRecordAttackForeground(void)
 
 	for (i = -12; i < (BASEVIDHEIGHT/height) + 12; i++)
 	{
-		INT32 y = ((i*height) - (height - ((recatkdrawtimer*2)%height)));
+		INT32 y = ((i*height) - (height - ((FixedInt(recatkdrawtimer*2))%height)));
 		// don't draw above the screen
 		{
 			INT32 sy = FixedMul(y, dupz<<FRACBITS) >> FRACBITS;
@@ -5819,17 +5822,18 @@ static void M_DrawRecordAttackForeground(void)
 	}
 
 	// draw clock
-	fa = (FixedAngle(((recatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
+	fa = (FixedAngle(((FixedInt(recatkdrawtimer * 4)) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
 	V_DrawSciencePatch(160<<FRACBITS, (80<<FRACBITS) + (4*FINESINE(fa)), 0, clock, FRACUNIT);
 
 	// Increment timer.
-	recatkdrawtimer++;
+	recatkdrawtimer += renderdeltatics;
+	if (recatkdrawtimer < 0) recatkdrawtimer = 0;
 }
 
 // NiGHTS Attack background.
 static void M_DrawNightsAttackMountains(void)
 {
-	static INT32 bgscrollx;
+	static fixed_t bgscrollx;
 	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
 	patch_t *background = W_CachePatchName(curbgname, PU_PATCH);
 	INT16 w = background->width;
@@ -5845,7 +5849,7 @@ static void M_DrawNightsAttackMountains(void)
 	if (x < BASEVIDWIDTH)
 		V_DrawScaledPatch(x, y, V_SNAPTOLEFT, background);
 
-	bgscrollx += (FRACUNIT/2);
+	bgscrollx += FixedMul(FRACUNIT/2, renderdeltatics);
 	if (bgscrollx > w<<FRACBITS)
 		bgscrollx &= 0xFFFF;
 }
@@ -5876,7 +5880,7 @@ static void M_DrawNightsAttackBackground(void)
 	M_DrawNightsAttackMountains();
 
 	// back top foreground patch
-	x = 0-(ntsatkdrawtimer%backtopwidth);
+	x = 0-(FixedInt(ntsatkdrawtimer)%backtopwidth);
 	V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, backtopfg);
 	for (i = 0; i < 3; i++)
 	{
@@ -5887,7 +5891,7 @@ static void M_DrawNightsAttackBackground(void)
 	}
 
 	// front top foreground patch
-	x = 0-((ntsatkdrawtimer*2)%fronttopwidth);
+	x = 0-(FixedInt(ntsatkdrawtimer*2)%fronttopwidth);
 	V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, fronttopfg);
 	for (i = 0; i < 3; i++)
 	{
@@ -5898,7 +5902,7 @@ static void M_DrawNightsAttackBackground(void)
 	}
 
 	// back bottom foreground patch
-	x = 0-(ntsatkdrawtimer%backbottomwidth);
+	x = 0-(FixedInt(ntsatkdrawtimer)%backbottomwidth);
 	y = BASEVIDHEIGHT - backbottomheight;
 	V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, backbottomfg);
 	for (i = 0; i < 3; i++)
@@ -5910,7 +5914,7 @@ static void M_DrawNightsAttackBackground(void)
 	}
 
 	// front bottom foreground patch
-	x = 0-((ntsatkdrawtimer*2)%frontbottomwidth);
+	x = 0-(FixedInt(ntsatkdrawtimer*2)%frontbottomwidth);
 	y = BASEVIDHEIGHT - frontbottomheight;
 	V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, frontbottomfg);
 	for (i = 0; i < 3; i++)
@@ -5922,7 +5926,8 @@ static void M_DrawNightsAttackBackground(void)
 	}
 
 	// Increment timer.
-	ntsatkdrawtimer++;
+	ntsatkdrawtimer += renderdeltatics;
+	if (ntsatkdrawtimer < 0) ntsatkdrawtimer = 0;
 }
 
 // NiGHTS Attack floating Super Sonic.
@@ -5930,15 +5935,15 @@ static patch_t *ntssupersonic[2];
 static void M_DrawNightsAttackSuperSonic(void)
 {
 	const UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_YELLOW, GTC_CACHE);
-	INT32 timer = (ntsatkdrawtimer/4) % 2;
-	angle_t fa = (FixedAngle(((ntsatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
+	INT32 timer = FixedInt(ntsatkdrawtimer/4) % 2;
+	angle_t fa = (FixedAngle((FixedInt(ntsatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
 	V_DrawFixedPatch(235<<FRACBITS, (120<<FRACBITS) - (8*FINESINE(fa)), FRACUNIT, 0, ntssupersonic[timer], colormap);
 }
 
 static void M_DrawLevelPlatterMenu(void)
 {
 	UINT8 iter = lsrow, sizeselect = (lswide(lsrow) ? 1 : 0);
-	INT32 y = lsbasey + lsoffs[0] - getheadingoffset(lsrow);
+	INT32 y = lsbasey + FixedInt(lsoffs[0]) - getheadingoffset(lsrow);
 	const INT32 cursorx = (sizeselect ? 0 : (lscol*lshseperation));
 
 	if (currentMenu->prevMenu == &SP_TimeAttackDef)
@@ -6006,7 +6011,7 @@ static void M_DrawLevelPlatterMenu(void)
 
 	// draw cursor box
 	if (levellistmode != LLM_CREATESERVER || lsrow)
-		V_DrawSmallScaledPatch(lsbasex + cursorx + lsoffs[1], lsbasey+lsoffs[0], 0, (levselp[sizeselect][((skullAnimCounter/4) ? 1 : 0)]));
+		V_DrawSmallScaledPatch(lsbasex + cursorx + FixedInt(lsoffs[1]), lsbasey+FixedInt(lsoffs[0]), 0, (levselp[sizeselect][((skullAnimCounter/4) ? 1 : 0)]));
 
 #if 0
 	if (levelselect.rows[lsrow].maplist[lscol] > 0)
@@ -6014,13 +6019,26 @@ static void M_DrawLevelPlatterMenu(void)
 #endif
 
 	// handle movement of cursor box
-	if (lsoffs[0] > 1 || lsoffs[0] < -1)
-		lsoffs[0] = 2*lsoffs[0]/3;
+	fixed_t cursormovefrac = FixedDiv(2, 3);
+	if (lsoffs[0] > FRACUNIT || lsoffs[0] < -FRACUNIT)
+	{
+		fixed_t offs = lsoffs[0];
+		fixed_t newoffs = FixedMul(offs, cursormovefrac);
+		fixed_t deltaoffs = newoffs - offs;
+		newoffs = offs + FixedMul(deltaoffs, renderdeltatics);
+		lsoffs[0] = newoffs;
+	}
 	else
 		lsoffs[0] = 0;
 
-	if (lsoffs[1] > 1 || lsoffs[1] < -1)
-		lsoffs[1] = 2*lsoffs[1]/3;
+	if (lsoffs[1] > FRACUNIT || lsoffs[1] < -FRACUNIT)
+	{
+		fixed_t offs = lsoffs[1];
+		fixed_t newoffs = FixedMul(offs, cursormovefrac);
+		fixed_t deltaoffs = newoffs - offs;
+		newoffs = offs + FixedMul(deltaoffs, renderdeltatics);
+		lsoffs[1] = newoffs;
+	}
 	else
 		lsoffs[1] = 0;
 
@@ -7647,7 +7665,7 @@ static void M_HandleEmblemHints(INT32 choice)
 
 static musicdef_t *curplaying = NULL;
 static INT32 st_sel = 0, st_cc = 0;
-static tic_t st_time = 0;
+static fixed_t st_time = 0;
 static patch_t* st_radio[9];
 static patch_t* st_launchpad[4];
 
@@ -7711,16 +7729,17 @@ static void M_DrawSoundTest(void)
 		{
 			if (cv_soundtest.value)
 			{
-				frame[1] = (2-st_time);
+				frame[1] = (2 - (st_time >> FRACBITS));
 				frame[2] = ((cv_soundtest.value - 1) % 9);
 				frame[3] += (((cv_soundtest.value - 1) / 9) % (FIRSTSUPERCOLOR - frame[3]));
-				if (st_time < 2)
-					st_time++;
+				if (st_time < (2 << FRACBITS))
+					st_time += renderdeltatics;
 			}
 		}
 		else
 		{
-			if (curplaying->stoppingtics && st_time >= curplaying->stoppingtics)
+			fixed_t stoppingtics = (fixed_t)(curplaying->stoppingtics) << FRACBITS;
+			if (stoppingtics && st_time >= stoppingtics)
 			{
 				curplaying = NULL;
 				st_time = 0;
@@ -7731,11 +7750,11 @@ static void M_DrawSoundTest(void)
 				angle_t ang;
 				//bpm = FixedDiv((60*TICRATE)<<FRACBITS, bpm); -- bake this in on load
 
-				work = st_time<<FRACBITS;
+				work = st_time;
 				work %= bpm;
 
-				if (st_time >= (FRACUNIT>>1)) // prevent overflow jump - takes about 15 minutes of loop on the same song to reach
-					st_time = (work>>FRACBITS);
+				if (st_time >= (FRACUNIT << (FRACBITS - 2))) // prevent overflow jump - takes about 15 minutes of loop on the same song to reach
+					st_time = work;
 
 				work = FixedDiv(work*180, bpm);
 				frame[0] = 8-(work/(20<<FRACBITS));
@@ -7746,7 +7765,7 @@ static void M_DrawSoundTest(void)
 				hscale -= bounce/16;
 				vscale += bounce/16;
 
-				st_time++;
+				st_time += renderdeltatics;
 			}
 		}
 	}
@@ -7786,7 +7805,7 @@ static void M_DrawSoundTest(void)
 
 	V_DrawFill(y, 20, vid.width/vid.dupx, 24, 159);
 	{
-		static fixed_t st_scroll = -1;
+		static fixed_t st_scroll = -FRACUNIT;
 		const char* titl;
 		x = 16;
 		V_DrawString(x, 10, 0, "NOW PLAYING:");
@@ -7802,10 +7821,12 @@ static void M_DrawSoundTest(void)
 
 		i = V_LevelNameWidth(titl);
 
-		if (++st_scroll >= i)
-			st_scroll %= i;
+		st_scroll += renderdeltatics;
+
+		while (st_scroll >= (i << FRACBITS))
+			st_scroll -= i << FRACBITS;
 
-		x -= st_scroll;
+		x -= st_scroll >> FRACBITS;
 
 		while (x < BASEVIDWIDTH-y)
 			x += i;
@@ -8329,8 +8350,8 @@ static void M_StartTutorial(INT32 choice)
 // ==============
 
 static INT32 saveSlotSelected = 1;
-static INT32 loadgamescroll = 0;
-static UINT8 loadgameoffset = 0;
+static fixed_t loadgamescroll = 0;
+static fixed_t loadgameoffset = 0;
 
 static void M_CacheLoadGameData(void)
 {
@@ -8355,14 +8376,14 @@ static void M_DrawLoadGameData(void)
 	{
 		prev_i = i;
 		savetodraw = (saveSlotSelected + i + numsaves)%numsaves;
-		x = (BASEVIDWIDTH/2 - 42 + loadgamescroll) + (i*hsep);
+		x = (BASEVIDWIDTH/2 - 42 + FixedInt(loadgamescroll)) + (i*hsep);
 		y = 33 + 9;
 
 		{
 			INT32 diff = x - (BASEVIDWIDTH/2 - 42);
 			if (diff < 0)
 				diff = -diff;
-			diff = (42 - diff)/3 - loadgameoffset;
+			diff = (42 - diff)/3 - FixedInt(loadgameoffset);
 			if (diff < 0)
 				diff = 0;
 			y -= diff;
@@ -8647,14 +8668,23 @@ skiplife:
 static void M_DrawLoad(void)
 {
 	M_DrawMenuTitle();
+	fixed_t scrollfrac = FixedDiv(2, 3);
 
-	if (loadgamescroll > 1 || loadgamescroll < -1)
-		loadgamescroll = 2*loadgamescroll/3;
+	if (loadgamescroll > FRACUNIT || loadgamescroll < -FRACUNIT)
+	{
+		fixed_t newscroll = FixedMul(loadgamescroll, scrollfrac);
+		fixed_t deltascroll = FixedMul(newscroll - loadgamescroll, renderdeltatics);
+		loadgamescroll += deltascroll;
+	}
 	else
 		loadgamescroll = 0;
 
-	if (loadgameoffset > 1)
-		loadgameoffset = 2*loadgameoffset/3;
+	if (loadgameoffset > FRACUNIT)
+	{
+		fixed_t newoffs = FixedMul(loadgameoffset, scrollfrac);
+		fixed_t deltaoffs = FixedMul(newoffs - loadgameoffset, renderdeltatics);
+		loadgameoffset += deltaoffs;
+	}
 	else
 		loadgameoffset = 0;
 
@@ -8863,7 +8893,7 @@ static void M_ReadSaveStrings(void)
 	UINT8 lastseen = 0;
 
 	loadgamescroll = 0;
-	loadgameoffset = 14;
+	loadgameoffset = 14 * FRACUNIT;
 
 	for (i = 1; (i < MAXSAVEGAMES); i++) // slot 0 is no save
 	{
@@ -8954,7 +8984,7 @@ static void M_HandleLoadSave(INT32 choice)
 			++saveSlotSelected;
 			if (saveSlotSelected >= numsaves)
 				saveSlotSelected -= numsaves;
-			loadgamescroll = 90;
+			loadgamescroll = 90 * FRACUNIT;
 			break;
 
 		case KEY_LEFTARROW:
@@ -8962,7 +8992,7 @@ static void M_HandleLoadSave(INT32 choice)
 			--saveSlotSelected;
 			if (saveSlotSelected < 0)
 				saveSlotSelected += numsaves;
-			loadgamescroll = -90;
+			loadgamescroll = -90 * FRACUNIT;
 			break;
 
 		case KEY_ENTER:
@@ -8987,7 +9017,7 @@ static void M_HandleLoadSave(INT32 choice)
 			else if (!loadgameoffset)
 			{
 				S_StartSound(NULL, sfx_lose);
-				loadgameoffset = 14;
+				loadgameoffset = 14 * FRACUNIT;
 			}
 			break;
 
@@ -9013,7 +9043,7 @@ static void M_HandleLoadSave(INT32 choice)
 				}
 				else
 					S_StartSound(NULL, sfx_lose);
-				loadgameoffset = 14;
+				loadgameoffset = 14 * FRACUNIT;
 			}
 			break;
 	}
@@ -9073,13 +9103,13 @@ static void M_LoadGame(INT32 choice)
 //
 void M_ForceSaveSlotSelected(INT32 sslot)
 {
-	loadgameoffset = 14;
+	loadgameoffset = 14 * FRACUNIT;
 
 	// Already there? Whatever, then!
 	if (sslot == saveSlotSelected)
 		return;
 
-	loadgamescroll = 90;
+	loadgamescroll = 90 * FRACUNIT;
 	if (saveSlotSelected <= numsaves/2)
 		loadgamescroll = -loadgamescroll;
 
@@ -9323,8 +9353,8 @@ static void M_DrawSetupChoosePlayerMenu(void)
 	INT32 x, y;
 	INT32 w = (vid.width/vid.dupx);
 
-	if (abs(char_scroll) > FRACUNIT)
-		char_scroll -= (char_scroll>>2);
+	if (abs(char_scroll) > FRACUNIT/4)
+		char_scroll -= FixedMul((char_scroll>>2), renderdeltatics);
 	else // close enough.
 		char_scroll = 0; // just be exact now.
 
@@ -9352,7 +9382,7 @@ static void M_DrawSetupChoosePlayerMenu(void)
 
 	// Don't render the title map
 	hidetitlemap = true;
-	charseltimer++;
+	charseltimer += renderdeltatics;
 
 	// Background and borders
 	V_DrawFill(0, 0, bgwidth, vid.height, V_SNAPTOTOP|colormap[101]);
@@ -9364,7 +9394,7 @@ static void M_DrawSetupChoosePlayerMenu(void)
 			V_DrawFill(0, 0, bw, vid.height, V_NOSCALESTART|col);
 	}
 
-	y = (charseltimer%32);
+	y = (charseltimer / FRACUNIT) % 32;
 	V_DrawMappedPatch(0, y-bgheight, V_SNAPTOTOP, charbg, colormap);
 	V_DrawMappedPatch(0, y, V_SNAPTOTOP, charbg, colormap);
 	V_DrawMappedPatch(0, y+bgheight, V_SNAPTOTOP, charbg, colormap);
@@ -10631,7 +10661,7 @@ static void M_Marathon(INT32 choice)
 	titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
 	M_SetupNextMenu(&SP_MarathonDef);
 	itemOn = marathonstart; // "Start" is selected.
-	recatkdrawtimer = 50-8;
+	recatkdrawtimer = (50-8) * FRACUNIT;
 	char_scroll = 0;
 }
 
@@ -10712,13 +10742,16 @@ void M_DrawMarathon(void)
 	x = (((BASEVIDWIDTH-82)/2)+11)<<FRACBITS;
 	y = (((BASEVIDHEIGHT-82)/2)+12-10)<<FRACBITS;
 
-	cnt = (36*(recatkdrawtimer<<FRACBITS))/TICRATE;
+	cnt = (36 * recatkdrawtimer) / TICRATE;
 	fa = (FixedAngle(cnt)>>ANGLETOFINESHIFT) & FINEMASK;
 	y -= (10*FINECOSINE(fa));
 
-	recatkdrawtimer++;
+	if (renderisnewtic)
+	{
+		recatkdrawtimer += FRACUNIT;
+	}
 
-	soffset = cnt = (recatkdrawtimer%50);
+	soffset = cnt = ((recatkdrawtimer >> FRACBITS) % 50);
 	if (!useBlackRock)
 	{
 		if (cnt > 8)
@@ -10757,7 +10790,7 @@ void M_DrawMarathon(void)
 	}
 
 	w = char_scroll + (((8-cnt)*(8-cnt))<<(FRACBITS-5));
-	if (soffset == 50-1)
+	if (soffset == 50-1 && renderisnewtic)
 		w += FRACUNIT/2;
 
 	{
@@ -10812,11 +10845,11 @@ void M_DrawMarathon(void)
 
 	if (!soffset)
 	{
-		char_scroll += (360<<FRACBITS)/42; // like a clock, ticking at 42bpm!
+		char_scroll += (360 * renderdeltatics)/42; // like a clock, ticking at 42bpm!
 		if (char_scroll >= 360<<FRACBITS)
 			char_scroll -= 360<<FRACBITS;
-		if (recatkdrawtimer > (10*TICRATE))
-			recatkdrawtimer -= (10*TICRATE);
+		if (recatkdrawtimer > ((10 << FRACBITS) * TICRATE))
+			recatkdrawtimer -= ((10 << FRACBITS) * TICRATE);
 	}
 
 	M_DrawMenuTitle();
@@ -11028,7 +11061,7 @@ static INT32 menuRoomIndex = 0;
 
 static void M_DrawRoomMenu(void)
 {
-	static int frame = -12;
+	static fixed_t frame = -(12 << FRACBITS);
 	int dot_frame;
 	char text[4];
 
@@ -11039,7 +11072,7 @@ static void M_DrawRoomMenu(void)
 
 	if (m_waiting_mode)
 	{
-		dot_frame = frame / 4;
+		dot_frame = (int)(frame >> FRACBITS) / 4;
 		dots = dot_frame + 3;
 
 		strcpy(text, "   ");
@@ -11052,8 +11085,9 @@ static void M_DrawRoomMenu(void)
 			strncpy(&text[dot_frame], "...", min(dots, 3 - dot_frame));
 		}
 
-		if (++frame == 12)
-			frame = -12;
+		frame += renderdeltatics;
+		while (frame >= (12 << FRACBITS))
+			frame -= 12 << FRACBITS;
 
 		currentMenu->menuitems[0].text = text;
 	}
@@ -11819,7 +11853,7 @@ static void M_HandleConnectIP(INT32 choice)
 // ========================
 // Tails 03-02-2002
 
-static UINT8      multi_tics;
+static fixed_t    multi_tics;
 static UINT8      multi_frame;
 static UINT8      multi_spr2;
 
@@ -11887,10 +11921,11 @@ static void M_DrawSetupMultiPlayerMenu(void)
 	y += 11;
 
 	// anim the player in the box
-	if (--multi_tics <= 0)
+	multi_tics -= renderdeltatics;
+	while (multi_tics <= 0)
 	{
 		multi_frame++;
-		multi_tics = 4;
+		multi_tics += 4*FRACUNIT;
 	}
 
 #define charw 74
@@ -12161,7 +12196,7 @@ static void M_SetupMultiPlayer(INT32 choice)
 	(void)choice;
 
 	multi_frame = 0;
-	multi_tics = 4;
+	multi_tics = 4*FRACUNIT;
 	strcpy(setupm_name, cv_playername.string);
 
 	// set for player 1
@@ -12205,7 +12240,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 	(void)choice;
 
 	multi_frame = 0;
-	multi_tics = 4;
+	multi_tics = 4*FRACUNIT;
 	strcpy (setupm_name, cv_playername2.string);
 
 	// set for splitscreen secondary player
@@ -13536,7 +13571,8 @@ void M_QuitResponse(INT32 ch)
 		{
 			V_DrawScaledPatch(0, 0, 0, W_CachePatchName("GAMEQUIT", PU_PATCH)); // Demo 3 Quit Screen Tails 06-16-2001
 			I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001
-			I_Sleep();
+			I_Sleep(cv_sleep.value);
+			I_UpdateTime(cv_timescale.value);
 		}
 	}
 	I_Quit();
diff --git a/src/m_misc.c b/src/m_misc.c
index bb5f2568728cf3f8db1d4677447d93e3692a39ba..d98c949cf476882199a0eefac99235086b71577d 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -36,6 +36,7 @@
 #include "v_video.h"
 #include "z_zone.h"
 #include "g_input.h"
+#include "i_time.h"
 #include "i_video.h"
 #include "d_main.h"
 #include "m_argv.h"
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 9fc41000d24548deb4a58f3b5071c3f6a9fa77e1..9f65a7616abfe427eaa1abd69fe16c128d559fba 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -17,6 +17,7 @@
 #include "i_system.h"
 #include "z_zone.h"
 #include "p_local.h"
+#include "r_fps.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -122,6 +123,12 @@ perfstatrow_t commoncounter_rows[] = {
 	{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},
@@ -261,7 +268,7 @@ static INT32 PS_GetMetricAverage(ps_metric_t *metric, boolean time_metric)
 	for (i = 0; i < cv_ps_samplesize.value; i++)
 	{
 		if (time_metric)
-			sum += I_PreciseToMicros(*((precise_t*)history_read_pos));
+			sum += (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
 		else
 			sum += *((INT32*)history_read_pos);
 		history_read_pos += value_size;
@@ -281,7 +288,7 @@ static INT32 PS_GetMetricMinOrMax(ps_metric_t *metric, boolean time_metric, bool
 	{
 		INT32 value;
 		if (time_metric)
-			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+			value = (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
 		else
 			value = *((INT32*)history_read_pos);
 
@@ -309,7 +316,7 @@ static INT32 PS_GetMetricSD(ps_metric_t *metric, boolean time_metric)
 	{
 		INT64 value;
 		if (time_metric)
-			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+			value = (*((precise_t*)history_read_pos)) / (I_GetPrecisePrecision() / 1000000);
 		else
 			value = *((INT32*)history_read_pos);
 
@@ -339,7 +346,7 @@ static INT32 PS_GetMetricScreenValue(ps_metric_t *metric, boolean time_metric)
 	else
 	{
 		if (time_metric)
-			return I_PreciseToMicros(metric->value.p);
+			return (metric->value.p) / (I_GetPrecisePrecision() / 1000000);
 		else
 			return metric->value.i;
 	}
@@ -473,6 +480,9 @@ static void PS_UpdateFrameStats(void)
 		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)
 		{
@@ -634,7 +644,7 @@ static void PS_DrawRenderStats(void)
 {
 	const boolean hires = PS_HighResolution();
 	const int half_row = hires ? 5 : 4;
-	int x, y;
+	int x, y, cy = 10;
 
 	PS_DrawDescriptorHeader();
 
@@ -645,7 +655,7 @@ static void PS_DrawRenderStats(void)
 	if (PS_IsLevelActive())
 	{
 		x = hires ? 115 : 90;
-		PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows);
+		cy = PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows) + half_row;
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl && cv_glbatching.value)
@@ -659,6 +669,12 @@ static void PS_DrawRenderStats(void)
 		}
 #endif
 	}
+
+	if (R_UsingFrameInterpolation())
+	{
+		x = hires ? 115 : 90;
+		PS_DrawPerfRows(x, cy, V_ROSYMAP, interpolation_rows);
+	}
 }
 
 static void PS_DrawGameLogicStats(void)
diff --git a/src/p_ceilng.c b/src/p_ceilng.c
index d88d9be86a2416d9fa9ff7c7fa0a3a5d01743385..66f2dd58ecfba7e346d746166f31f8edb942c77a 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -13,6 +13,7 @@
 
 #include "doomdef.h"
 #include "p_local.h"
+#include "r_fps.h"
 #include "r_main.h"
 #include "s_sound.h"
 #include "z_zone.h"
@@ -334,6 +335,9 @@ INT32 EV_DoCeiling(mtag_t tag, line_t *line, ceiling_e type)
 
 		ceiling->type = type;
 		firstone = 0;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, true);
 	}
 	return rtn;
 }
@@ -398,6 +402,10 @@ INT32 EV_DoCrush(mtag_t tag, line_t *line, ceiling_e type)
 		}
 
 		ceiling->type = type;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, false);
+		R_CreateInterpolator_SectorPlane(&ceiling->thinker, sec, true);
 	}
 	return rtn;
 }
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 558b8a795fbdce896758d1fda49812691cc67f2b..8a5c2c743f2ca2136cc5ffc3d26c92306e0c824a 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -1333,7 +1333,7 @@ void A_FaceStabHurl(mobj_t *actor)
 					hwork->destscale = FixedSqrt(step*basesize);
 					P_SetScale(hwork, hwork->destscale);
 					hwork->fuse = 2;
-					P_TeleportMove(hwork, actor->x + xo*(15-step), actor->y + yo*(15-step), actor->z + (actor->height - hwork->height)/2 + (P_MobjFlip(actor)*(8<<FRACBITS)));
+					P_MoveOrigin(hwork, actor->x + xo*(15-step), actor->y + yo*(15-step), actor->z + (actor->height - hwork->height)/2 + (P_MobjFlip(actor)*(8<<FRACBITS)));
 					step -= NUMGRADS;
 				}
 
@@ -2095,7 +2095,7 @@ void A_CrushclawAim(mobj_t *actor)
 #undef anglimit
 #undef angfactor
 
-	P_TeleportMove(actor,
+	P_MoveOrigin(actor,
 		crab->x + P_ReturnThrustX(actor, actor->angle, locvar1*crab->scale),
 		crab->y + P_ReturnThrustY(actor, actor->angle, locvar1*crab->scale),
 		crab->z + locvar2*crab->scale);
@@ -2233,7 +2233,7 @@ void A_CrushclawLaunch(mobj_t *actor)
 		fixed_t idx = dx, idy = dy, idz = dz;
 		while (chain)
 		{
-			P_TeleportMove(chain, actor->target->x + idx, actor->target->y + idy, actor->target->z + idz);
+			P_MoveOrigin(chain, actor->target->x + idx, actor->target->y + idy, actor->target->z + idz);
 			chain->movefactor = chain->z;
 			idx += dx;
 			idy += dy;
@@ -11181,7 +11181,7 @@ void A_VileAttack(mobj_t *actor)
 		// move the fire between the vile and the player
 		//fire->x = actor->target->x - FixedMul (24*FRACUNIT, finecosine[an]);
 		//fire->y = actor->target->y - FixedMul (24*FRACUNIT, finesine[an]);
-		P_TeleportMove(fire,
+		P_MoveOrigin(fire,
 						actor->target->x - P_ReturnThrustX(fire, actor->angle, FixedMul(24*FRACUNIT, fire->scale)),
 						actor->target->y - P_ReturnThrustY(fire, actor->angle, FixedMul(24*FRACUNIT, fire->scale)),
 						fire->z);
@@ -11226,7 +11226,7 @@ void A_VileAttack(mobj_t *actor)
 			// move the fire between the vile and the player
 			//fire->x = actor->target->x - FixedMul (24*FRACUNIT, finecosine[an]);
 			//fire->y = actor->target->y - FixedMul (24*FRACUNIT, finesine[an]);
-			P_TeleportMove(fire,
+			P_MoveOrigin(fire,
 							actor->target->x - P_ReturnThrustX(fire, actor->angle, FixedMul(24*FRACUNIT, fire->scale)),
 							actor->target->y - P_ReturnThrustY(fire, actor->angle, FixedMul(24*FRACUNIT, fire->scale)),
 							fire->z);
@@ -11893,12 +11893,12 @@ void A_FlickyCenter(mobj_t *actor)
 		if (actor->target && P_AproxDistance(actor->target->x - originx, actor->target->y - originy) < actor->extravalue1)
 		{
 			actor->extravalue2 = 1;
-		 	P_TeleportMove(actor, actor->target->x, actor->target->y, actor->target->z);
+		 	P_SetOrigin(actor, actor->target->x, actor->target->y, actor->target->z);
 		}
 		else if(actor->extravalue2)
 		{
 			actor->extravalue2 = 0;
-			P_TeleportMove(actor, originx, originy, originz);
+			P_SetOrigin(actor, originx, originy, originz);
 		}
 	}
 }
@@ -12431,7 +12431,7 @@ void A_LightBeamReset(mobj_t *actor)
 	actor->momy = (P_SignedRandom()*FINECOSINE(((actor->spawnpoint->angle*ANG1)>>ANGLETOFINESHIFT) & FINEMASK))/128;
 	actor->momz = (P_SignedRandom()*FRACUNIT)/128;
 
-	P_TeleportMove(actor,
+	P_SetOrigin(actor,
 		actor->spawnpoint->x*FRACUNIT - (P_SignedRandom()*FINESINE(((actor->spawnpoint->angle*ANG1)>>ANGLETOFINESHIFT) & FINEMASK))/2,
 		actor->spawnpoint->y*FRACUNIT + (P_SignedRandom()*FINECOSINE(((actor->spawnpoint->angle*ANG1)>>ANGLETOFINESHIFT) & FINEMASK))/2,
 		actor->spawnpoint->z*FRACUNIT + (P_SignedRandom()*FRACUNIT)/2);
@@ -12988,7 +12988,7 @@ void A_DoNPCSkid(mobj_t *actor)
 		actor->momy = (2*actor->momy)/3;
 	}
 
-	P_TeleportMove(actor, x, y, z);
+	P_MoveOrigin(actor, x, y, z);
 
 	// Spawn a particle every 3 tics.
 	if (!(leveltime % 3))
@@ -13329,7 +13329,7 @@ void A_Boss5MakeJunk(mobj_t *actor)
 		if (locvar1 > 0)
 			P_SetMobjState(broked, locvar1);
 		if (!P_MobjWasRemoved(broked))
-			P_TeleportMove(broked, broked->x + broked->momx, broked->y + broked->momy, broked->z);
+			P_MoveOrigin(broked, broked->x + broked->momx, broked->y + broked->momy, broked->z);
 		ang += ANGLE_45;
 	}
 
@@ -13538,7 +13538,7 @@ void A_DustDevilThink(mobj_t *actor)
 	//Chained thinker for the spiralling dust column.
 	while (layer && !P_MobjWasRemoved(layer)) {
 		angle_t fa = layer->angle >> ANGLETOFINESHIFT;
-		P_TeleportMove(layer, layer->x + 5 * FixedMul(scale, FINECOSINE(fa)), layer->y + 5 * FixedMul(scale, FINESINE(fa)), layer->z);
+		P_MoveOrigin(layer, layer->x + 5 * FixedMul(scale, FINECOSINE(fa)), layer->y + 5 * FixedMul(scale, FINESINE(fa)), layer->z);
 		layer->scale = scale;
 		layer->angle += ANG10 / 2;
 		layer->momx = actor->momx;
@@ -14547,7 +14547,7 @@ void A_DragonWing(mobj_t *actor)
 	actor->angle = target->angle + actor->movedir;
 	x = target->x + P_ReturnThrustX(actor, actor->angle, -target->radius);
 	y = target->y + P_ReturnThrustY(actor, actor->angle, -target->radius);
-	P_TeleportMove(actor, x, y, target->z);
+	P_MoveOrigin(actor, x, y, target->z);
 }
 
 // Function: A_DragonSegment
@@ -14588,7 +14588,7 @@ void A_DragonSegment(mobj_t *actor)
 	zdist = P_ReturnThrustY(target, zangle, radius);
 
 	actor->angle = hangle;
-	P_TeleportMove(actor, target->x + xdist, target->y + ydist, target->z + zdist);
+	P_MoveOrigin(actor, target->x + xdist, target->y + ydist, target->z + zdist);
 }
 
 // Function: A_ChangeHeight
diff --git a/src/p_floor.c b/src/p_floor.c
index d9e0ee0f267722c4055cdbc133b7face082090fd..8a4a87f69fd33dc1830fe7f7e0b291252bb62cfb 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -17,6 +17,7 @@
 #include "m_random.h"
 #include "p_local.h"
 #include "p_slopes.h"
+#include "r_fps.h"
 #include "r_state.h"
 #include "s_sound.h"
 #include "z_zone.h"
@@ -523,6 +524,8 @@ void T_ContinuousFalling(continuousfall_t *faller)
 	{
 		faller->sector->ceilingheight = faller->ceilingstartheight;
 		faller->sector->floorheight = faller->floorstartheight;
+
+		R_ClearLevelInterpolatorState(&faller->thinker);
 	}
 
 	P_CheckSector(faller->sector, false); // you might think this is irrelevant. you would be wrong
@@ -1709,6 +1712,9 @@ void EV_DoFloor(mtag_t tag, line_t *line, floor_e floortype)
 		}
 
 		firstone = 0;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&dofloor->thinker, sec, false);
 	}
 }
 
@@ -1805,6 +1811,10 @@ void EV_DoElevator(mtag_t tag, line_t *line, elevator_e elevtype)
 		}
 
 		elevator->ceilingdestheight = elevator->floordestheight + sec->ceilingheight - sec->floorheight;
+
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&elevator->thinker, sec, false);
+		R_CreateInterpolator_SectorPlane(&elevator->thinker, sec, true);
 	}
 }
 
@@ -1953,6 +1963,10 @@ void EV_BounceSector(sector_t *sec, fixed_t momz, line_t *sourceline)
 	bouncer->speed = momz/2;
 	bouncer->distance = FRACUNIT;
 	bouncer->low = true;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&bouncer->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&bouncer->thinker, sec, true);
 }
 
 // For T_ContinuousFalling special
@@ -1978,6 +1992,10 @@ void EV_DoContinuousFall(sector_t *sec, sector_t *backsector, fixed_t spd, boole
 
 	faller->destheight = backwards ? backsector->ceilingheight : backsector->floorheight;
 	faller->direction = backwards ? 1 : -1;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&faller->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&faller->thinker, sec, true);
 }
 
 // Some other 3dfloor special things Tails 03-11-2002 (Search p_mobj.c for description)
@@ -2030,6 +2048,10 @@ INT32 EV_StartCrumble(sector_t *sec, ffloor_t *rover, boolean floating,
 
 	crumble->sector->crumblestate = CRUMBLE_ACTIVATED;
 
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&crumble->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&crumble->thinker, sec, true);
+
 	TAG_ITER_SECTORS(tag, i)
 	{
 		foundsec = &sectors[i];
@@ -2081,6 +2103,10 @@ void EV_MarioBlock(ffloor_t *rover, sector_t *sector, mobj_t *puncher)
 		block->ceilingstartheight = block->sector->ceilingheight;
 		block->tag = (INT16)rover->master->args[0];
 
+		// interpolation
+		R_CreateInterpolator_SectorPlane(&block->thinker, roversec, false);
+		R_CreateInterpolator_SectorPlane(&block->thinker, roversec, true);
+
 		if (itsamonitor)
 		{
 			oldx = thing->x;
@@ -2089,9 +2115,9 @@ void EV_MarioBlock(ffloor_t *rover, sector_t *sector, mobj_t *puncher)
 		}
 
 		P_UnsetThingPosition(thing);
-		thing->x = sector->soundorg.x;
-		thing->y = sector->soundorg.y;
-		thing->z = topheight;
+		thing->x = thing->old_x = sector->soundorg.x;
+		thing->y = thing->old_y = sector->soundorg.y;
+		thing->z = thing->old_z = topheight;
 		thing->momz = FixedMul(6*FRACUNIT, thing->scale);
 		P_SetThingPosition(thing);
 		if (thing->flags & MF_SHOOTABLE)
@@ -2112,9 +2138,9 @@ void EV_MarioBlock(ffloor_t *rover, sector_t *sector, mobj_t *puncher)
 		if (itsamonitor && thing)
 		{
 			P_UnsetThingPosition(thing);
-			thing->x = oldx;
-			thing->y = oldy;
-			thing->z = oldz;
+			thing->x = thing->old_x = oldx;
+			thing->y = thing->old_y = oldy;
+			thing->z = thing->old_z = oldz;
 			thing->momx = 1;
 			thing->momy = 1;
 			P_SetThingPosition(thing);
diff --git a/src/p_local.h b/src/p_local.h
index f50606117cd91566d3a9f2dc46de434efc66c6d2..49af03f3666a29ec703beb7dc4a0ae8fa673b297 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -91,6 +91,7 @@ typedef struct camera_s
 	// Camera demobjerization
 	// Info for drawing: position.
 	fixed_t x, y, z;
+	boolean reset;
 
 	//More drawing info: to determine current sprite.
 	angle_t angle; // orientation
@@ -413,7 +414,8 @@ boolean P_CheckCameraPosition(fixed_t x, fixed_t y, camera_t *thiscam);
 boolean P_CheckMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff);
 boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff);
 boolean P_Move(mobj_t *actor, fixed_t speed);
-boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z);
+boolean P_SetOrigin(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z);
+boolean P_MoveOrigin(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z);
 void P_SlideMove(mobj_t *mo);
 void P_BounceMove(mobj_t *mo);
 boolean P_CheckSight(mobj_t *t1, mobj_t *t2);
diff --git a/src/p_map.c b/src/p_map.c
index 2bb02503f464fde0b166e6940e9c40ad166a171c..117d49ea491e05310d4c358f7708d8d2b9473d4c 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -19,6 +19,7 @@
 #include "m_random.h"
 #include "p_local.h"
 #include "p_setup.h" // NiGHTS stuff
+#include "r_fps.h"
 #include "r_state.h"
 #include "r_main.h"
 #include "r_sky.h"
@@ -74,7 +75,7 @@ camera_t *mapcampointer;
 //
 // P_TeleportMove
 //
-boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
+static boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
 {
 	// the move is ok,
 	// so link the thing into its new position
@@ -106,6 +107,30 @@ boolean P_TeleportMove(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
 	return true;
 }
 
+// P_SetOrigin - P_TeleportMove which RESETS interpolation values.
+//
+boolean P_SetOrigin(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
+{
+	boolean result = P_TeleportMove(thing, x, y, z);
+
+	if (result == true)
+	{
+		thing->old_x = thing->x;
+		thing->old_y = thing->y;
+		thing->old_z = thing->z;
+	}
+
+	return result;
+}
+
+//
+// P_MoveOrigin - P_TeleportMove which KEEPS interpolation values.
+//
+boolean P_MoveOrigin(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z)
+{
+	return P_TeleportMove(thing, x, y, z);
+}
+
 // =========================================================================
 //                       MOVEMENT ITERATOR FUNCTIONS
 // =========================================================================
@@ -1153,9 +1178,9 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			return true; // underneath
 
 		if (tmthing->eflags & MFE_VERTICALFLIP)
-			P_TeleportMove(thing, thing->x, thing->y, tmthing->z - thing->height - FixedMul(FRACUNIT, tmthing->scale));
+			P_SetOrigin(thing, thing->x, thing->y, tmthing->z - thing->height - FixedMul(FRACUNIT, tmthing->scale));
 		else
-			P_TeleportMove(thing, thing->x, thing->y, tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale));
+			P_SetOrigin(thing, thing->x, thing->y, tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale));
 		if (thing->flags & MF_SHOOTABLE)
 			P_DamageMobj(thing, tmthing, tmthing, 1, DMG_SPIKE);
 		return true;
@@ -1915,7 +1940,7 @@ static boolean PIT_CheckLine(line_t *ld)
 			cosradius = FixedMul(dist, FINECOSINE(langle>>ANGLETOFINESHIFT));
 			sinradius = FixedMul(dist, FINESINE(langle>>ANGLETOFINESHIFT));
 			tmthing->flags |= MF_NOCLIP;
-			P_TeleportMove(tmthing, result.x + cosradius - tmthing->momx, result.y + sinradius - tmthing->momy, tmthing->z);
+			P_MoveOrigin(tmthing, result.x + cosradius - tmthing->momx, result.y + sinradius - tmthing->momy, tmthing->z);
 			tmthing->flags &= ~MF_NOCLIP;
 		}
 #endif
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 2dbfac3bd0c5462b276e833f11d3dfe20eba99cc..9bb68b1e81ef2d0d36727a1d70b9b20d14925132 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -19,6 +19,7 @@
 #include "hu_stuff.h"
 #include "p_local.h"
 #include "p_setup.h"
+#include "r_fps.h"
 #include "r_main.h"
 #include "r_skins.h"
 #include "r_sky.h"
@@ -4023,15 +4024,22 @@ void P_NullPrecipThinker(precipmobj_t *mobj)
 
 void P_SnowThinker(precipmobj_t *mobj)
 {
+	R_ResetPrecipitationMobjInterpolationState(mobj);
+
 	P_CycleStateAnimation((mobj_t *)mobj);
 
 	// adjust height
 	if ((mobj->z += mobj->momz) <= mobj->floorz)
+	{
 		mobj->z = mobj->ceilingz;
+		R_ResetPrecipitationMobjInterpolationState(mobj);
+	}
 }
 
 void P_RainThinker(precipmobj_t *mobj)
 {
+	R_ResetPrecipitationMobjInterpolationState(mobj);
+
 	P_CycleStateAnimation((mobj_t *)mobj);
 
 	if (mobj->state != &states[S_RAIN1])
@@ -4051,6 +4059,7 @@ void P_RainThinker(precipmobj_t *mobj)
 			return;
 
 		mobj->z = mobj->ceilingz;
+		R_ResetPrecipitationMobjInterpolationState(mobj);
 		P_SetPrecipMobjState(mobj, S_RAIN1);
 
 		return;
@@ -4657,7 +4666,7 @@ static void P_Boss4MoveSpikeballs(mobj_t *mobj, angle_t angle, fixed_t fz)
 	while ((base = base->tracer))
 	{
 		for (seg = base, dist = 172*FRACUNIT, s = 9; seg; seg = seg->hnext, dist += 124*FRACUNIT, --s)
-			P_TeleportMove(seg, mobj->x + P_ReturnThrustX(mobj, angle, dist), mobj->y + P_ReturnThrustY(mobj, angle, dist), bz + FixedMul(fz, FixedDiv(s<<FRACBITS, 9<<FRACBITS)));
+			P_MoveOrigin(seg, mobj->x + P_ReturnThrustX(mobj, angle, dist), mobj->y + P_ReturnThrustY(mobj, angle, dist), bz + FixedMul(fz, FixedDiv(s<<FRACBITS, 9<<FRACBITS)));
 		angle += ANGLE_MAX/3;
 	}
 }
@@ -5578,7 +5587,7 @@ static void P_Boss9Thinker(mobj_t *mobj)
 					mobj->hprev->destscale = FRACUNIT + (2*TICRATE - mobj->fuse)*(FRACUNIT/2)/TICRATE + FixedMul(FINECOSINE(angle>>ANGLETOFINESHIFT),FRACUNIT/2);
 					P_SetScale(mobj->hprev, mobj->hprev->destscale);
 
-					P_TeleportMove(mobj->hprev, mobj->x, mobj->y, mobj->z + mobj->height/2 - mobj->hprev->height/2);
+					P_MoveOrigin(mobj->hprev, mobj->x, mobj->y, mobj->z + mobj->height/2 - mobj->hprev->height/2);
 					mobj->hprev->momx = mobj->momx;
 					mobj->hprev->momy = mobj->momy;
 					mobj->hprev->momz = mobj->momz;
@@ -6433,7 +6442,7 @@ void P_Attract(mobj_t *source, mobj_t *dest, boolean nightsgrab) // Home in on y
 		if (dist < source->movefactor)
 		{
 			source->momx = source->momy = source->momz = 0;
-			P_TeleportMove(source, tx, ty, tz);
+			P_MoveOrigin(source, tx, ty, tz);
 		}
 		else
 		{
@@ -7042,7 +7051,7 @@ static void P_UpdateMinecartSegments(mobj_t *mobj)
 		dx = seg->extravalue1;
 		dy = seg->extravalue2;
 		sang = seg->cusval;
-		P_TeleportMove(seg, x + s*dx + c*dy, y - c*dx + s*dy, z);
+		P_MoveOrigin(seg, x + s*dx + c*dy, y - c*dx + s*dy, z);
 		seg->angle = ang + FixedAngle(FRACUNIT*sang);
 		seg->flags2 = (seg->flags2 & ~MF2_DONTDRAW) | (mobj->flags2 & MF2_DONTDRAW);
 		seg = seg->tracer;
@@ -7809,6 +7818,9 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 			mobj->z = mobj->target->z + mobj->target->height + FixedMul((16 + abs((signed)(leveltime % TICRATE) - TICRATE/2))*FRACUNIT, mobj->target->scale);
 		else
 			mobj->z = mobj->target->z - FixedMul((16 + abs((signed)(leveltime % TICRATE) - TICRATE/2))*FRACUNIT, mobj->target->scale) - mobj->height;
+
+		mobj->old_z = mobj->z;
+
 		break;
 	case MT_LOCKONINF:
 		if (!(mobj->flags2 & MF2_STRONGBOX))
@@ -7820,6 +7832,9 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 			mobj->z = mobj->threshold + FixedMul((16 + abs((signed)(leveltime % TICRATE) - TICRATE/2))*FRACUNIT, mobj->scale);
 		else
 			mobj->z = mobj->threshold - FixedMul((16 + abs((signed)(leveltime % TICRATE) - TICRATE/2))*FRACUNIT, mobj->scale);
+
+		mobj->old_z = mobj->z;
+
 		break;
 	case MT_DROWNNUMBERS:
 		if (!P_DrownNumbersSceneryThink(mobj))
@@ -8658,7 +8673,7 @@ static boolean P_EggRobo1Think(mobj_t *mobj)
 						< mobj->scale)
 						S_StartSound(mobj, mobj->info->seesound);
 
-					P_TeleportMove(mobj,
+					P_MoveOrigin(mobj,
 						(15*(mobj->x >> 4)) + (basex >> 4) + P_ReturnThrustX(mobj, mobj->angle, SPECTATORRADIUS >> 4),
 						(15*(mobj->y >> 4)) + (basey >> 4) + P_ReturnThrustY(mobj, mobj->angle, SPECTATORRADIUS >> 4),
 						mobj->z);
@@ -8684,9 +8699,9 @@ static boolean P_EggRobo1Think(mobj_t *mobj)
 			if (!didmove)
 			{
 				if (P_AproxDistance(mobj->x - basex, mobj->y - basey) < mobj->scale)
-					P_TeleportMove(mobj, basex, basey, mobj->z);
+					P_MoveOrigin(mobj, basex, basey, mobj->z);
 				else
-					P_TeleportMove(mobj,
+					P_MoveOrigin(mobj,
 					(15*(mobj->x >> 4)) + (basex >> 4),
 						(15*(mobj->y >> 4)) + (basey >> 4),
 						mobj->z);
@@ -8808,11 +8823,11 @@ static void P_NiGHTSDroneThink(mobj_t *mobj)
 			sparkleoffset = goaloffset + FixedMul(15*FRACUNIT, mobj->scale);
 		}
 
-		P_TeleportMove(goalpost, mobj->x, mobj->y, mobj->z + goaloffset);
-		P_TeleportMove(sparkle, mobj->x, mobj->y, mobj->z + sparkleoffset);
+		P_MoveOrigin(goalpost, mobj->x, mobj->y, mobj->z + goaloffset);
+		P_MoveOrigin(sparkle, mobj->x, mobj->y, mobj->z + sparkleoffset);
 		if (goalpost->movefactor != mobj->z || goalpost->friction != mobj->height)
 		{
-			P_TeleportMove(droneman, mobj->x, mobj->y, mobj->z + dronemanoffset);
+			P_MoveOrigin(droneman, mobj->x, mobj->y, mobj->z + dronemanoffset);
 			goalpost->movefactor = mobj->z;
 			goalpost->friction = mobj->height;
 		}
@@ -8822,12 +8837,12 @@ static void P_NiGHTSDroneThink(mobj_t *mobj)
 	{
 		if (goalpost->x != mobj->x || goalpost->y != mobj->y)
 		{
-			P_TeleportMove(goalpost, mobj->x, mobj->y, goalpost->z);
-			P_TeleportMove(sparkle, mobj->x, mobj->y, sparkle->z);
+			P_MoveOrigin(goalpost, mobj->x, mobj->y, goalpost->z);
+			P_MoveOrigin(sparkle, mobj->x, mobj->y, sparkle->z);
 		}
 
 		if (droneman->x != mobj->x || droneman->y != mobj->y)
-			P_TeleportMove(droneman, mobj->x, mobj->y,
+			P_MoveOrigin(droneman, mobj->x, mobj->y,
 				droneman->z >= mobj->floorz && droneman->z <= mobj->ceilingz ? droneman->z : mobj->z);
 	}
 
@@ -9026,7 +9041,7 @@ static void P_SaloonDoorThink(mobj_t *mobj)
 	fma = (mobj->angle >> ANGLETOFINESHIFT) & FINEMASK;
 	c = 48*FINECOSINE(fma);
 	s = 48*FINESINE(fma);
-	P_TeleportMove(mobj, x + c0 + c, y + s0 + s, z);
+	P_MoveOrigin(mobj, x + c0 + c, y + s0 + s, z);
 }
 
 static void P_PyreFlyThink(mobj_t *mobj)
@@ -9581,7 +9596,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 			P_RemoveMobj(mobj);
 			return false;
 		}
-		P_TeleportMove(mobj, mobj->target->x, mobj->target->y, mobj->target->z - mobj->height);
+		P_MoveOrigin(mobj, mobj->target->x, mobj->target->y, mobj->target->z - mobj->height);
 		break;
 	case MT_HAMMER:
 		if (mobj->z <= mobj->floorz)
@@ -10993,6 +11008,8 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 	if (CheckForReverseGravity && !(mobj->flags & MF_NOBLOCKMAP))
 		P_CheckGravity(mobj, false);
 
+	R_AddMobjInterpolator(mobj);
+
 	return mobj;
 }
 
@@ -11040,6 +11057,8 @@ static precipmobj_t *P_SpawnPrecipMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype
 	 || mobj->subsector->sector->floorpic == skyflatnum)
 		mobj->precipflags |= PCF_PIT;
 
+	R_ResetPrecipitationMobjInterpolationState(mobj);
+
 	return mobj;
 }
 
@@ -11159,6 +11178,8 @@ void P_RemoveMobj(mobj_t *mobj)
 	memset((UINT8 *)mobj + sizeof(thinker_t), 0xff, sizeof(mobj_t) - sizeof(thinker_t));
 #endif
 
+	R_RemoveMobjInterpolator(mobj);
+
 	// free block
 	if (!mobj->thinker.next)
 	{ // Uh-oh, the mobj doesn't think, P_RemoveThinker would never go through!
@@ -12562,7 +12583,7 @@ static boolean P_SetupNiGHTSDrone(mapthing_t *mthing, mobj_t *mobj)
 	dronemangoaldiff = max(mobjinfo[MT_NIGHTSDRONE_MAN].height - mobjinfo[MT_NIGHTSDRONE_GOAL].height, 0);
 
 	if (flip && mobj->height != oldheight)
-		P_TeleportMove(mobj, mobj->x, mobj->y, mobj->z - (mobj->height - oldheight));
+		P_MoveOrigin(mobj, mobj->x, mobj->y, mobj->z - (mobj->height - oldheight));
 
 	if (!flip)
 	{
@@ -12631,9 +12652,9 @@ static boolean P_SetupNiGHTSDrone(mapthing_t *mthing, mobj_t *mobj)
 		// correct Z position
 		if (flip)
 		{
-			P_TeleportMove(goalpost, goalpost->x, goalpost->y, mobj->z + goaloffset);
-			P_TeleportMove(sparkle, sparkle->x, sparkle->y, mobj->z + sparkleoffset);
-			P_TeleportMove(droneman, droneman->x, droneman->y, mobj->z + dronemanoffset);
+			P_MoveOrigin(goalpost, goalpost->x, goalpost->y, mobj->z + goaloffset);
+			P_MoveOrigin(sparkle, sparkle->x, sparkle->y, mobj->z + sparkleoffset);
+			P_MoveOrigin(droneman, droneman->x, droneman->y, mobj->z + dronemanoffset);
 		}
 
 		// Remember position preference for later
@@ -14086,9 +14107,43 @@ mobj_t *P_SpawnMobjFromMobj(mobj_t *mobj, fixed_t xofs, fixed_t yofs, fixed_t zo
 		newmobj->eflags |= MFE_VERTICALFLIP;
 		newmobj->flags2 |= MF2_OBJECTFLIP;
 		newmobj->z = mobj->z + mobj->height - zofs - elementheight;
+
+		newmobj->old_z = mobj->old_z + mobj->height - zofs - elementheight;
+		newmobj->old_z2 = mobj->old_z2 + mobj->height - zofs - elementheight;
+	}
+	else
+	{
+		newmobj->old_z = mobj->old_z + zofs;
+		newmobj->old_z2 = mobj->old_z2 + zofs;
 	}
 
 	newmobj->destscale = mobj->destscale;
 	P_SetScale(newmobj, mobj->scale);
+
+	newmobj->old_x2 = mobj->old_x2 + xofs;
+	newmobj->old_y2 = mobj->old_y2 + yofs;
+	newmobj->old_x = mobj->old_x + xofs;
+	newmobj->old_y = mobj->old_y + yofs;
+
+	// This angle hack is needed for Lua scripts that set the angle after
+	// spawning, to avoid erroneous interpolation.
+	if (mobj->player)
+	{
+		newmobj->old_angle2 = mobj->player->old_drawangle2;
+		newmobj->old_angle = mobj->player->old_drawangle;
+	}
+	else
+	{
+		newmobj->old_angle2 = mobj->old_angle2;
+		newmobj->old_angle = mobj->old_angle;
+	}
+
+	newmobj->old_scale2 = mobj->old_scale2;
+	newmobj->old_scale = mobj->old_scale;
+	newmobj->old_spritexscale = mobj->old_spritexscale;
+	newmobj->old_spriteyscale = mobj->old_spriteyscale;
+	newmobj->old_spritexoffset = mobj->old_spritexoffset;
+	newmobj->old_spriteyoffset = mobj->old_spriteyoffset;
+
 	return newmobj;
 }
diff --git a/src/p_mobj.h b/src/p_mobj.h
index da8d9ea2b6d9b18abdb720d32cd78037fd1ac6ca..60601692cf7f7b294f41f77eb25daea1880993d2 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -281,6 +281,8 @@ typedef struct mobj_s
 
 	// Info for drawing: position.
 	fixed_t x, y, z;
+	fixed_t old_x, old_y, old_z; // position interpolation
+	fixed_t old_x2, old_y2, old_z2;
 
 	// More list: links in sector (if needed)
 	struct mobj_s *snext;
@@ -288,6 +290,8 @@ typedef struct mobj_s
 
 	// More drawing info: to determine current sprite.
 	angle_t angle, pitch, roll; // orientation
+	angle_t old_angle, old_pitch, old_roll; // orientation interpolation
+	angle_t old_angle2, old_pitch2, old_roll2;
 	angle_t rollangle;
 	spritenum_t sprite; // used to find patch_t and flip value
 	UINT32 frame; // frame number, plus bits see p_pspr.h
@@ -298,6 +302,8 @@ typedef struct mobj_s
 	INT32 blendmode; // blend mode
 	fixed_t spritexscale, spriteyscale;
 	fixed_t spritexoffset, spriteyoffset;
+	fixed_t old_spritexscale, old_spriteyscale;
+	fixed_t old_spritexoffset, old_spriteyoffset;
 	struct pslope_s *floorspriteslope; // The slope that the floorsprite is rotated by
 
 	struct msecnode_s *touching_sectorlist; // a linked list of sectors where this object appears
@@ -373,6 +379,8 @@ typedef struct mobj_s
 	UINT32 mobjnum; // A unique number for this mobj. Used for restoring pointers on save games.
 
 	fixed_t scale;
+	fixed_t old_scale; // interpolation
+	fixed_t old_scale2;
 	fixed_t destscale;
 	fixed_t scalespeed;
 
@@ -387,6 +395,7 @@ typedef struct mobj_s
 
 	struct pslope_s *standingslope; // The slope that the object is standing on (shouldn't need synced in savegames, right?)
 
+	boolean resetinterp; // if true, some fields should not be interpolated (see R_InterpolateMobjState implementation)
 	boolean colorized; // Whether the mobj uses the rainbow colormap
 	boolean mirrored; // The object's rotations will be mirrored left to right, e.g., see frame AL from the right and AR from the left
 	fixed_t shadowscale; // If this object casts a shadow, and the size relative to radius
@@ -408,6 +417,8 @@ typedef struct precipmobj_s
 
 	// Info for drawing: position.
 	fixed_t x, y, z;
+	fixed_t old_x, old_y, old_z; // position interpolation
+	fixed_t old_x2, old_y2, old_z2;
 
 	// More list: links in sector (if needed)
 	struct precipmobj_s *snext;
@@ -415,6 +426,8 @@ typedef struct precipmobj_s
 
 	// More drawing info: to determine current sprite.
 	angle_t angle, pitch, roll; // orientation
+	angle_t old_angle, old_pitch, old_roll; // orientation interpolation
+	angle_t old_angle2, old_pitch2, old_roll2;
 	angle_t rollangle;
 	spritenum_t sprite; // used to find patch_t and flip value
 	UINT32 frame; // frame number, plus bits see p_pspr.h
@@ -425,6 +438,8 @@ typedef struct precipmobj_s
 	INT32 blendmode; // blend mode
 	fixed_t spritexscale, spriteyscale;
 	fixed_t spritexoffset, spriteyoffset;
+	fixed_t old_spritexscale, old_spriteyscale;
+	fixed_t old_spritexoffset, old_spriteyoffset;
 	struct pslope_s *floorspriteslope; // The slope that the floorsprite is rotated by
 
 	struct mprecipsecnode_s *touching_sectorlist; // a linked list of sectors where this object appears
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index 77e514bee7a7a648069992d20160aef70cc7bddc..4062f9a9fcd361fcf6043843314d3e8041ba19cc 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -24,6 +24,7 @@
 #include "p_tick.h"
 #include "p_local.h"
 #include "p_polyobj.h"
+#include "r_fps.h"
 #include "r_main.h"
 #include "r_state.h"
 #include "r_defs.h"
@@ -2049,6 +2050,9 @@ boolean EV_DoPolyObjRotate(polyrotdata_t *prdata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+	
 	th->turnobjs = 0;
 	if (!(prdata->flags & TMPR_DONTROTATEOTHERS))
 		th->turnobjs |= PTF_OTHERS;
@@ -2114,6 +2118,9 @@ boolean EV_DoPolyObjMove(polymovedata_t *pmdata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// apply action to mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
@@ -2129,8 +2136,10 @@ boolean EV_DoPolyObjMove(polymovedata_t *pmdata)
 boolean EV_DoPolyObjWaypoint(polywaypointdata_t *pwdata)
 {
 	polyobj_t *po;
+	polyobj_t *oldpo;
 	polywaypoint_t *th;
 	mobj_t *first = NULL;
+	INT32 start;
 
 	if (!(po = Polyobj_GetForNum(pwdata->polyObjNum)))
 	{
@@ -2181,6 +2190,26 @@ boolean EV_DoPolyObjWaypoint(polywaypointdata_t *pwdata)
 		th->continuous = false;
 	}
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+	// T_PolyObjWaypoint is the only polyobject movement
+	// that can adjust z, so we add these ones too.
+	R_CreateInterpolator_SectorPlane(&th->thinker, po->lines[0]->backsector, false); 
+	R_CreateInterpolator_SectorPlane(&th->thinker, po->lines[0]->backsector, true);
+
+	// Most other polyobject functions handle children by recursively
+	// giving each child another thinker. T_PolyObjWaypoint handles
+	// it manually though, which means we need to manually give them
+	// interpolation here instead.
+	start = 0;
+	oldpo = po;
+	while ((po = Polyobj_GetChild(oldpo, &start)))
+	{
+		R_CreateInterpolator_Polyobj(&th->thinker, po);
+		R_CreateInterpolator_SectorPlane(&th->thinker, po->lines[0]->backsector, false);
+		R_CreateInterpolator_SectorPlane(&th->thinker, po->lines[0]->backsector, true);
+	}
+
 	th->pointnum = first->health;
 
 	return true;
@@ -2229,6 +2258,9 @@ static void Polyobj_doSlideDoor(polyobj_t *po, polydoordata_t *doordata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// start action on mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
@@ -2269,6 +2301,9 @@ static void Polyobj_doSwingDoor(polyobj_t *po, polydoordata_t *doordata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// start action on mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
@@ -2340,6 +2375,9 @@ boolean EV_DoPolyObjDisplace(polydisplacedata_t *prdata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// apply action to mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
@@ -2386,6 +2424,9 @@ boolean EV_DoPolyObjRotDisplace(polyrotdisplacedata_t *prdata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// apply action to mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
@@ -2490,6 +2531,9 @@ boolean EV_DoPolyObjFlag(polyflagdata_t *pfdata)
 
 	oldpo = po;
 
+	// interpolation
+	R_CreateInterpolator_Polyobj(&th->thinker, po);
+
 	// apply action to mirroring polyobjects as well
 	start = 0;
 	while ((po = Polyobj_GetChild(oldpo, &start)))
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 07b3d8640ecdbc588f28d087463a2b5bc7ff2c27..c5195a17aea460c568b58a2eeb4e341e97373ccb 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -22,6 +22,7 @@
 #include "p_setup.h"
 #include "p_saveg.h"
 #include "r_data.h"
+#include "r_fps.h"
 #include "r_textures.h"
 #include "r_things.h"
 #include "r_skins.h"
@@ -3060,6 +3061,8 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 
 	mobj->info = (mobjinfo_t *)next; // temporarily, set when leave this function
 
+	R_AddMobjInterpolator(mobj);
+
 	return &mobj->thinker;
 }
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 21291eb8bbe0699de35382b9814fc654afecbb5c..90538de0b7f22c5a4f5c02e45dbf5879cb2aad2e 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -21,6 +21,7 @@
 #include "p_spec.h"
 #include "p_saveg.h"
 
+#include "i_time.h"
 #include "i_sound.h" // for I_PlayCD()..
 #include "i_video.h" // for I_FinishUpdate()..
 #include "r_sky.h"
@@ -33,6 +34,7 @@
 #include "r_picformats.h"
 #include "r_sky.h"
 #include "r_draw.h"
+#include "r_fps.h" // R_ResetViewInterpolation in level load
 
 #include "s_sound.h"
 #include "st_stuff.h"
@@ -7313,7 +7315,10 @@ static void P_RunSpecialStageWipe(void)
 	{
 		// wait loop
 		while (!((nowtime = I_GetTime()) - lastwipetic))
-			I_Sleep();
+		{
+			I_Sleep(cv_sleep.value);
+			I_UpdateTime(cv_timescale.value);
+		}
 		lastwipetic = nowtime;
 		if (moviemode) // make sure we save frames for the white hold too
 			M_SaveFrame();
@@ -7615,7 +7620,10 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	Patch_FreeTag(PU_PATCH_ROTATED);
 	Z_FreeTags(PU_LEVEL, PU_PURGELEVEL - 1);
 
+	R_InitializeLevelInterpolators();
+
 	P_InitThinkers();
+	R_InitMobjInterpolators();
 	P_InitCachedActions();
 
 	if (!fromnetsave && savedata.lives > 0)
diff --git a/src/p_slopes.c b/src/p_slopes.c
index 6048f478d63f8ddc278df3f4d6b542c4de28ab42..8e00d58241d2ed4d4aeacaa387e539155609bcab 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -22,6 +22,7 @@
 #include "r_main.h"
 #include "p_maputl.h"
 #include "w_wad.h"
+#include "r_fps.h"
 
 pslope_t *slopelist = NULL;
 UINT16 slopecount = 0;
@@ -189,6 +190,9 @@ static inline void P_AddDynLineSlopeThinker (pslope_t* slope, dynplanetype_t typ
 	th->sourceline = sourceline;
 	th->extent = extent;
 	P_AddThinker(THINK_DYNSLOPE, &th->thinker);
+
+	// interpolation
+	R_CreateInterpolator_DynSlope(&th->thinker, slope);
 }
 
 static inline void P_AddDynVertexSlopeThinker (pslope_t* slope, const INT16 tags[3], const vector3_t vx[3])
diff --git a/src/p_spec.c b/src/p_spec.c
index 72bc587142b2e574bc9262ddc83bec2ac795a8a9..9215b05e24ae40f0eeda789cdfc1194ce3f1efdf 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -21,6 +21,7 @@
 #include "p_local.h"
 #include "p_setup.h" // levelflats for flat animation
 #include "r_data.h"
+#include "r_fps.h"
 #include "r_textures.h"
 #include "m_random.h"
 #include "p_mobj.h"
@@ -2480,7 +2481,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (mo->player)
 					{
 						if (bot) // This might put poor Tails in a wall if he's too far behind! D: But okay, whatever! >:3
-							P_TeleportMove(bot, bot->x + x, bot->y + y, bot->z + z);
+							P_SetOrigin(bot, bot->x + x, bot->y + y, bot->z + z);
 						if (splitscreen && mo->player == &players[secondarydisplayplayer] && camera2.chase)
 						{
 							camera2.x += x;
@@ -2726,7 +2727,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				// Reset bot too.
 				if (bot) {
 					if (line->args[0])
-						P_TeleportMove(bot, mo->x, mo->y, mo->z);
+						P_SetOrigin(bot, mo->x, mo->y, mo->z);
 					bot->momx = bot->momy = bot->momz = 1;
 					bot->pmomz = 0;
 					bot->player->rmomx = bot->player->rmomy = 1;
@@ -2767,7 +2768,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				// (Teleport them to you so they don't break it.)
 				if (bot && (bot->flags2 & MF2_TWOD) != (mo->flags2 & MF2_TWOD)) {
 					bot->flags2 = (bot->flags2 & ~MF2_TWOD) | (mo->flags2 & MF2_TWOD);
-					P_TeleportMove(bot, mo->x, mo->y, mo->z);
+					P_SetOrigin(bot, mo->x, mo->y, mo->z);
 				}
 			}
 			break;
@@ -5703,6 +5704,10 @@ static void P_AddFloatThinker(sector_t *sec, UINT16 tag, line_t *sourceline)
 	floater->sector = sec;
 	floater->tag = (INT16)tag;
 	floater->sourceline = sourceline;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&floater->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&floater->thinker, sec, true);
 }
 
 /**
@@ -5732,6 +5737,9 @@ static void P_AddPlaneDisplaceThinker(INT32 type, fixed_t speed, INT32 control,
 	displace->speed = speed;
 	displace->type = type;
 	displace->reverse = reverse;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&displace->thinker, &sectors[affectee], false);
 }
 
 /** Adds a Mario block thinker, which changes the block's texture between blank
@@ -5791,6 +5799,10 @@ static void P_AddRaiseThinker(sector_t *sec, INT16 tag, fixed_t speed, fixed_t c
 		raise->flags |= RF_REVERSE;
 	if (spindash)
 		raise->flags |= RF_SPINDASH;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&raise->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&raise->thinker, sec, true);
 }
 
 static void P_AddAirbob(sector_t *sec, INT16 tag, fixed_t dist, boolean raise, boolean spindash, boolean dynamic)
@@ -5816,6 +5828,10 @@ static void P_AddAirbob(sector_t *sec, INT16 tag, fixed_t dist, boolean raise, b
 		airbob->flags |= RF_SPINDASH;
 	if (dynamic)
 		airbob->flags |= RF_DYNAMIC;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&airbob->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&airbob->thinker, sec, true);
 }
 
 /** Adds a thwomp thinker.
@@ -5856,6 +5872,10 @@ static inline void P_AddThwompThinker(sector_t *sec, line_t *sourceline, fixed_t
 	sec->ceilingdata = thwomp;
 	// Start with 'resting' texture
 	sides[sourceline->sidenum[0]].midtexture = sides[sourceline->sidenum[0]].bottomtexture;
+
+	// interpolation
+	R_CreateInterpolator_SectorPlane(&thwomp->thinker, sec, false);
+	R_CreateInterpolator_SectorPlane(&thwomp->thinker, sec, true);
 }
 
 /** Adds a thinker which checks if any MF_ENEMY objects with health are in the defined area.
@@ -7524,6 +7544,22 @@ static void Add_Scroller(INT32 type, fixed_t dx, fixed_t dy, INT32 control, INT3
 		}
 	}
 	P_AddThinker(THINK_MAIN, &s->thinker);
+
+	// interpolation
+	switch (type)
+	{
+		case sc_side:
+			R_CreateInterpolator_SideScroll(&s->thinker, &sides[affectee]);
+			break;
+		case sc_floor:
+			R_CreateInterpolator_SectorScroll(&s->thinker, &sectors[affectee], false);
+			break;
+		case sc_ceiling:
+			R_CreateInterpolator_SectorScroll(&s->thinker, &sectors[affectee], true);
+			break;
+		default:
+			break;
+	}
 }
 
 static void P_SpawnPlaneScroller(line_t *l, fixed_t dx, fixed_t dy, INT32 control, INT32 affectee, INT32 accel, INT32 exclusive)
diff --git a/src/p_telept.c b/src/p_telept.c
index cbbd0ff6bcffc7b91db3a5d6c7323d55394a4b4c..4d10f1df30a807908c32c21629a275c334a8c851 100644
--- a/src/p_telept.c
+++ b/src/p_telept.c
@@ -122,7 +122,7 @@ void P_MixUp(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle,
 */
 boolean P_Teleport(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle, boolean flash, boolean dontstopmove)
 {
-	if (!P_TeleportMove(thing, x, y, z))
+	if (!P_SetOrigin(thing, x, y, z))
 		return false;
 
 	if (!dontstopmove)
diff --git a/src/p_tick.c b/src/p_tick.c
index 28ace92883a6c9d5078e1b1fe30098b07536639a..bf6e8f42ed1246b4067d626bbefe048b66b68baf 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -23,6 +23,9 @@
 #include "lua_hook.h"
 #include "m_perfstats.h"
 #include "i_system.h" // I_GetPreciseTime
+#include "r_main.h"
+#include "r_fps.h"
+#include "i_video.h" // rendermode
 
 // Object place
 #include "m_cheat.h"
@@ -254,6 +257,7 @@ void P_RemoveThinkerDelayed(thinker_t *thinker)
 	* thinker->prev->next = thinker->next */
 	(next->prev = currentthinker = thinker->prev)->next = next;
 
+	R_DestroyLevelInterpolators(thinker);
 	Z_Free(thinker);
 }
 
@@ -616,8 +620,10 @@ void P_Ticker(boolean run)
 		if (OP_FreezeObjectplace())
 		{
 			P_MapStart();
+			R_UpdateMobjInterpolators();
 			OP_ObjectplaceMovement(&players[0]);
 			P_MoveChaseCamera(&players[0], &camera, false);
+			R_UpdateViewInterpolation();
 			P_MapEnd();
 			S_SetStackAdjustmentStart();
 			return;
@@ -640,6 +646,8 @@ void P_Ticker(boolean run)
 
 	if (run)
 	{
+		R_UpdateMobjInterpolators();
+
 		if (demorecording)
 			G_WriteDemoTiccmd(&players[consoleplayer].cmd, 0);
 		if (demoplayback)
@@ -763,6 +771,42 @@ void P_Ticker(boolean run)
 		LUA_HOOK(PostThinkFrame);
 	}
 
+	if (run)
+	{
+		R_UpdateLevelInterpolators();
+		R_UpdateViewInterpolation();
+
+		// Hack: ensure newview is assigned every tic.
+		// Ensures view interpolation is T-1 to T in poor network conditions
+		// We need a better way to assign view state decoupled from game logic
+		if (rendermode != render_none)
+		{
+			player_t *player1 = &players[displayplayer];
+			if (player1->mo && skyboxmo[0] && cv_skybox.value)
+			{
+				R_SkyboxFrame(player1);
+			}
+			if (player1->mo)
+			{
+				R_SetupFrame(player1);
+			}
+
+			if (splitscreen)
+			{
+				player_t *player2 = &players[secondarydisplayplayer];
+				if (player2->mo && skyboxmo[0] && cv_skybox.value)
+				{
+					R_SkyboxFrame(player2);
+				}
+				if (player2->mo)
+				{
+					R_SetupFrame(player2);
+				}
+			}
+		}
+		
+	}
+
 	P_MapEnd();
 
 //	Z_CheckMemCleanup();
@@ -783,6 +827,8 @@ void P_PreTicker(INT32 frames)
 	{
 		P_MapStart();
 
+		R_UpdateMobjInterpolators();
+
 		LUA_HOOK(PreThinkFrame);
 
 		for (i = 0; i < MAXPLAYERS; i++)
@@ -821,6 +867,10 @@ void P_PreTicker(INT32 frames)
 
 		LUA_HOOK(PostThinkFrame);
 
+		R_UpdateLevelInterpolators();
+		R_UpdateViewInterpolation();
+		R_ResetViewInterpolation(0);
+
 		P_MapEnd();
 	}
 
diff --git a/src/p_user.c b/src/p_user.c
index 2f522ad4b1eb8c33d753184da0bcb2fe8e327937..49f21494126f1a704fb6eb32c52dc520e8504c91 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -20,6 +20,7 @@
 #include "d_net.h"
 #include "g_game.h"
 #include "p_local.h"
+#include "r_fps.h"
 #include "r_main.h"
 #include "s_sound.h"
 #include "r_skins.h"
@@ -1996,6 +1997,14 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 		ghost2->flags2 |= (mobj->player->followmobj->flags2 & MF2_LINKDRAW);
 	}
 
+	// Copy interpolation data :)
+	ghost->old_x = mobj->old_x2;
+	ghost->old_y = mobj->old_y2;
+	ghost->old_z = mobj->old_z2;
+	ghost->old_angle = (mobj->player ? mobj->player->old_drawangle2 : mobj->old_angle2);
+	ghost->old_pitch = mobj->old_pitch2;
+	ghost->old_roll = mobj->old_roll2;
+
 	return ghost;
 }
 
@@ -6931,7 +6940,7 @@ static void P_MoveNiGHTSToDrone(player_t *player)
 	}
 
 	player->mo->momx = player->mo->momy = player->mo->momz = 0;
-	P_TeleportMove(player->mo, player->drone->x, player->drone->y, player->drone->z + zofs);
+	P_MoveOrigin(player->mo, player->drone->x, player->drone->y, player->drone->z + zofs);
 	P_SetTarget(&player->drone, NULL);
 }
 
@@ -9647,6 +9656,7 @@ void P_ResetCamera(player_t *player, camera_t *thiscam)
 	thiscam->x = x;
 	thiscam->y = y;
 	thiscam->z = z;
+	thiscam->reset = true;
 
 	if ((thiscam == &camera && G_ControlStyle(1) == CS_SIMPLE)
 	|| (thiscam == &camera2 && G_ControlStyle(2) == CS_SIMPLE))
@@ -10997,7 +11007,7 @@ static void P_MinecartThink(player_t *player)
 	}
 
 	// Move player to minecart.
-	P_TeleportMove(player->mo, minecart->x - minecart->momx, minecart->y - minecart->momy, minecart->z + max(minecart->momz, 0) + 8*FRACUNIT);
+	P_MoveOrigin(player->mo, minecart->x - minecart->momx, minecart->y - minecart->momy, minecart->z + max(minecart->momz, 0) + 8*FRACUNIT);
 	if (player->powers[pw_carry] != CR_MINECART)
 		return;
 	player->mo->momx = player->mo->momy = player->mo->momz = 0;
@@ -11236,6 +11246,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	tic_t dashmode = min(player->dashmode, DASHMODE_MAX);
 	boolean underwater = mo->eflags & MFE_UNDERWATER;
 	statenum_t stat = fume->state-states;
+	boolean resetinterp = false;
 
 	if (panim != PA_WALK && panim != PA_RUN && panim != PA_DASH) // turn invisible when not in a coherent movement state
 	{
@@ -11287,6 +11298,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	{
 		P_SetMobjState(fume, (stat = fume->info->seestate));
 		P_SetScale(fume, mo->scale);
+		resetinterp = true;
 	}
 
 	if (dashmode > DASHMODE_THRESHOLD && stat != fume->info->seestate) // If in dashmode, grow really big and flash
@@ -11330,6 +11342,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	fume->y = mo->y + P_ReturnThrustY(fume, angle, dist);
 	fume->z = mo->z + heightoffset - (fume->height >> 1);
 	P_SetThingPosition(fume);
+	if (resetinterp) R_ResetMobjInterpolationState(fume);
 
 	// If dashmode is high enough, spawn a trail
 	if (player->normalspeed >= skins[player->skin].normalspeed*2)
@@ -11732,7 +11745,7 @@ void P_PlayerThink(player_t *player)
 			// P_TouchSpecialThing case MT_NIGHTSBUMPER, that position is fudged in the time
 			// between that routine in the previous tic
 			// and reaching here in the current tic
-			P_TeleportMove(player->mo, player->mo->hnext->x, player->mo->hnext->y
+			P_MoveOrigin(player->mo, player->mo->hnext->x, player->mo->hnext->y
 				, player->mo->hnext->z + FixedMul(player->mo->hnext->height/4, player->mo->hnext->scale));
 			P_SetTarget(&player->mo->hnext, NULL);
 		}
@@ -12556,7 +12569,7 @@ void P_PlayerAfterThink(player_t *player)
 				player->mo->momx = (chain->x - player->mo->x)*2;
 				player->mo->momy = (chain->y - player->mo->y)*2;
 				player->mo->momz = (chain->z - (player->mo->height-chain->height/2) - player->mo->z)*2;
-				P_TeleportMove(player->mo, chain->x, chain->y, chain->z - (player->mo->height-chain->height/2));
+				P_MoveOrigin(player->mo, chain->x, chain->y, chain->z - (player->mo->height-chain->height/2));
 				if (!player->powers[pw_flashing]) // handle getting hurt
 				{
 					player->pflags |= PF_JUMPED;
@@ -12650,7 +12663,7 @@ void P_PlayerAfterThink(player_t *player)
 					mo->tics = walktics;
 				}
 
-				P_TeleportMove(player->mo, rock->x, rock->y, rock->z + ((mo->eflags & MFE_VERTICALFLIP) ? -mo->height : rock->height));
+				P_MoveOrigin(player->mo, rock->x, rock->y, rock->z + ((mo->eflags & MFE_VERTICALFLIP) ? -mo->height : rock->height));
 				break;
 			}
 			case CR_PTERABYTE: // being carried by a Pterabyte
diff --git a/src/r_bsp.c b/src/r_bsp.c
index c9591b254469a2e512841c781aecb2d77c298118..bf238a4357991bdd7eaeb533a2531e6550c2908b 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -840,6 +840,7 @@ static void R_Subsector(size_t num)
 	extracolormap_t *floorcolormap;
 	extracolormap_t *ceilingcolormap;
 	fixed_t floorcenterz, ceilingcenterz;
+	ffloor_t *rover;
 
 #ifdef RANGECHECK
 	if (num >= numsubsectors)
@@ -866,7 +867,23 @@ static void R_Subsector(size_t num)
 	// Check and prep all 3D floors. Set the sector floor/ceiling light levels and colormaps.
 	if (frontsector->ffloors)
 	{
-		if (frontsector->moved)
+		boolean anyMoved = frontsector->moved;
+
+		if (anyMoved == false)
+		{
+			for (rover = frontsector->ffloors; rover; rover = rover->next)
+			{
+				sector_t *controlSec = &sectors[rover->secnum];
+
+				if (controlSec->moved == true)
+				{
+					anyMoved = true;
+					break;
+				}
+			}
+		}
+
+		if (anyMoved == true)
 		{
 			frontsector->numlights = sub->sector->numlights = 0;
 			R_Prep3DFloors(frontsector);
@@ -914,7 +931,6 @@ static void R_Subsector(size_t num)
 	ffloor[numffloors].polyobj = NULL;
 	if (frontsector->ffloors)
 	{
-		ffloor_t *rover;
 		fixed_t heightcheck, planecenterz;
 
 		for (rover = frontsector->ffloors; rover && numffloors < MAXFFLOORS; rover = rover->next)
diff --git a/src/r_fps.c b/src/r_fps.c
new file mode 100644
index 0000000000000000000000000000000000000000..f3675ab97ef2e1a57677da60c08582c9f5f835da
--- /dev/null
+++ b/src/r_fps.c
@@ -0,0 +1,817 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2000 by Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze, Andrey Budko (prboom)
+// Copyright (C) 1999-2019 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  r_fps.h
+/// \brief Uncapped framerate stuff.
+
+#include "r_fps.h"
+
+#include "r_main.h"
+#include "g_game.h"
+#include "i_video.h"
+#include "r_plane.h"
+#include "p_spec.h"
+#include "r_state.h"
+#include "z_zone.h"
+#include "console.h" // con_startup_loadprogress
+#include "m_perfstats.h" // ps_metric_t
+#ifdef HWRENDER
+#include "hardware/hw_main.h" // for cv_glshearing
+#endif
+
+static CV_PossibleValue_t fpscap_cons_t[] = {
+#ifdef DEVELOP
+	// Lower values are actually pretty useful for debugging interp problems!
+	{1, "MIN"},
+#else
+	{TICRATE, "MIN"},
+#endif
+	{300, "MAX"},
+	{-1, "Unlimited"},
+	{0, "Match refresh rate"},
+	{0, NULL}
+};
+consvar_t cv_fpscap = CVAR_INIT ("fpscap", "Match refresh rate", CV_SAVE, fpscap_cons_t, NULL);
+
+ps_metric_t ps_interp_frac = {0};
+ps_metric_t ps_interp_lag = {0};
+
+UINT32 R_GetFramerateCap(void)
+{
+	if (rendermode == render_none)
+	{
+		// If we're not rendering (dedicated server),
+		// we shouldn't be using any interpolation.
+		return TICRATE;
+	}
+
+	if (cv_fpscap.value == 0)
+	{
+		// 0: Match refresh rate
+		return I_GetRefreshRate();
+	}
+
+	if (cv_fpscap.value < 0)
+	{
+		// -1: Unlimited
+		return 0;
+	}
+
+	return cv_fpscap.value;
+}
+
+boolean R_UsingFrameInterpolation(void)
+{
+	return (R_GetFramerateCap() != TICRATE || cv_timescale.value < FRACUNIT);
+}
+
+static viewvars_t p1view_old;
+static viewvars_t p1view_new;
+static viewvars_t p2view_old;
+static viewvars_t p2view_new;
+static viewvars_t sky1view_old;
+static viewvars_t sky1view_new;
+static viewvars_t sky2view_old;
+static viewvars_t sky2view_new;
+
+static viewvars_t *oldview = &p1view_old;
+static int oldview_invalid[MAXSPLITSCREENPLAYERS] = {0, 0};
+viewvars_t *newview = &p1view_new;
+
+
+enum viewcontext_e viewcontext = VIEWCONTEXT_PLAYER1;
+
+static levelinterpolator_t **levelinterpolators;
+static size_t levelinterpolators_len;
+static size_t levelinterpolators_size;
+
+
+static fixed_t R_LerpFixed(fixed_t from, fixed_t to, fixed_t frac)
+{
+	return from + FixedMul(frac, to - from);
+}
+
+static angle_t R_LerpAngle(angle_t from, angle_t to, fixed_t frac)
+{
+	return from + FixedMul(frac, to - from);
+}
+
+static vector2_t *R_LerpVector2(const vector2_t *from, const vector2_t *to, fixed_t frac, vector2_t *out)
+{
+	FV2_SubEx(to, from, out);
+	FV2_MulEx(out, frac, out);
+	FV2_AddEx(from, out, out);
+	return out;
+}
+
+static vector3_t *R_LerpVector3(const vector3_t *from, const vector3_t *to, fixed_t frac, vector3_t *out)
+{
+	FV3_SubEx(to, from, out);
+	FV3_MulEx(out, frac, out);
+	FV3_AddEx(from, out, out);
+	return out;
+}
+
+// recalc necessary stuff for mouseaiming
+// slopes are already calculated for the full possible view (which is 4*viewheight).
+// 18/08/18: (No it's actually 16*viewheight, thanks Jimita for finding this out)
+static void R_SetupFreelook(player_t *player, boolean skybox)
+{
+#ifndef HWRENDER
+	(void)player;
+	(void)skybox;
+#endif
+
+	// clip it in the case we are looking a hardware 90 degrees full aiming
+	// (lmps, network and use F12...)
+	if (rendermode == render_soft
+#ifdef HWRENDER
+		|| (rendermode == render_opengl
+			&& (cv_glshearing.value == 1
+			|| (cv_glshearing.value == 2 && R_IsViewpointThirdPerson(player, skybox))))
+#endif
+		)
+	{
+		G_SoftwareClipAimingPitch((INT32 *)&aimingangle);
+	}
+
+	centeryfrac = (viewheight/2)<<FRACBITS;
+
+	if (rendermode == render_soft)
+		centeryfrac += FixedMul(AIMINGTODY(aimingangle), FixedDiv(viewwidth<<FRACBITS, BASEVIDWIDTH<<FRACBITS));
+
+	centery = FixedInt(FixedRound(centeryfrac));
+
+	if (rendermode == render_soft)
+		yslope = &yslopetab[viewheight*8 - centery];
+}
+
+#undef AIMINGTODY
+
+void R_InterpolateView(fixed_t frac)
+{
+	viewvars_t* prevview = oldview;
+	boolean skybox = 0;
+	UINT8 i;
+
+	if (FIXED_TO_FLOAT(frac) < 0)
+		frac = 0;
+	if (frac > FRACUNIT)
+		frac = FRACUNIT;
+
+	if (viewcontext == VIEWCONTEXT_SKY1 || viewcontext == VIEWCONTEXT_PLAYER1)
+	{
+		i = 0;
+	}
+	else
+	{
+		i = 1;
+	}
+
+	if (oldview_invalid[i] != 0)
+	{
+		// interpolate from newview to newview
+		prevview = newview;
+	}
+
+	viewx = R_LerpFixed(prevview->x, newview->x, frac);
+	viewy = R_LerpFixed(prevview->y, newview->y, frac);
+	viewz = R_LerpFixed(prevview->z, newview->z, frac);
+
+	viewangle = R_LerpAngle(prevview->angle, newview->angle, frac);
+	aimingangle = R_LerpAngle(prevview->aim, newview->aim, frac);
+
+	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
+	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
+
+	// this is gonna create some interesting visual errors for long distance teleports...
+	// might want to recalculate the view sector every frame instead...
+	viewplayer = newview->player;
+	viewsector = R_PointInSubsector(viewx, viewy)->sector;
+
+	// well, this ain't pretty
+	if (newview == &sky1view_new || newview == &sky2view_new)
+	{
+		skybox = 1;
+	}
+
+	R_SetupFreelook(newview->player, skybox);
+}
+
+void R_UpdateViewInterpolation(void)
+{
+	p1view_old = p1view_new;
+	p2view_old = p2view_new;
+	sky1view_old = sky1view_new;
+	sky2view_old = sky2view_new;
+	if (oldview_invalid[0] > 0) oldview_invalid[0]--;
+	if (oldview_invalid[1] > 0) oldview_invalid[1]--;
+}
+
+void R_ResetViewInterpolation(UINT8 p)
+{
+	if (p == 0)
+	{
+		UINT8 i;
+		for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
+		{
+			oldview_invalid[i]++;
+		}
+	}
+	else
+	{
+		oldview_invalid[p - 1]++;
+	}
+}
+
+void R_SetViewContext(enum viewcontext_e _viewcontext)
+{
+	I_Assert(_viewcontext == VIEWCONTEXT_PLAYER1
+			|| _viewcontext == VIEWCONTEXT_PLAYER2
+			|| _viewcontext == VIEWCONTEXT_SKY1
+			|| _viewcontext == VIEWCONTEXT_SKY2);
+	viewcontext = _viewcontext;
+
+	switch (viewcontext)
+	{
+		case VIEWCONTEXT_PLAYER1:
+			oldview = &p1view_old;
+			newview = &p1view_new;
+			break;
+		case VIEWCONTEXT_PLAYER2:
+			oldview = &p2view_old;
+			newview = &p2view_new;
+			break;
+		case VIEWCONTEXT_SKY1:
+			oldview = &sky1view_old;
+			newview = &sky1view_new;
+			break;
+		case VIEWCONTEXT_SKY2:
+			oldview = &sky2view_old;
+			newview = &sky2view_new;
+			break;
+		default:
+			I_Error("viewcontext value is invalid: we should never get here without an assert!!");
+			break;
+	}
+}
+
+fixed_t R_InterpolateFixed(fixed_t from, fixed_t to)
+{
+	if (!R_UsingFrameInterpolation())
+	{
+		return to;
+	}
+
+	return (R_LerpFixed(from, to, rendertimefrac));
+}
+
+angle_t R_InterpolateAngle(angle_t from, angle_t to)
+{
+	if (!R_UsingFrameInterpolation())
+	{
+		return to;
+	}
+
+	return (R_LerpAngle(from, to, rendertimefrac));
+}
+
+void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
+{
+	if (frac == FRACUNIT)
+	{
+		out->x = mobj->x;
+		out->y = mobj->y;
+		out->z = mobj->z;
+		out->scale = mobj->scale;
+		out->subsector = mobj->subsector;
+		out->angle = mobj->player ? mobj->player->drawangle : mobj->angle;
+		out->spritexscale = mobj->spritexscale;
+		out->spriteyscale = mobj->spriteyscale;
+		out->spritexoffset = mobj->spritexoffset;
+		out->spriteyoffset = mobj->spriteyoffset;
+		return;
+	}
+
+	out->x = R_LerpFixed(mobj->old_x, mobj->x, frac);
+	out->y = R_LerpFixed(mobj->old_y, mobj->y, frac);
+	out->z = R_LerpFixed(mobj->old_z, mobj->z, frac);
+	out->scale = mobj->resetinterp ? mobj->scale : R_LerpFixed(mobj->old_scale, mobj->scale, frac);
+	out->spritexscale = mobj->resetinterp ? mobj->spritexscale : R_LerpFixed(mobj->old_spritexscale, mobj->spritexscale, frac);
+	out->spriteyscale = mobj->resetinterp ? mobj->spriteyscale : R_LerpFixed(mobj->old_spriteyscale, mobj->spriteyscale, frac);
+
+	// Sprite offsets are not interpolated until we have a way to interpolate them explicitly in Lua.
+	// It seems existing mods visually break more often than not if it is interpolated.
+	out->spritexoffset = mobj->spritexoffset;
+	out->spriteyoffset = mobj->spriteyoffset;
+
+	out->subsector = R_PointInSubsector(out->x, out->y);
+
+	if (mobj->player)
+	{
+		out->angle = mobj->resetinterp ? mobj->player->drawangle : R_LerpAngle(mobj->player->old_drawangle, mobj->player->drawangle, frac);
+	}
+	else
+	{
+		out->angle = mobj->resetinterp ? mobj->angle : R_LerpAngle(mobj->old_angle, mobj->angle, frac);
+	}
+}
+
+void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
+{
+	if (frac == FRACUNIT)
+	{
+		out->x = mobj->x;
+		out->y = mobj->y;
+		out->z = mobj->z;
+		out->scale = FRACUNIT;
+		out->subsector = mobj->subsector;
+		out->angle = mobj->angle;
+		out->spritexscale = mobj->spritexscale;
+		out->spriteyscale = mobj->spriteyscale;
+		out->spritexoffset = mobj->spritexoffset;
+		out->spriteyoffset = mobj->spriteyoffset;
+		return;
+	}
+
+	out->x = R_LerpFixed(mobj->old_x, mobj->x, frac);
+	out->y = R_LerpFixed(mobj->old_y, mobj->y, frac);
+	out->z = R_LerpFixed(mobj->old_z, mobj->z, frac);
+	out->scale = FRACUNIT;
+	out->spritexscale = R_LerpFixed(mobj->old_spritexscale, mobj->spritexscale, frac);
+	out->spriteyscale = R_LerpFixed(mobj->old_spriteyscale, mobj->spriteyscale, frac);
+	out->spritexoffset = R_LerpFixed(mobj->old_spritexoffset, mobj->spritexoffset, frac);
+	out->spriteyoffset = R_LerpFixed(mobj->old_spriteyoffset, mobj->spriteyoffset, frac);
+
+	out->subsector = R_PointInSubsector(out->x, out->y);
+
+	out->angle = R_LerpAngle(mobj->old_angle, mobj->angle, frac);
+}
+
+static void AddInterpolator(levelinterpolator_t* interpolator)
+{
+	if (levelinterpolators_len >= levelinterpolators_size)
+	{
+		if (levelinterpolators_size == 0)
+		{
+			levelinterpolators_size = 128;
+		}
+		else
+		{
+			levelinterpolators_size *= 2;
+		}
+		
+		levelinterpolators = Z_ReallocAlign(
+			(void*) levelinterpolators,
+			sizeof(levelinterpolator_t*) * levelinterpolators_size,
+			PU_LEVEL,
+			NULL,
+			sizeof(levelinterpolator_t*) * 8
+		);
+	}
+
+	levelinterpolators[levelinterpolators_len] = interpolator;
+	levelinterpolators_len += 1;
+}
+
+static levelinterpolator_t *CreateInterpolator(levelinterpolator_type_e type, thinker_t *thinker)
+{
+	levelinterpolator_t *ret = (levelinterpolator_t*) Z_CallocAlign(
+		sizeof(levelinterpolator_t),
+		PU_LEVEL,
+		NULL,
+		sizeof(levelinterpolator_t) * 8
+	);
+
+	ret->type = type;
+	ret->thinker = thinker;
+
+	AddInterpolator(ret);
+
+	return ret;
+}
+
+void R_CreateInterpolator_SectorPlane(thinker_t *thinker, sector_t *sector, boolean ceiling)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_SectorPlane, thinker);
+	interp->sectorplane.sector = sector;
+	interp->sectorplane.ceiling = ceiling;
+	if (ceiling)
+	{
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight = sector->ceilingheight;
+	}
+	else
+	{
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight = sector->floorheight;
+	}
+}
+
+void R_CreateInterpolator_SectorScroll(thinker_t *thinker, sector_t *sector, boolean ceiling)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_SectorScroll, thinker);
+	interp->sectorscroll.sector = sector;
+	interp->sectorscroll.ceiling = ceiling;
+	if (ceiling)
+	{
+		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->ceiling_xoffs;
+		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->ceiling_yoffs;
+	}
+	else
+	{
+		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->floor_xoffs;
+		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->floor_yoffs;
+	}
+}
+
+void R_CreateInterpolator_SideScroll(thinker_t *thinker, side_t *side)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_SideScroll, thinker);
+	interp->sidescroll.side = side;
+	interp->sidescroll.oldtextureoffset = interp->sidescroll.baktextureoffset = side->textureoffset;
+	interp->sidescroll.oldrowoffset = interp->sidescroll.bakrowoffset = side->rowoffset;
+}
+
+void R_CreateInterpolator_Polyobj(thinker_t *thinker, polyobj_t *polyobj)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_Polyobj, thinker);
+	interp->polyobj.polyobj = polyobj;
+	interp->polyobj.vertices_size = polyobj->numVertices;
+
+	interp->polyobj.oldvertices = Z_CallocAlign(sizeof(fixed_t) * 2 * polyobj->numVertices, PU_LEVEL, NULL, 32);
+	interp->polyobj.bakvertices = Z_CallocAlign(sizeof(fixed_t) * 2 * polyobj->numVertices, PU_LEVEL, NULL, 32);
+	for (size_t i = 0; i < polyobj->numVertices; i++)
+	{
+		interp->polyobj.oldvertices[i * 2    ] = interp->polyobj.bakvertices[i * 2    ] = polyobj->vertices[i]->x;
+		interp->polyobj.oldvertices[i * 2 + 1] = interp->polyobj.bakvertices[i * 2 + 1] = polyobj->vertices[i]->y;
+	}
+
+	interp->polyobj.oldcx = interp->polyobj.bakcx = polyobj->centerPt.x;
+	interp->polyobj.oldcy = interp->polyobj.bakcy = polyobj->centerPt.y;
+}
+
+void R_CreateInterpolator_DynSlope(thinker_t *thinker, pslope_t *slope)
+{
+	levelinterpolator_t *interp = CreateInterpolator(LVLINTERP_DynSlope, thinker);
+	interp->dynslope.slope = slope;
+
+	FV3_Copy(&interp->dynslope.oldo, &slope->o);
+	FV3_Copy(&interp->dynslope.bako, &slope->o);
+
+	FV2_Copy(&interp->dynslope.oldd, &slope->d);
+	FV2_Copy(&interp->dynslope.bakd, &slope->d);
+
+	interp->dynslope.oldzdelta = interp->dynslope.bakzdelta = slope->zdelta;
+}
+
+void R_InitializeLevelInterpolators(void)
+{
+	levelinterpolators_len = 0;
+	levelinterpolators_size = 0;
+	levelinterpolators = NULL;
+}
+
+static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
+{
+	size_t i;
+
+	switch (interp->type)
+	{
+	case LVLINTERP_SectorPlane:
+		interp->sectorplane.oldheight = interp->sectorplane.bakheight;
+		interp->sectorplane.bakheight = interp->sectorplane.ceiling ? interp->sectorplane.sector->ceilingheight : interp->sectorplane.sector->floorheight;
+		break;
+	case LVLINTERP_SectorScroll:
+		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs;
+		interp->sectorscroll.bakxoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceiling_xoffs : interp->sectorscroll.sector->floor_xoffs;
+		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs;
+		interp->sectorscroll.bakyoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceiling_yoffs : interp->sectorscroll.sector->floor_yoffs;
+		break;
+	case LVLINTERP_SideScroll:
+		interp->sidescroll.oldtextureoffset = interp->sidescroll.baktextureoffset;
+		interp->sidescroll.baktextureoffset = interp->sidescroll.side->textureoffset;
+		interp->sidescroll.oldrowoffset = interp->sidescroll.bakrowoffset;
+		interp->sidescroll.bakrowoffset = interp->sidescroll.side->rowoffset;
+		break;
+	case LVLINTERP_Polyobj:
+		for (i = 0; i < interp->polyobj.vertices_size; i++)
+		{
+			interp->polyobj.oldvertices[i * 2    ] = interp->polyobj.bakvertices[i * 2    ];
+			interp->polyobj.oldvertices[i * 2 + 1] = interp->polyobj.bakvertices[i * 2 + 1];
+			interp->polyobj.bakvertices[i * 2    ] = interp->polyobj.polyobj->vertices[i]->x;
+			interp->polyobj.bakvertices[i * 2 + 1] = interp->polyobj.polyobj->vertices[i]->y;
+		}
+		interp->polyobj.oldcx = interp->polyobj.bakcx;
+		interp->polyobj.oldcy = interp->polyobj.bakcy;
+		interp->polyobj.bakcx = interp->polyobj.polyobj->centerPt.x;
+		interp->polyobj.bakcy = interp->polyobj.polyobj->centerPt.y;
+		break;
+	case LVLINTERP_DynSlope:
+		FV3_Copy(&interp->dynslope.oldo, &interp->dynslope.bako);
+		FV2_Copy(&interp->dynslope.oldd, &interp->dynslope.bakd);
+		interp->dynslope.oldzdelta = interp->dynslope.bakzdelta;
+
+		FV3_Copy(&interp->dynslope.bako, &interp->dynslope.slope->o);
+		FV2_Copy(&interp->dynslope.bakd, &interp->dynslope.slope->d);
+		interp->dynslope.bakzdelta = interp->dynslope.slope->zdelta;
+		break;
+	}
+}
+
+void R_UpdateLevelInterpolators(void)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+
+		UpdateLevelInterpolatorState(interp);
+	}
+}
+
+void R_ClearLevelInterpolatorState(thinker_t *thinker)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+
+		if (interp->thinker == thinker)
+		{
+			// Do it twice to make the old state match the new
+			UpdateLevelInterpolatorState(interp);
+			UpdateLevelInterpolatorState(interp);
+		}
+	}
+}
+
+void R_ApplyLevelInterpolators(fixed_t frac)
+{
+	size_t i, ii;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+
+		switch (interp->type)
+		{
+		case LVLINTERP_SectorPlane:
+			if (interp->sectorplane.ceiling)
+			{
+				interp->sectorplane.sector->ceilingheight = R_LerpFixed(interp->sectorplane.oldheight, interp->sectorplane.bakheight, frac);
+			}
+			else
+			{
+				interp->sectorplane.sector->floorheight = R_LerpFixed(interp->sectorplane.oldheight, interp->sectorplane.bakheight, frac);
+			}
+			interp->sectorplane.sector->moved = true;
+			break;
+		case LVLINTERP_SectorScroll:
+			if (interp->sectorscroll.ceiling)
+			{
+				interp->sectorscroll.sector->ceiling_xoffs = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
+				interp->sectorscroll.sector->ceiling_yoffs = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
+			}
+			else
+			{
+				interp->sectorscroll.sector->floor_xoffs = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
+				interp->sectorscroll.sector->floor_yoffs = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
+			}
+			break;
+		case LVLINTERP_SideScroll:
+			interp->sidescroll.side->textureoffset = R_LerpFixed(interp->sidescroll.oldtextureoffset, interp->sidescroll.baktextureoffset, frac);
+			interp->sidescroll.side->rowoffset = R_LerpFixed(interp->sidescroll.oldrowoffset, interp->sidescroll.bakrowoffset, frac);
+			break;
+		case LVLINTERP_Polyobj:
+			for (ii = 0; ii < interp->polyobj.vertices_size; ii++)
+			{
+				interp->polyobj.polyobj->vertices[ii]->x = R_LerpFixed(interp->polyobj.oldvertices[ii * 2    ], interp->polyobj.bakvertices[ii * 2    ], frac);
+				interp->polyobj.polyobj->vertices[ii]->y = R_LerpFixed(interp->polyobj.oldvertices[ii * 2 + 1], interp->polyobj.bakvertices[ii * 2 + 1], frac);
+			}
+			interp->polyobj.polyobj->centerPt.x = R_LerpFixed(interp->polyobj.oldcx, interp->polyobj.bakcx, frac);
+			interp->polyobj.polyobj->centerPt.y = R_LerpFixed(interp->polyobj.oldcy, interp->polyobj.bakcy, frac);
+			break;
+		case LVLINTERP_DynSlope:
+			R_LerpVector3(&interp->dynslope.oldo, &interp->dynslope.bako, frac, &interp->dynslope.slope->o);
+			R_LerpVector2(&interp->dynslope.oldd, &interp->dynslope.bakd, frac, &interp->dynslope.slope->d);
+			interp->dynslope.slope->zdelta = R_LerpFixed(interp->dynslope.oldzdelta, interp->dynslope.bakzdelta, frac);
+			break;
+		}
+	}
+}
+
+void R_RestoreLevelInterpolators(void)
+{
+	size_t i, ii;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+		
+		switch (interp->type)
+		{
+		case LVLINTERP_SectorPlane:
+			if (interp->sectorplane.ceiling)
+			{
+				interp->sectorplane.sector->ceilingheight = interp->sectorplane.bakheight;
+			}
+			else
+			{
+				interp->sectorplane.sector->floorheight = interp->sectorplane.bakheight;
+			}
+			interp->sectorplane.sector->moved = true;
+			break;
+		case LVLINTERP_SectorScroll:
+			if (interp->sectorscroll.ceiling)
+			{
+				interp->sectorscroll.sector->ceiling_xoffs = interp->sectorscroll.bakxoffs;
+				interp->sectorscroll.sector->ceiling_yoffs = interp->sectorscroll.bakyoffs;
+			}
+			else
+			{
+				interp->sectorscroll.sector->floor_xoffs = interp->sectorscroll.bakxoffs;
+				interp->sectorscroll.sector->floor_yoffs = interp->sectorscroll.bakyoffs;
+			}
+			break;
+		case LVLINTERP_SideScroll:
+			interp->sidescroll.side->textureoffset = interp->sidescroll.baktextureoffset;
+			interp->sidescroll.side->rowoffset = interp->sidescroll.bakrowoffset;
+			break;
+		case LVLINTERP_Polyobj:
+			for (ii = 0; ii < interp->polyobj.vertices_size; ii++)
+			{
+				interp->polyobj.polyobj->vertices[ii]->x = interp->polyobj.bakvertices[ii * 2    ];
+				interp->polyobj.polyobj->vertices[ii]->y = interp->polyobj.bakvertices[ii * 2 + 1];
+			}
+			interp->polyobj.polyobj->centerPt.x = interp->polyobj.bakcx;
+			interp->polyobj.polyobj->centerPt.y = interp->polyobj.bakcy;
+			break;
+		case LVLINTERP_DynSlope:
+			FV3_Copy(&interp->dynslope.slope->o, &interp->dynslope.bako);
+			FV2_Copy(&interp->dynslope.slope->d, &interp->dynslope.bakd);
+			interp->dynslope.slope->zdelta = interp->dynslope.bakzdelta;
+			break;
+		}
+	}
+}
+
+void R_DestroyLevelInterpolators(thinker_t *thinker)
+{
+	size_t i;
+
+	for (i = 0; i < levelinterpolators_len; i++)
+	{
+		levelinterpolator_t *interp = levelinterpolators[i];
+		
+		if (interp->thinker == thinker)
+		{
+			// Swap the tail of the level interpolators to this spot
+			levelinterpolators[i] = levelinterpolators[levelinterpolators_len - 1];
+			levelinterpolators_len -= 1;
+
+			Z_Free(interp);
+			i -= 1;
+		}
+	}
+}
+
+static mobj_t **interpolated_mobjs = NULL;
+static size_t interpolated_mobjs_len = 0;
+static size_t interpolated_mobjs_capacity = 0;
+
+// NOTE: This will NOT check that the mobj has already been added, for perf
+// reasons.
+void R_AddMobjInterpolator(mobj_t *mobj)
+{
+	if (interpolated_mobjs_len >= interpolated_mobjs_capacity)
+	{
+		if (interpolated_mobjs_capacity == 0)
+		{
+			interpolated_mobjs_capacity = 256;
+		}
+		else
+		{
+			interpolated_mobjs_capacity *= 2;
+		}
+
+		interpolated_mobjs = Z_ReallocAlign(
+			interpolated_mobjs,
+			sizeof(mobj_t *) * interpolated_mobjs_capacity,
+			PU_LEVEL,
+			NULL,
+			64
+		);
+	}
+
+	interpolated_mobjs[interpolated_mobjs_len] = mobj;
+	interpolated_mobjs_len += 1;
+
+	R_ResetMobjInterpolationState(mobj);
+	mobj->resetinterp = true;
+}
+
+void R_RemoveMobjInterpolator(mobj_t *mobj)
+{
+	size_t i;
+
+	if (interpolated_mobjs_len == 0) return;
+
+	for (i = 0; i < interpolated_mobjs_len - 1; i++)
+	{
+		if (interpolated_mobjs[i] == mobj)
+		{
+			interpolated_mobjs[i] = interpolated_mobjs[
+				interpolated_mobjs_len - 1
+			];
+			interpolated_mobjs_len -= 1;
+			return;
+		}
+	}
+}
+
+void R_InitMobjInterpolators(void)
+{
+	// apparently it's not acceptable to free something already unallocated
+	// Z_Free(interpolated_mobjs);
+	interpolated_mobjs = NULL;
+	interpolated_mobjs_len = 0;
+	interpolated_mobjs_capacity = 0;
+}
+
+void R_UpdateMobjInterpolators(void)
+{
+	size_t i;
+	for (i = 0; i < interpolated_mobjs_len; i++)
+	{
+		mobj_t *mobj = interpolated_mobjs[i];
+		if (!P_MobjWasRemoved(mobj))
+			R_ResetMobjInterpolationState(mobj);
+	}
+}
+
+//
+// P_ResetMobjInterpolationState
+//
+// Reset the rendering interpolation state of the mobj.
+//
+void R_ResetMobjInterpolationState(mobj_t *mobj)
+{
+	mobj->old_x2 = mobj->old_x;
+	mobj->old_y2 = mobj->old_y;
+	mobj->old_z2 = mobj->old_z;
+	mobj->old_angle2 = mobj->old_angle;
+	mobj->old_pitch2 = mobj->old_pitch;
+	mobj->old_roll2 = mobj->old_roll;
+	mobj->old_scale2 = mobj->old_scale;
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+	mobj->old_angle = mobj->angle;
+	mobj->old_pitch = mobj->pitch;
+	mobj->old_roll = mobj->roll;
+	mobj->old_scale = mobj->scale;
+	mobj->old_spritexscale = mobj->spritexscale;
+	mobj->old_spriteyscale = mobj->spriteyscale;
+	mobj->old_spritexoffset = mobj->spritexoffset;
+	mobj->old_spriteyoffset = mobj->spriteyoffset;
+
+	if (mobj->player)
+	{
+		mobj->player->old_drawangle2 = mobj->player->old_drawangle;
+		mobj->player->old_drawangle = mobj->player->drawangle;
+	}
+
+	mobj->resetinterp = false;
+}
+
+//
+// P_ResetPrecipitationMobjInterpolationState
+//
+// Reset the rendering interpolation state of the precipmobj.
+//
+void R_ResetPrecipitationMobjInterpolationState(precipmobj_t *mobj)
+{
+	mobj->old_x2 = mobj->old_x;
+	mobj->old_y2 = mobj->old_y;
+	mobj->old_z2 = mobj->old_z;
+	mobj->old_angle2 = mobj->old_angle;
+	mobj->old_pitch2 = mobj->old_pitch;
+	mobj->old_roll2 = mobj->old_roll;
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+	mobj->old_angle = mobj->angle;
+	mobj->old_spritexscale = mobj->spritexscale;
+	mobj->old_spriteyscale = mobj->spriteyscale;
+	mobj->old_spritexoffset = mobj->spritexoffset;
+	mobj->old_spriteyoffset = mobj->spriteyoffset;
+}
diff --git a/src/r_fps.h b/src/r_fps.h
new file mode 100644
index 0000000000000000000000000000000000000000..85c87a2f49ff1c3177f7fb8b8e9136e88316ebc9
--- /dev/null
+++ b/src/r_fps.h
@@ -0,0 +1,162 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2000 by Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze, Andrey Budko (prboom)
+// Copyright (C) 1999-2019 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  r_fps.h
+/// \brief Uncapped framerate stuff.
+
+#ifndef __R_FPS_H__
+#define __R_FPS_H__
+
+#include "m_fixed.h"
+#include "p_local.h"
+#include "r_state.h"
+#include "m_perfstats.h" // ps_metric_t
+
+extern consvar_t cv_fpscap;
+
+extern ps_metric_t ps_interp_frac;
+extern ps_metric_t ps_interp_lag;
+
+UINT32 R_GetFramerateCap(void);
+boolean R_UsingFrameInterpolation(void);
+
+enum viewcontext_e
+{
+	VIEWCONTEXT_PLAYER1 = 0,
+	VIEWCONTEXT_PLAYER2,
+	VIEWCONTEXT_SKY1,
+	VIEWCONTEXT_SKY2
+};
+
+typedef struct {
+	fixed_t x;
+	fixed_t y;
+	fixed_t z;
+	boolean sky;
+	sector_t *sector;
+	player_t *player;
+
+	angle_t angle;
+	angle_t aim;
+	fixed_t cos;
+	fixed_t sin;
+	mobj_t *mobj;
+} viewvars_t;
+
+extern viewvars_t *newview;
+
+typedef struct {
+	fixed_t x;
+	fixed_t y;
+	fixed_t z;
+	subsector_t *subsector;
+	angle_t angle;
+	fixed_t scale;
+	fixed_t spritexscale;
+	fixed_t spriteyscale;
+	fixed_t spritexoffset;
+	fixed_t spriteyoffset;
+} interpmobjstate_t;
+
+// Level interpolators
+
+// The union tag for levelinterpolator_t
+typedef enum {
+	LVLINTERP_SectorPlane,
+	LVLINTERP_SectorScroll,
+	LVLINTERP_SideScroll,
+	LVLINTERP_Polyobj,
+	LVLINTERP_DynSlope,
+} levelinterpolator_type_e;
+
+// Tagged union of a level interpolator
+typedef struct levelinterpolator_s {
+	levelinterpolator_type_e type;
+	thinker_t *thinker;
+	union {
+		struct {
+			sector_t *sector;
+			fixed_t oldheight;
+			fixed_t bakheight;
+			boolean ceiling;
+		} sectorplane;
+		struct {
+			sector_t *sector;
+			fixed_t oldxoffs, oldyoffs, bakxoffs, bakyoffs;
+			boolean ceiling;
+		} sectorscroll;
+		struct {
+			side_t *side;
+			fixed_t oldtextureoffset, oldrowoffset, baktextureoffset, bakrowoffset;
+		} sidescroll;
+		struct {
+			polyobj_t *polyobj;
+			fixed_t *oldvertices;
+			fixed_t *bakvertices;
+			size_t vertices_size;
+			fixed_t oldcx, oldcy, bakcx, bakcy;
+		} polyobj;
+		struct {
+			pslope_t *slope;
+			vector3_t oldo, bako;
+			vector2_t oldd, bakd;
+			fixed_t oldzdelta, bakzdelta;
+		} dynslope;
+	};
+} levelinterpolator_t;
+
+// Interpolates the current view variables (r_state.h) against the selected view context in R_SetViewContext
+void R_InterpolateView(fixed_t frac);
+// Buffer the current new views into the old views. Call once after each real tic.
+void R_UpdateViewInterpolation(void);
+// Reset the view states (e.g. after level load) so R_InterpolateView doesn't interpolate invalid data
+void R_ResetViewInterpolation(UINT8 p);
+// Set the current view context (the viewvars pointed to by newview)
+void R_SetViewContext(enum viewcontext_e _viewcontext);
+
+fixed_t R_InterpolateFixed(fixed_t from, fixed_t to);
+angle_t R_InterpolateAngle(angle_t from, angle_t to);
+
+// Evaluate the interpolated mobj state for the given mobj
+void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out);
+// Evaluate the interpolated mobj state for the given precipmobj
+void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjstate_t *out);
+
+void R_CreateInterpolator_SectorPlane(thinker_t *thinker, sector_t *sector, boolean ceiling);
+void R_CreateInterpolator_SectorScroll(thinker_t *thinker, sector_t *sector, boolean ceiling);
+void R_CreateInterpolator_SideScroll(thinker_t *thinker, side_t *side);
+void R_CreateInterpolator_Polyobj(thinker_t *thinker, polyobj_t *polyobj);
+void R_CreateInterpolator_DynSlope(thinker_t *thinker, pslope_t *slope);
+
+// Initialize level interpolators after a level change
+void R_InitializeLevelInterpolators(void);
+// Update level interpolators, storing the previous and current states.
+void R_UpdateLevelInterpolators(void);
+// Clear states for all level interpolators for the thinker
+void R_ClearLevelInterpolatorState(thinker_t *thinker);
+// Apply level interpolators to the actual game state
+void R_ApplyLevelInterpolators(fixed_t frac);
+// Restore level interpolators to the real game state
+void R_RestoreLevelInterpolators(void);
+// Destroy interpolators associated with a thinker
+void R_DestroyLevelInterpolators(thinker_t *thinker);
+
+// Initialize internal mobj interpolator list (e.g. during level loading)
+void R_InitMobjInterpolators(void);
+// Add interpolation state for the given mobj
+void R_AddMobjInterpolator(mobj_t *mobj);
+// Remove the interpolation state for the given mobj
+void R_RemoveMobjInterpolator(mobj_t *mobj);
+void R_UpdateMobjInterpolators(void);
+void R_ResetMobjInterpolationState(mobj_t *mobj);
+void R_ResetPrecipitationMobjInterpolationState(precipmobj_t *mobj);
+
+#endif
diff --git a/src/r_main.c b/src/r_main.c
index f19962d412667646804fc72c6ad9f5535fb20315..cd5e255ed4059ef0680e782298284f422dc27611 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -35,6 +35,7 @@
 #include "r_portal.h"
 #include "r_main.h"
 #include "i_system.h" // I_GetPreciseTime
+#include "r_fps.h" // Frame interpolation/uncapped
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -75,6 +76,10 @@ sector_t *viewsector;
 player_t *viewplayer;
 mobj_t *r_viewmobj;
 
+fixed_t rendertimefrac;
+fixed_t renderdeltatics;
+boolean renderisnewtic;
+
 //
 // precalculated math tables
 //
@@ -1084,41 +1089,6 @@ subsector_t *R_PointInSubsectorOrNull(fixed_t x, fixed_t y)
 //
 // R_SetupFrame
 //
-
-// recalc necessary stuff for mouseaiming
-// slopes are already calculated for the full possible view (which is 4*viewheight).
-// 18/08/18: (No it's actually 16*viewheight, thanks Jimita for finding this out)
-static void R_SetupFreelook(player_t *player, boolean skybox)
-{
-#ifndef HWRENDER
-	(void)player;
-	(void)skybox;
-#endif
-
-	// clip it in the case we are looking a hardware 90 degrees full aiming
-	// (lmps, network and use F12...)
-	if (rendermode == render_soft
-#ifdef HWRENDER
-		|| (rendermode == render_opengl
-			&& (cv_glshearing.value == 1
-			|| (cv_glshearing.value == 2 && R_IsViewpointThirdPerson(player, skybox))))
-#endif
-		)
-	{
-		G_SoftwareClipAimingPitch((INT32 *)&aimingangle);
-	}
-
-	centeryfrac = (viewheight/2)<<FRACBITS;
-
-	if (rendermode == render_soft)
-		centeryfrac += FixedMul(AIMINGTODY(aimingangle), FixedDiv(viewwidth<<FRACBITS, BASEVIDWIDTH<<FRACBITS));
-
-	centery = FixedInt(FixedRound(centeryfrac));
-
-	if (rendermode == render_soft)
-		yslope = &yslopetab[viewheight*8 - centery];
-}
-
 void R_SetupFrame(player_t *player)
 {
 	camera_t *thiscam;
@@ -1129,11 +1099,13 @@ void R_SetupFrame(player_t *player)
 	{
 		thiscam = &camera2;
 		chasecam = (cv_chasecam2.value != 0);
+		R_SetViewContext(VIEWCONTEXT_PLAYER2);
 	}
 	else
 	{
 		thiscam = &camera;
 		chasecam = (cv_chasecam.value != 0);
+		R_SetViewContext(VIEWCONTEXT_PLAYER1);
 	}
 
 	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
@@ -1149,81 +1121,83 @@ void R_SetupFrame(player_t *player)
 	else if (!chasecam)
 		thiscam->chase = false;
 
+	newview->sky = false;
+
 	if (player->awayviewtics)
 	{
 		// cut-away view stuff
 		r_viewmobj = player->awayviewmobj; // should be a MT_ALTVIEWMAN
 		I_Assert(r_viewmobj != NULL);
-		viewz = r_viewmobj->z + 20*FRACUNIT;
-		aimingangle = player->awayviewaiming;
-		viewangle = r_viewmobj->angle;
+		newview->z = r_viewmobj->z + 20*FRACUNIT;
+		newview->aim = player->awayviewaiming;
+		newview->angle = r_viewmobj->angle;
 	}
 	else if (!player->spectator && chasecam)
 	// use outside cam view
 	{
 		r_viewmobj = NULL;
-		viewz = thiscam->z + (thiscam->height>>1);
-		aimingangle = thiscam->aiming;
-		viewangle = thiscam->angle;
+		newview->z = thiscam->z + (thiscam->height>>1);
+		newview->aim = thiscam->aiming;
+		newview->angle = thiscam->angle;
 	}
 	else
 	// use the player's eyes view
 	{
-		viewz = player->viewz;
+		newview->z = player->viewz;
 
 		r_viewmobj = player->mo;
 		I_Assert(r_viewmobj != NULL);
 
-		aimingangle = player->aiming;
-		viewangle = r_viewmobj->angle;
+		newview->aim = player->aiming;
+		newview->angle = r_viewmobj->angle;
 
 		if (!demoplayback && player->playerstate != PST_DEAD)
 		{
 			if (player == &players[consoleplayer])
 			{
-				viewangle = localangle; // WARNING: camera uses this
-				aimingangle = localaiming;
+				newview->angle = localangle; // WARNING: camera uses this
+				newview->aim = localaiming;
 			}
 			else if (player == &players[secondarydisplayplayer])
 			{
-				viewangle = localangle2;
-				aimingangle = localaiming2;
+				newview->angle = localangle2;
+				newview->aim = localaiming2;
 			}
 		}
 	}
-	viewz += quake.z;
+	newview->z += quake.z;
 
-	viewplayer = player;
+	newview->player = player;
 
 	if (chasecam && !player->awayviewtics && !player->spectator)
 	{
-		viewx = thiscam->x;
-		viewy = thiscam->y;
-		viewx += quake.x;
-		viewy += quake.y;
+		newview->x = thiscam->x;
+		newview->y = thiscam->y;
+		newview->x += quake.x;
+		newview->y += quake.y;
 
 		if (thiscam->subsector)
-			viewsector = thiscam->subsector->sector;
+			newview->sector = thiscam->subsector->sector;
 		else
-			viewsector = R_PointInSubsector(viewx, viewy)->sector;
+			newview->sector = R_PointInSubsector(newview->x, newview->y)->sector;
 	}
 	else
 	{
-		viewx = r_viewmobj->x;
-		viewy = r_viewmobj->y;
-		viewx += quake.x;
-		viewy += quake.y;
+		newview->x = r_viewmobj->x;
+		newview->y = r_viewmobj->y;
+		newview->x += quake.x;
+		newview->y += quake.y;
 
 		if (r_viewmobj->subsector)
-			viewsector = r_viewmobj->subsector->sector;
+			newview->sector = r_viewmobj->subsector->sector;
 		else
-			viewsector = R_PointInSubsector(viewx, viewy)->sector;
+			newview->sector = R_PointInSubsector(newview->x, newview->y)->sector;
 	}
 
-	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
-	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
+	// newview->sin = FINESINE(viewangle>>ANGLETOFINESHIFT);
+	// newview->cos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
-	R_SetupFreelook(player, false);
+	R_InterpolateView(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 }
 
 void R_SkyboxFrame(player_t *player)
@@ -1232,11 +1206,18 @@ void R_SkyboxFrame(player_t *player)
 
 	if (splitscreen && player == &players[secondarydisplayplayer]
 	&& player != &players[consoleplayer])
+	{
 		thiscam = &camera2;
+		R_SetViewContext(VIEWCONTEXT_SKY2);
+	}
 	else
+	{
 		thiscam = &camera;
+		R_SetViewContext(VIEWCONTEXT_SKY1);
+	}
 
 	// cut-away view stuff
+	newview->sky = true;
 	r_viewmobj = skyboxmo[0];
 #ifdef PARANOIA
 	if (!r_viewmobj)
@@ -1247,39 +1228,39 @@ void R_SkyboxFrame(player_t *player)
 #endif
 	if (player->awayviewtics)
 	{
-		aimingangle = player->awayviewaiming;
-		viewangle = player->awayviewmobj->angle;
+		newview->aim = player->awayviewaiming;
+		newview->angle = player->awayviewmobj->angle;
 	}
 	else if (thiscam->chase)
 	{
-		aimingangle = thiscam->aiming;
-		viewangle = thiscam->angle;
+		newview->aim = thiscam->aiming;
+		newview->angle = thiscam->angle;
 	}
 	else
 	{
-		aimingangle = player->aiming;
-		viewangle = player->mo->angle;
+		newview->aim = player->aiming;
+		newview->angle = player->mo->angle;
 		if (!demoplayback && player->playerstate != PST_DEAD)
 		{
 			if (player == &players[consoleplayer])
 			{
-				viewangle = localangle; // WARNING: camera uses this
-				aimingangle = localaiming;
+				newview->angle = localangle; // WARNING: camera uses this
+				newview->aim = localaiming;
 			}
 			else if (player == &players[secondarydisplayplayer])
 			{
-				viewangle = localangle2;
-				aimingangle = localaiming2;
+				newview->angle = localangle2;
+				newview->aim = localaiming2;
 			}
 		}
 	}
-	viewangle += r_viewmobj->angle;
+	newview->angle += r_viewmobj->angle;
 
-	viewplayer = player;
+	newview->player = player;
 
-	viewx = r_viewmobj->x;
-	viewy = r_viewmobj->y;
-	viewz = r_viewmobj->z; // 26/04/17: use actual Z position instead of spawnpoint angle!
+	newview->x = r_viewmobj->x;
+	newview->y = r_viewmobj->y;
+	newview->z = r_viewmobj->z; // 26/04/17: use actual Z position instead of spawnpoint angle!
 
 	if (mapheaderinfo[gamemap-1])
 	{
@@ -1321,56 +1302,75 @@ void R_SkyboxFrame(player_t *player)
 
 			if (r_viewmobj->angle == 0)
 			{
-				viewx += x;
-				viewy += y;
+				newview->x += x;
+				newview->y += y;
 			}
 			else if (r_viewmobj->angle == ANGLE_90)
 			{
-				viewx -= y;
-				viewy += x;
+				newview->x -= y;
+				newview->y += x;
 			}
 			else if (r_viewmobj->angle == ANGLE_180)
 			{
-				viewx -= x;
-				viewy -= y;
+				newview->x -= x;
+				newview->y -= y;
 			}
 			else if (r_viewmobj->angle == ANGLE_270)
 			{
-				viewx += y;
-				viewy -= x;
+				newview->x += y;
+				newview->y -= x;
 			}
 			else
 			{
 				angle_t ang = r_viewmobj->angle>>ANGLETOFINESHIFT;
-				viewx += FixedMul(x,FINECOSINE(ang)) - FixedMul(y,  FINESINE(ang));
-				viewy += FixedMul(x,  FINESINE(ang)) + FixedMul(y,FINECOSINE(ang));
+				newview->x += FixedMul(x,FINECOSINE(ang)) - FixedMul(y,  FINESINE(ang));
+				newview->y += FixedMul(x,  FINESINE(ang)) + FixedMul(y,FINECOSINE(ang));
 			}
 		}
 		if (mh->skybox_scalez > 0)
-			viewz += campos.z / mh->skybox_scalez;
+			newview->z += campos.z / mh->skybox_scalez;
 		else if (mh->skybox_scalez < 0)
-			viewz += campos.z * -mh->skybox_scalez;
+			newview->z += campos.z * -mh->skybox_scalez;
 	}
 
 	if (r_viewmobj->subsector)
-		viewsector = r_viewmobj->subsector->sector;
+		newview->sector = r_viewmobj->subsector->sector;
 	else
-		viewsector = R_PointInSubsector(viewx, viewy)->sector;
+		newview->sector = R_PointInSubsector(newview->x, newview->y)->sector;
 
-	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
-	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
+	// newview->sin = FINESINE(viewangle>>ANGLETOFINESHIFT);
+	// newview->cos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
-	R_SetupFreelook(player, true);
+	R_InterpolateView(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 }
 
 boolean R_ViewpointHasChasecam(player_t *player)
 {
+	camera_t *thiscam;
 	boolean chasecam = false;
 
 	if (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer])
+	{
+		thiscam = &camera2;
 		chasecam = (cv_chasecam2.value != 0);
+		R_SetViewContext(VIEWCONTEXT_PLAYER2);
+		if (thiscam->reset)
+		{
+			R_ResetViewInterpolation(2);
+			thiscam->reset = false;
+		}
+	}
 	else
+	{
+		thiscam = &camera;
 		chasecam = (cv_chasecam.value != 0);
+		R_SetViewContext(VIEWCONTEXT_PLAYER1);
+		if (thiscam->reset)
+		{
+			R_ResetViewInterpolation(1);
+			thiscam->reset = false;
+		}
+	}
 
 	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
 		chasecam = true; // force chasecam on
@@ -1642,4 +1642,7 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_maxportals);
 
 	CV_RegisterVar(&cv_movebob);
+
+	// Frame interpolation/uncapped
+	CV_RegisterVar(&cv_fpscap);
 }
diff --git a/src/r_main.h b/src/r_main.h
index c0edb31b30175295ecbf9fba247ef49a699953b9..94103ceed9ee5017b7f08831bd17a833c23f9d4b 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -35,6 +35,13 @@ extern fixed_t fovtan;
 
 extern size_t validcount, linecount, loopcount, framecount;
 
+// The fraction of a tic being drawn (for interpolation between two tics)
+extern fixed_t rendertimefrac;
+// Evaluated delta tics for this frame (how many tics since the last frame)
+extern fixed_t renderdeltatics;
+// The current render is a new logical tic
+extern boolean renderisnewtic;
+
 //
 // Lighting LUT.
 // Used for z-depth cuing per column/row,
diff --git a/src/r_things.c b/src/r_things.c
index e689e284a96b1e90a9bc68b5d0260f4061990516..82292957475b494e9191b08911f3ed5601b094fb 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -23,6 +23,7 @@
 #include "info.h" // spr2names
 #include "i_video.h" // rendermode
 #include "i_system.h"
+#include "r_fps.h"
 #include "r_things.h"
 #include "r_patch.h"
 #include "r_patchrotation.h"
@@ -1134,13 +1135,33 @@ static void R_SplitSprite(vissprite_t *sprite)
 //
 fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 {
+	fixed_t halfHeight;
 	boolean isflipped = thing->eflags & MFE_VERTICALFLIP;
+	fixed_t floorz;
+	fixed_t ceilingz;
 	fixed_t z, groundz = isflipped ? INT32_MAX : INT32_MIN;
 	pslope_t *slope, *groundslope = NULL;
 	msecnode_t *node;
 	sector_t *sector;
 	ffloor_t *rover;
-#define CHECKZ (isflipped ? z > thing->z+thing->height/2 && z < groundz : z < thing->z+thing->height/2 && z > groundz)
+
+	// for frame interpolation
+	interpmobjstate_t interp = {0};
+
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	halfHeight = interp.z + (thing->height >> 1);
+	floorz = P_GetFloorZ(thing, interp.subsector->sector, interp.x, interp.y, NULL);
+	ceilingz = P_GetCeilingZ(thing, interp.subsector->sector, interp.x, interp.y, NULL);
+
+#define CHECKZ (isflipped ? z > halfHeight && z < groundz : z < halfHeight && z > groundz)
 
 	for (node = thing->touching_sectorlist; node; node = node->m_sectorlist_next)
 	{
@@ -1151,7 +1172,7 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 		if (sector->heightsec != -1)
 			z = isflipped ? sectors[sector->heightsec].ceilingheight : sectors[sector->heightsec].floorheight;
 		else
-			z = isflipped ? P_GetSectorCeilingZAt(sector, thing->x, thing->y) : P_GetSectorFloorZAt(sector, thing->x, thing->y);
+			z = isflipped ? P_GetSectorCeilingZAt(sector, interp.x, interp.y) : P_GetSectorFloorZAt(sector, interp.x, interp.y);
 
 		if CHECKZ
 		{
@@ -1165,7 +1186,7 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 				if (!(rover->fofflags & FOF_EXISTS) || !(rover->fofflags & FOF_RENDERPLANES) || (rover->alpha < 90 && !(rover->fofflags & FOF_SWIMMABLE)))
 					continue;
 
-				z = isflipped ? P_GetFFloorBottomZAt(rover, thing->x, thing->y) : P_GetFFloorTopZAt(rover, thing->x, thing->y);
+				z = isflipped ? P_GetFFloorBottomZAt(rover, interp.x, interp.y) : P_GetFFloorTopZAt(rover, interp.x, interp.y);
 				if CHECKZ
 				{
 					groundz = z;
@@ -1174,71 +1195,38 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 			}
 	}
 
-	if (isflipped ? (thing->ceilingz < groundz - (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2)))
-		: (thing->floorz > groundz + (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2))))
+	// Check polyobjects and see if groundz needs to be altered
 	{
-		groundz = isflipped ? thing->ceilingz : thing->floorz;
-		groundslope = NULL;
-	}
+		// This isn't very precise, but the precise method was far too slow.
+		// (Polies are just naturally pretty flickery anyway :P)
+		polyobj_t *po = interp.subsector->polyList;
 
-#if 0 // Unfortunately, this drops CEZ2 down to sub-17 FPS on my i7.
-	// NOTE: this section was not updated to reflect reverse gravity support
-	// Check polyobjects and see if groundz needs to be altered, for rings only because they don't update floorz
-	if (thing->type == MT_RING)
-	{
-		INT32 xl, xh, yl, yh, bx, by;
-
-		xl = (unsigned)(thing->x - thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
-		xh = (unsigned)(thing->x + thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
-		yl = (unsigned)(thing->y - thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
-		yh = (unsigned)(thing->y + thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
-
-		BMBOUNDFIX(xl, xh, yl, yh);
-
-		validcount++;
-
-		for (by = yl; by <= yh; by++)
-			for (bx = xl; bx <= xh; bx++)
+		while (po)
+		{
+			if (!(po->flags & POF_RENDERPLANES) || !P_MobjInsidePolyobj(po, thing))
 			{
-				INT32 offset;
-				polymaplink_t *plink; // haleyjd 02/22/06
-
-				if (bx < 0 || by < 0 || bx >= bmapwidth || by >= bmapheight)
-					continue;
-
-				offset = by*bmapwidth + bx;
-
-				// haleyjd 02/22/06: consider polyobject lines
-				plink = polyblocklinks[offset];
-
-				while (plink)
-				{
-					polyobj_t *po = plink->po;
-
-					if (po->validcount != validcount) // if polyobj hasn't been checked
-					{
-						po->validcount = validcount;
+				po = (polyobj_t *)(po->link.next);
+				continue;
+			}
 
-						if (!P_MobjInsidePolyobj(po, thing) || !(po->flags & POF_RENDERPLANES))
-						{
-							plink = (polymaplink_t *)(plink->link.next);
-							continue;
-						}
+			// We're inside it! Yess...
+			z = isflipped ? po->lines[0]->backsector->floorheight : po->lines[0]->backsector->ceilingheight;
+			if CHECKZ
+			{
+				groundz = z;
+				groundslope = NULL;
+			}
 
-						// We're inside it! Yess...
-						z = po->lines[0]->backsector->ceilingheight;
+			po = (polyobj_t *)(po->link.next);
+		}
+	}
 
-						if (z < thing->z+thing->height/2 && z > groundz)
-						{
-							groundz = z;
-							groundslope = NULL;
-						}
-					}
-					plink = (polymaplink_t *)(plink->link.next);
-				}
-			}
+	if (isflipped ? (ceilingz < groundz - (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2)))
+		: (floorz > groundz + (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2))))
+	{
+		groundz = isflipped ? ceilingz : floorz;
+		groundslope = NULL;
 	}
-#endif
 
 	if (shadowslope != NULL)
 		*shadowslope = groundslope;
@@ -1252,9 +1240,24 @@ static void R_SkewShadowSprite(
 			fixed_t groundz, INT32 spriteheight, fixed_t scalemul,
 			fixed_t *shadowyscale, fixed_t *shadowskew)
 {
+
 	// haha let's try some dumb stuff
 	fixed_t xslope, zslope;
-	angle_t sloperelang = (R_PointToAngle(thing->x, thing->y) - groundslope->xydirection) >> ANGLETOFINESHIFT;
+	angle_t sloperelang;
+
+	// for frame interpolation
+	interpmobjstate_t interp = {0};
+
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	sloperelang = (R_PointToAngle(interp.x, interp.y) - groundslope->xydirection) >> ANGLETOFINESHIFT;
 
 	xslope = FixedMul(FINESINE(sloperelang), groundslope->zdelta);
 	zslope = FixedMul(FINECOSINE(sloperelang), groundslope->zdelta);
@@ -1282,11 +1285,21 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	fixed_t groundz;
 	pslope_t *groundslope;
 	boolean isflipped = thing->eflags & MFE_VERTICALFLIP;
+	interpmobjstate_t interp = {0};
 
 	groundz = R_GetShadowZ(thing, &groundslope);
 
 	if (abs(groundz-viewz)/tz > 4) return; // Prevent stretchy shadows and possible crashes
 
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
 	heightsec = thing->subsector->sector->heightsec;
 	if (viewplayer->mo && viewplayer->mo->subsector)
 		phs = viewplayer->mo->subsector->sector->heightsec;
@@ -1305,7 +1318,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 			return;
 	}
 
-	floordiff = abs((isflipped ? thing->height : 0) + thing->z - groundz);
+	floordiff = abs((isflipped ? thing->height : 0) + interp.z - groundz);
 
 	trans = floordiff / (100*FRACUNIT) + 3;
 	if (trans >= 9) return;
@@ -1345,11 +1358,11 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	shadow->mobjflags = 0;
 	shadow->sortscale = vis->sortscale;
 	shadow->dispoffset = vis->dispoffset - 5;
-	shadow->gx = thing->x;
-	shadow->gy = thing->y;
+	shadow->gx = interp.x;
+	shadow->gy = interp.y;
 	shadow->gzt = (isflipped ? shadow->pzt : shadow->pz) + patch->height * shadowyscale / 2;
 	shadow->gz = shadow->gzt - patch->height * shadowyscale;
-	shadow->texturemid = FixedMul(thing->scale, FixedDiv(shadow->gzt - viewz, shadowyscale));
+	shadow->texturemid = FixedMul(interp.scale, FixedDiv(shadow->gzt - viewz, shadowyscale));
 	if (thing->skin && ((skin_t *)thing->skin)->flags & SF_HIRES)
 		shadow->texturemid = FixedMul(shadow->texturemid, ((skin_t *)thing->skin)->highresscale);
 	shadow->scalestep = 0;
@@ -1363,7 +1376,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 
 	shadow->xscale = FixedMul(xscale, shadowxscale); //SoM: 4/17/2000
 	shadow->scale = FixedMul(yscale, shadowyscale);
-	shadow->thingscale = thing->scale;
+	shadow->thingscale = interp.scale;
 	shadow->sector = vis->sector;
 	shadow->szt = (INT16)((centeryfrac - FixedMul(shadow->gzt - viewz, yscale))>>FRACBITS);
 	shadow->sz = (INT16)((centeryfrac - FixedMul(shadow->gz - viewz, yscale))>>FRACBITS);
@@ -1391,7 +1404,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 
 			// R_GetPlaneLight won't work on sloped lights!
 			for (lightnum = 1; lightnum < thing->subsector->sector->numlights; lightnum++) {
-				fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], thing->x, thing->y);
+				fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], interp.x, interp.y);
 				if (h <= shadow->gzt) {
 					light = lightnum - 1;
 					break;
@@ -1469,7 +1482,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	fixed_t gz = 0, gzt = 0;
 	INT32 heightsec, phs;
 	INT32 light = 0;
-	fixed_t this_scale = thing->scale;
+	fixed_t this_scale;
 	fixed_t spritexscale, spriteyscale;
 
 	// rotsprite
@@ -1481,9 +1494,24 @@ static void R_ProjectSprite(mobj_t *thing)
 	INT32 rollangle = 0;
 #endif
 
+	// uncapped/interpolation
+	interpmobjstate_t interp = {0};
+
+	// do interpolation
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolateMobjState(oldthing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(oldthing, FRACUNIT, &interp);
+	}
+
+	this_scale = interp.scale;
+
 	// transform the origin point
-	tr_x = thing->x - viewx;
-	tr_y = thing->y - viewy;
+	tr_x = interp.x - viewx;
+	tr_y = interp.y - viewy;
 
 	basetz = tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
 
@@ -1560,7 +1588,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	if (sprframe->rotate != SRF_SINGLE || papersprite)
 	{
-		ang = R_PointToAngle (thing->x, thing->y) - (thing->player ? thing->player->drawangle : thing->angle);
+		ang = R_PointToAngle (interp.x, interp.y) - interp.angle;
 		if (mirrored)
 			ang = InvAngle(ang);
 	}
@@ -1575,7 +1603,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	else
 	{
 		// choose a different rotation based on player view
-		//ang = R_PointToAngle (thing->x, thing->y) - thing->angle;
+		//ang = R_PointToAngle (interp.x, interp.y) - interpangle;
 
 		if ((sprframe->rotate & SRF_RIGHT) && (ang < ANGLE_180)) // See from right
 			rot = 6; // F7 slot
@@ -1644,15 +1672,15 @@ static void R_ProjectSprite(mobj_t *thing)
 	flip = !flip != !hflip;
 
 	// calculate edges of the shape
-	spritexscale = thing->spritexscale;
-	spriteyscale = thing->spriteyscale;
+	spritexscale = interp.spritexscale;
+	spriteyscale = interp.spriteyscale;
 	if (spritexscale < 1 || spriteyscale < 1)
 		return;
 
 	if (thing->renderflags & RF_ABSOLUTEOFFSETS)
 	{
-		spr_offset = thing->spritexoffset;
-		spr_topoffset = thing->spriteyoffset;
+		spr_offset = interp.spritexoffset;
+		spr_topoffset = interp.spriteyoffset;
 	}
 	else
 	{
@@ -1661,8 +1689,8 @@ static void R_ProjectSprite(mobj_t *thing)
 		if ((thing->renderflags & RF_FLIPOFFSETS) && flip)
 			flipoffset = -1;
 
-		spr_offset += thing->spritexoffset * flipoffset;
-		spr_topoffset += thing->spriteyoffset * flipoffset;
+		spr_offset += interp.spritexoffset * flipoffset;
+		spr_topoffset += interp.spriteyoffset * flipoffset;
 	}
 
 	if (flip)
@@ -1684,8 +1712,8 @@ static void R_ProjectSprite(mobj_t *thing)
 			offset2 *= -1;
 		}
 
-		cosmul = FINECOSINE(thing->angle>>ANGLETOFINESHIFT);
-		sinmul = FINESINE(thing->angle>>ANGLETOFINESHIFT);
+		cosmul = FINECOSINE(interp.angle >> ANGLETOFINESHIFT);
+		sinmul = FINESINE(interp.angle >> ANGLETOFINESHIFT);
 
 		tr_x += FixedMul(offset, cosmul);
 		tr_y += FixedMul(offset, sinmul);
@@ -1701,7 +1729,7 @@ static void R_ProjectSprite(mobj_t *thing)
 			paperoffset = -paperoffset;
 			paperdistance = -paperdistance;
 		}
-		centerangle = viewangle - thing->angle;
+		centerangle = viewangle - interp.angle;
 
 		tr_x += FixedMul(offset2, cosmul);
 		tr_y += FixedMul(offset2, sinmul);
@@ -1789,6 +1817,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	if ((thing->flags2 & MF2_LINKDRAW) && thing->tracer) // toast 16/09/16 (SYMMETRY)
 	{
+		interpmobjstate_t tracer_interp = {0};
 		fixed_t linkscale;
 
 		thing = thing->tracer;
@@ -1796,8 +1825,17 @@ static void R_ProjectSprite(mobj_t *thing)
 		if (! R_ThingVisible(thing))
 			return;
 
-		tr_x = (thing->x + sort_x) - viewx;
-		tr_y = (thing->y + sort_y) - viewy;
+		if (R_UsingFrameInterpolation() && !paused)
+		{
+			R_InterpolateMobjState(thing, rendertimefrac, &tracer_interp);
+		}
+		else
+		{
+			R_InterpolateMobjState(thing, FRACUNIT, &tracer_interp);
+		}
+
+		tr_x = (tracer_interp.x + sort_x) - viewx;
+		tr_y = (tracer_interp.y + sort_y) - viewy;
 		tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin);
 		linkscale = FixedDiv(projectiony, tz);
 
@@ -1812,8 +1850,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	}
 	else if (splat)
 	{
-		tr_x = (thing->x + sort_x) - viewx;
-		tr_y = (thing->y + sort_y) - viewy;
+		tr_x = (interp.x + sort_x) - viewx;
+		tr_y = (interp.y + sort_y) - viewy;
 		sort_z = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin);
 		sortscale = FixedDiv(projectiony, sort_z);
 	}
@@ -1821,8 +1859,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	// Calculate the splat's sortscale
 	if (splat)
 	{
-		tr_x = (thing->x - sort_x) - viewx;
-		tr_y = (thing->y - sort_y) - viewy;
+		tr_x = (interp.x - sort_x) - viewx;
+		tr_y = (interp.y - sort_y) - viewy;
 		sort_z = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin);
 		sortsplat = FixedDiv(projectiony, sort_z);
 	}
@@ -1833,7 +1871,7 @@ static void R_ProjectSprite(mobj_t *thing)
 		if (x2 < portalclipstart || x1 >= portalclipend)
 			return;
 
-		if (P_PointOnLineSide(thing->x, thing->y, portalclipline) != 0)
+		if (P_PointOnLineSide(interp.x, interp.y, portalclipline) != 0)
 			return;
 	}
 
@@ -1868,6 +1906,16 @@ static void R_ProjectSprite(mobj_t *thing)
 		if (shadoweffects)
 		{
 			mobj_t *caster = thing->target;
+			interpmobjstate_t casterinterp = {};
+
+			if (R_UsingFrameInterpolation() && !paused)
+			{
+				R_InterpolateMobjState(caster, rendertimefrac, &casterinterp);
+			}
+			else
+			{
+				R_InterpolateMobjState(caster, FRACUNIT, &casterinterp);
+			}
 
 			if (caster && !P_MobjWasRemoved(caster))
 			{
@@ -1876,9 +1924,9 @@ static void R_ProjectSprite(mobj_t *thing)
 				if (abs(groundz-viewz)/tz > 4)
 					return; // Prevent stretchy shadows and possible crashes
 
-				floordiff = abs((isflipped ? caster->height : 0) + caster->z - groundz);
+				floordiff = abs((isflipped ? caster->height : 0) + casterinterp.z - groundz);
 				trans += ((floordiff / (100*FRACUNIT)) + 3);
-				shadowscale = FixedMul(FRACUNIT - floordiff/640, caster->scale);
+				shadowscale = FixedMul(FRACUNIT - floordiff/640, casterinterp.scale);
 			}
 			else
 				trans += 3;
@@ -1907,7 +1955,7 @@ static void R_ProjectSprite(mobj_t *thing)
 		{
 			R_SkewShadowSprite(thing, thing->standingslope, groundz, patch->height, shadowscale, &spriteyscale, &sheartan);
 
-			gzt = (isflipped ? (thing->z + thing->height) : thing->z) + patch->height * spriteyscale / 2;
+			gzt = (isflipped ? (interp.z + thing->height) : interp.z) + patch->height * spriteyscale / 2;
 			gz = gzt - patch->height * spriteyscale;
 
 			cut |= SC_SHEAR;
@@ -1922,12 +1970,12 @@ static void R_ProjectSprite(mobj_t *thing)
 			// When vertical flipped, draw sprites from the top down, at least as far as offsets are concerned.
 			// sprite height - sprite topoffset is the proper inverse of the vertical offset, of course.
 			// remember gz and gzt should be seperated by sprite height, not thing height - thing height can be shorter than the sprite itself sometimes!
-			gz = oldthing->z + oldthing->height - FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
+			gz = interp.z + oldthing->height - FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
 			gzt = gz + FixedMul(spr_height, FixedMul(spriteyscale, this_scale));
 		}
 		else
 		{
-			gzt = oldthing->z + FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
+			gzt = interp.z + FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
 			gz = gzt - FixedMul(spr_height, FixedMul(spriteyscale, this_scale));
 		}
 	}
@@ -1946,7 +1994,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 		// R_GetPlaneLight won't work on sloped lights!
 		for (lightnum = 1; lightnum < thing->subsector->sector->numlights; lightnum++) {
-			fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], thing->x, thing->y);
+			fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], interp.x, interp.y);
 			if (h <= top) {
 				light = lightnum - 1;
 				break;
@@ -1972,7 +2020,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	if (heightsec != -1 && phs != -1) // only clip things which are in special sectors
 	{
 		fixed_t top = gzt;
-		fixed_t bottom = thing->z;
+		fixed_t bottom = interp.z;
 
 		if (splat)
 			top = bottom;
@@ -1996,12 +2044,12 @@ static void R_ProjectSprite(mobj_t *thing)
 	vis->sortscale = sortscale;
 	vis->sortsplat = sortsplat;
 	vis->dispoffset = dispoffset; // Monster Iestyn: 23/11/15
-	vis->gx = thing->x;
-	vis->gy = thing->y;
+	vis->gx = interp.x;
+	vis->gy = interp.y;
 	vis->gz = gz;
 	vis->gzt = gzt;
 	vis->thingheight = thing->height;
-	vis->pz = thing->z;
+	vis->pz = interp.z;
 	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = FixedDiv(gzt - viewz, spriteyscale);
 	vis->scalestep = scalestep;
@@ -2036,7 +2084,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	vis->xscale = FixedMul(spritexscale, xscale); //SoM: 4/17/2000
 	vis->scale = FixedMul(spriteyscale, yscale); //<<detailshift;
-	vis->thingscale = oldthing->scale;
+	vis->thingscale = interp.scale;
 
 	vis->spritexscale = spritexscale;
 	vis->spriteyscale = spriteyscale;
@@ -2144,9 +2192,22 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	//SoM: 3/17/2000
 	fixed_t gz, gzt;
 
+	// uncapped/interpolation
+	interpmobjstate_t interp = {0};
+
+	// do interpolation
+	if (R_UsingFrameInterpolation() && !paused)
+	{
+		R_InterpolatePrecipMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolatePrecipMobjState(thing, FRACUNIT, &interp);
+	}
+
 	// transform the origin point
-	tr_x = thing->x - viewx;
-	tr_y = thing->y - viewy;
+	tr_x = interp.x - viewx;
+	tr_y = interp.y - viewy;
 
 	tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
 
@@ -2210,13 +2271,13 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 		if (x2 < portalclipstart || x1 >= portalclipend)
 			return;
 
-		if (P_PointOnLineSide(thing->x, thing->y, portalclipline) != 0)
+		if (P_PointOnLineSide(interp.x, interp.y, portalclipline) != 0)
 			return;
 	}
 
 
 	//SoM: 3/17/2000: Disregard sprites that are out of view..
-	gzt = thing->z + spritecachedinfo[lump].topoffset;
+	gzt = interp.z + spritecachedinfo[lump].topoffset;
 	gz = gzt - spritecachedinfo[lump].height;
 
 	if (thing->subsector->sector->cullheight)
@@ -2229,12 +2290,12 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis = R_NewVisSprite();
 	vis->scale = vis->sortscale = yscale; //<<detailshift;
 	vis->dispoffset = 0; // Monster Iestyn: 23/11/15
-	vis->gx = thing->x;
-	vis->gy = thing->y;
+	vis->gx = interp.x;
+	vis->gy = interp.y;
 	vis->gz = gz;
 	vis->gzt = gzt;
 	vis->thingheight = 4*FRACUNIT;
-	vis->pz = thing->z;
+	vis->pz = interp.z;
 	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = vis->gzt - viewz;
 	vis->scalestep = 0;
diff --git a/src/screen.c b/src/screen.c
index 73af4313deab86ec5bdf4418f8428d6b035a4e70..ef02a477154ca3cf85969cc0881835438487a4a2 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -15,6 +15,7 @@
 #include "screen.h"
 #include "console.h"
 #include "am_map.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "i_video.h"
 #include "r_local.h"
@@ -33,12 +34,15 @@
 #include "s_sound.h" // ditto
 #include "g_game.h" // ditto
 #include "p_local.h" // P_AutoPause()
+
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
 #include "hardware/hw_light.h"
 #include "hardware/hw_model.h"
 #endif
 
+// SRB2Kart
+#include "r_fps.h" // R_GetFramerateCap
 
 #if defined (USEASM) && !defined (NORUSEASM)//&& (!defined (_MSC_VER) || (_MSC_VER <= 1200))
 #define RUSEASM //MSC.NET can't patch itself
@@ -67,6 +71,7 @@ static CV_PossibleValue_t scr_depth_cons_t[] = {{8, "8 bits"}, {16, "16 bits"},
 consvar_t cv_scr_width = CVAR_INIT ("scr_width", "1280", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_scr_height = CVAR_INIT ("scr_height", "800", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_scr_depth = CVAR_INIT ("scr_depth", "16 bits", CV_SAVE, scr_depth_cons_t, NULL);
+
 consvar_t cv_renderview = CVAR_INIT ("renderview", "On", 0, CV_OnOff, NULL);
 
 CV_PossibleValue_t cv_renderer_t[] = {
@@ -447,46 +452,117 @@ boolean SCR_IsAspectCorrect(INT32 width, INT32 height)
 	 );
 }
 
-// XMOD FPS display
-// moved out of os-specific code for consistency
-static boolean fpsgraph[TICRATE];
-static tic_t lasttic;
+double averageFPS = 0.0f;
+
+#define USE_FPS_SAMPLES
+
+#ifdef USE_FPS_SAMPLES
+#define FPS_SAMPLE_RATE (0.05) // How often to update FPS samples, in seconds
+#define NUM_FPS_SAMPLES (16) // Number of samples to store
+
+static double fps_samples[NUM_FPS_SAMPLES];
+static double updateElapsed = 0.0;
+#endif
+
+static boolean fps_init = false;
+static precise_t fps_enter = 0;
+
+void SCR_CalculateFPS(void)
+{
+	precise_t fps_finish = 0;
+
+	double frameElapsed = 0.0;
+
+	if (fps_init == false)
+	{
+		fps_enter = I_GetPreciseTime();
+		fps_init = true;
+	}
+
+	fps_finish = I_GetPreciseTime();
+	frameElapsed = (double)((INT64)(fps_finish - fps_enter)) / I_GetPrecisePrecision();
+	fps_enter = fps_finish;
+
+#ifdef USE_FPS_SAMPLES
+	updateElapsed += frameElapsed;
+
+	if (updateElapsed >= FPS_SAMPLE_RATE)
+	{
+		static int sampleIndex = 0;
+		int i;
+
+		fps_samples[sampleIndex] = frameElapsed;
+
+		sampleIndex++;
+		if (sampleIndex >= NUM_FPS_SAMPLES)
+			sampleIndex = 0;
+
+		averageFPS = 0.0;
+		for (i = 0; i < NUM_FPS_SAMPLES; i++)
+		{
+			averageFPS += fps_samples[i];
+		}
+
+		if (averageFPS > 0.0)
+		{
+			averageFPS = 1.0 / (averageFPS / NUM_FPS_SAMPLES);
+		}
+	}
+
+	while (updateElapsed >= FPS_SAMPLE_RATE)
+	{
+		updateElapsed -= FPS_SAMPLE_RATE;
+	}
+#else
+	// Direct, unsampled counter.
+	averageFPS = 1.0 / frameElapsed;
+#endif
+}
 
 void SCR_DisplayTicRate(void)
 {
-	tic_t i;
-	tic_t ontic = I_GetTime();
-	tic_t totaltics = 0;
 	INT32 ticcntcolor = 0;
 	const INT32 h = vid.height-(8*vid.dupy);
+	UINT32 cap = R_GetFramerateCap();
+	double fps = round(averageFPS);
 
 	if (gamestate == GS_NULL)
 		return;
 
-	for (i = lasttic + 1; i < TICRATE+lasttic && i < ontic; ++i)
-		fpsgraph[i % TICRATE] = false;
-
-	fpsgraph[ontic % TICRATE] = true;
-
-	for (i = 0;i < TICRATE;++i)
-		if (fpsgraph[i])
-			++totaltics;
-
-	if (totaltics <= TICRATE/2) ticcntcolor = V_REDMAP;
-	else if (totaltics == TICRATE) ticcntcolor = V_GREENMAP;
+	if (cap > 0)
+	{
+		if (fps <= cap / 2.0) ticcntcolor = V_REDMAP;
+		else if (fps <= cap * 0.90) ticcntcolor = V_YELLOWMAP;
+		else ticcntcolor = V_GREENMAP;
+	}
+	else
+	{
+		ticcntcolor = V_GREENMAP;
+	}
 
 	if (cv_ticrate.value == 2) // compact counter
-		V_DrawString(vid.width-(16*vid.dupx), h,
-			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%02d", totaltics));
+	{
+		V_DrawRightAlignedString(vid.width, h,
+			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%04.2f", averageFPS)); // use averageFPS directly
+	}
 	else if (cv_ticrate.value == 1) // full counter
 	{
-		V_DrawString(vid.width-(72*vid.dupx), h,
+		const char *drawnstr;
+		INT32 width;
+
+		// The highest assignable cap is < 1000, so 3 characters is fine.
+		if (cap > 0)
+			drawnstr = va("%3.0f/%3u", fps, cap);
+		else
+			drawnstr = va("%4.2f", averageFPS);
+
+		width = V_StringWidth(drawnstr, V_NOSCALESTART);
+
+		V_DrawString(vid.width - ((7 * 8 * vid.dupx) + V_StringWidth("FPS: ", V_NOSCALESTART)), h,
 			V_YELLOWMAP|V_NOSCALESTART|V_USERHUDTRANS, "FPS:");
-		V_DrawString(vid.width-(40*vid.dupx), h,
-			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%02d/%02u", totaltics, TICRATE));
+		V_DrawString(vid.width - width, h,
+			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, drawnstr);
 	}
-
-	lasttic = ontic;
 }
 
 void SCR_DisplayLocalPing(void)
diff --git a/src/screen.h b/src/screen.h
index 37695316916ad839be0e8211a8531e8addf0180d..da27aa8f083b2abfcb45572278ff4b9bbfadc4de 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -182,6 +182,8 @@ extern viddef_t vid;
 extern INT32 setmodeneeded; // mode number to set if needed, or 0
 extern UINT8 setrenderneeded;
 
+extern double averageFPS;
+
 void SCR_ChangeRenderer(void);
 
 extern CV_PossibleValue_t cv_renderer_t[];
@@ -192,6 +194,7 @@ extern UINT8 *scr_borderpatch; // patch used to fill the view borders
 extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_renderer, cv_fullscreen;
 // wait for page flipping to end or not
 extern consvar_t cv_vidwait;
+extern consvar_t cv_timescale;
 
 // Initialize the screen
 void SCR_Startup(void);
@@ -211,6 +214,8 @@ void SCR_CheckDefaultMode(void);
 // Set the mode number which is saved in the config
 void SCR_SetDefaultMode(void);
 
+void SCR_CalculateFPS(void);
+
 FUNCMATH boolean SCR_IsAspectCorrect(INT32 width, INT32 height);
 
 // move out to main code for consistency
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 3f16e8c6bb1cadb0ba3b5f59739f7d56dd9704b1..818d0f0c4bca17fdd15059d5a92e9db8bd57d0e4 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -181,6 +181,7 @@ static char returnWadPath[256];
 
 #include "../doomdef.h"
 #include "../m_misc.h"
+#include "../i_time.h"
 #include "../i_video.h"
 #include "../i_sound.h"
 #include "../i_system.h"
@@ -196,7 +197,8 @@ static char returnWadPath[256];
 
 #include "../m_argv.h"
 
-#include "../m_menu.h"
+#include "../r_main.h" // Frame interpolation/uncapped
+#include "../r_fps.h"
 
 #ifdef MAC_ALERT
 #include "macosx/mac_alert.h"
@@ -2153,52 +2155,78 @@ ticcmd_t *I_BaseTiccmd2(void)
 
 static Uint64 timer_frequency;
 
-static double tic_frequency;
-static Uint64 tic_epoch;
-
-tic_t I_GetTime(void)
+precise_t I_GetPreciseTime(void)
 {
-	static double elapsed;
+	return SDL_GetPerformanceCounter();
+}
 
-	const Uint64 now = SDL_GetPerformanceCounter();
+UINT64 I_GetPrecisePrecision(void)
+{
+	return SDL_GetPerformanceFrequency();
+}
 
-	elapsed += (now - tic_epoch) / tic_frequency;
-	tic_epoch = now; // moving epoch
+static UINT32 frame_rate;
 
-	return (tic_t)elapsed;
-}
+static double frame_frequency;
+static UINT64 frame_epoch;
+static double elapsed_frames;
 
-precise_t I_GetPreciseTime(void)
+static void I_InitFrameTime(const UINT64 now, const UINT32 cap)
 {
-	return SDL_GetPerformanceCounter();
+	frame_rate = cap;
+	frame_epoch = now;
+
+	//elapsed_frames = 0.0;
+
+	if (frame_rate == 0)
+	{
+		// Shouldn't be used, but just in case...?
+		frame_frequency = 1.0;
+		return;
+	}
+
+	frame_frequency = timer_frequency / (double)frame_rate;
 }
 
-int I_PreciseToMicros(precise_t d)
+double I_GetFrameTime(void)
 {
-	// d is going to be converted into a double. So remove the highest bits
-	// to avoid loss of precision in the lower bits, for the (probably rare) case
-	// that the higher bits are actually used.
-	d &= ((precise_t)1 << 53) - 1; // The mantissa of a double can handle 53 bits at most.
-	// The resulting double from the calculation is converted first to UINT64 to avoid overflow,
-	// which is undefined behaviour when converting floating point values to integers.
-	return (int)(UINT64)(d / (timer_frequency / 1000000.0));
+	const UINT64 now = SDL_GetPerformanceCounter();
+	const UINT32 cap = R_GetFramerateCap();
+
+	if (cap != frame_rate)
+	{
+		// Maybe do this in a OnChange function for cv_fpscap?
+		I_InitFrameTime(now, cap);
+	}
+
+	if (frame_rate == 0)
+	{
+		// Always advance a frame.
+		elapsed_frames += 1.0;
+	}
+	else
+	{
+		elapsed_frames += (now - frame_epoch) / frame_frequency;
+	}
+
+	frame_epoch = now; // moving epoch
+	return elapsed_frames;
 }
 
 //
-//I_StartupTimer
+// I_StartupTimer
 //
 void I_StartupTimer(void)
 {
 	timer_frequency = SDL_GetPerformanceFrequency();
-	tic_epoch       = SDL_GetPerformanceCounter();
 
-	tic_frequency   = timer_frequency / (double)NEWTICRATE;
+	I_InitFrameTime(0, R_GetFramerateCap());
+	elapsed_frames  = 0.0;
 }
 
-void I_Sleep(void)
+void I_Sleep(UINT32 ms)
 {
-	if (cv_sleep.value != -1)
-		SDL_Delay(cv_sleep.value);
+	SDL_Delay(ms);
 }
 
 #ifdef NEWSIGNALHANDLER
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index ab30cf0ca95297a84afe928517cb95bbb4a97410..9cec2a552aecfcc80fc9173b5d37c13d8178c5af 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1178,12 +1178,15 @@ void I_UpdateNoBlit(void)
 // from PrBoom's src/SDL/i_video.c
 static inline boolean I_SkipFrame(void)
 {
-#if 0
+#if 1
+	// While I fixed the FPS counter bugging out with this,
+	// I actually really like being able to pause and
+	// use perfstats to measure rendering performance
+	// without game logic changes.
+	return false;
+#else
 	static boolean skip = false;
 
-	if (rendermode != render_soft)
-		return false;
-
 	skip = !skip;
 
 	switch (gamestate)
@@ -1199,17 +1202,20 @@ static inline boolean I_SkipFrame(void)
 			return false;
 	}
 #endif
-	return false;
 }
 
 //
 // I_FinishUpdate
 //
+static SDL_Rect src_rect = { 0, 0, 0, 0 };
+
 void I_FinishUpdate(void)
 {
 	if (rendermode == render_none)
 		return; //Alam: No software or OpenGl surface
 
+	SCR_CalculateFPS();
+
 	if (I_SkipFrame())
 		return;
 
@@ -1228,27 +1234,22 @@ void I_FinishUpdate(void)
 
 	if (rendermode == render_soft && screens[0])
 	{
-		SDL_Rect rect;
-
-		rect.x = 0;
-		rect.y = 0;
-		rect.w = vid.width;
-		rect.h = vid.height;
-
 		if (!bufSurface) //Double-Check
 		{
 			Impl_VideoSetupSDLBuffer();
 		}
+
 		if (bufSurface)
 		{
-			SDL_BlitSurface(bufSurface, NULL, vidSurface, &rect);
+			SDL_BlitSurface(bufSurface, &src_rect, vidSurface, &src_rect);
 			// Fury -- there's no way around UpdateTexture, the GL backend uses it anyway
 			SDL_LockSurface(vidSurface);
-			SDL_UpdateTexture(texture, &rect, vidSurface->pixels, vidSurface->pitch);
+			SDL_UpdateTexture(texture, &src_rect, vidSurface->pixels, vidSurface->pitch);
 			SDL_UnlockSurface(vidSurface);
 		}
+
 		SDL_RenderClear(renderer);
-		SDL_RenderCopy(renderer, texture, NULL, NULL);
+		SDL_RenderCopy(renderer, texture, &src_rect, NULL);
 		SDL_RenderPresent(renderer);
 	}
 #ifdef HWRENDER
@@ -1257,6 +1258,7 @@ void I_FinishUpdate(void)
 		OglSdlFinishUpdate(cv_vidwait.value);
 	}
 #endif
+
 	exposevideo = SDL_FALSE;
 }
 
@@ -1473,8 +1475,15 @@ static SDL_bool Impl_CreateContext(void)
 		int flags = 0; // Use this to set SDL_RENDERER_* flags now
 		if (usesdl2soft)
 			flags |= SDL_RENDERER_SOFTWARE;
+#if 0
+		// This shit is BROKEN.
+		// - The version of SDL we're using cannot toggle VSync at runtime. We'll need a new SDL version implemented to have this work properly.
+		// - cv_vidwait is initialized before config is loaded, so it's forced to default value at runtime, and forced off when switching. The config loading code would need restructured.
+		// - With both this & frame interpolation on, I_FinishUpdate takes x10 longer. At this point, it is simpler to use a standard FPS cap.
+		// So you can probably guess why I'm kinda over this, I'm just disabling it.
 		else if (cv_vidwait.value)
 			flags |= SDL_RENDERER_PRESENTVSYNC;
+#endif
 
 		if (!renderer)
 			renderer = SDL_CreateRenderer(window, -1, flags);
@@ -1593,6 +1602,27 @@ boolean VID_CheckRenderer(void)
 	return rendererchanged;
 }
 
+static UINT32 refresh_rate;
+static UINT32 VID_GetRefreshRate(void)
+{
+	int index = SDL_GetWindowDisplayIndex(window);
+	SDL_DisplayMode m;
+
+	if (SDL_WasInit(SDL_INIT_VIDEO) == 0)
+	{
+		// Video not init yet.
+		return 0;
+	}
+
+	if (SDL_GetCurrentDisplayMode(index, &m) != 0)
+	{
+		// Error has occurred.
+		return 0;
+	}
+
+	return m.refresh_rate;
+}
+
 INT32 VID_SetMode(INT32 modeNum)
 {
 	SDLdoUngrabMouse();
@@ -1610,6 +1640,11 @@ INT32 VID_SetMode(INT32 modeNum)
 	vid.modenum = modeNum;
 
 	//Impl_SetWindowName("SRB2 "VERSIONSTRING);
+	src_rect.w = vid.width;
+	src_rect.h = vid.height;
+
+	refresh_rate = VID_GetRefreshRate();
+
 	VID_CheckRenderer();
 	return SDL_TRUE;
 }
@@ -1961,3 +1996,13 @@ void I_GetCursorPosition(INT32 *x, INT32 *y)
 {
 	SDL_GetMouseState(x, y);
 }
+
+UINT32 I_GetRefreshRate(void)
+{
+	// Moved to VID_GetRefreshRate.
+	// Precalculating it like that won't work as
+	// well for windowed mode since you can drag
+	// the window around, but very slow PCs might have
+	// trouble querying mode over and over again.
+	return refresh_rate;
+}
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 6c9a0eeca1461dfdfa7630b0d33ab379673edfdd..ed130c912c5c97bc9ef8f38aa1eba5872f67fe3c 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -43,8 +43,11 @@
 #endif
 
 #include "lua_hud.h"
+#include "lua_hudlib_drawlist.h"
 #include "lua_hook.h"
 
+#include "r_fps.h"
+
 UINT16 objectsdrawn = 0;
 
 //
@@ -161,6 +164,9 @@ hudinfo_t hudinfo[NUMHUDITEMS] =
 	{ 288, 176, V_SNAPTORIGHT|V_SNAPTOBOTTOM}, // HUD_POWERUPS
 };
 
+static huddrawlist_h luahuddrawlist_game;
+static huddrawlist_h luahuddrawlist_titlecard;
+
 //
 // STATUS BAR CODE
 //
@@ -420,6 +426,9 @@ void ST_Init(void)
 		return;
 
 	ST_LoadGraphics();
+
+	luahuddrawlist_game = LUA_HUD_CreateDrawList();
+	luahuddrawlist_titlecard = LUA_HUD_CreateDrawList();
 }
 
 // change the status bar too, when pressing F12 while viewing a demo.
@@ -1402,7 +1411,12 @@ void ST_drawTitleCard(void)
 	lt_lasttic = lt_ticker;
 
 luahook:
-	LUA_HUDHOOK(titlecard);
+	if (renderisnewtic)
+	{
+		LUA_HUD_ClearDrawList(luahuddrawlist_titlecard);
+		LUA_HUDHOOK(titlecard, luahuddrawlist_titlecard);
+	}
+	LUA_HUD_DrawList(luahuddrawlist_titlecard);
 }
 
 //
@@ -2526,7 +2540,7 @@ static void ST_doHuntIconsAndSound(void)
 			interval = newinterval;
 	}
 
-	if (!(P_AutoPause() || paused) && interval > 0 && leveltime && leveltime % interval == 0)
+	if (!(P_AutoPause() || paused) && interval > 0 && leveltime && leveltime % interval == 0 && renderisnewtic)
 		S_StartSound(NULL, sfx_emfind);
 }
 
@@ -2588,7 +2602,7 @@ static void ST_doItemFinderIconsAndSound(void)
 
 	}
 
-	if (!(P_AutoPause() || paused) && interval > 0 && leveltime && leveltime % interval == 0)
+	if (!(P_AutoPause() || paused) && interval > 0 && leveltime && leveltime % interval == 0 && renderisnewtic)
 		S_StartSound(NULL, sfx_emfind);
 }
 
@@ -2742,7 +2756,12 @@ static void ST_overlayDrawer(void)
 		ST_drawPowerupHUD(); // same as it ever was...
 
 	if (!(netgame || multiplayer) || !hu_showscores)
-		LUA_HUDHOOK(game);
+	{
+		if (renderisnewtic)
+		{
+			LUA_HUDHOOK(game, luahuddrawlist_game);
+		}
+	}
 
 	// draw level title Tails
 	if (stagetitle && (!WipeInAction) && (!WipeStageTitle))
@@ -2820,6 +2839,10 @@ void ST_Drawer(void)
 
 	if (st_overlay)
 	{
+		if (renderisnewtic)
+		{
+			LUA_HUD_ClearDrawList(luahuddrawlist_game);
+		}
 		// No deadview!
 		stplyr = &players[displayplayer];
 		ST_overlayDrawer();
@@ -2829,5 +2852,7 @@ void ST_Drawer(void)
 			stplyr = &players[secondarydisplayplayer];
 			ST_overlayDrawer();
 		}
+
+		LUA_HUD_DrawList(luahuddrawlist_game);
 	}
 }
diff --git a/src/w_wad.c b/src/w_wad.c
index 7e5f056cfa0ad51e51d282001e4393f69505fa5e..42c6bf83be9e7dd8a89e67aee6aa7373d96bc53e 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -59,6 +59,7 @@
 #include "r_textures.h"
 #include "r_patch.h"
 #include "r_picformats.h"
+#include "i_time.h"
 #include "i_system.h"
 #include "i_video.h" // rendermode
 #include "md5.h"
diff --git a/src/y_inter.c b/src/y_inter.c
index 34e58494f1b9b9cc1c04d068d2db43b1cccf2c7d..4f7b0499df639b87de71ea8b036ef38950f5c559 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -38,6 +38,7 @@
 #include "lua_hook.h" // IntermissionThinker hook
 
 #include "lua_hud.h"
+#include "lua_hudlib_drawlist.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -83,6 +84,10 @@ typedef union
 		INT32 passedx3;
 		INT32 passedx4;
 
+		INT32 emeraldbounces;
+		INT32 emeraldmomy;
+		INT32 emeraldy;
+
 		y_bonus_t bonuses[2];
 		patch_t *bonuspatches[2];
 
@@ -161,6 +166,8 @@ static INT32 endtic = -1;
 intertype_t intertype = int_none;
 intertype_t intermissiontypes[NUMGAMETYPES];
 
+static huddrawlist_h luahuddrawlist_intermission;
+
 static void Y_RescaleScreenBuffer(void);
 static void Y_AwardCoopBonuses(void);
 static void Y_AwardSpecialStageBonus(void);
@@ -429,7 +436,13 @@ void Y_IntermissionDrawer(void)
 	else if (bgtile)
 		V_DrawPatchFill(bgtile);
 
-	LUA_HUDHOOK(intermission);
+	if (renderisnewtic)
+	{
+		LUA_HUD_ClearDrawList(luahuddrawlist_intermission);
+		LUA_HUDHOOK(intermission, luahuddrawlist_intermission);
+	}
+	LUA_HUD_DrawList(luahuddrawlist_intermission);
+
 	if (!LUA_HudEnabled(hud_intermissiontally))
 		goto skiptallydrawer;
 
@@ -646,7 +659,6 @@ void Y_IntermissionDrawer(void)
 		}
 
 		// draw the emeralds
-		//if (intertic & 1)
 		if (LUA_HudEnabled(hud_intermissionemeralds))
 		{
 			boolean drawthistic = !(ALL7EMERALDS(emeralds) && (intertic & 1));
@@ -663,10 +675,6 @@ void Y_IntermissionDrawer(void)
 			}
 			else if (em < 7)
 			{
-				static UINT8 emeraldbounces = 0;
-				static INT32 emeraldmomy = 20;
-				static INT32 emeraldy = -40;
-
 				if (drawthistic)
 					for (i = 0; i < 7; ++i)
 					{
@@ -677,45 +685,15 @@ void Y_IntermissionDrawer(void)
 
 				emeraldx = 152 + (em-3)*28;
 
-				if (intertic <= 1)
-				{
-					emeraldbounces = 0;
-					emeraldmomy = 20;
-					emeraldy = -40;
-				}
-				else
+				if (intertic > 1)
 				{
-					if (!stagefailed)
-					{
-						if (emeraldbounces < 3)
-						{
-							emeraldy += (++emeraldmomy);
-							if (emeraldy > 74)
-							{
-								S_StartSound(NULL, sfx_tink); // tink
-								emeraldbounces++;
-								emeraldmomy = -(emeraldmomy/2);
-								emeraldy = 74;
-							}
-						}
-					}
-					else
+					if (stagefailed && data.spec.emeraldy < (vid.height/vid.dupy)+16)
 					{
-						if (emeraldy < (vid.height/vid.dupy)+16)
-						{
-							emeraldy += (++emeraldmomy);
-							emeraldx += intertic - 6;
-						}
-						if (emeraldbounces < 1 && emeraldy > 74)
-						{
-							S_StartSound(NULL, sfx_shldls); // nope
-							emeraldbounces++;
-							emeraldmomy = -(emeraldmomy/2);
-							emeraldy = 74;
-						}
+						emeraldx += intertic - 6;
 					}
+
 					if (drawthistic)
-						V_DrawScaledPatch(emeraldx, emeraldy, 0, emeraldpics[0][em]);
+						V_DrawScaledPatch(emeraldx, data.spec.emeraldy, 0, emeraldpics[0][em]);
 				}
 			}
 		}
@@ -1153,6 +1131,45 @@ void Y_Ticker(void)
 				S_ChangeMusicInternal("_clear", false); // don't loop it
 			tallydonetic = -1;
 		}
+		
+		// emerald bounce
+		if (intertic <= 1)
+		{
+			data.spec.emeraldbounces = 0;
+			data.spec.emeraldmomy = 20;
+			data.spec.emeraldy = -40;
+		}
+		else
+		{
+			if (!stagefailed)
+			{
+				if (data.spec.emeraldbounces < 3)
+				{
+					data.spec.emeraldy += (++data.spec.emeraldmomy);
+					if (data.spec.emeraldy > 74)
+					{
+						S_StartSound(NULL, sfx_tink); // tink
+						data.spec.emeraldbounces++;
+						data.spec.emeraldmomy = -(data.spec.emeraldmomy/2);
+						data.spec.emeraldy = 74;
+					}
+				}
+			}
+			else
+			{
+				if (data.spec.emeraldy < (vid.height/vid.dupy)+16)
+				{
+					data.spec.emeraldy += (++data.spec.emeraldmomy);
+				}
+				if (data.spec.emeraldbounces < 1 && data.spec.emeraldy > 74)
+				{
+					S_StartSound(NULL, sfx_shldls); // nope
+					data.spec.emeraldbounces++;
+					data.spec.emeraldmomy = -(data.spec.emeraldmomy/2);
+					data.spec.emeraldy = 74;
+				}
+			}
+		}
 
 		if (intertic < 2*TICRATE) // TWO second pause before tally begins, thank you mazmazz
 			return;
@@ -1583,6 +1600,9 @@ void Y_StartIntermission(void)
 		default:
 			break;
 	}
+
+	LUA_HUD_DestroyDrawList(luahuddrawlist_intermission);
+	luahuddrawlist_intermission = LUA_HUD_CreateDrawList();
 }
 
 //