diff --git a/src/p_spec.c b/src/p_spec.c
index 04b333628e40d93f581a09ed306c0b521a622716..6147a83920c19528dab8e14a978a3d26c77eca26 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -7413,6 +7413,7 @@ static void Add_Scroller(INT32 type, fixed_t dx, fixed_t dy, INT32 control, INT3
 	switch (type)
 	{
 		case sc_side:
+			R_CreateInterpolator_SideScroll(&s->thinker, &sides[affectee]);
 			break;
 		case sc_floor:
 			R_CreateInterpolator_SectorScroll(&s->thinker, &sectors[affectee], false);
diff --git a/src/r_fps.c b/src/r_fps.c
index b672a6f0f7d61a688a805c71dbcc23eb77b8e796..e42e8745da3492d742aacfdb29e311ca550baa8f 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -276,6 +276,14 @@ void R_CreateInterpolator_SectorScroll(thinker_t *thinker, sector_t *sector, boo
 	}
 }
 
+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_InitializeLevelInterpolators(void)
 {
 	levelinterpolators_len = 0;
@@ -297,6 +305,12 @@ static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
 		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;
 	}
 }
 
@@ -362,6 +376,10 @@ void R_ApplyLevelInterpolators(fixed_t 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;
 		}
 	}
 }
@@ -398,6 +416,10 @@ void R_RestoreLevelInterpolators(void)
 				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;
 		}
 	}
 }
diff --git a/src/r_fps.h b/src/r_fps.h
index cc240ffc8cc184cc41b9fc23124705ce526be530..0210a24f263ff03b5de1492990782c00c88a3df0 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -57,6 +57,7 @@ typedef struct {
 typedef enum {
 	LVLINTERP_SectorPlane,
 	LVLINTERP_SectorScroll,
+	LVLINTERP_SideScroll,
 } levelinterpolator_type_e;
 
 // Tagged union of a level interpolator
@@ -75,6 +76,10 @@ typedef struct levelinterpolator_s {
 			fixed_t oldxoffs, oldyoffs, bakxoffs, bakyoffs;
 			boolean ceiling;
 		} sectorscroll;
+		struct {
+			side_t *side;
+			fixed_t oldtextureoffset, oldrowoffset, baktextureoffset, bakrowoffset;
+		} sidescroll;
 	};
 } levelinterpolator_t;
 
@@ -93,6 +98,7 @@ void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjst
 
 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);
 
 // Initialize level interpolators after a level change
 void R_InitializeLevelInterpolators(void);