diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 2972502a7b7b273003ec3efda9a7ee5bf1296893..74f3dd959951a20c961ca2e3b50dc5fa9eba217b 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -5233,7 +5233,7 @@ boolean 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;
diff --git a/src/d_main.c b/src/d_main.c
index 1559133208c606f1d6664e1a43f345739fd12db9..bb01976cd7939882f7899d78ed4a61d84f0ea1ec 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -477,7 +477,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				R_ApplyLevelInterpolators(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
+				R_ApplyLevelInterpolators(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
@@ -687,6 +687,29 @@ static void D_Display(void)
 	}
 }
 
+static boolean D_CheckFrameCap(void)
+{
+	static boolean init = false;
+	static precise_t startCap = 0;
+	precise_t endCap = 0;
+
+	endCap = I_GetPreciseTime();
+
+	if (init == false)
+	{
+		startCap = endCap;
+		init = true;
+	}
+	else if (I_CheckFrameCap(startCap, endCap))
+	{
+		// Framerate should be capped.
+		return true;
+	}
+
+	startCap = endCap;
+	return false;
+}
+
 // =========================================================================
 // D_SRB2Loop
 // =========================================================================
@@ -698,6 +721,8 @@ void D_SRB2Loop(void)
 	tic_t oldentertics = 0, entertic = 0, realtics = 0, rendertimeout = INFTICS;
 	static lumpnum_t gstartuplumpnum;
 	boolean ticked;
+	boolean interp;
+	boolean doDisplay = false;
 
 	if (dedicated)
 		server = true;
@@ -762,14 +787,25 @@ void D_SRB2Loop(void)
 
 		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)
 			if (debugload)
 				debugload--;
 #endif
 
-		if (!realtics && !singletics && cv_frameinterpolation.value != 1)
+		interp = R_UsingFrameInterpolation();
+		doDisplay = false;
+		ticked = false;
+
+		if (!realtics && !singletics && !interp)
 		{
+			// Non-interp sleep
 			I_Sleep();
 			continue;
 		}
@@ -778,85 +814,115 @@ void D_SRB2Loop(void)
 		HW3S_BeginFrameUpdate();
 #endif
 
-		// don't skip more than 10 frames at a time
-		// (fadein / fadeout cause massive frame skip!)
-		if (realtics > 8)
-			realtics = 1;
+		if (realtics > 0 || singletics)
+		{
+			// 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)
-		ticked = TryRunTics(realtics);
+			// process tics (but maybe not if realtic == 0)
+			ticked = TryRunTics(realtics);
 
-		if (cv_frameinterpolation.value == 1 && !(paused || P_AutoPause()))
+			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();
+				}
+
+				doDisplay = true;
+			}
+		}
+
+		if (interp)
 		{
-			static float tictime;
+			static float tictime = 0.0f;
+			static float prevtime = 0.0f;
 			float entertime = I_GetTimeFrac();
 
-			fixed_t entertimefrac;
+			fixed_t entertimefrac = FRACUNIT;
 
 			if (ticked)
+			{
 				tictime = entertime;
+			}
 
-			entertimefrac = FLOAT_TO_FIXED(entertime - tictime);
+			// Handle interp sleep / framerate cap here.
+			// TryRunTics needs ran if possible to prevent lagged map changes,
+			// (and if that runs, the code above needs to also run)
+			// so this is done here after TryRunTics.
+			if (D_CheckFrameCap())
+			{
+				continue;
+			}
 
-			// renderdeltatics is a bit awkard to evaluate, since the system time interface is whole tic-based
-			renderdeltatics = realtics * FRACUNIT;
-			if (entertimefrac > rendertimefrac)
-				renderdeltatics += entertimefrac - rendertimefrac;
-			else
-				renderdeltatics -= rendertimefrac - entertimefrac;
+			if (!(paused || P_AutoPause()))
+			{
+#if 0
+				CONS_Printf("prevtime = %f\n", prevtime);
+				CONS_Printf("entertime = %f\n", entertime);
+				CONS_Printf("tictime = %f\n", tictime);
+				CONS_Printf("entertime - prevtime = %f\n", entertime - prevtime);
+				CONS_Printf("entertime - tictime = %f\n", entertime - tictime);
+				CONS_Printf("========\n");
+#endif
+
+				if (entertime - prevtime >= 1.0f)
+				{
+					// Lagged for more frames than a gametic...
+					// No need for interpolation.
+					entertimefrac = FRACUNIT;
+				}
+				else
+				{
+					entertimefrac = min(FRACUNIT, FLOAT_TO_FIXED(entertime - tictime));
+				}
+
+				// renderdeltatics is a bit awkard to evaluate, since the system time interface is whole tic-based
+				renderdeltatics = realtics * FRACUNIT;
+				if (entertimefrac > rendertimefrac)
+					renderdeltatics += entertimefrac - rendertimefrac;
+				else
+					renderdeltatics -= rendertimefrac - entertimefrac;
+
+				rendertimefrac = entertimefrac;
+			}
 
-			rendertimefrac = entertimefrac;
+			prevtime = entertime;
 		}
 		else
 		{
-			rendertimefrac = FRACUNIT;
 			renderdeltatics = realtics * FRACUNIT;
+			rendertimefrac = FRACUNIT;
 		}
 
-		if (cv_frameinterpolation.value == 1)
+		if (interp || doDisplay)
 		{
 			D_Display();
 		}
 
-		if (lastdraw || singletics || gametic > rendergametic)
-		{
-			rendergametic = gametic;
-			rendertimeout = entertic+TICRATE/17;
-
-			// Update display, next frame, with current state.
-			// (Only display if not already done for frame interp)
-			cv_frameinterpolation.value == 0 ? D_Display() : 0;
-
-			if (moviemode)
-				M_SaveFrame();
-			if (takescreenshot) // Only take screenshots after drawing.
-				M_DoScreenShot();
-		}
-		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();
-				
-			}
-			// (Only display if not already done for frame interp)
-			cv_frameinterpolation.value == 0 ? D_Display() : 0;
-
-			if (moviemode)
-				M_SaveFrame();
-			if (takescreenshot) // Only take screenshots after drawing.
-				M_DoScreenShot();
-		}
+		if (moviemode)
+			M_SaveFrame();
+		if (takescreenshot) // Only take screenshots after drawing.
+			M_DoScreenShot();
 
 		// consoleplayer -> displayplayer (hear sounds from viewpoint)
 		S_UpdateSounds(); // move positional sounds
@@ -867,6 +933,11 @@ void D_SRB2Loop(void)
 #endif
 
 		LUA_Step();
+
+		// Moved to here from I_FinishUpdate.
+		// It doesn't track fades properly anymore by being here (might be easy fix),
+		// but it's a little more accurate for actual game logic when its here.
+		SCR_CalculateFPS();
 	}
 }
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index d9080d342a432a87e44d7c1b4e54a981111eb4b5..9b14e0913ab605235c634cd80ee405d24149294e 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -191,7 +191,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/hardware/hw_main.c b/src/hardware/hw_main.c
index b47000b1fe7c46cd849d0116ae48246bf24ce9cf..f77925f2c97b321de5e0b65a6f1e0fcc8bb989a3 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -3647,7 +3647,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	// uncapped/interpolation
 	interpmobjstate_t interp = {0};
 
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -5072,7 +5072,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	dispoffset = thing->info->dispoffset;
 
 
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -5479,7 +5479,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	interpmobjstate_t interp = {0};
 
 	// do interpolation
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolatePrecipMobjState(thing, rendertimefrac, &interp);
 	}
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index e7de93a62e4633806b16d365e3ad89285ff096c7..b6bcdf0658276f6306e290de3ab750f1515ab634 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -1344,7 +1344,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		float finalscale;
 		interpmobjstate_t interp;
 
-		if (cv_frameinterpolation.value == 1 && !paused)
+		if (R_UsingFrameInterpolation() && !paused)
 		{
 			R_InterpolateMobjState(spr->mobj, rendertimefrac, &interp);
 		}
diff --git a/src/i_system.h b/src/i_system.h
index 93bb34a2155ab06df407e3d336ec69c99b2971d5..fefe0a7c12fe56ef626d8a0dee8fd33348951dec 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -56,7 +56,7 @@ precise_t I_GetPreciseTime(void);
 
 /**	\brief	Converts a precise_t to microseconds and casts it to a 32 bit integer.
   */
-int I_PreciseToMicros(precise_t);
+int I_PreciseToMicros(precise_t d);
 
 /**	\brief	The I_Sleep function
 
@@ -64,6 +64,8 @@ int I_PreciseToMicros(precise_t);
 */
 void I_Sleep(void);
 
+boolean I_CheckFrameCap(precise_t start, precise_t end);
+
 /**	\brief Get events
 
 	Called by D_SRB2Loop,
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/m_menu.c b/src/m_menu.c
index 9b29d56ae12d3dbaa09411c65bb9c4582baf9361..f170799b07c5961c813eb19080e5b1c93b54d7fb 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"
@@ -1380,16 +1381,14 @@ static menuitem_t OP_VideoOptionsMenu[] =
 
 	{IT_HEADER, NULL, "Diagnostic", NULL, 184},
 	{IT_STRING | IT_CVAR, NULL, "Show FPS",                  &cv_ticrate,         190},
-	{IT_STRING | IT_CVAR, NULL, "Clear Before Redraw",       &cv_homremoval,      195},
-	{IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"",       &cv_showfocuslost,   200},
+	{IT_STRING | IT_CVAR, NULL, "FPS Cap",                   &cv_fpscap,          195},
+	{IT_STRING | IT_CVAR, NULL, "Clear Before Redraw",       &cv_homremoval,      200},
+	{IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"",       &cv_showfocuslost,   205},
 
 #ifdef HWRENDER
-	{IT_HEADER, NULL, "Renderer", NULL, 208},
-	{IT_CALL | IT_STRING, NULL, "OpenGL Options...",         M_OpenGLOptionsMenu, 214},
+	{IT_HEADER, NULL, "Renderer", NULL, 213},
+	{IT_CALL | IT_STRING, NULL, "OpenGL Options...",         M_OpenGLOptionsMenu, 219},
 #endif
-
-	{IT_HEADER, NULL, "Experimental", NULL, 222},
-	{IT_STRING | IT_CVAR, NULL, "Frame Interpolation",       &cv_frameinterpolation, 228},
 };
 
 static menuitem_t OP_VideoModeMenu[] =
diff --git a/src/r_fps.c b/src/r_fps.c
index 708add82097da1a90fcb0bde2211ffbdb17e7126..9c3a9db53d05ca626f24aa187a20c2e9f7a650fc 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -21,10 +21,52 @@
 #include "p_spec.h"
 #include "r_state.h"
 #include "z_zone.h"
+#include "console.h" // con_startup_loadprogress
 #ifdef HWRENDER
 #include "hardware/hw_main.h" // for cv_glshearing
 #endif
 
+static CV_PossibleValue_t fpscap_cons_t[] = {
+	{-1, "Match refresh rate"},
+	{0, "Unlimited"},
+#ifdef DEVELOP
+	// Lower values are actually pretty useful for debugging interp problems!
+	{1, "One Singular Frame"},
+	{10, "10"},
+	{20, "20"},
+	{25, "25"},
+	{30, "30"},
+#endif
+	{35, "35"},
+	{50, "50"},
+	{60, "60"},
+	{70, "70"},
+	{75, "75"},
+	{90, "90"},
+	{100, "100"},
+	{120, "120"},
+	{144, "144"},
+	{200, "200"},
+	{240, "240"},
+	{0, NULL}
+};
+consvar_t cv_fpscap = CVAR_INIT ("fpscap", "Match refresh rate", CV_SAVE, fpscap_cons_t, NULL);
+
+UINT32 R_GetFramerateCap(void)
+{
+	if (cv_fpscap.value < 0)
+	{
+		return I_GetRefreshRate();
+	}
+
+	return cv_fpscap.value;
+}
+
+boolean R_UsingFrameInterpolation(void)
+{
+	return (R_GetFramerateCap() != TICRATE); // maybe use ">" instead?
+}
+
 static viewvars_t p1view_old;
 static viewvars_t p1view_new;
 static viewvars_t p2view_old;
@@ -179,7 +221,7 @@ void R_SetViewContext(enum viewcontext_e _viewcontext)
 
 fixed_t R_InterpolateFixed(fixed_t from, fixed_t to)
 {
-	if (cv_frameinterpolation.value == 0)
+	if (!R_UsingFrameInterpolation())
 	{
 		return to;
 	}
@@ -189,7 +231,7 @@ fixed_t R_InterpolateFixed(fixed_t from, fixed_t to)
 
 angle_t R_InterpolateAngle(angle_t from, angle_t to)
 {
-	if (cv_frameinterpolation.value == 0)
+	if (!R_UsingFrameInterpolation())
 	{
 		return to;
 	}
diff --git a/src/r_fps.h b/src/r_fps.h
index 1eb53b3469d41bcb1691fbcecc6bbbe9d5aa35e1..75d9ead3d74e50e130b7bb95c825ae92e9e0d9b5 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -19,6 +19,11 @@
 #include "p_local.h"
 #include "r_state.h"
 
+extern consvar_t cv_fpscap;
+
+UINT32 R_GetFramerateCap(void);
+boolean R_UsingFrameInterpolation(void);
+
 enum viewcontext_e
 {
 	VIEWCONTEXT_PLAYER1 = 0,
diff --git a/src/r_main.c b/src/r_main.c
index e7f567b583f596245387e5a6d463be51776290db..896b6a29b989ab620cc263f36a871734a0d51aaf 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -168,9 +168,6 @@ 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);
 
@@ -1199,7 +1196,7 @@ void R_SetupFrame(player_t *player)
 	// newview->sin = FINESINE(viewangle>>ANGLETOFINESHIFT);
 	// newview->cos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
-	R_InterpolateView(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
+	R_InterpolateView(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 }
 
 void R_SkyboxFrame(player_t *player)
@@ -1343,7 +1340,7 @@ void R_SkyboxFrame(player_t *player)
 	// newview->sin = FINESINE(viewangle>>ANGLETOFINESHIFT);
 	// newview->cos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
-	R_InterpolateView(cv_frameinterpolation.value == 1 ? rendertimefrac : FRACUNIT);
+	R_InterpolateView(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
 }
 
 boolean R_ViewpointHasChasecam(player_t *player)
@@ -1627,5 +1624,5 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_movebob);
 
 	// Frame interpolation/uncapped
-	CV_RegisterVar(&cv_frameinterpolation);
+	CV_RegisterVar(&cv_fpscap);
 }
diff --git a/src/r_main.h b/src/r_main.h
index 08654a6949624e5e1ff1aca47a6b6fd382a20aa8..ccbc0ad8cf51649e0995963eb265a7dd18b885bd 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -119,9 +119,6 @@ extern consvar_t cv_fov;
 extern consvar_t cv_skybox;
 extern consvar_t cv_tailspickup;
 
-// Frame interpolation (uncapped framerate)
-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 c74718a331b056e7c84a69fe0ef299a946886f20..8c8af9630f5a7b15c6d6f2f9e90e8ddb26fee3f9 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -1148,7 +1148,7 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 	// for frame interpolation
 	interpmobjstate_t interp = {0};
 
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -1281,7 +1281,7 @@ static void R_SkewShadowSprite(
 	// for frame interpolation
 	interpmobjstate_t interp = {0};
 
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -1323,7 +1323,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 
 	if (abs(groundz-viewz)/tz > 4) return; // Prevent stretchy shadows and possible crashes
 
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -1511,7 +1511,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	interpmobjstate_t interp = {0};
 
 	// do interpolation
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolateMobjState(thing, rendertimefrac, &interp);
 	}
@@ -1822,7 +1822,7 @@ static void R_ProjectSprite(mobj_t *thing)
 		fixed_t linkscale;
 
 		thing = thing->tracer;
-		if (cv_frameinterpolation.value == 1 && !paused)
+		if (R_UsingFrameInterpolation() && !paused)
 		{
 			R_InterpolateMobjState(thing, rendertimefrac, &interp);
 		}
@@ -2176,7 +2176,7 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	interpmobjstate_t interp = {0};
 
 	// do interpolation
-	if (cv_frameinterpolation.value == 1 && !paused)
+	if (R_UsingFrameInterpolation() && !paused)
 	{
 		R_InterpolatePrecipMobjState(thing, rendertimefrac, &interp);
 	}
diff --git a/src/screen.c b/src/screen.c
index f81a6563d6a19833f867917ab29142730ad7c714..88898c503ae6b16ba43b47f3cc5ba5454a5c1e1a 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -33,12 +33,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 +70,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,86 +451,82 @@ 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;
 static tic_t totaltics;
+double averageFPS = 0.0f;
 
-static UINT32 fpstime = 0;
-static UINT32 lastupdatetime = 0;
+#define FPS_SAMPLE_RATE (50000) // How often to update FPS samples, in microseconds
+#define NUM_FPS_SAMPLES 16 // Number of samples to store
 
-#define FPSUPDATERATE 1/20 // What fraction of a second to update at. The fraction will not simplify to 0, trust me.
-#define FPSMAXSAMPLES 16
+static double fps_samples[NUM_FPS_SAMPLES];
 
-static UINT32 fpssamples[FPSMAXSAMPLES];
-static UINT32 fpssampleslen = 0;
-static UINT32 fpssum = 0;
-double aproxfps = 0.0f;
-
-void SCR_CalcAproxFps(void)
+void SCR_CalculateFPS(void)
 {
-	tic_t i = 0;
-	tic_t ontic = I_GetTime();
+	static boolean init = false;
 
-	totaltics = 0;
+	static precise_t startTime = 0;
+	precise_t endTime = 0;
 
-	// Update FPS time
-	if (I_PreciseToMicros(fpstime - lastupdatetime) > 1000000 * FPSUPDATERATE)
-	{
-		if (fpssampleslen == FPSMAXSAMPLES)
-		{
-			fpssum -= fpssamples[0];
+	static precise_t updateTime = 0;
+	int updateElapsed = 0;
+	int i;
 
-			for (i = 1; i < fpssampleslen; i++)
-				fpssamples[i-1] = fpssamples[i];
-		}
-		else
-			fpssampleslen++;
+	endTime = I_GetPreciseTime();
 
-		fpssamples[fpssampleslen-1] = I_GetPreciseTime() - fpstime;
-		fpssum += fpssamples[fpssampleslen-1];
+	if (init == false)
+	{
+		startTime = updateTime = endTime;
+		init = true;
+		return;
+	}
 
-		aproxfps = 1000000 / (I_PreciseToMicros(fpssum) / (double)fpssampleslen);
+	updateElapsed = I_PreciseToMicros(endTime - updateTime);
 
-		lastupdatetime = I_GetPreciseTime();
-	}
+	if (updateElapsed >= FPS_SAMPLE_RATE)
+	{
+		static int sampleIndex = 0;
+		int frameElapsed = I_PreciseToMicros(endTime - startTime);
 
-	fpstime = I_GetPreciseTime();
+		fps_samples[sampleIndex] = frameElapsed / 1000.0f;
 
-	// Update ticrate time
-	for (i = lasttic + 1; i < TICRATE+lasttic && i < ontic; ++i)
-		fpsgraph[i % TICRATE] = false;
+		sampleIndex++;
+		if (sampleIndex >= NUM_FPS_SAMPLES)
+			sampleIndex = 0;
 
-	fpsgraph[ontic % TICRATE] = true;
+		averageFPS = 0.0f;
+		for (i = 0; i < NUM_FPS_SAMPLES; i++)
+		{
+			averageFPS += fps_samples[i];
+		}
+		averageFPS = 1000.0f / (averageFPS / NUM_FPS_SAMPLES);
 
-	for (i = 0;i < TICRATE;++i)
-		if (fpsgraph[i])
-			++totaltics;
+		updateTime = endTime;
+	}
 
-	lasttic = ontic;
+	startTime = endTime;
 }
 
 void SCR_DisplayTicRate(void)
 {
 	INT32 ticcntcolor = 0;
 	const INT32 h = vid.height-(8*vid.dupy);
+	UINT32 cap = R_GetFramerateCap();
+	double fps = ceil(averageFPS);
 
 	if (gamestate == GS_NULL)
 		return;
 
-	if (totaltics <= TICRATE/2) ticcntcolor = V_REDMAP;
-	else if (totaltics == TICRATE) ticcntcolor = V_GREENMAP;
+	if (totaltics <= cap/2) ticcntcolor = V_REDMAP;
+	else if (totaltics >= cap) ticcntcolor = V_GREENMAP;
 
 	if (cv_ticrate.value == 2) // compact counter
-		V_DrawString(vid.width-(24*vid.dupx), h,
-			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%03.0f", aproxfps));
+		V_DrawString(vid.width-(32*vid.dupx), h,
+			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%04.0f", fps));
 	else if (cv_ticrate.value == 1) // full counter
 	{
-		V_DrawString(vid.width-(88*vid.dupx), h,
+		V_DrawString(vid.width-(104*vid.dupx), h,
 			V_YELLOWMAP|V_NOSCALESTART|V_USERHUDTRANS, "FPS:");
-		V_DrawString(vid.width-(56*vid.dupx), h,
-			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%03.0f/%03u", aproxfps, TICRATE));
+		V_DrawString(vid.width-(72*vid.dupx), h,
+			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, va("%4.0f/%4u", fps, cap));
 	}
 }
 
diff --git a/src/screen.h b/src/screen.h
index 425d10954a525fa39cdfcf687708d881b4900e60..bfb9e7fbf365580239341487fc33ce3aae0f15cd 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -181,7 +181,8 @@ extern boolean R_SSE2;
 extern viddef_t vid;
 extern INT32 setmodeneeded; // mode number to set if needed, or 0
 extern UINT8 setrenderneeded;
-extern double aproxfps;
+
+extern double averageFPS;
 
 void SCR_ChangeRenderer(void);
 
@@ -212,7 +213,7 @@ void SCR_CheckDefaultMode(void);
 // Set the mode number which is saved in the config
 void SCR_SetDefaultMode(void);
 
-void SCR_CalcAproxFps(void);
+void SCR_CalculateFPS(void);
 
 FUNCMATH boolean SCR_IsAspectCorrect(INT32 width, INT32 height);
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index bf2e42cfd83a00a8822a710628081c4d609d9e7f..6b9b113279723d04eb95487eaaca256ed472d57b 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -186,6 +186,7 @@ static char returnWadPath[256];
 #include "../m_argv.h"
 
 #include "../r_main.h" // Frame interpolation/uncapped
+#include "../r_fps.h"
 
 #ifdef MAC_ALERT
 #include "macosx/mac_alert.h"
@@ -2200,6 +2201,57 @@ void I_Sleep(void)
 		SDL_Delay(cv_sleep.value);
 }
 
+boolean I_CheckFrameCap(precise_t start, precise_t end)
+{
+	UINT32 capFrames = R_GetFramerateCap();
+	int capMicros = 0;
+
+	int elapsed;
+
+	if (capFrames == 0)
+	{
+		// We don't want to cap.
+		return false;
+	}
+
+	elapsed = I_PreciseToMicros(end - start);
+	capMicros = 1000000 / capFrames;
+
+	if (elapsed < capMicros)
+	{
+		// Wait to draw the next frame.
+		UINT32 wait = ((capMicros - elapsed) / 1000);
+
+		if (cv_sleep.value > 1)
+		{
+			// 1 is the default, and in non-interpolated mode is just the bare minimum wait.
+			// Since we're already adding some wait with an FPS cap, only apply when it's above 1.
+			wait += cv_sleep.value - 1;
+		}
+
+		// If the wait's greater than our granularity value,
+		// we'll just burn the couple extra cycles in the main loop
+		// in order to get to the next frame.
+		// This makes us get to the exact FPS cap more often.
+
+		// Higher values have more wasted CPU cycles, but the in-game frame performance is better.
+		// 10ms is the average clock tick of most OS scheduling.
+		// 15ms is a little more than that, for leniency on slow machines. (This helps mine reach a stable 60, at least!)
+		// (https://www.libsdl.org/release/SDL-1.2.15/docs/html/sdldelay.html)
+#define DELAY_GRANULARITY 15
+		if (wait >= DELAY_GRANULARITY)
+		{
+			SDL_Delay(wait);
+		}
+#undef DELAY_GRANULARITY
+
+		return true;
+	}
+
+	// Waited enough to draw again.
+	return false;
+}
+
 #ifdef NEWSIGNALHANDLER
 static void newsignalhandler_Warn(const char *pr)
 {
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index d8899e9324ad6d8011cfc2e6a9731229645d2316..eedb60e09126451e63a6bea2b511c6d8d6b55278 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1214,7 +1214,7 @@ void I_FinishUpdate(void)
 	if (rendermode == render_none)
 		return; //Alam: No software or OpenGl surface
 
-	SCR_CalcAproxFps();
+	//SCR_CalculateFPS(); // Moved to main loop
 
 	if (I_SkipFrame())
 		return;
@@ -1475,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);
@@ -1961,3 +1968,23 @@ void I_GetCursorPosition(INT32 *x, INT32 *y)
 {
 	SDL_GetMouseState(x, y);
 }
+
+UINT32 I_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_GetDesktopDisplayMode(index, &m) != 0)
+	{
+		// Error has occurred.
+		return 0;
+	}
+
+	return m.refresh_rate;
+}