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..ef9fbdc13c015750ed1d770964faa183b0ef5077 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -55,6 +55,7 @@ tables.c
 r_bsp.c
 r_data.c
 r_draw.c
+r_fps.c
 r_main.c
 r_plane.c
 r_segs.c
diff --git a/src/android/i_system.c b/src/android/i_system.c
index 752e9f6c6bb4138caaf17004cbcc0fcaa6b66cee..7256bac345c11b59e5cb39b25e30f29013f9b241 100644
--- a/src/android/i_system.c
+++ b/src/android/i_system.c
@@ -88,6 +88,12 @@ tic_t I_GetTime(void)
   return (since_start*TICRATE)/1000000;
 }
 
+fixed_t I_GetTimeFrac(void)
+{
+  //stub
+  return 0;
+}
+
 void I_Sleep(void){}
 
 void I_GetEvent(void){}
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index ac8bba608288063b7b34dff533f9d2ef0e9c84e2..70e283bba8428ce42fd0938943a0d6d4fa824443 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -5270,6 +5270,7 @@ void TryRunTics(tic_t realtics)
 				boolean update_stats = !(paused || P_AutoPause());
 
 				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
+				prev_tics = I_GetTime();
 
 				if (update_stats)
 					PS_START_TIMING(ps_tictime);
diff --git a/src/d_main.c b/src/d_main.c
index fa9e21337ced42286e27b75a7bd2987ee62538e5..0281ef86c5110c0c011aaa77241c185116ff5f42 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -64,6 +64,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
@@ -147,6 +148,7 @@ event_t events[MAXEVENTS];
 INT32 eventhead, eventtail;
 
 boolean dedicated = false;
+boolean tic_happened = false; // Frame interpolation/uncapped
 
 //
 // D_PostEvent
@@ -764,7 +766,7 @@ void D_SRB2Loop(void)
 				debugload--;
 #endif
 
-		if (!realtics && !singletics)
+		if (!realtics && !singletics && cv_frameinterpolation.value != 1)
 		{
 			I_Sleep();
 			continue;
@@ -780,15 +782,27 @@ void D_SRB2Loop(void)
 			realtics = 1;
 
 		// process tics (but maybe not if realtic == 0)
+		tic_happened = realtics ? true : false;
 		TryRunTics(realtics);
 
+		if (cv_frameinterpolation.value == 1)
+			rendertimefrac = I_GetTimeFrac();
+		else
+			rendertimefrac = FRACUNIT;
+
+		if (cv_frameinterpolation.value == 1)
+		{
+			D_Display();
+		}
+
 		if (lastdraw || singletics || gametic > rendergametic)
 		{
 			rendergametic = gametic;
 			rendertimeout = entertic+TICRATE/17;
 
 			// Update display, next frame, with current state.
-			D_Display();
+			// (Only display if not already done for frame interp)
+			cv_frameinterpolation.value == 0 ? D_Display() : 0;
 
 			if (moviemode)
 				M_SaveFrame();
@@ -805,7 +819,8 @@ void D_SRB2Loop(void)
 				if (camera.chase)
 					P_MoveChaseCamera(&players[displayplayer], &camera, false);
 			}
-			D_Display();
+			// (Only display if not already done for frame interp)
+			cv_frameinterpolation.value == 0 ? D_Display() : 0;
 
 			if (moviemode)
 				M_SaveFrame();
diff --git a/src/d_main.h b/src/d_main.h
index 8189a9f2b39f277ba6d5c854551a8e9d21bd35c1..a06334b6e7fe48c95af6f256ea89b2d51efbee10 100644
--- a/src/d_main.h
+++ b/src/d_main.h
@@ -26,6 +26,7 @@ extern char srb2home[256]; //Alam: My Home
 extern boolean usehome; //Alam: which path?
 extern const char *pandf; //Alam: how to path?
 extern char srb2path[256]; //Alam: SRB2's Home
+extern boolean tic_happened; // Frame interpolation/uncapped
 
 // the infinite loop of D_SRB2Loop() called from win_main for windows version
 void D_SRB2Loop(void) FUNCNORETURN;
diff --git a/src/dummy/i_system.c b/src/dummy/i_system.c
index 4a657ed19db3cb0166c10c3d9f38bd28a54b139f..f3d0bc5e8bf15ea68695eaddb38af3c10e7063b2 100644
--- a/src/dummy/i_system.c
+++ b/src/dummy/i_system.c
@@ -16,7 +16,7 @@ tic_t I_GetTime(void)
 	return 0;
 }
 
-int I_GetTimeMicros(void)
+fixed_t I_GetTimeFrac(void)
 {
 	return 0;
 }
diff --git a/src/g_game.c b/src/g_game.c
index 39d0030565c468823819c0a35d2653d2a773db01..fb1c0d522b5a3bc02085af81276be61c913935e8 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -46,6 +46,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"
 
@@ -2362,6 +2363,8 @@ void G_Ticker(boolean run)
 			F_TextPromptTicker();
 			AM_Ticker();
 			HU_Ticker();
+			R_UpdateViewInterpolation();
+
 			break;
 
 		case GS_INTERMISSION:
@@ -2414,7 +2417,12 @@ void G_Ticker(boolean run)
 			break;
 
 		case GS_TITLESCREEN:
-			if (titlemapinaction) P_Ticker(run); // then intentionally fall through
+			if (titlemapinaction)
+			{
+				P_Ticker(run);
+				R_UpdateViewInterpolation();
+				// then intentionally fall through
+			}
 			/* FALLTHRU */
 		case GS_WAITINGPLAYERS:
 			F_MenuPresTicker(run);
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index fd8a23013e84ebceaafa6cfd9008e3acf6743b2d..968ebc54e173a3b56fa75151842c0f187aede7e0 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -38,6 +38,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"
 
@@ -5050,6 +5051,12 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	INT32 rollangle = 0;
 #endif
 
+	// uncapped/interpolation
+	fixed_t interpx;
+	fixed_t interpy;
+	fixed_t interpz;
+	angle_t interpangle;
+
 	if (!thing)
 		return;
 
@@ -5071,13 +5078,26 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	dispoffset = thing->info->dispoffset;
 
+	interpx = thing->x;
+	interpy = thing->y;
+	interpz = thing->z;
+	interpangle = mobjangle;
+
+	if (cv_frameinterpolation.value == 1 && !paused)
+	{
+		interpx = thing->old_x + FixedMul(rendertimefrac, thing->x - thing->old_x);
+		interpy = thing->old_y + FixedMul(rendertimefrac, thing->y - thing->old_y);
+		interpz = thing->old_z + FixedMul(rendertimefrac, thing->z - thing->old_z);
+		interpangle = mobjangle;
+	}
+
 	this_scale = FIXED_TO_FLOAT(thing->scale);
 	spritexscale = FIXED_TO_FLOAT(thing->spritexscale);
 	spriteyscale = FIXED_TO_FLOAT(thing->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(interpx) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interpy) - gl_viewy;
 
 	// rotation around vertical axis
 	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
@@ -5100,8 +5120,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(interpx);
+	tr_y = FIXED_TO_FLOAT(interpy);
 
 	// decide which patch to use for sprite relative to player
 #ifdef RANGECHECK
@@ -5149,7 +5169,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 (interpx, interpy) - interpangle;
 	if (mirrored)
 		ang = InvAngle(ang);
 
@@ -5295,12 +5315,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(interpz + 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(interpz) + (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
 		gz = gzt - (FIXED_TO_FLOAT(spr_height) * this_yscale);
 	}
 
@@ -5461,6 +5481,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)
 	{
@@ -5468,9 +5491,22 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 			return;
 	}
 
+	// uncapped/interpolation
+	fixed_t interpx = thing->x;
+	fixed_t interpy = thing->y;
+	fixed_t interpz = thing->z;
+
+	// do interpolation
+	if (cv_frameinterpolation.value == 1 && !paused)
+	{
+		interpx = thing->old_x + FixedMul(rendertimefrac, thing->x - thing->old_x);
+		interpy = thing->old_y + FixedMul(rendertimefrac, thing->y - thing->old_y);
+		interpz = thing->old_z + FixedMul(rendertimefrac, thing->z - thing->old_z);
+	}
+
 	// 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(interpx) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interpy) - gl_viewy;
 
 	// rotation around vertical axis
 	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
@@ -5479,8 +5515,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(interpx);
+	tr_y = FIXED_TO_FLOAT(interpy);
 
 	// decide which patch to use for sprite relative to player
 	if ((unsigned)thing->sprite >= numsprites)
@@ -5542,7 +5578,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(interpz + spritecachedinfo[lumpoff].topoffset);
 	vis->gz = vis->gzt - FIXED_TO_FLOAT(spritecachedinfo[lumpoff].height);
 
 	vis->precip = true;
@@ -6678,6 +6714,8 @@ void HWR_DoPostProcessor(player_t *player)
 		// 10 by 10 grid. 2 coordinates (xy)
 		float v[SCREENVERTS][SCREENVERTS][2];
 		static double disStart = 0;
+		static float last_fractime = 0;
+
 		UINT8 x, y;
 		INT32 WAVELENGTH;
 		INT32 AMPLITUDE;
@@ -6709,6 +6747,16 @@ void HWR_DoPostProcessor(player_t *player)
 		HWD.pfnPostImgRedraw(v);
 		if (!(paused || P_AutoPause()))
 			disStart += 1;
+		fixed_t fractime = I_GetTimeFrac();
+		if (tic_happened)
+		{
+			disStart = disStart - last_fractime + 1 + FIXED_TO_FLOAT(fractime);
+		}
+		else
+		{
+			disStart = disStart - last_fractime + FIXED_TO_FLOAT(fractime);
+		}
+		last_fractime = fractime;
 
 		// Capture the screen again for screen waving on the intermission
 		if(gamestate != GS_INTERMISSION)
diff --git a/src/i_system.h b/src/i_system.h
index 27fcdeb3f21d247e1001d12d7e8b1bee028e62a8..0cdc7db855688c919816a907e718a5bc81e15b2c 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -46,6 +46,10 @@ UINT32 I_GetFreeMem(UINT32 *total);
 */
 tic_t I_GetTime(void);
 
+/** \brief  Get the current time as a fraction of a tic since the last tic.
+*/
+fixed_t I_GetTimeFrac(void);
+
 /**	\brief	Returns precise time value for performance measurement.
   */
 precise_t I_GetPreciseTime(void);
diff --git a/src/p_mobj.c b/src/p_mobj.c
index e88cd4b9f64fce4bef6d51e3d6ffd6e8b1129c8c..b52b9734ffaaeb20975a105e69ecb6216598328b 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -4027,6 +4027,11 @@ void P_NullPrecipThinker(precipmobj_t *mobj)
 
 void P_SnowThinker(precipmobj_t *mobj)
 {
+	// reset old state (for interpolation)
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+
 	P_CycleStateAnimation((mobj_t *)mobj);
 
 	// adjust height
@@ -4036,6 +4041,11 @@ void P_SnowThinker(precipmobj_t *mobj)
 
 void P_RainThinker(precipmobj_t *mobj)
 {
+	// reset old state (for interpolation)
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+
 	P_CycleStateAnimation((mobj_t *)mobj);
 
 	if (mobj->state != &states[S_RAIN1])
@@ -10032,6 +10042,11 @@ void P_MobjThinker(mobj_t *mobj)
 	I_Assert(mobj != NULL);
 	I_Assert(!P_MobjWasRemoved(mobj));
 
+	// Set old position (for interpolation)
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+
 	if (mobj->flags & MF_NOTHINK)
 		return;
 
@@ -10897,6 +10912,11 @@ 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);
 
+	// set old state too (for interpolation)
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+
 	return mobj;
 }
 
@@ -10944,6 +10964,11 @@ static precipmobj_t *P_SpawnPrecipMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype
 	 || mobj->subsector->sector->floorpic == skyflatnum)
 		mobj->precipflags |= PCF_PIT;
 
+	// set initial old positions (for interpolation)
+	mobj->old_x = mobj->x;
+	mobj->old_y = mobj->y;
+	mobj->old_z = mobj->z;
+
 	return mobj;
 }
 
diff --git a/src/p_mobj.h b/src/p_mobj.h
index b1b79fd82e8a14009ece5e5a7b2cb55d261621d8..998baa19e842532dedcddf676a7b341404bdf930 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -281,6 +281,7 @@ typedef struct mobj_s
 
 	// Info for drawing: position.
 	fixed_t x, y, z;
+	fixed_t old_x, old_y, old_z; // position interpolation
 
 	// More list: links in sector (if needed)
 	struct mobj_s *snext;
@@ -408,6 +409,7 @@ typedef struct precipmobj_s
 
 	// Info for drawing: position.
 	fixed_t x, y, z;
+	fixed_t old_x, old_y, old_z; // position interpolation
 
 	// More list: links in sector (if needed)
 	struct precipmobj_s *snext;
diff --git a/src/p_tick.c b/src/p_tick.c
index 28ace92883a6c9d5078e1b1fe30098b07536639a..ca70fb92662bdf2d898c7dce7a46b6acea40abc2 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -23,6 +23,7 @@
 #include "lua_hook.h"
 #include "m_perfstats.h"
 #include "i_system.h" // I_GetPreciseTime
+#include "r_fps.h"
 
 // Object place
 #include "m_cheat.h"
diff --git a/src/r_fps.c b/src/r_fps.c
new file mode 100644
index 0000000000000000000000000000000000000000..519288babb1b5ebafac3a31c87d7b363487a1170
--- /dev/null
+++ b/src/r_fps.c
@@ -0,0 +1,169 @@
+// 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"
+#ifdef HWRENDER
+#include "hardware/hw_main.h" // for cv_glshearing
+#endif
+
+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;
+viewvars_t *newview = &p1view_new;
+
+
+enum viewcontext_e viewcontext = VIEWCONTEXT_PLAYER1;
+
+// 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)
+{
+	boolean skybox = 0;
+	if (FIXED_TO_FLOAT(frac) < 0)
+		frac = 0;
+
+	viewx = oldview->x + R_LerpFixed(oldview->x, newview->x, frac);
+	viewy = oldview->y + R_LerpFixed(oldview->y, newview->y, frac);
+	viewz = oldview->z + R_LerpFixed(oldview->z, newview->z, frac);
+
+	viewangle = oldview->angle + R_LerpAngle(oldview->angle, newview->angle, frac);
+	aimingangle = oldview->aim + R_LerpAngle(oldview->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...
+	if (frac >= FRACUNIT)
+	{
+		viewplayer = newview->player;
+		viewsector = newview->sector;
+	}
+	else
+	{
+		viewplayer = oldview->player;
+		viewsector = oldview->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;
+}
+
+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_LerpFixed(fixed_t from, fixed_t to, fixed_t frac)
+{
+	return FixedMul(frac, to - from);
+}
+
+INT32 R_LerpInt32(INT32 from, INT32 to, fixed_t frac)
+{
+	return FixedInt(FixedMul(frac, (to*FRACUNIT) - (from*FRACUNIT)));
+}
+
+angle_t R_LerpAngle(angle_t from, angle_t to, fixed_t frac)
+{
+	return FixedMul(frac, to - from);
+}
diff --git a/src/r_fps.h b/src/r_fps.h
new file mode 100644
index 0000000000000000000000000000000000000000..f9ee6f0de1dfe6d73ca9437170e569b4639bbc9d
--- /dev/null
+++ b/src/r_fps.h
@@ -0,0 +1,59 @@
+// 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"
+
+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;
+
+// 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);
+// Set the current view context (the viewvars pointed to by newview)
+void R_SetViewContext(enum viewcontext_e _viewcontext);
+
+fixed_t R_LerpFixed(fixed_t from, fixed_t to, fixed_t frac);
+INT32 R_LerpInt32(INT32 from, INT32 to, fixed_t frac);
+UINT32 R_LerpUInt32(UINT32 from, UINT32 to, fixed_t frac);
+angle_t R_LerpAngle(angle_t from, angle_t to, fixed_t frac);
+
+#endif
diff --git a/src/r_main.c b/src/r_main.c
index 13d2413fae4611ab4a1079e08e260466790b1529..fe5daa00b3ba256bbd30f068538c39387bbec5bc 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,8 @@ sector_t *viewsector;
 player_t *viewplayer;
 mobj_t *r_viewmobj;
 
+fixed_t rendertimefrac;
+
 //
 // precalculated math tables
 //
@@ -96,6 +99,9 @@ lighttable_t *scalelight[LIGHTLEVELS][MAXLIGHTSCALE];
 lighttable_t *scalelightfixed[MAXLIGHTSCALE];
 lighttable_t *zlight[LIGHTLEVELS][MAXLIGHTZ];
 
+// Frame interpolation/uncapped
+tic_t prev_tics;
+
 // Hack to support extra boom colormaps.
 extracolormap_t *extra_colormaps = NULL;
 
@@ -164,6 +170,9 @@ consvar_t cv_drawdist_precip = CVAR_INIT ("drawdist_precip", "1024", CV_SAVE, dr
 //consvar_t cv_precipdensity = CVAR_INIT ("precipdensity", "Moderate", CV_SAVE, precipdensity_cons_t, NULL);
 consvar_t cv_fov = CVAR_INIT ("fov", "90", CV_FLOAT|CV_CALL, fov_cons_t, Fov_OnChange);
 
+// Frame interpolation/uncapped
+consvar_t cv_frameinterpolation = {"frameinterpolation", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 // Okay, whoever said homremoval causes a performance hit should be shot.
 consvar_t cv_homremoval = CVAR_INIT ("homremoval", "No", CV_SAVE, homremoval_cons_t, NULL);
 
@@ -1084,41 +1093,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 +1103,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 +1125,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(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
 }
 
 void R_SkyboxFrame(player_t *player)
@@ -1232,11 +1210,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 +1232,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,46 +1306,46 @@ 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(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
 }
 
 boolean R_ViewpointHasChasecam(player_t *player)
@@ -1642,4 +1627,7 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_maxportals);
 
 	CV_RegisterVar(&cv_movebob);
+
+	// Frame interpolation/uncapped
+	CV_RegisterVar(&cv_frameinterpolation);
 }
diff --git a/src/r_main.h b/src/r_main.h
index c0edb31b30175295ecbf9fba247ef49a699953b9..860bfa9208ddbdae361d1c97390950e65805574e 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -35,6 +35,8 @@ extern fixed_t fovtan;
 
 extern size_t validcount, linecount, loopcount, framecount;
 
+extern fixed_t rendertimefrac;
+
 //
 // Lighting LUT.
 // Used for z-depth cuing per column/row,
@@ -114,6 +116,10 @@ extern consvar_t cv_fov;
 extern consvar_t cv_skybox;
 extern consvar_t cv_tailspickup;
 
+// Frame interpolation (uncapped framerate)
+extern tic_t prev_tics;
+extern consvar_t cv_frameinterpolation;
+
 // Called by startup code.
 void R_Init(void);
 
diff --git a/src/r_things.c b/src/r_things.c
index db4263a6aca9819f1de6e4ebbd5bf11308d5240e..55e84d6b79868eb2ec61831877b3bad25ddd8678 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -1480,9 +1480,34 @@ static void R_ProjectSprite(mobj_t *thing)
 	INT32 rollangle = 0;
 #endif
 
+	// uncapped/interpolation
+	fixed_t interpx = thing->x;
+	fixed_t interpy = thing->y;
+	fixed_t interpz = thing->z;
+	angle_t interpangle = thing->angle;
+
+	// use player drawangle if player
+	if (thing->player) interpangle = thing->player->drawangle;
+
+	// do interpolation
+	if (cv_frameinterpolation.value == 1 && !paused)
+	{
+		interpx = thing->old_x + FixedMul(rendertimefrac, thing->x - thing->old_x);
+		interpy = thing->old_y + FixedMul(rendertimefrac, thing->y - thing->old_y);
+		interpz = thing->old_z + FixedMul(rendertimefrac, thing->z - thing->old_z);
+		if (thing->player)
+		{
+			interpangle = thing->player->drawangle;
+		}
+		else
+		{
+			interpangle = thing->angle;
+		}
+	}
+
 	// transform the origin point
-	tr_x = thing->x - viewx;
-	tr_y = thing->y - viewy;
+	tr_x = interpx - viewx;
+	tr_y = interpy - viewy;
 
 	basetz = tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
 
@@ -1559,7 +1584,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 (interpx, interpy) - interpangle;
 		if (mirrored)
 			ang = InvAngle(ang);
 	}
@@ -1574,7 +1599,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 (interpx, interpy) - interpangle;
 
 		if ((sprframe->rotate & SRF_RIGHT) && (ang < ANGLE_180)) // See from right
 			rot = 6; // F7 slot
@@ -1791,12 +1816,17 @@ static void R_ProjectSprite(mobj_t *thing)
 		fixed_t linkscale;
 
 		thing = thing->tracer;
+		if (cv_frameinterpolation.value == 1 && !paused)
+		{
+			interpx = thing->old_x + FixedMul(thing->x - thing->old_x, rendertimefrac);
+			interpy = thing->old_y + FixedMul(thing->y - thing->old_y, rendertimefrac);
+		}
 
 		if (! R_ThingVisible(thing))
 			return;
 
-		tr_x = (thing->x + sort_x) - viewx;
-		tr_y = (thing->y + sort_y) - viewy;
+		tr_x = (interpx + sort_x) - viewx;
+		tr_y = (interpy + sort_y) - viewy;
 		tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin);
 		linkscale = FixedDiv(projectiony, tz);
 
@@ -1832,7 +1862,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(interpx, interpy, portalclipline) != 0)
 			return;
 	}
 
@@ -1921,12 +1951,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 = interpz + 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 = interpz + FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
 			gz = gzt - FixedMul(spr_height, FixedMul(spriteyscale, this_scale));
 		}
 	}
@@ -1945,7 +1975,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], interpx, interpy);
 			if (h <= top) {
 				light = lightnum - 1;
 				break;
@@ -1995,12 +2025,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 = interpx;
+	vis->gy = interpy;
 	vis->gz = gz;
 	vis->gzt = gzt;
 	vis->thingheight = thing->height;
-	vis->pz = thing->z;
+	vis->pz = interpz;
 	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = FixedDiv(gzt - viewz, spriteyscale);
 	vis->scalestep = scalestep;
@@ -2139,9 +2169,22 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	//SoM: 3/17/2000
 	fixed_t gz, gzt;
 
+	// uncapped/interpolation
+	fixed_t interpx = thing->x;
+	fixed_t interpy = thing->y;
+	fixed_t interpz = thing->z;
+
+	// do interpolation
+	if (cv_frameinterpolation.value == 1 && !paused)
+	{
+		interpx = thing->old_x + FixedMul(rendertimefrac, thing->x - thing->old_x);
+		interpy = thing->old_y + FixedMul(rendertimefrac, thing->y - thing->old_y);
+		interpz = thing->old_z + FixedMul(rendertimefrac, thing->z - thing->old_z);
+	}
+
 	// transform the origin point
-	tr_x = thing->x - viewx;
-	tr_y = thing->y - viewy;
+	tr_x = interpx - viewx;
+	tr_y = interpy - viewy;
 
 	tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
 
@@ -2205,13 +2248,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(interpx, interpy, portalclipline) != 0)
 			return;
 	}
 
 
 	//SoM: 3/17/2000: Disregard sprites that are out of view..
-	gzt = thing->z + spritecachedinfo[lump].topoffset;
+	gzt = interpz + spritecachedinfo[lump].topoffset;
 	gz = gzt - spritecachedinfo[lump].height;
 
 	if (thing->subsector->sector->cullheight)
@@ -2224,12 +2267,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 = interpx;
+	vis->gy = interpy;
 	vis->gz = gz;
 	vis->gzt = gzt;
 	vis->thingheight = 4*FRACUNIT;
-	vis->pz = thing->z;
+	vis->pz = interpz;
 	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = vis->gzt - viewz;
 	vis->scalestep = 0;
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 9de632734de97440b62e831a39c12a93020e7429..2d99f5c1fcbecb8dc15225dbbec935fd5a71cd1d 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -185,7 +185,7 @@ static char returnWadPath[256];
 
 #include "../m_argv.h"
 
-#include "../m_menu.h"
+#include "../r_main.h" // Frame interpolation/uncapped
 
 #ifdef MAC_ALERT
 #include "macosx/mac_alert.h"
@@ -2144,17 +2144,27 @@ static Uint64 timer_frequency;
 
 static double tic_frequency;
 static Uint64 tic_epoch;
+static double elapsed_tics;
 
-tic_t I_GetTime(void)
+static void UpdateElapsedTics(void)
 {
-	static double elapsed;
-
 	const Uint64 now = SDL_GetPerformanceCounter();
 
-	elapsed += (now - tic_epoch) / tic_frequency;
+	elapsed_tics += (now - tic_epoch) / tic_frequency;
 	tic_epoch = now; // moving epoch
+}
+
+tic_t I_GetTime(void)
+{
+	UpdateElapsedTics();
+	return (tic_t) floor(elapsed_tics);
+}
 
-	return (tic_t)elapsed;
+fixed_t I_GetTimeFrac(void)
+{
+	UpdateElapsedTics();
+	
+	return FLOAT_TO_FIXED((float) (elapsed_tics - floor(elapsed_tics)));
 }
 
 precise_t I_GetPreciseTime(void)
@@ -2182,6 +2192,7 @@ void I_StartupTimer(void)
 	tic_epoch       = SDL_GetPerformanceCounter();
 
 	tic_frequency   = timer_frequency / (double)NEWTICRATE;
+	elapsed_tics    = 0.0;
 }
 
 void I_Sleep(void)
diff --git a/src/win32/win_sys.c b/src/win32/win_sys.c
index ff443935fa7f5a925e3539508b2fe5ce18244e09..393b913dadf688f3c38372c6a40d8e768615fdbe 100644
--- a/src/win32/win_sys.c
+++ b/src/win32/win_sys.c
@@ -45,6 +45,7 @@
 #include "../d_main.h"
 
 #include "../m_argv.h"
+#include "../m_fixed.h"
 
 #include "../w_wad.h"
 #include "../z_zone.h"
@@ -261,6 +262,11 @@ tic_t I_GetTime(void)
 	return newtics;
 }
 
+fixed_t I_GetTimeFrac(void)
+{
+	return 0;
+}
+
 void I_Sleep(void)
 {
 	if (cv_sleep.value != -1)