diff --git a/include/types.h b/include/types.h
index b3dc27e..c46bdf0 100644
--- a/include/types.h
+++ b/include/types.h
@@ -118,6 +118,10 @@ struct GraphNodeObject_sub
     /*0x0A 0x42*/ u16 animTimer;
     /*0x0C 0x44*/ s32 animFrameAccelAssist;
     /*0x10 0x48*/ s32 animAccel;
+    s16 prevAnimFrame;
+    s16 prevAnimID;
+    u32 prevAnimFrameTimestamp;
+    struct Animation *prevAnimPtr;
 };
 
 struct GraphNodeObject
@@ -128,11 +132,22 @@ struct GraphNodeObject
     /*0x19*/ s8 unk19;
     /*0x1A*/ Vec3s angle;
     /*0x20*/ Vec3f pos;
+    Vec3s prevAngle;
+    Vec3f prevPos;
+    u32 prevTimestamp;
+    Vec3f prevShadowPos;
+    u32 prevShadowPosTimestamp;
     /*0x2C*/ Vec3f scale;
+    Vec3f prevScale;
+    u32 prevScaleTimestamp;
     /*0x38*/ struct GraphNodeObject_sub unk38;
     /*0x4C*/ struct SpawnInfo *unk4C;
     /*0x50*/ Mat4 *throwMatrix; // matrix ptr
+    Mat4 prevThrowMatrix;
+    u32 prevThrowMatrixTimestamp;
+    Mat4 *throwMatrixInterpolated;
     /*0x54*/ Vec3f cameraToObject;
+    u32 skipInterpolationTimestamp;
 };
 
 struct ObjectNode
@@ -243,6 +258,10 @@ struct Surface
     } normal;
     /*0x28*/ f32 originOffset;
     /*0x2C*/ struct Object *object;
+    Vec3s prevVertex1;
+    Vec3s prevVertex2;
+    Vec3s prevVertex3;
+    u32 modifiedTimestamp;
 };
 
 struct MarioBodyState
diff --git a/src/engine/graph_node.h b/src/engine/graph_node.h
index 802d97a..1b0d677 100644
--- a/src/engine/graph_node.h
+++ b/src/engine/graph_node.h
@@ -110,6 +110,8 @@ struct GraphNodePerspective
     /*0x1C*/ f32 fov;   // horizontal field of view in degrees
     /*0x20*/ s16 near;  // near clipping plane
     /*0x22*/ s16 far;   // far clipping plane
+    f32 prevFov;
+    f32 prevTimestamp;
 };
 
 /** An entry in the master list. It is a linked list of display lists
@@ -118,7 +120,9 @@ struct GraphNodePerspective
 struct DisplayListNode
 {
     Mtx *transform;
+    void *transformInterpolated;
     void *displayList;
+    void *displayListInterpolated;
     struct DisplayListNode *next;
 };
 
@@ -185,7 +189,11 @@ struct GraphNodeCamera
     } config;
     /*0x1C*/ Vec3f pos;
     /*0x28*/ Vec3f focus;
+    Vec3f prevPos;
+    Vec3f prevFocus;
+    u32 prevTimestamp;
     /*0x34*/ Mat4 *matrixPtr; // pointer to look-at matrix of this camera as a Mat4
+    Mat4 *matrixPtrInterpolated;
     /*0x38*/ s16 roll; // roll in look at matrix. Doesn't account for light direction unlike rollScreen.
     /*0x3A*/ s16 rollScreen; // rolls screen while keeping the light direction consistent
 };
@@ -226,7 +234,8 @@ struct GraphNodeRotation
     /*0x00*/ struct GraphNode node;
     /*0x14*/ void *displayList;
     /*0x18*/ Vec3s rotation;
-    u8 pad1E[2];
+    Vec3s prevRotation;
+    u32 prevTimestamp;
 };
 
 /** GraphNode part that transforms itself and its children based on animation
@@ -323,6 +332,9 @@ struct GraphNodeBackground
     /*0x00*/ struct FnGraphNode fnNode;
     /*0x18*/ s32 unused;
     /*0x1C*/ s32 background; // background ID, or rgba5551 color if fnNode.func is null
+    Vec3f prevCameraPos;
+    Vec3f prevCameraFocus;
+    u32 prevCameraTimestamp;
 };
 
 /** Renders the object that Mario is holding.
@@ -333,6 +345,8 @@ struct GraphNodeHeldObject
     /*0x18*/ s32 playerIndex;
     /*0x1C*/ struct Object *objNode;
     /*0x20*/ Vec3s translation;
+    Vec3f prevShadowPos;
+    u32 prevShadowPosTimestamp;
 };
 
 /** A node that allows an object to specify a different culling radius than the
diff --git a/src/engine/surface_collision.c b/src/engine/surface_collision.c
index 5b6775f..2c11e25 100644
--- a/src/engine/surface_collision.c
+++ b/src/engine/surface_collision.c
@@ -8,6 +8,7 @@
 #include "surface_collision.h"
 #include "surface_load.h"
 #include "math_util.h"
+#include "game/game_init.h"
 
 /**************************************************
  *                      WALLS                     *
@@ -394,26 +395,44 @@ f32 find_floor_height_and_data(f32 xPos, f32 yPos, f32 zPos, struct FloorGeometr
     return floorHeight;
 }
 
+u8 gInterpolatingSurfaces;
+
 /**
  * Iterate through the list of floors and find the first floor under a given point.
  */
 static struct Surface *find_floor_from_list(struct SurfaceNode *surfaceNode, s32 x, s32 y, s32 z, f32 *pheight) {
     register struct Surface *surf;
-    register s32 x1, z1, x2, z2, x3, z3;
+    register f32 x1, z1, x2, z2, x3, z3;
     f32 nx, ny, nz;
     f32 oo;
     f32 height;
     struct Surface *floor = NULL;
+    s32 interpolate;
 
     // Iterate through the list of floors until there are no more floors.
     while (surfaceNode != NULL) {
         surf = surfaceNode->surface;
         surfaceNode = surfaceNode->next;
+        interpolate = gInterpolatingSurfaces && surf->modifiedTimestamp == gGlobalTimer;
 
         x1 = surf->vertex1[0];
         z1 = surf->vertex1[2];
         x2 = surf->vertex2[0];
         z2 = surf->vertex2[2];
+        if (interpolate) {
+            f32 diff = (surf->prevVertex1[0] - x1) * (surf->prevVertex1[0] - x1);
+            diff += (surf->prevVertex1[1] - surf->vertex1[1]) * (surf->prevVertex1[1] - surf->vertex1[1]);
+            diff += (surf->prevVertex1[2] - z1) * (surf->prevVertex1[2] - z1);
+            //printf("%f\n", sqrtf(diff));
+            if (diff > 10000) {
+                interpolate = FALSE;
+            } else {
+                x1 = (surf->prevVertex1[0] + x1) / 2;
+                z1 = (surf->prevVertex1[2] + z1) / 2;
+                x2 = (surf->prevVertex2[0] + x2) / 2;
+                z2 = (surf->prevVertex2[2] + z2) / 2;
+            }
+        }
 
         // Check that the point is within the triangle bounds.
         if ((z1 - z) * (x2 - x1) - (x1 - x) * (z2 - z1) < 0) {
@@ -423,6 +442,10 @@ static struct Surface *find_floor_from_list(struct SurfaceNode *surfaceNode, s32
         // To slightly save on computation time, set this later.
         x3 = surf->vertex3[0];
         z3 = surf->vertex3[2];
+        if (interpolate) {
+            x3 = (surf->prevVertex3[0] + x3) / 2;
+            z3 = (surf->prevVertex3[2] + z3) / 2;
+        }
 
         if ((z2 - z) * (x3 - x2) - (x2 - x) * (z3 - z2) < 0) {
             continue;
@@ -442,10 +465,30 @@ static struct Surface *find_floor_from_list(struct SurfaceNode *surfaceNode, s32
             continue;
         }
 
-        nx = surf->normal.x;
-        ny = surf->normal.y;
-        nz = surf->normal.z;
-        oo = surf->originOffset;
+        if (interpolate) {
+            f32 y1, y2, y3;
+            f32 mag;
+            y1 = (surf->prevVertex1[1] + surf->vertex1[1]) / 2;
+            y2 = (surf->prevVertex2[1] + surf->vertex2[1]) / 2;
+            y3 = (surf->prevVertex3[1] + surf->vertex3[1]) / 2;
+            nx = (y2 - y1) * (z3 - z2) - (z2 - z1) * (y3 - y2);
+            ny = (z2 - z1) * (x3 - x2) - (x2 - x1) * (z3 - z2);
+            nz = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
+            mag = sqrtf(nx * nx + ny * ny + nz * nz);
+            if (mag < 0.0001) {
+                continue;
+            }
+            mag = (f32)(1.0 / mag);
+            nx *= mag;
+            ny *= mag;
+            nz *= mag;
+            oo = -(nx * x1 + ny * y1 + nz * z1);
+        } else {
+            nx = surf->normal.x;
+            ny = surf->normal.y;
+            nz = surf->normal.z;
+            oo = surf->originOffset;
+        }
 
         // If a wall, ignore it. Likely a remnant, should never occur.
         if (ny == 0.0f) {
@@ -460,6 +503,15 @@ static struct Surface *find_floor_from_list(struct SurfaceNode *surfaceNode, s32
         }
 
         *pheight = height;
+        if (interpolate) {
+            static struct Surface s;
+            s.type = surf->type;
+            s.normal.x = nx;
+            s.normal.y = ny;
+            s.normal.z = nz;
+            s.originOffset = oo;
+            return &s;
+        }
         floor = surf;
         break;
     }
diff --git a/src/engine/surface_load.c b/src/engine/surface_load.c
index ac2ee50..323b7d0 100644
--- a/src/engine/surface_load.c
+++ b/src/engine/surface_load.c
@@ -14,6 +14,7 @@
 #include "game/mario.h"
 #include "game/object_list_processor.h"
 #include "surface_load.h"
+#include "game/game_init.h"
 
 s32 unused8038BE90;
 
@@ -359,6 +360,11 @@ static struct Surface *read_surface_data(s16 *vertexData, s16 **vertexIndices) {
 
     surface = alloc_surface();
 
+    vec3s_copy(surface->prevVertex1, surface->vertex1);
+    vec3s_copy(surface->prevVertex2, surface->vertex2);
+    vec3s_copy(surface->prevVertex3, surface->vertex3);
+    surface->modifiedTimestamp = gGlobalTimer;
+
     surface->vertex1[0] = x1;
     surface->vertex2[0] = x2;
     surface->vertex3[0] = x3;
diff --git a/src/game/camera.c b/src/game/camera.c
index bde0662..9351dea 100644
--- a/src/game/camera.c
+++ b/src/game/camera.c
@@ -484,6 +484,10 @@ CameraTransition sModeTransitions[] = {
 extern u8 sDanceCutsceneIndexTable[][4];
 extern u8 sZoomOutAreaMasks[];
 
+static void skip_camera_interpolation(void) {
+    gLakituState.skipCameraInterpolationTimestamp = gGlobalTimer;
+}
+
 /**
  * Starts a camera shake triggered by an interaction
  */
@@ -5552,6 +5556,7 @@ s32 set_camera_mode_fixed(struct Camera *c, s16 x, s16 y, s16 z) {
         c->mode = CAMERA_MODE_FIXED;
         vec3f_set(c->pos, sFixedModeBasePosition[0], sMarioCamState->pos[1],
                   sFixedModeBasePosition[2]);
+        skip_camera_interpolation();
     }
     return basePosSet;
 }
@@ -5714,6 +5719,7 @@ BAD_RETURN(s32) cam_rr_enter_building_side(struct Camera *c) {
     if (c->mode != CAMERA_MODE_FIXED) {
         sStatusFlags &= ~CAM_FLAG_SMOOTH_MOVEMENT;
         c->mode = CAMERA_MODE_FIXED;
+        skip_camera_interpolation();
     }
 }
 
@@ -5909,6 +5915,7 @@ BAD_RETURN(s32) cam_castle_enter_lobby(struct Camera *c) {
         sStatusFlags &= ~CAM_FLAG_SMOOTH_MOVEMENT;
         set_fixed_cam_axis_sa_lobby(c->mode);
         c->mode = CAMERA_MODE_FIXED;
+        skip_camera_interpolation();
     }
 }
 
@@ -7279,6 +7286,7 @@ BAD_RETURN(s32) cutscene_unused_loop(UNUSED struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_mario_fall_start(struct Camera *c) {
     vec3f_set(c->focus, -26.f, 0.f, -137.f);
     vec3f_set(c->pos, 165.f, 4725.f, 324.f);
+    skip_camera_interpolation();
 }
 
 /**
@@ -7311,6 +7319,7 @@ BAD_RETURN(s32) cutscene_ending_mario_fall(struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_mario_land_closeup(struct Camera *c) {
     vec3f_set(c->focus, 85.f, 826.f, 250.f);
     vec3f_set(c->pos, -51.f, 988.f, -202.f);
+    skip_camera_interpolation();
     player2_rotate_cam(c, -0x2000, 0x2000, -0x2000, 0x2000);
 }
 
@@ -7320,6 +7329,7 @@ BAD_RETURN(s32) cutscene_ending_mario_land_closeup(struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_reset_spline(UNUSED struct Camera *c) {
     sCutsceneVars[9].point[0] = 0.f;
     cutscene_reset_spline();
+    skip_camera_interpolation();
 }
 
 /**
@@ -7355,6 +7365,7 @@ BAD_RETURN(s32) cutscene_ending_peach_appear_closeup(struct Camera *c) {
     vec3f_set(c->pos, 179.f, 2463.f, -1216.f);
     c->pos[1] = gCutsceneFocus->oPosY + 35.f;
     vec3f_set(c->focus, gCutsceneFocus->oPosX, gCutsceneFocus->oPosY + 125.f, gCutsceneFocus->oPosZ);
+    skip_camera_interpolation();
 }
 
 /**
@@ -7373,6 +7384,7 @@ BAD_RETURN(s32) cutscene_ending_peach_appears(struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_peach_descends_start(UNUSED struct Camera *c) {
     cutscene_reset_spline();
     sCutsceneVars[2].point[1] = 150.f;
+    skip_camera_interpolation();
 }
 
 /**
@@ -7459,6 +7471,7 @@ BAD_RETURN(s32) cutscene_ending_peach_wakeup(struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_dialog(struct Camera *c) {
     vec3f_set(c->focus, 11.f, 983.f, -1273.f);
     vec3f_set(c->pos, -473.f, 970.f, -1152.f);
+    skip_camera_interpolation();
     player2_rotate_cam(c, -0x800, 0x2000, -0x2000, 0x2000);
 }
 
@@ -7469,6 +7482,7 @@ BAD_RETURN(s32) cutscene_ending_kiss_closeup(struct Camera *c) {
     set_fov_function(CAM_FOV_SET_29);
     vec3f_set(c->focus, 350.f, 1034.f, -1216.f);
     vec3f_set(c->pos, -149.f, 1021.f, -1216.f);
+    skip_camera_interpolation();
 }
 
 /**
@@ -7504,6 +7518,7 @@ BAD_RETURN(s32) cutscene_ending_kiss(struct Camera *c) {
 BAD_RETURN(s32) cutscene_ending_look_at_sky(struct Camera *c) {
     move_point_along_spline(c->focus, sEndingLookAtSkyFocus, &sCutsceneSplineSegment, &sCutsceneSplineSegmentProgress);
     vec3f_set(c->pos, 699.f, 1680.f, -703.f);
+    skip_camera_interpolation();
 }
 
 /**
@@ -10340,6 +10355,7 @@ BAD_RETURN(s32) cutscene_door_start(struct Camera *c) {
 BAD_RETURN(s32) cutscene_door_fix_cam(struct Camera *c) {
     vec3f_copy(c->pos, sCutsceneVars[0].point);
     vec3f_copy(c->focus, sCutsceneVars[1].point);
+    skip_camera_interpolation();
 }
 
 /**
@@ -10373,6 +10389,7 @@ BAD_RETURN(s32) cutscene_door_move_behind_mario(struct Camera *c) {
     }
 
     offset_rotated(c->pos, sMarioCamState->pos, camOffset, sCutsceneVars[0].angle);
+    skip_camera_interpolation();
 }
 
 /**
diff --git a/src/game/camera.h b/src/game/camera.h
index 173ab8a..b1abdc4 100644
--- a/src/game/camera.h
+++ b/src/game/camera.h
@@ -657,6 +657,8 @@ struct LakituState
     /// Mario's action from the previous frame. Only used to determine if Mario just finished a dive.
     /*0xB8*/ u32 lastFrameAction;
     /*0xBC*/ s16 unused;
+
+    u32 skipCameraInterpolationTimestamp;
 };
 
 // bss order hack to not affect BSS order. if possible, remove me, but it will be hard to match otherwise
diff --git a/src/game/envfx_bubbles.c b/src/game/envfx_bubbles.c
index 16a9272..ee1b029 100644
--- a/src/game/envfx_bubbles.c
+++ b/src/game/envfx_bubbles.c
@@ -35,6 +35,20 @@ Vtx_t gBubbleTempVtx[3] = {
     { { 0, 0, 0 }, 0, { -498, 964 }, { 0xFF, 0xFF, 0xFF, 0xFF } },
 };
 
+static Gfx sGfxSaved[60 / 5];
+static Gfx *sBubbleInterpolatedDisplayListPos[60 / 5];
+static Vec3s sPrevBubblePositions[60];
+
+void patch_interpolated_bubble_particles(void) {
+    s32 i;
+    for (i = 0; i < 60 / 5; i++) {
+        if (sBubbleInterpolatedDisplayListPos[i] != NULL) {
+            *sBubbleInterpolatedDisplayListPos[i] = sGfxSaved[i];
+            sBubbleInterpolatedDisplayListPos[i] = NULL;
+        }
+    }
+}
+
 /**
  * Check whether the particle with the given index is
  * laterally within distance of point (x, z). Used to
@@ -241,6 +255,7 @@ void envfx_update_whirlpool(void) {
             (gEnvFxBuffer + i)->yPos = (i + gEnvFxBuffer)->bubbleY;
             (gEnvFxBuffer + i)->unusedBubbleVar = 0;
             (gEnvFxBuffer + i)->isAlive = 1;
+            (gEnvFxBuffer + i)->spawnTimestamp = gGlobalTimer;
 
             envfx_rotate_around_whirlpool(&(gEnvFxBuffer + i)->xPos, &(gEnvFxBuffer + i)->yPos,
                                           &(gEnvFxBuffer + i)->zPos);
@@ -299,6 +314,7 @@ void envfx_update_jetstream(void) {
                 + coss((gEnvFxBuffer + i)->angleAndDist[0]) * (gEnvFxBuffer + i)->angleAndDist[1];
             (gEnvFxBuffer + i)->yPos =
                 gEnvFxBubbleConfig[ENVFX_STATE_SRC_Y] + (random_float() * 400.0f - 200.0f);
+            (gEnvFxBuffer + i)->spawnTimestamp = gGlobalTimer;
         } else {
             (gEnvFxBuffer + i)->angleAndDist[1] += 10;
             (gEnvFxBuffer + i)->xPos += sins((gEnvFxBuffer + i)->angleAndDist[0]) * 10.0f;
@@ -506,6 +522,12 @@ Gfx *envfx_update_bubble_particles(s32 mode, UNUSED Vec3s marioPos, Vec3s camFro
     Vec3s vertex1;
     Vec3s vertex2;
     Vec3s vertex3;
+    Vec3s interpolatedVertices[3];
+
+    static Vec3s prevVertex1;
+    static Vec3s prevVertex2;
+    static Vec3s prevVertex3;
+    static u32 prevTimestamp;
 
     Gfx *gfxStart;
 
@@ -521,18 +543,52 @@ Gfx *envfx_update_bubble_particles(s32 mode, UNUSED Vec3s marioPos, Vec3s camFro
     envfx_bubbles_update_switch(mode, camTo, vertex1, vertex2, vertex3);
     rotate_triangle_vertices(vertex1, vertex2, vertex3, pitch, yaw);
 
+    if (gGlobalTimer == prevTimestamp + 1) {
+        interpolate_vectors_s16(interpolatedVertices[0], prevVertex1, vertex1);
+        interpolate_vectors_s16(interpolatedVertices[1], prevVertex2, vertex2);
+        interpolate_vectors_s16(interpolatedVertices[2], prevVertex3, vertex3);
+    }
+    vec3s_copy(prevVertex1, vertex1);
+    vec3s_copy(prevVertex2, vertex2);
+    vec3s_copy(prevVertex3, vertex3);
+    prevTimestamp = gGlobalTimer;
+
     gSPDisplayList(sGfxCursor++, &tiny_bubble_dl_0B006D38);
 
     for (i = 0; i < sBubbleParticleMaxCount; i += 5) {
+        Vtx *interpolatedVertBuf = alloc_display_list(15 * sizeof(Vtx));
+        s32 j, k;
         gDPPipeSync(sGfxCursor++);
         envfx_set_bubble_texture(mode, i);
-        append_bubble_vertex_buffer(sGfxCursor++, i, vertex1, vertex2, vertex3, (Vtx *) gBubbleTempVtx);
+        sBubbleInterpolatedDisplayListPos[i / 5] = sGfxCursor;
+        for (j = 0; j < 5; j++) {
+            for (k = 0; k < 3; k++) {
+                Vtx *v = &interpolatedVertBuf[j * 3 + k];
+                v->v = gBubbleTempVtx[k];
+                if (gGlobalTimer != gEnvFxBuffer[i + j].spawnTimestamp && mode != ENVFX_LAVA_BUBBLES) {
+                    v->v.ob[0] = (sPrevBubblePositions[i + j][0] + gEnvFxBuffer[i + j].xPos) / 2.0f + interpolatedVertices[k][0];
+                    v->v.ob[1] = (sPrevBubblePositions[i + j][1] + gEnvFxBuffer[i + j].yPos) / 2.0f + interpolatedVertices[k][1];
+                    v->v.ob[2] = (sPrevBubblePositions[i + j][2] + gEnvFxBuffer[i + j].zPos) / 2.0f + interpolatedVertices[k][2];
+                } else {
+                    v->v.ob[0] = gEnvFxBuffer[i + j].xPos + interpolatedVertices[k][0];
+                    v->v.ob[1] = gEnvFxBuffer[i + j].yPos + interpolatedVertices[k][1];
+                    v->v.ob[2] = gEnvFxBuffer[i + j].zPos + interpolatedVertices[k][2];
+                }
+            }
+        }
+        gSPVertex(sGfxCursor++, VIRTUAL_TO_PHYSICAL(interpolatedVertBuf), 15, 0);
+        append_bubble_vertex_buffer(&sGfxSaved[i / 5], i, vertex1, vertex2, vertex3, (Vtx *) gBubbleTempVtx);
         gSP1Triangle(sGfxCursor++, 0, 1, 2, 0);
         gSP1Triangle(sGfxCursor++, 3, 4, 5, 0);
         gSP1Triangle(sGfxCursor++, 6, 7, 8, 0);
         gSP1Triangle(sGfxCursor++, 9, 10, 11, 0);
         gSP1Triangle(sGfxCursor++, 12, 13, 14, 0);
     }
+    for (i = 0; i < sBubbleParticleMaxCount; i++) {
+        sPrevBubblePositions[i][0] = gEnvFxBuffer[i].xPos;
+        sPrevBubblePositions[i][1] = gEnvFxBuffer[i].yPos;
+        sPrevBubblePositions[i][2] = gEnvFxBuffer[i].zPos;
+    }
 
     gSPDisplayList(sGfxCursor++, &tiny_bubble_dl_0B006AB0);
     gSPEndDisplayList(sGfxCursor++);
diff --git a/src/game/envfx_snow.c b/src/game/envfx_snow.c
index c3c14a5..d2212ef 100644
--- a/src/game/envfx_snow.c
+++ b/src/game/envfx_snow.c
@@ -54,6 +54,26 @@ extern void *tiny_bubble_dl_0B006AB0;
 extern void *tiny_bubble_dl_0B006A50;
 extern void *tiny_bubble_dl_0B006CD8;
 
+static struct {
+    Gfx *pos;
+    Vtx vertices[15];
+} sPrevSnowVertices[140 / 5];
+static s16 sPrevSnowParticleCount;
+static u32 sPrevSnowTimestamp;
+
+void patch_interpolated_snow_particles(void) {
+    int i;
+
+    if (gGlobalTimer != sPrevSnowTimestamp + 1) {
+        return;
+    }
+
+    for (i = 0; i < sPrevSnowParticleCount; i += 5) {
+        gSPVertex(sPrevSnowVertices[i / 5].pos,
+                  VIRTUAL_TO_PHYSICAL(sPrevSnowVertices[i / 5].vertices), 15, 0);
+    }
+}
+
 /**
  * Initialize snow particles by allocating a buffer for storing their state
  * and setting a start amount.
@@ -217,6 +237,7 @@ void envfx_update_snow_normal(s32 snowCylinderX, s32 snowCylinderY, s32 snowCyli
                 400.0f * random_float() - 200.0f + snowCylinderZ + (s16)(deltaZ * 2);
             (gEnvFxBuffer + i)->yPos = 200.0f * random_float() + snowCylinderY;
             (gEnvFxBuffer + i)->isAlive = 1;
+            (gEnvFxBuffer + i)->spawnTimestamp = gGlobalTimer;
         } else {
             (gEnvFxBuffer + i)->xPos += random_float() * 2 - 1.0f + (s16)(deltaX / 1.2);
             (gEnvFxBuffer + i)->yPos -= 2 -(s16)(deltaY * 0.8);
@@ -251,6 +272,7 @@ void envfx_update_snow_blizzard(s32 snowCylinderX, s32 snowCylinderY, s32 snowCy
                 400.0f * random_float() - 200.0f + snowCylinderZ + (s16)(deltaZ * 2);
             (gEnvFxBuffer + i)->yPos = 400.0f * random_float() - 200.0f + snowCylinderY;
             (gEnvFxBuffer + i)->isAlive = 1;
+            (gEnvFxBuffer + i)->spawnTimestamp = gGlobalTimer;
         } else {
             (gEnvFxBuffer + i)->xPos += random_float() * 2 - 1.0f + (s16)(deltaX / 1.2) + 20.0f;
             (gEnvFxBuffer + i)->yPos -= 5 -(s16)(deltaY * 0.8);
@@ -294,6 +316,7 @@ void envfx_update_snow_water(s32 snowCylinderX, s32 snowCylinderY, s32 snowCylin
             (gEnvFxBuffer + i)->zPos = 400.0f * random_float() - 200.0f + snowCylinderZ;
             (gEnvFxBuffer + i)->yPos = 400.0f * random_float() - 200.0f + snowCylinderY;
             (gEnvFxBuffer + i)->isAlive = 1;
+            (gEnvFxBuffer + i)->spawnTimestamp = gGlobalTimer;
         }
     }
 }
@@ -346,6 +369,8 @@ void rotate_triangle_vertices(Vec3s vertex1, Vec3s vertex2, Vec3s vertex3, s16 p
 void append_snowflake_vertex_buffer(Gfx *gfx, s32 index, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3) {
     s32 i = 0;
     Vtx *vertBuf = (Vtx *) alloc_display_list(15 * sizeof(Vtx));
+    Vtx *vertBufInterpolated = (Vtx *) alloc_display_list(15 * sizeof(Vtx));
+    Vtx *v;
 #ifdef VERSION_EU
     Vtx *p;
 #endif
@@ -395,7 +420,23 @@ void append_snowflake_vertex_buffer(Gfx *gfx, s32 index, Vec3s vertex1, Vec3s ve
 #endif
     }
 
-    gSPVertex(gfx, VIRTUAL_TO_PHYSICAL(vertBuf), 15, 0);
+    for (i = 0; i < 15; i++) {
+        v = &sPrevSnowVertices[index / 5].vertices[i];
+        vertBufInterpolated[i] = gSnowTempVtx[i % 3];
+        if (index < sPrevSnowParticleCount && gGlobalTimer == sPrevSnowTimestamp + 1 &&
+            gGlobalTimer != gEnvFxBuffer[index + i / 3].spawnTimestamp) {
+            vertBufInterpolated[i].v.ob[0] = (v->v.ob[0] + vertBuf[i].v.ob[0]) / 2;
+            vertBufInterpolated[i].v.ob[1] = (v->v.ob[1] + vertBuf[i].v.ob[1]) / 2;
+            vertBufInterpolated[i].v.ob[2] = (v->v.ob[2] + vertBuf[i].v.ob[2]) / 2;
+        } else {
+            vertBufInterpolated[i].v.ob[0] = vertBuf[i].v.ob[0];
+            vertBufInterpolated[i].v.ob[1] = vertBuf[i].v.ob[1];
+            vertBufInterpolated[i].v.ob[2] = vertBuf[i].v.ob[2];
+        }
+        *v = vertBuf[i];
+    }
+    sPrevSnowVertices[index / 5].pos = gfx;
+    gSPVertex(gfx, VIRTUAL_TO_PHYSICAL(vertBufInterpolated), 15, 0);
 }
 
 /**
@@ -479,6 +520,8 @@ Gfx *envfx_update_snow(s32 snowMode, Vec3s marioPos, Vec3s camFrom, Vec3s camTo)
         gSP1Triangle(gfx++, 9, 10, 11, 0);
         gSP1Triangle(gfx++, 12, 13, 14, 0);
     }
+    sPrevSnowParticleCount = gSnowParticleCount;
+    sPrevSnowTimestamp = gGlobalTimer;
 
     gSPDisplayList(gfx++, &tiny_bubble_dl_0B006AB0) gSPEndDisplayList(gfx++);
 
diff --git a/src/game/envfx_snow.h b/src/game/envfx_snow.h
index 7a83b53..f4acc2d 100644
--- a/src/game/envfx_snow.h
+++ b/src/game/envfx_snow.h
@@ -25,7 +25,8 @@ struct EnvFxParticle {
     s32 angleAndDist[2]; // for whirpools, [0] = angle from center, [1] = distance from center
     s32 unusedBubbleVar; // set to zero for bubbles when respawning, never used elsewhere
     s32 bubbleY; // for Bubbles, yPos is always set to this
-    s8 filler20[56 - 0x20];
+    //s8 filler20[56 - 0x20];
+    u32 spawnTimestamp;
 };
 
 extern s8 gEnvFxMode;
diff --git a/src/game/hud.c b/src/game/hud.c
index 1540b67..0de6e0b 100644
--- a/src/game/hud.c
+++ b/src/game/hud.c
@@ -59,6 +59,20 @@ static struct UnusedHUDStruct sUnusedHUDValues = { 0x00, 0x0A, 0x00 };
 
 static struct CameraHUD sCameraHUD = { CAM_STATUS_NONE };
 
+static u32 sPowerMeterLastRenderTimestamp;
+static s16 sPowerMeterLastY;
+static Gfx *sPowerMeterDisplayListPos;
+
+void patch_interpolated_hud(void) {
+    if (sPowerMeterDisplayListPos != NULL) {
+        Mtx *mtx = alloc_display_list(sizeof(Mtx));
+        guTranslate(mtx, (f32) sPowerMeterHUD.x, (f32) sPowerMeterHUD.y, 0);
+        gSPMatrix(sPowerMeterDisplayListPos, VIRTUAL_TO_PHYSICAL(mtx),
+              G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH);
+        sPowerMeterDisplayListPos = NULL;
+    }
+}
+
 /**
  * Renders a rgba16 16x16 glyph texture from a table list.
  */
@@ -111,6 +125,7 @@ void render_power_meter_health_segment(s16 numHealthWedges) {
  */
 void render_dl_power_meter(s16 numHealthWedges) {
     Mtx *mtx;
+    f32 interpolatedY;
 
     mtx = alloc_display_list(sizeof(Mtx));
 
@@ -118,7 +133,15 @@ void render_dl_power_meter(s16 numHealthWedges) {
         return;
     }
 
-    guTranslate(mtx, (f32) sPowerMeterHUD.x, (f32) sPowerMeterHUD.y, 0);
+    if (gGlobalTimer == sPowerMeterLastRenderTimestamp + 1) {
+        interpolatedY = (sPowerMeterLastY + sPowerMeterHUD.y) / 2.0f;
+    } else {
+        interpolatedY = sPowerMeterHUD.y;
+    }
+    guTranslate(mtx, (f32) sPowerMeterHUD.x, interpolatedY, 0);
+    sPowerMeterLastY = sPowerMeterHUD.y;
+    sPowerMeterLastRenderTimestamp = gGlobalTimer;
+    sPowerMeterDisplayListPos = gDisplayListHead;
 
     gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(mtx++),
               G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH);
diff --git a/src/game/ingame_menu.c b/src/game/ingame_menu.c
index 7ae9f1e..1c23c96 100644
--- a/src/game/ingame_menu.c
+++ b/src/game/ingame_menu.c
@@ -130,6 +130,42 @@ s32 gDialogResponse = 0;
 static struct CachedChar { u8 used; u8 data[CHCACHE_BUFLEN]; } charCache[256];
 #endif // VERSION
 
+static Gfx *sInterpolatedDialogOffsetPos;
+static f32 sInterpolatedDialogOffset;
+static Gfx *sInterpolatedDialogRotationPos;
+static f32 sInterpolatedDialogScale;
+static f32 sInterpolatedDialogRotation;
+static Gfx *sInterpolatedDialogZoomPos;
+
+void patch_interpolated_dialog(void) {
+    Mtx *matrix;
+
+    if (sInterpolatedDialogOffsetPos != NULL) {
+        matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
+        guTranslate(matrix, 0, sInterpolatedDialogOffset, 0);
+        gSPMatrix(sInterpolatedDialogOffsetPos, VIRTUAL_TO_PHYSICAL(matrix), G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
+        sInterpolatedDialogOffsetPos = NULL;
+    }
+    if (sInterpolatedDialogRotationPos != NULL) {
+        matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
+        guScale(matrix, 1.0 / sInterpolatedDialogScale, 1.0 / sInterpolatedDialogScale, 1.0f);
+        gSPMatrix(sInterpolatedDialogRotationPos++, VIRTUAL_TO_PHYSICAL(matrix), G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
+        matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
+        guRotate(matrix, sInterpolatedDialogRotation * 4.0f, 0, 0, 1.0f);
+        gSPMatrix(sInterpolatedDialogRotationPos, VIRTUAL_TO_PHYSICAL(matrix), G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
+        sInterpolatedDialogRotationPos = NULL;
+    }
+    if (sInterpolatedDialogZoomPos != NULL) {
+        matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
+        guTranslate(matrix, 65.0 - (65.0 / sInterpolatedDialogScale), (40.0 / sInterpolatedDialogScale) - 40, 0);
+        gSPMatrix(sInterpolatedDialogZoomPos++, VIRTUAL_TO_PHYSICAL(matrix), G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
+        matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
+        guScale(matrix, 1.0 / sInterpolatedDialogScale, 1.0 / sInterpolatedDialogScale, 1.0f);
+        gSPMatrix(sInterpolatedDialogZoomPos, VIRTUAL_TO_PHYSICAL(matrix), G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
+        sInterpolatedDialogZoomPos = NULL;
+    }
+}
+
 void create_dl_identity_matrix(void) {
     Mtx *matrix = (Mtx *) alloc_display_list(sizeof(Mtx));
 
@@ -969,6 +1005,14 @@ void render_dialog_box_type(struct DialogEntry *dialog, s8 linesPerBox) {
     switch (gDialogBoxType) {
         case DIALOG_TYPE_ROTATE: // Renders a dialog black box with zoom and rotation
             if (gDialogBoxState == DIALOG_STATE_OPENING || gDialogBoxState == DIALOG_STATE_CLOSING) {
+                sInterpolatedDialogRotationPos = gDisplayListHead;
+                if (gDialogBoxState == DIALOG_STATE_OPENING) {
+                    sInterpolatedDialogScale = gDialogBoxScale - 2 / 2;
+                    sInterpolatedDialogRotation = gDialogBoxOpenTimer - 7.5f / 2;
+                } else {
+                    sInterpolatedDialogScale = gDialogBoxScale + 2 / 2;
+                    sInterpolatedDialogRotation = gDialogBoxOpenTimer + 7.5f / 2;
+                }
                 create_dl_scale_matrix(MENU_MTX_NOPUSH, 1.0 / gDialogBoxScale, 1.0 / gDialogBoxScale, 1.0f);
                 // convert the speed into angle
                 create_dl_rotation_matrix(MENU_MTX_NOPUSH, gDialogBoxOpenTimer * 4.0f, 0, 0, 1.0f);
@@ -977,6 +1021,12 @@ void render_dialog_box_type(struct DialogEntry *dialog, s8 linesPerBox) {
             break;
         case DIALOG_TYPE_ZOOM: // Renders a dialog white box with zoom
             if (gDialogBoxState == DIALOG_STATE_OPENING || gDialogBoxState == DIALOG_STATE_CLOSING) {
+                sInterpolatedDialogZoomPos = gDisplayListHead;
+                if (gDialogBoxState == DIALOG_STATE_OPENING) {
+                    sInterpolatedDialogScale = gDialogBoxScale - 2 / 2;
+                } else {
+                    sInterpolatedDialogScale = gDialogBoxScale + 2 / 2;
+                }
                 create_dl_translation_matrix(MENU_MTX_NOPUSH, 65.0 - (65.0 / gDialogBoxScale),
                                               (40.0 / gDialogBoxScale) - 40, 0);
                 create_dl_scale_matrix(MENU_MTX_NOPUSH, 1.0 / gDialogBoxScale, 1.0 / gDialogBoxScale, 1.0f);
@@ -1259,6 +1309,8 @@ void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 l
 #ifdef VERSION_EU
         gDialogY -= gDialogScrollOffsetY;
 #else
+        sInterpolatedDialogOffset = gDialogScrollOffsetY + dialog->linesPerBox;
+        sInterpolatedDialogOffsetPos = gDisplayListHead;
         create_dl_translation_matrix(MENU_MTX_NOPUSH, 0, (f32) gDialogScrollOffsetY, 0);
 #endif
     }
diff --git a/src/game/level_geo.c b/src/game/level_geo.c
index 4c98e70..abc5121 100644
--- a/src/game/level_geo.c
+++ b/src/game/level_geo.c
@@ -34,12 +34,16 @@ Gfx *geo_envfx_main(s32 callContext, struct GraphNode *node, Mat4 mtxf) {
             vec3f_to_vec3s(marioPos, gPlayerCameraState->pos);
             particleList = envfx_update_particles(snowMode, marioPos, camTo, camFrom);
             if (particleList != NULL) {
+#if 0
                 Mtx *mtx = alloc_display_list(sizeof(*mtx));
 
                 gfx = alloc_display_list(2 * sizeof(*gfx));
                 mtxf_to_mtx(mtx, mtxf);
                 gSPMatrix(&gfx[0], VIRTUAL_TO_PHYSICAL(mtx), G_MTX_MODELVIEW | G_MTX_LOAD | G_MTX_NOPUSH);
                 gSPBranchList(&gfx[1], VIRTUAL_TO_PHYSICAL(particleList));
+#else
+                gfx = particleList;
+#endif
                 execNode->fnNode.node.flags = (execNode->fnNode.node.flags & 0xFF) | 0x400;
             }
             SET_HIGH_U16_OF_32(*params, gAreaUpdateCounter);
diff --git a/src/game/object_helpers.c b/src/game/object_helpers.c
index 22b45b3..109d7f7 100644
--- a/src/game/object_helpers.c
+++ b/src/game/object_helpers.c
@@ -1554,6 +1554,7 @@ void cur_obj_set_pos_to_home(void) {
     o->oPosX = o->oHomeX;
     o->oPosY = o->oHomeY;
     o->oPosZ = o->oHomeZ;
+    o->header.gfx.skipInterpolationTimestamp = gGlobalTimer;
 }
 
 void cur_obj_set_pos_to_home_and_stop(void) {
diff --git a/src/game/paintings.c b/src/game/paintings.c
index 6cae19c..a304d4a 100644
--- a/src/game/paintings.c
+++ b/src/game/paintings.c
@@ -189,6 +189,32 @@ struct Painting **sPaintingGroups[] = {
 s16 gPaintingUpdateCounter = 1;
 s16 gLastPaintingUpdateCounter = 0;
 
+static Vtx sLastVertices[2 * 264 * 3];
+static u32 sLastVerticesTimestamp;
+static Vtx *sVerticesPtr[2];
+static s32 sVerticesCount;
+
+void patch_interpolated_paintings(void) {
+    if (sVerticesPtr[0] != NULL) {
+        s32 i;
+        if (sVerticesPtr[1] != NULL) {
+            for (i = 0; i < sVerticesCount / 2; i++) {
+                sVerticesPtr[0][i] = sLastVertices[i];
+            }
+            for (; i < sVerticesCount; i++) {
+                sVerticesPtr[1][i - sVerticesCount / 2] = sLastVertices[i];
+            }
+        } else {
+            for (i = 0; i < sVerticesCount; i++) {
+                sVerticesPtr[0][i] = sLastVertices[i];
+            }
+        }
+        sVerticesPtr[0] = NULL;
+        sVerticesPtr[1] = NULL;
+        sVerticesCount = 0;
+    }
+}
+
 /**
  * Stop paintings in paintingGroup from rippling if their id is different from *idptr.
  */
@@ -890,6 +916,23 @@ Gfx *render_painting(u8 *img, s16 tWidth, s16 tHeight, s16 *textureMap, s16 mapV
         gSP1Triangle(gfx++, group * 3, group * 3 + 1, group * 3 + 2, 0);
     }
 
+    if (sVerticesCount >= numVtx * 2) {
+        sVerticesCount = 0;
+    }
+    for (map = 0; map < numVtx; map++) {
+        Vtx v = verts[map];
+        if (gGlobalTimer == sLastVerticesTimestamp + 1) {
+            s32 i;
+            for (i = 0; i < 3; i++) {
+                verts[map].n.ob[i] = (v.n.ob[i] + sLastVertices[sVerticesCount + map].n.ob[i]) / 2;
+                verts[map].n.n[i] = (v.n.n[i] + sLastVertices[sVerticesCount + map].n.n[i]) / 2;
+            }
+        }
+        sLastVertices[sVerticesCount + map] = v;
+    }
+    sVerticesPtr[sVerticesCount / numVtx] = verts;
+    sVerticesCount += numVtx;
+
     gSPEndDisplayList(gfx);
     return dlist;
 }
@@ -954,6 +997,7 @@ Gfx *painting_ripple_image(struct Painting *painting) {
         meshTris = textureMap[meshVerts * 3 + 1];
         gSPDisplayList(gfx++, render_painting(textures[i], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha));
     }
+    sLastVerticesTimestamp = gGlobalTimer;
 
     // Update the ripple, may automatically reset the painting's state.
     painting_update_ripple_state(painting);
@@ -991,6 +1035,7 @@ Gfx *painting_ripple_env_mapped(struct Painting *painting) {
     meshVerts = textureMap[0];
     meshTris = textureMap[meshVerts * 3 + 1];
     gSPDisplayList(gfx++, render_painting(tArray[0], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha));
+    sLastVerticesTimestamp = gGlobalTimer;
 
     // Update the ripple, may automatically reset the painting's state.
     painting_update_ripple_state(painting);
diff --git a/src/game/rendering_graph_node.c b/src/game/rendering_graph_node.c
index d5bf577..71656b4 100644
--- a/src/game/rendering_graph_node.c
+++ b/src/game/rendering_graph_node.c
@@ -39,6 +39,8 @@
 s16 gMatStackIndex;
 Mat4 gMatStack[32];
 Mtx *gMatStackFixed[32];
+Mat4 gMatStackInterpolated[32];
+Mtx *gMatStackInterpolatedFixed[32];
 
 /**
  * Animation nodes have state in global variables, so this struct captures
@@ -52,6 +54,7 @@ struct GeoAnimState {
     /*0x04*/ f32 translationMultiplier;
     /*0x08*/ u16 *attribute;
     /*0x0C*/ s16 *data;
+    s16 prevFrame;
 };
 
 // For some reason, this is a GeoAnimState struct, but the current state consists
@@ -61,6 +64,7 @@ struct GeoAnimState gGeoTempState;
 u8 gCurAnimType;
 u8 gCurAnimEnabled;
 s16 gCurrAnimFrame;
+s16 gPrevAnimFrame;
 f32 gCurAnimTranslationMultiplier;
 u16 *gCurrAnimAttribute;
 s16 *gCurAnimData;
@@ -129,6 +133,46 @@ u16 gAreaUpdateCounter = 0;
 LookAt lookAt;
 #endif
 
+static Gfx *sPerspectivePos;
+static Mtx *sPerspectiveMtx;
+
+struct {
+    Gfx *pos;
+    void *mtx;
+    void *displayList;
+} gMtxTbl[6400];
+s32 gMtxTblSize;
+
+static Gfx *sViewportPos;
+static Vp sPrevViewport;
+
+void mtx_patch_interpolated(void) {
+    s32 i;
+
+    if (sPerspectivePos != NULL) {
+        gSPMatrix(sPerspectivePos, VIRTUAL_TO_PHYSICAL(sPerspectiveMtx), G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH);
+    }
+
+    for (i = 0; i < gMtxTblSize; i++) {
+        Gfx *pos = gMtxTbl[i].pos;
+        gSPMatrix(pos++, VIRTUAL_TO_PHYSICAL(gMtxTbl[i].mtx),
+                  G_MTX_MODELVIEW | G_MTX_LOAD | G_MTX_NOPUSH);
+        gSPDisplayList(pos++, gMtxTbl[i].displayList);
+    }
+
+    if (sViewportPos != NULL) {
+        Gfx *saved = gDisplayListHead;
+        gDisplayListHead = sViewportPos;
+        make_viewport_clip_rect(&sPrevViewport);
+        gSPViewport(gDisplayListHead, VIRTUAL_TO_PHYSICAL(&sPrevViewport));
+        gDisplayListHead = saved;
+    }
+
+    gMtxTblSize = 0;
+    sPerspectivePos = NULL;
+    sViewportPos = NULL;
+}
+
 /**
  * Process a master list node.
  */
@@ -156,9 +200,14 @@ static void geo_process_master_list_sub(struct GraphNodeMasterList *node) {
         if ((currList = node->listHeads[i]) != NULL) {
             gDPSetRenderMode(gDisplayListHead++, modeList->modes[i], mode2List->modes[i]);
             while (currList != NULL) {
-                gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(currList->transform),
+                if ((u32) gMtxTblSize < sizeof(gMtxTbl) / sizeof(gMtxTbl[0])) {
+                    gMtxTbl[gMtxTblSize].pos = gDisplayListHead;
+                    gMtxTbl[gMtxTblSize].mtx = currList->transform;
+                    gMtxTbl[gMtxTblSize++].displayList = currList->displayList;
+                }
+                gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(currList->transformInterpolated),
                           G_MTX_MODELVIEW | G_MTX_LOAD | G_MTX_NOPUSH);
-                gSPDisplayList(gDisplayListHead++, currList->displayList);
+                gSPDisplayList(gDisplayListHead++, currList->displayListInterpolated);
                 currList = currList->next;
             }
         }
@@ -174,7 +223,7 @@ static void geo_process_master_list_sub(struct GraphNodeMasterList *node) {
  * parameter. Look at the RenderModeContainer struct to see the corresponding
  * render modes of layers.
  */
-static void geo_append_display_list(void *displayList, s16 layer) {
+static void geo_append_display_list2(void *displayList, void *displayListInterpolated, s16 layer) {
 
 #ifdef F3DEX_GBI_2
     gSPLookAt(gDisplayListHead++, &lookAt);
@@ -184,7 +233,9 @@ static void geo_append_display_list(void *displayList, s16 layer) {
             alloc_only_pool_alloc(gDisplayListHeap, sizeof(struct DisplayListNode));
 
         listNode->transform = gMatStackFixed[gMatStackIndex];
+        listNode->transformInterpolated = gMatStackInterpolatedFixed[gMatStackIndex];
         listNode->displayList = displayList;
+        listNode->displayListInterpolated = displayListInterpolated;
         listNode->next = 0;
         if (gCurGraphNodeMasterList->listHeads[layer] == 0) {
             gCurGraphNodeMasterList->listHeads[layer] = listNode;
@@ -195,6 +246,10 @@ static void geo_append_display_list(void *displayList, s16 layer) {
     }
 }
 
+static void geo_append_display_list(void *displayList, s16 layer) {
+    geo_append_display_list2(displayList, displayList, layer);
+}
+
 /**
  * Process the master list node.
  */
@@ -241,7 +296,9 @@ static void geo_process_perspective(struct GraphNodePerspective *node) {
     }
     if (node->fnNode.node.children != NULL) {
         u16 perspNorm;
+        Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
         Mtx *mtx = alloc_display_list(sizeof(*mtx));
+        f32 fovInterpolated;
 
 #ifdef VERSION_EU
         f32 aspect = ((f32) gCurGraphNodeRoot->width / (f32) gCurGraphNodeRoot->height) * 1.1f;
@@ -250,9 +307,23 @@ static void geo_process_perspective(struct GraphNodePerspective *node) {
 #endif
 
         guPerspective(mtx, &perspNorm, node->fov, aspect, node->near, node->far, 1.0f);
-        gSPPerspNormalize(gDisplayListHead++, perspNorm);
 
-        gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(mtx), G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH);
+        if (gGlobalTimer == node->prevTimestamp + 1 && gGlobalTimer != gLakituState.skipCameraInterpolationTimestamp) {
+
+            fovInterpolated = (node->prevFov + node->fov) / 2.0f;
+            guPerspective(mtxInterpolated, &perspNorm, fovInterpolated, aspect, node->near, node->far, 1.0f);
+            gSPPerspNormalize(gDisplayListHead++, perspNorm);
+
+            sPerspectivePos = gDisplayListHead;
+            sPerspectiveMtx = mtx;
+            gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(mtxInterpolated),
+                      G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH);
+        } else {
+            gSPPerspNormalize(gDisplayListHead++, perspNorm);
+            gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(mtx), G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH);
+        }
+        node->prevFov = node->fov;
+        node->prevTimestamp = gGlobalTimer;
 
         gCurGraphNodeCamFrustum = node;
         geo_process_node_and_siblings(node->fnNode.node.children);
@@ -297,6 +368,39 @@ static void geo_process_switch(struct GraphNodeSwitchCase *node) {
     }
 }
 
+void interpolate_vectors(Vec3f res, Vec3f a, Vec3f b) {
+    res[0] = (a[0] + b[0]) / 2.0f;
+    res[1] = (a[1] + b[1]) / 2.0f;
+    res[2] = (a[2] + b[2]) / 2.0f;
+}
+
+void interpolate_vectors_s16(Vec3s res, Vec3s a, Vec3s b) {
+    res[0] = (a[0] + b[0]) / 2;
+    res[1] = (a[1] + b[1]) / 2;
+    res[2] = (a[2] + b[2]) / 2;
+}
+
+static s16 interpolate_angle(s16 a, s16 b) {
+    s32 absDiff = b - a;
+    if (absDiff < 0) {
+        absDiff = -absDiff;
+    }
+    if (absDiff >= 0x4000 && absDiff <= 0xC000) {
+        return b;
+    }
+    if (absDiff <= 0x8000) {
+        return (a + b) / 2;
+    } else {
+        return (a + b) / 2 + 0x8000;
+    }
+}
+
+static void interpolate_angles(Vec3s res, Vec3s a, Vec3s b) {
+    res[0] = interpolate_angle(a[0], b[0]);
+    res[1] = interpolate_angle(a[1], b[1]);
+    res[2] = interpolate_angle(a[2], b[2]);
+}
+
 /**
  * Process a camera node.
  */
@@ -304,6 +408,9 @@ static void geo_process_camera(struct GraphNodeCamera *node) {
     Mat4 cameraTransform;
     Mtx *rollMtx = alloc_display_list(sizeof(*rollMtx));
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
+    Vec3f posInterpolated;
+    Vec3f focusInterpolated;
 
     if (node->fnNode.func != NULL) {
         node->fnNode.func(GEO_CONTEXT_RENDER, &node->fnNode.node, gMatStack[gMatStackIndex]);
@@ -314,12 +421,40 @@ static void geo_process_camera(struct GraphNodeCamera *node) {
 
     mtxf_lookat(cameraTransform, node->pos, node->focus, node->roll);
     mtxf_mul(gMatStack[gMatStackIndex + 1], cameraTransform, gMatStack[gMatStackIndex]);
+
+    if (gGlobalTimer == node->prevTimestamp + 1 && gGlobalTimer != gLakituState.skipCameraInterpolationTimestamp) {
+        interpolate_vectors(posInterpolated, node->prevPos, node->pos);
+        interpolate_vectors(focusInterpolated, node->prevFocus, node->focus);
+        float magnitude = 0;
+        for (int i = 0; i < 3; i++) {
+            float diff = node->pos[i] - node->prevPos[i];
+            magnitude += diff * diff;
+        }
+        if (magnitude > 500000) {
+            // Observed ~479000 in BBH when toggling R camera
+            // Can get over 3 million in VCUTM though...
+            vec3f_copy(posInterpolated, node->pos);
+            vec3f_copy(focusInterpolated, node->focus);
+        }
+    } else {
+        vec3f_copy(posInterpolated, node->pos);
+        vec3f_copy(focusInterpolated, node->focus);
+    }
+    vec3f_copy(node->prevPos, node->pos);
+    vec3f_copy(node->prevFocus, node->focus);
+    node->prevTimestamp = gGlobalTimer;
+    mtxf_lookat(cameraTransform, posInterpolated, focusInterpolated, node->roll);
+    mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], cameraTransform, gMatStackInterpolated[gMatStackIndex]);
+
     gMatStackIndex++;
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->fnNode.node.children != 0) {
         gCurGraphNodeCamera = node;
         node->matrixPtr = &gMatStack[gMatStackIndex];
+        node->matrixPtrInterpolated = &gMatStackInterpolated[gMatStackIndex];
         geo_process_node_and_siblings(node->fnNode.node.children);
         gCurGraphNodeCamera = NULL;
     }
@@ -336,13 +471,17 @@ static void geo_process_translation_rotation(struct GraphNodeTranslationRotation
     Mat4 mtxf;
     Vec3f translation;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
 
     vec3s_to_vec3f(translation, node->translation);
     mtxf_rotate_zxy_and_translate(mtxf, translation, node->rotation);
     mtxf_mul(gMatStack[gMatStackIndex + 1], mtxf, gMatStack[gMatStackIndex]);
+    mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mtxf, gMatStackInterpolated[gMatStackIndex]);
     gMatStackIndex++;
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -361,13 +500,17 @@ static void geo_process_translation(struct GraphNodeTranslation *node) {
     Mat4 mtxf;
     Vec3f translation;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
 
     vec3s_to_vec3f(translation, node->translation);
     mtxf_rotate_zxy_and_translate(mtxf, translation, gVec3sZero);
     mtxf_mul(gMatStack[gMatStackIndex + 1], mtxf, gMatStack[gMatStackIndex]);
+    mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mtxf, gMatStackInterpolated[gMatStackIndex]);
     gMatStackIndex++;
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -385,12 +528,23 @@ static void geo_process_translation(struct GraphNodeTranslation *node) {
 static void geo_process_rotation(struct GraphNodeRotation *node) {
     Mat4 mtxf;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
+    Vec3s rotationInterpolated;
 
     mtxf_rotate_zxy_and_translate(mtxf, gVec3fZero, node->rotation);
     mtxf_mul(gMatStack[gMatStackIndex + 1], mtxf, gMatStack[gMatStackIndex]);
+    if (gGlobalTimer == node->prevTimestamp + 1) {
+        interpolate_angles(rotationInterpolated, node->prevRotation, node->rotation);
+        mtxf_rotate_zxy_and_translate(mtxf, gVec3fZero, rotationInterpolated);
+    }
+    vec3s_copy(node->prevRotation, node->rotation);
+    node->prevTimestamp = gGlobalTimer;
+    mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mtxf, gMatStackInterpolated[gMatStackIndex]);
     gMatStackIndex++;
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -409,12 +563,16 @@ static void geo_process_scale(struct GraphNodeScale *node) {
     UNUSED Mat4 transform;
     Vec3f scaleVec;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
 
     vec3f_set(scaleVec, node->scale, node->scale, node->scale);
     mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex], scaleVec);
+    mtxf_scale_vec3f(gMatStackInterpolated[gMatStackIndex + 1], gMatStackInterpolated[gMatStackIndex], scaleVec);
     gMatStackIndex++;
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -433,21 +591,30 @@ static void geo_process_scale(struct GraphNodeScale *node) {
 static void geo_process_billboard(struct GraphNodeBillboard *node) {
     Vec3f translation;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
 
     gMatStackIndex++;
     vec3s_to_vec3f(translation, node->translation);
     mtxf_billboard(gMatStack[gMatStackIndex], gMatStack[gMatStackIndex - 1], translation,
                    gCurGraphNodeCamera->roll);
+    mtxf_billboard(gMatStackInterpolated[gMatStackIndex], gMatStackInterpolated[gMatStackIndex - 1], translation,
+                   gCurGraphNodeCamera->roll);
     if (gCurGraphNodeHeldObject != NULL) {
         mtxf_scale_vec3f(gMatStack[gMatStackIndex], gMatStack[gMatStackIndex],
                          gCurGraphNodeHeldObject->objNode->header.gfx.scale);
+        mtxf_scale_vec3f(gMatStackInterpolated[gMatStackIndex], gMatStackInterpolated[gMatStackIndex],
+                         gCurGraphNodeHeldObject->objNode->header.gfx.scale);
     } else if (gCurGraphNodeObject != NULL) {
         mtxf_scale_vec3f(gMatStack[gMatStackIndex], gMatStack[gMatStackIndex],
                          gCurGraphNodeObject->scale);
+        mtxf_scale_vec3f(gMatStackInterpolated[gMatStackIndex], gMatStackInterpolated[gMatStackIndex],
+                         gCurGraphNodeObject->scale);
     }
 
     mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = mtx;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -496,13 +663,39 @@ static void geo_process_generated_list(struct GraphNodeGenerated *node) {
  */
 static void geo_process_background(struct GraphNodeBackground *node) {
     Gfx *list = NULL;
+    Gfx *listInterpolated = NULL;
 
     if (node->fnNode.func != NULL) {
+        Vec3f posCopy;
+        Vec3f focusCopy;
+        Vec3f posInterpolated;
+        Vec3f focusInterpolated;
+
+        if (gGlobalTimer == node->prevCameraTimestamp + 1 &&
+            gGlobalTimer != gLakituState.skipCameraInterpolationTimestamp) {
+            interpolate_vectors(posInterpolated, node->prevCameraPos, gLakituState.pos);
+            interpolate_vectors(focusInterpolated, node->prevCameraFocus, gLakituState.focus);
+        } else {
+            vec3f_copy(posInterpolated, gLakituState.pos);
+            vec3f_copy(focusInterpolated, gLakituState.focus);
+        }
+        vec3f_copy(node->prevCameraPos, gLakituState.pos);
+        vec3f_copy(node->prevCameraFocus, gLakituState.focus);
+        node->prevCameraTimestamp = gGlobalTimer;
+
         list = node->fnNode.func(GEO_CONTEXT_RENDER, &node->fnNode.node,
                                  (struct AllocOnlyPool *) gMatStack[gMatStackIndex]);
+        vec3f_copy(posCopy, gLakituState.pos);
+        vec3f_copy(focusCopy, gLakituState.focus);
+        vec3f_copy(gLakituState.pos, posInterpolated);
+        vec3f_copy(gLakituState.focus, focusInterpolated);
+        listInterpolated = node->fnNode.func(GEO_CONTEXT_RENDER, &node->fnNode.node, NULL);
+        vec3f_copy(gLakituState.pos, posCopy);
+        vec3f_copy(gLakituState.focus, focusCopy);
     }
     if (list != 0) {
-        geo_append_display_list((void *) VIRTUAL_TO_PHYSICAL(list), node->fnNode.node.flags >> 8);
+        geo_append_display_list2((void *) VIRTUAL_TO_PHYSICAL(list),
+                                 (void *) VIRTUAL_TO_PHYSICAL(listInterpolated), node->fnNode.node.flags >> 8);
     } else if (gCurGraphNodeMasterList != NULL) {
 #ifndef F3DEX_GBI_2E
         Gfx *gfxStart = alloc_display_list(sizeof(Gfx) * 7);
@@ -527,61 +720,81 @@ static void geo_process_background(struct GraphNodeBackground *node) {
     }
 }
 
-/**
- * Render an animated part. The current animation state is not part of the node
- * but set in global variables. If an animated part is skipped, everything afterwards desyncs.
- */
-static void geo_process_animated_part(struct GraphNodeAnimatedPart *node) {
-    Mat4 matrix;
-    Vec3s rotation;
-    Vec3f translation;
-    Mtx *matrixPtr = alloc_display_list(sizeof(*matrixPtr));
-
-    vec3s_copy(rotation, gVec3sZero);
-    vec3f_set(translation, node->translation[0], node->translation[1], node->translation[2]);
-    if (gCurAnimType == ANIM_TYPE_TRANSLATION) {
-        translation[0] += gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+static void anim_process(Vec3f translation, Vec3s rotation, u8 *animType, s16 animFrame, u16 **animAttribute) {
+    if (*animType == ANIM_TYPE_TRANSLATION) {
+        translation[0] += gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                           * gCurAnimTranslationMultiplier;
-        translation[1] += gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+        translation[1] += gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                           * gCurAnimTranslationMultiplier;
-        translation[2] += gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+        translation[2] += gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                           * gCurAnimTranslationMultiplier;
-        gCurAnimType = ANIM_TYPE_ROTATION;
+        *animType = ANIM_TYPE_ROTATION;
     } else {
-        if (gCurAnimType == ANIM_TYPE_LATERAL_TRANSLATION) {
+        if (*animType == ANIM_TYPE_LATERAL_TRANSLATION) {
             translation[0] +=
-                gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+                gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                 * gCurAnimTranslationMultiplier;
-            gCurrAnimAttribute += 2;
+            *animAttribute += 2;
             translation[2] +=
-                gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+                gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                 * gCurAnimTranslationMultiplier;
-            gCurAnimType = ANIM_TYPE_ROTATION;
+            *animType = ANIM_TYPE_ROTATION;
         } else {
-            if (gCurAnimType == ANIM_TYPE_VERTICAL_TRANSLATION) {
-                gCurrAnimAttribute += 2;
+            if (*animType == ANIM_TYPE_VERTICAL_TRANSLATION) {
+                *animAttribute += 2;
                 translation[1] +=
-                    gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)]
+                    gCurAnimData[retrieve_animation_index(animFrame, animAttribute)]
                     * gCurAnimTranslationMultiplier;
-                gCurrAnimAttribute += 2;
-                gCurAnimType = ANIM_TYPE_ROTATION;
-            } else if (gCurAnimType == ANIM_TYPE_NO_TRANSLATION) {
-                gCurrAnimAttribute += 6;
-                gCurAnimType = ANIM_TYPE_ROTATION;
+                *animAttribute += 2;
+                *animType = ANIM_TYPE_ROTATION;
+            } else if (*animType == ANIM_TYPE_NO_TRANSLATION) {
+                *animAttribute += 6;
+                *animType = ANIM_TYPE_ROTATION;
             }
         }
     }
 
-    if (gCurAnimType == ANIM_TYPE_ROTATION) {
-        rotation[0] = gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)];
-        rotation[1] = gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)];
-        rotation[2] = gCurAnimData[retrieve_animation_index(gCurrAnimFrame, &gCurrAnimAttribute)];
+    if (*animType == ANIM_TYPE_ROTATION) {
+        rotation[0] = gCurAnimData[retrieve_animation_index(animFrame, animAttribute)];
+        rotation[1] = gCurAnimData[retrieve_animation_index(animFrame, animAttribute)];
+        rotation[2] = gCurAnimData[retrieve_animation_index(animFrame, animAttribute)];
     }
+}
+
+/**
+ * Render an animated part. The current animation state is not part of the node
+ * but set in global variables. If an animated part is skipped, everything afterwards desyncs.
+ */
+static void geo_process_animated_part(struct GraphNodeAnimatedPart *node) {
+    Mat4 matrix;
+    Vec3s rotation;
+    Vec3f translation;
+    Vec3s rotationInterpolated;
+    Vec3f translationInterpolated;
+    Mtx *matrixPtr = alloc_display_list(sizeof(*matrixPtr));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
+    u16 *animAttribute = gCurrAnimAttribute;
+    u8 animType = gCurAnimType;
+
+    vec3s_copy(rotation, gVec3sZero);
+    vec3f_set(translation, node->translation[0], node->translation[1], node->translation[2]);
+    vec3s_copy(rotationInterpolated, rotation);
+    vec3f_copy(translationInterpolated, translation);
+
+    anim_process(translationInterpolated, rotationInterpolated, &animType, gPrevAnimFrame, &animAttribute);
+    anim_process(translation, rotation, &gCurAnimType, gCurrAnimFrame, &gCurrAnimAttribute);
+    interpolate_vectors(translationInterpolated, translationInterpolated, translation);
+    interpolate_angles(rotationInterpolated, rotationInterpolated, rotation);
+
     mtxf_rotate_xyz_and_translate(matrix, translation, rotation);
     mtxf_mul(gMatStack[gMatStackIndex + 1], matrix, gMatStack[gMatStackIndex]);
+    mtxf_rotate_xyz_and_translate(matrix, translationInterpolated, rotationInterpolated);
+    mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], matrix, gMatStackInterpolated[gMatStackIndex]);
     gMatStackIndex++;
     mtxf_to_mtx(matrixPtr, gMatStack[gMatStackIndex]);
     gMatStackFixed[gMatStackIndex] = matrixPtr;
+    mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+    gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
     if (node->displayList != NULL) {
         geo_append_display_list(node->displayList, node->node.flags >> 8);
     }
@@ -613,6 +826,17 @@ void geo_set_animation_globals(struct GraphNodeObject_sub *node, s32 hasAnimatio
     }
 
     gCurrAnimFrame = node->animFrame;
+    if (node->prevAnimPtr == anim && node->prevAnimID == node->animID &&
+        gGlobalTimer == node->prevAnimFrameTimestamp + 1) {
+        gPrevAnimFrame = node->prevAnimFrame;
+    } else {
+        gPrevAnimFrame = node->animFrame;
+    }
+    node->prevAnimPtr = anim;
+    node->prevAnimID = node->animID;
+    node->prevAnimFrame = node->animFrame;
+    node->prevAnimFrameTimestamp = gGlobalTimer;
+
     gCurAnimEnabled = (anim->flags & ANIM_FLAG_5) == 0;
     gCurrAnimAttribute = segmented_to_virtual((void *) anim->index);
     gCurAnimData = segmented_to_virtual((void *) anim->values);
@@ -631,8 +855,10 @@ void geo_set_animation_globals(struct GraphNodeObject_sub *node, s32 hasAnimatio
  */
 static void geo_process_shadow(struct GraphNodeShadow *node) {
     Gfx *shadowList;
+    Gfx *shadowListInterpolated;
     Mat4 mtxf;
     Vec3f shadowPos;
+    Vec3f shadowPosInterpolated;
     Vec3f animOffset;
     f32 objScale;
     f32 shadowScale;
@@ -640,6 +866,7 @@ static void geo_process_shadow(struct GraphNodeShadow *node) {
     f32 cosAng;
     struct GraphNode *geo;
     Mtx *mtx;
+    Mtx *mtxInterpolated;
 
     if (gCurGraphNodeCamera != NULL && gCurGraphNodeObject != NULL) {
         if (gCurGraphNodeHeldObject != NULL) {
@@ -678,21 +905,57 @@ static void geo_process_shadow(struct GraphNodeShadow *node) {
             }
         }
 
+        if (gCurGraphNodeHeldObject != NULL) {
+            if (gGlobalTimer == gCurGraphNodeHeldObject->prevShadowPosTimestamp + 1) {
+                interpolate_vectors(shadowPosInterpolated, gCurGraphNodeHeldObject->prevShadowPos, shadowPos);
+            } else {
+                vec3f_copy(shadowPosInterpolated, shadowPos);
+            }
+            vec3f_copy(gCurGraphNodeHeldObject->prevShadowPos, shadowPos);
+            gCurGraphNodeHeldObject->prevShadowPosTimestamp = gGlobalTimer;
+        } else {
+            if (gGlobalTimer == gCurGraphNodeObject->prevShadowPosTimestamp + 1 &&
+                gGlobalTimer != gCurGraphNodeObject->skipInterpolationTimestamp) {
+                interpolate_vectors(shadowPosInterpolated, gCurGraphNodeObject->prevShadowPos, shadowPos);
+            } else {
+                vec3f_copy(shadowPosInterpolated, shadowPos);
+            }
+            vec3f_copy(gCurGraphNodeObject->prevShadowPos, shadowPos);
+            gCurGraphNodeObject->prevShadowPosTimestamp = gGlobalTimer;
+        }
+
+        extern u8 gInterpolatingSurfaces;
+        gInterpolatingSurfaces = TRUE;
+        shadowListInterpolated = create_shadow_below_xyz(shadowPosInterpolated[0], shadowPosInterpolated[1],
+                                                         shadowPosInterpolated[2], shadowScale,
+                                                         node->shadowSolidity, node->shadowType);
+        gInterpolatingSurfaces = FALSE;
         shadowList = create_shadow_below_xyz(shadowPos[0], shadowPos[1], shadowPos[2], shadowScale,
                                              node->shadowSolidity, node->shadowType);
-        if (shadowList != NULL) {
+        if (shadowListInterpolated != NULL && shadowList != NULL) {
             mtx = alloc_display_list(sizeof(*mtx));
+            mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
             gMatStackIndex++;
+
             mtxf_translate(mtxf, shadowPos);
             mtxf_mul(gMatStack[gMatStackIndex], mtxf, *gCurGraphNodeCamera->matrixPtr);
             mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
             gMatStackFixed[gMatStackIndex] = mtx;
+
+            mtxf_translate(mtxf, shadowPosInterpolated);
+            mtxf_mul(gMatStackInterpolated[gMatStackIndex], mtxf, *gCurGraphNodeCamera->matrixPtrInterpolated);
+            mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+            gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
+
             if (gShadowAboveWaterOrLava == 1) {
-                geo_append_display_list((void *) VIRTUAL_TO_PHYSICAL(shadowList), 4);
+                geo_append_display_list2((void *) VIRTUAL_TO_PHYSICAL(shadowList),
+                                         (void *) VIRTUAL_TO_PHYSICAL(shadowListInterpolated), 4);
             } else if (gMarioOnIceOrCarpet == 1) {
-                geo_append_display_list((void *) VIRTUAL_TO_PHYSICAL(shadowList), 5);
+                geo_append_display_list2((void *) VIRTUAL_TO_PHYSICAL(shadowList),
+                                         (void *) VIRTUAL_TO_PHYSICAL(shadowListInterpolated), 5);
             } else {
-                geo_append_display_list((void *) VIRTUAL_TO_PHYSICAL(shadowList), 6);
+                geo_append_display_list2((void *) VIRTUAL_TO_PHYSICAL(shadowList),
+                                         (void *) VIRTUAL_TO_PHYSICAL(shadowListInterpolated), 6);
             }
             gMatStackIndex--;
         }
@@ -789,31 +1052,101 @@ static int obj_is_in_view(struct GraphNodeObject *node, Mat4 matrix) {
     return TRUE;
 }
 
+static void interpolate_matrix(Mat4 result, Mat4 a, Mat4 b) {
+    s32 i, j;
+    for (i = 0; i < 4; i++) {
+        for (j = 0; j < 4; j++) {
+            result[i][j] = (a[i][j] + b[i][j]) / 2.0f;
+        }
+    }
+}
+
 /**
  * Process an object node.
  */
 static void geo_process_object(struct Object *node) {
     Mat4 mtxf;
     s32 hasAnimation = (node->header.gfx.node.flags & GRAPH_RENDER_HAS_ANIMATION) != 0;
+    Vec3f scaleInterpolated;
 
     if (node->header.gfx.unk18 == gCurGraphNodeRoot->areaIndex) {
         if (node->header.gfx.throwMatrix != NULL) {
             mtxf_mul(gMatStack[gMatStackIndex + 1], *node->header.gfx.throwMatrix,
                      gMatStack[gMatStackIndex]);
+            if (gGlobalTimer == node->header.gfx.prevThrowMatrixTimestamp + 1 &&
+                gGlobalTimer != node->header.gfx.skipInterpolationTimestamp) {
+                interpolate_matrix(mtxf, *node->header.gfx.throwMatrix, node->header.gfx.prevThrowMatrix);
+                mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mtxf,
+                     gMatStackInterpolated[gMatStackIndex]);
+            } else {
+                mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], (void *) node->header.gfx.throwMatrix,
+                         gMatStackInterpolated[gMatStackIndex]);
+            }
+            mtxf_copy(node->header.gfx.prevThrowMatrix, *node->header.gfx.throwMatrix);
+            node->header.gfx.prevThrowMatrixTimestamp = gGlobalTimer;
         } else if (node->header.gfx.node.flags & GRAPH_RENDER_CYLBOARD) {
+            Vec3f posInterpolated;
+            if (gGlobalTimer == node->header.gfx.prevTimestamp + 1 &&
+                gGlobalTimer != node->header.gfx.skipInterpolationTimestamp) {
+                interpolate_vectors(posInterpolated, node->header.gfx.prevPos, node->header.gfx.pos);
+            } else {
+                vec3f_copy(posInterpolated, node->header.gfx.pos);
+            }
+            vec3f_copy(node->header.gfx.prevPos, node->header.gfx.pos);
+            node->header.gfx.prevTimestamp = gGlobalTimer;
             mtxf_cylboard(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex],
                            node->header.gfx.pos, gCurGraphNodeCamera->roll);
+            mtxf_cylboard(gMatStackInterpolated[gMatStackIndex + 1], gMatStackInterpolated[gMatStackIndex],
+                           posInterpolated, gCurGraphNodeCamera->roll);
         } else if (node->header.gfx.node.flags & GRAPH_RENDER_BILLBOARD) {
+            Vec3f posInterpolated;
+            if (gGlobalTimer == node->header.gfx.prevTimestamp + 1 &&
+                gGlobalTimer != node->header.gfx.skipInterpolationTimestamp) {
+                interpolate_vectors(posInterpolated, node->header.gfx.prevPos, node->header.gfx.pos);
+            } else {
+                vec3f_copy(posInterpolated, node->header.gfx.pos);
+            }
+            vec3f_copy(node->header.gfx.prevPos, node->header.gfx.pos);
+            node->header.gfx.prevTimestamp = gGlobalTimer;
             mtxf_billboard(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex],
                            node->header.gfx.pos, gCurGraphNodeCamera->roll);
+            mtxf_billboard(gMatStackInterpolated[gMatStackIndex + 1], gMatStackInterpolated[gMatStackIndex],
+                           posInterpolated, gCurGraphNodeCamera->roll);
         } else {
+            Vec3f posInterpolated;
+            Vec3s angleInterpolated;
+            if (gGlobalTimer == node->header.gfx.prevTimestamp + 1 &&
+                gGlobalTimer != node->header.gfx.skipInterpolationTimestamp) {
+                interpolate_vectors(posInterpolated, node->header.gfx.prevPos, node->header.gfx.pos);
+                interpolate_angles(angleInterpolated, node->header.gfx.prevAngle, node->header.gfx.angle);
+            } else {
+                vec3f_copy(posInterpolated, node->header.gfx.pos);
+                vec3s_copy(angleInterpolated, node->header.gfx.angle);
+            }
+            vec3f_copy(node->header.gfx.prevPos, node->header.gfx.pos);
+            vec3s_copy(node->header.gfx.prevAngle, node->header.gfx.angle);
+            node->header.gfx.prevTimestamp = gGlobalTimer;
             mtxf_rotate_zxy_and_translate(mtxf, node->header.gfx.pos, node->header.gfx.angle);
             mtxf_mul(gMatStack[gMatStackIndex + 1], mtxf, gMatStack[gMatStackIndex]);
+            mtxf_rotate_zxy_and_translate(mtxf, posInterpolated, angleInterpolated);
+            mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mtxf, gMatStackInterpolated[gMatStackIndex]);
+        }
+
+        if (gGlobalTimer == node->header.gfx.prevScaleTimestamp + 1 &&
+            gGlobalTimer != node->header.gfx.skipInterpolationTimestamp) {
+            interpolate_vectors(scaleInterpolated, node->header.gfx.prevScale, node->header.gfx.scale);
+        } else {
+            vec3f_copy(scaleInterpolated, node->header.gfx.scale);
         }
+        vec3f_copy(node->header.gfx.prevScale, node->header.gfx.scale);
+        node->header.gfx.prevScaleTimestamp = gGlobalTimer;
 
         mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex + 1],
                          node->header.gfx.scale);
+        mtxf_scale_vec3f(gMatStackInterpolated[gMatStackIndex + 1], gMatStackInterpolated[gMatStackIndex + 1],
+                         scaleInterpolated);
         node->header.gfx.throwMatrix = &gMatStack[++gMatStackIndex];
+        node->header.gfx.throwMatrixInterpolated = &gMatStackInterpolated[gMatStackIndex];
         node->header.gfx.cameraToObject[0] = gMatStack[gMatStackIndex][3][0];
         node->header.gfx.cameraToObject[1] = gMatStack[gMatStackIndex][3][1];
         node->header.gfx.cameraToObject[2] = gMatStack[gMatStackIndex][3][2];
@@ -824,9 +1157,12 @@ static void geo_process_object(struct Object *node) {
         }
         if (obj_is_in_view(&node->header.gfx, gMatStack[gMatStackIndex])) {
             Mtx *mtx = alloc_display_list(sizeof(*mtx));
+            Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
 
             mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
             gMatStackFixed[gMatStackIndex] = mtx;
+            mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+            gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
             if (node->header.gfx.sharedChild != NULL) {
                 gCurGraphNodeObject = (struct GraphNodeObject *) node;
                 node->header.gfx.sharedChild->parent = &node->header.gfx.node;
@@ -837,11 +1173,16 @@ static void geo_process_object(struct Object *node) {
             if (node->header.gfx.node.children != NULL) {
                 geo_process_node_and_siblings(node->header.gfx.node.children);
             }
+        } else {
+            node->header.gfx.prevThrowMatrixTimestamp = 0;
+            node->header.gfx.prevTimestamp = 0;
+            node->header.gfx.prevScaleTimestamp = 0;
         }
 
         gMatStackIndex--;
         gCurAnimType = ANIM_TYPE_NONE;
         node->header.gfx.throwMatrix = NULL;
+        node->header.gfx.throwMatrixInterpolated = NULL;
     }
 }
 
@@ -868,6 +1209,8 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) {
     Mat4 mat;
     Vec3f translation;
     Mtx *mtx = alloc_display_list(sizeof(*mtx));
+    Mtx *mtxInterpolated = alloc_display_list(sizeof(*mtxInterpolated));
+    Vec3f scaleInterpolated;
 
 #ifdef F3DEX_GBI_2
     gSPLookAt(gDisplayListHead++, &lookAt);
@@ -883,6 +1226,14 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) {
         translation[1] = node->translation[1] / 4.0f;
         translation[2] = node->translation[2] / 4.0f;
 
+        if (gGlobalTimer == node->objNode->header.gfx.prevScaleTimestamp + 1) {
+            interpolate_vectors(scaleInterpolated, node->objNode->header.gfx.prevScale, node->objNode->header.gfx.scale);
+        } else {
+            vec3f_copy(scaleInterpolated, node->objNode->header.gfx.scale);
+        }
+        vec3f_copy(node->objNode->header.gfx.prevScale, node->objNode->header.gfx.scale);
+        node->objNode->header.gfx.prevScaleTimestamp = gGlobalTimer;
+
         mtxf_translate(mat, translation);
         mtxf_copy(gMatStack[gMatStackIndex + 1], *gCurGraphNodeObject->throwMatrix);
         gMatStack[gMatStackIndex + 1][3][0] = gMatStack[gMatStackIndex][3][0];
@@ -891,6 +1242,13 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) {
         mtxf_mul(gMatStack[gMatStackIndex + 1], mat, gMatStack[gMatStackIndex + 1]);
         mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex + 1],
                          node->objNode->header.gfx.scale);
+        mtxf_copy(gMatStackInterpolated[gMatStackIndex + 1], (void *) gCurGraphNodeObject->throwMatrixInterpolated);
+        gMatStackInterpolated[gMatStackIndex + 1][3][0] = gMatStackInterpolated[gMatStackIndex][3][0];
+        gMatStackInterpolated[gMatStackIndex + 1][3][1] = gMatStackInterpolated[gMatStackIndex][3][1];
+        gMatStackInterpolated[gMatStackIndex + 1][3][2] = gMatStackInterpolated[gMatStackIndex][3][2];
+        mtxf_mul(gMatStackInterpolated[gMatStackIndex + 1], mat, gMatStackInterpolated[gMatStackIndex + 1]);
+        mtxf_scale_vec3f(gMatStackInterpolated[gMatStackIndex + 1], gMatStackInterpolated[gMatStackIndex + 1],
+                         scaleInterpolated);
         if (node->fnNode.func != NULL) {
             node->fnNode.func(GEO_CONTEXT_HELD_OBJ, &node->fnNode.node,
                               (struct AllocOnlyPool *) gMatStack[gMatStackIndex + 1]);
@@ -898,12 +1256,15 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) {
         gMatStackIndex++;
         mtxf_to_mtx(mtx, gMatStack[gMatStackIndex]);
         gMatStackFixed[gMatStackIndex] = mtx;
+        mtxf_to_mtx(mtxInterpolated, gMatStackInterpolated[gMatStackIndex]);
+        gMatStackInterpolatedFixed[gMatStackIndex] = mtxInterpolated;
         gGeoTempState.type = gCurAnimType;
         gGeoTempState.enabled = gCurAnimEnabled;
         gGeoTempState.frame = gCurrAnimFrame;
         gGeoTempState.translationMultiplier = gCurAnimTranslationMultiplier;
         gGeoTempState.attribute = gCurrAnimAttribute;
         gGeoTempState.data = gCurAnimData;
+        gGeoTempState.prevFrame = gPrevAnimFrame;
         gCurAnimType = 0;
         gCurGraphNodeHeldObject = (void *) node;
         if (node->objNode->header.gfx.unk38.curAnim != NULL) {
@@ -918,6 +1279,7 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) {
         gCurAnimTranslationMultiplier = gGeoTempState.translationMultiplier;
         gCurrAnimAttribute = gGeoTempState.attribute;
         gCurAnimData = gGeoTempState.data;
+        gPrevAnimFrame = gGeoTempState.prevFrame;
         gMatStackIndex--;
     }
 
@@ -1039,6 +1401,7 @@ void geo_process_root(struct GraphNodeRoot *node, Vp *b, Vp *c, s32 clearColor)
     if (node->node.flags & GRAPH_RENDER_ACTIVE) {
         Mtx *initialMatrix;
         Vp *viewport = alloc_display_list(sizeof(*viewport));
+        Vp *viewportInterpolated = viewport;
 
         gDisplayListHeap = alloc_only_pool_init(main_pool_available() - sizeof(struct AllocOnlyPool),
                                                 MEMORY_POOL_LEFT);
@@ -1049,7 +1412,12 @@ void geo_process_root(struct GraphNodeRoot *node, Vp *b, Vp *c, s32 clearColor)
         vec3s_set(viewport->vp.vscale, node->width * 4, node->height * 4, 511);
         if (b != NULL) {
             clear_frame_buffer(clearColor);
-            make_viewport_clip_rect(b);
+            viewportInterpolated = alloc_display_list(sizeof(*viewportInterpolated));
+            interpolate_vectors_s16(viewportInterpolated->vp.vtrans, sPrevViewport.vp.vtrans, b->vp.vtrans);
+            interpolate_vectors_s16(viewportInterpolated->vp.vscale, sPrevViewport.vp.vscale, b->vp.vscale);
+
+            sViewportPos = gDisplayListHead;
+            make_viewport_clip_rect(viewportInterpolated);
             *viewport = *b;
         }
 
@@ -1057,11 +1425,16 @@ void geo_process_root(struct GraphNodeRoot *node, Vp *b, Vp *c, s32 clearColor)
             clear_frame_buffer(clearColor);
             make_viewport_clip_rect(c);
         }
+        sPrevViewport = *viewport;
 
         mtxf_identity(gMatStack[gMatStackIndex]);
         mtxf_to_mtx(initialMatrix, gMatStack[gMatStackIndex]);
         gMatStackFixed[gMatStackIndex] = initialMatrix;
-        gSPViewport(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(viewport));
+
+        mtxf_identity(gMatStackInterpolated[gMatStackIndex]);
+        gMatStackInterpolatedFixed[gMatStackIndex] = initialMatrix;
+
+        gSPViewport(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(viewportInterpolated));
         gSPMatrix(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(gMatStackFixed[gMatStackIndex]),
                   G_MTX_MODELVIEW | G_MTX_LOAD | G_MTX_NOPUSH);
         gCurGraphNodeRoot = node;
diff --git a/src/game/screen_transition.c b/src/game/screen_transition.c
index b49ddaf..d6656af 100644
--- a/src/game/screen_transition.c
+++ b/src/game/screen_transition.c
@@ -16,6 +16,19 @@
 u8 sTransitionColorFadeCount[4] = { 0 };
 u16 sTransitionTextureFadeCount[2] = { 0 };
 
+static Gfx *sScreenTransitionVerticesPos[2];
+static Vtx *sScreenTransitionVertices;
+
+void patch_screen_transition_interpolated(void) {
+    if (sScreenTransitionVerticesPos[0] != NULL) {
+        gSPVertex(sScreenTransitionVerticesPos[0], VIRTUAL_TO_PHYSICAL(sScreenTransitionVertices), 8, 0);
+        gSPVertex(sScreenTransitionVerticesPos[1], VIRTUAL_TO_PHYSICAL(sScreenTransitionVertices), 4, 0);
+        sScreenTransitionVerticesPos[0] = NULL;
+        sScreenTransitionVerticesPos[1] = NULL;
+        sScreenTransitionVertices = NULL;
+    }
+}
+
 s32 set_and_reset_transition_fade_timer(s8 fadeTimer, u8 transTime) {
     s32 reset = FALSE;
 
@@ -85,14 +98,29 @@ s32 render_fade_transition_into_color(s8 fadeTimer, u8 transTime, struct WarpTra
     return dl_transition_color(fadeTimer, transTime, transData, alpha);
 }
 
+#if 0
+
 s16 calc_tex_transition_radius(s8 fadeTimer, s8 transTime, struct WarpTransitionData *transData) {
     f32 texRadius = transData->endTexRadius - transData->startTexRadius;
     f32 radiusTime = sTransitionColorFadeCount[fadeTimer] * texRadius / (f32)(transTime - 1);
     f32 result = transData->startTexRadius + radiusTime;
 
-    return (s16)(result + 0.5);;
+    return (s16)(result + 0.5);
 }
 
+#else
+
+s16 calc_tex_transition_radius(s8 fadeTimer, f32 interpolationFraction, s8 transTime, struct WarpTransitionData *transData) {
+    f32 texRadius = transData->endTexRadius - transData->startTexRadius;
+    f32 radiusTime = (sTransitionColorFadeCount[fadeTimer] == 0 ? 0 :
+                      sTransitionColorFadeCount[fadeTimer] - 1 + interpolationFraction) * texRadius / (f32)(transTime - 1);
+    f32 result = transData->startTexRadius + radiusTime;
+
+    return (s16)(result + 0.5);
+}
+
+#endif
+
 f32 calc_tex_transition_time(s8 fadeTimer, s8 transTime, struct WarpTransitionData *transData) {
     f32 startX = transData->startTexX;
     f32 startY = transData->startTexY;
@@ -166,6 +194,8 @@ void *sTextureTransitionID[] = {
     texture_transition_bowser_half,
 };
 
+#if 0
+
 s32 render_textured_transition(s8 fadeTimer, s8 transTime, struct WarpTransitionData *transData, s8 texID, s8 transTexType) {
     f32 texTransTime = calc_tex_transition_time(fadeTimer, transTime, transData);
     u16 texTransPos = convert_tex_transition_angle_to_pos(transData);
@@ -206,6 +236,56 @@ s32 render_textured_transition(s8 fadeTimer, s8 transTime, struct WarpTransition
     return set_and_reset_transition_fade_timer(fadeTimer, transTime);
 }
 
+#else
+
+s32 render_textured_transition(s8 fadeTimer, s8 transTime, struct WarpTransitionData *transData, s8 texID, s8 transTexType) {
+    f32 texTransTime = calc_tex_transition_time(fadeTimer, transTime, transData);
+    u16 texTransPos = convert_tex_transition_angle_to_pos(transData);
+    s16 centerTransX = center_tex_transition_x(transData, texTransTime, texTransPos);
+    s16 centerTransY = center_tex_transition_y(transData, texTransTime, texTransPos);
+    s16 texTransRadius = calc_tex_transition_radius(fadeTimer, 1.0f, transTime, transData);
+    s16 texTransRadiusInterpolated = calc_tex_transition_radius(fadeTimer, 0.5f, transTime, transData);
+    Vtx *verts = alloc_display_list(8 * sizeof(*verts));
+    Vtx *vertsInterpolated = alloc_display_list(8 * sizeof(*vertsInterpolated));
+
+    if (verts != NULL && vertsInterpolated != NULL) {
+        load_tex_transition_vertex(verts, fadeTimer, transData, centerTransX, centerTransY, texTransRadius, transTexType);
+        load_tex_transition_vertex(vertsInterpolated, fadeTimer, transData, centerTransX, centerTransY, texTransRadiusInterpolated, transTexType);
+        sScreenTransitionVertices = verts;
+        gSPDisplayList(gDisplayListHead++, dl_proj_mtx_fullscreen)
+        gDPSetCombineMode(gDisplayListHead++, G_CC_SHADE, G_CC_SHADE);
+        gDPSetRenderMode(gDisplayListHead++, G_RM_AA_OPA_SURF, G_RM_AA_OPA_SURF2);
+        sScreenTransitionVerticesPos[0] = gDisplayListHead;
+        gSPVertex(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(vertsInterpolated), 8, 0);
+        gSPDisplayList(gDisplayListHead++, dl_transition_draw_filled_region);
+        gDPPipeSync(gDisplayListHead++);
+        gDPSetCombineMode(gDisplayListHead++, G_CC_MODULATEIDECALA, G_CC_MODULATEIDECALA);
+        gDPSetRenderMode(gDisplayListHead++, G_RM_AA_XLU_SURF, G_RM_AA_XLU_SURF2);
+        gDPSetTextureFilter(gDisplayListHead++, G_TF_BILERP);
+        switch (transTexType) {
+        case TRANS_TYPE_MIRROR:
+            gDPLoadTextureBlock(gDisplayListHead++, sTextureTransitionID[texID], G_IM_FMT_IA, G_IM_SIZ_8b, 32, 64, 0,
+                G_TX_WRAP | G_TX_MIRROR, G_TX_WRAP | G_TX_MIRROR, 5, 6, G_TX_NOLOD, G_TX_NOLOD);
+            break;
+        case TRANS_TYPE_CLAMP:
+            gDPLoadTextureBlock(gDisplayListHead++, sTextureTransitionID[texID], G_IM_FMT_IA, G_IM_SIZ_8b, 64, 64, 0,
+                G_TX_CLAMP, G_TX_CLAMP, 6, 6, G_TX_NOLOD, G_TX_NOLOD);
+            break;
+        }
+        gSPTexture(gDisplayListHead++, 0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON);
+        sScreenTransitionVerticesPos[1] = gDisplayListHead;
+        gSPVertex(gDisplayListHead++, VIRTUAL_TO_PHYSICAL(vertsInterpolated), 4, 0);
+        gSPDisplayList(gDisplayListHead++, dl_draw_quad_verts_0123);
+        gSPTexture(gDisplayListHead++, 0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_OFF);
+        gSPDisplayList(gDisplayListHead++, dl_screen_transition_end);
+        sTransitionTextureFadeCount[fadeTimer] += transData->texTimer;
+    } else {
+    }
+    return set_and_reset_transition_fade_timer(fadeTimer, transTime);
+}
+
+#endif
+
 int render_screen_transition(s8 fadeTimer, s8 transType, u8 transTime, struct WarpTransitionData *transData) {
     switch (transType) {
         case WARP_TRANSITION_FADE_FROM_COLOR:
diff --git a/src/menu/intro_geo.c b/src/menu/intro_geo.c
index 37c6752..d823d40 100644
--- a/src/menu/intro_geo.c
+++ b/src/menu/intro_geo.c
@@ -1,5 +1,6 @@
 #include <PR/ultratypes.h>
 
+#include "engine/math_util.h"
 #include "game/memory.h"
 #include "game/segment2.h"
 #include "game/segment7.h"
@@ -70,6 +71,18 @@ s8 gameOverBackgroundTable[] = {
 s8 gameOverBackgroundFlipOrder[] = { 0x00, 0x01, 0x02, 0x03, 0x07, 0x0B,
                                      0x0a, 0x09, 0x08, 0x04, 0x05, 0x06 };
 
+static Gfx *sIntroScalePos;
+static Vec3f sIntroScale;
+
+void patch_title_screen_scales(void) {
+    if (sIntroScalePos != NULL) {
+        Mtx *scaleMat = alloc_display_list(sizeof(*scaleMat));
+        guScale(scaleMat, sIntroScale[0], sIntroScale[1], sIntroScale[2]);
+        gSPMatrix(sIntroScalePos, scaleMat, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH);
+        sIntroScalePos = NULL;
+    }
+}
+
 Gfx *geo_title_screen(s32 sp50, struct GraphNode *sp54, UNUSED void *context) {
     struct GraphNode *graphNode; // sp4c
     Gfx *displayList;            // sp48
@@ -80,6 +93,8 @@ Gfx *geo_title_screen(s32 sp50, struct GraphNode *sp54, UNUSED void *context) {
     f32 scaleX;                  // sp34
     f32 scaleY;                  // sp30
     f32 scaleZ;                  // sp2c
+    Vec3f scale;
+    Vec3f scaleInterpolated;
     graphNode = sp54;
     displayList = NULL;
     displayListIter = NULL;
@@ -110,7 +125,11 @@ Gfx *geo_title_screen(s32 sp50, struct GraphNode *sp54, UNUSED void *context) {
             scaleY = 0.0f;
             scaleZ = 0.0f;
         }
-        guScale(scaleMat, scaleX, scaleY, scaleZ);
+        vec3f_set(scale, scaleX, scaleY, scaleZ);
+        interpolate_vectors(scaleInterpolated, sIntroScale, scale);
+        vec3f_set(sIntroScale, scaleX, scaleY, scaleZ);
+        guScale(scaleMat, scaleInterpolated[0], scaleInterpolated[1], scaleInterpolated[2]);
+        sIntroScalePos = displayListIter;
         gSPMatrix(displayListIter++, scaleMat, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH);
         gSPDisplayList(displayListIter++, &intro_seg7_dl_0700B3A0);
         gSPPopMatrix(displayListIter++, G_MTX_MODELVIEW);
diff --git a/src/pc/gfx/gfx_dxgi.cpp b/src/pc/gfx/gfx_dxgi.cpp
index 0467495..fa4eb33 100644
--- a/src/pc/gfx/gfx_dxgi.cpp
+++ b/src/pc/gfx/gfx_dxgi.cpp
@@ -36,10 +36,10 @@
 
 #ifdef VERSION_EU
 #define FRAME_INTERVAL_US_NUMERATOR 40000
-#define FRAME_INTERVAL_US_DENOMINATOR 1
+#define FRAME_INTERVAL_US_DENOMINATOR 2
 #else
 #define FRAME_INTERVAL_US_NUMERATOR 100000
-#define FRAME_INTERVAL_US_DENOMINATOR 3
+#define FRAME_INTERVAL_US_DENOMINATOR 6
 #endif
 
 using namespace Microsoft::WRL; // For ComPtr
diff --git a/src/pc/gfx/gfx_sdl2.c b/src/pc/gfx/gfx_sdl2.c
index aef2978..ff53b27 100644
--- a/src/pc/gfx/gfx_sdl2.c
+++ b/src/pc/gfx/gfx_sdl2.c
@@ -53,7 +53,7 @@ static void (*kb_all_keys_up)(void) = NULL;
 // whether to use timer for frame control
 static bool use_timer = true;
 // time between consequtive game frames
-static const Uint32 frame_time = 1000 / FRAMERATE;
+static const Uint32 frame_time = 1000 / (2 * FRAMERATE);
 static Uint32 frame_start = 0;
 
 const SDL_Scancode windows_scancode_table[] = {
@@ -142,7 +142,7 @@ static inline void gfx_sdl_set_vsync(int mode) {
     if (mode > 1) {
         // try to detect refresh rate
         SDL_GL_SetSwapInterval(1);
-        const int vblanks = test_vsync();
+        const int vblanks = test_vsync() / 2;
         if (vblanks) {
             printf("determined swap interval: %d\n", vblanks);
             SDL_GL_SetSwapInterval(vblanks);
diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c
index ed6ee74..63679ad 100644
--- a/src/pc/pc_main.c
+++ b/src/pc/pc_main.c
@@ -83,6 +83,25 @@ void send_display_list(struct SPTask *spTask) {
 #define SAMPLES_LOW 528
 #endif
 
+static inline void patch_interpolations(void) {
+    extern void mtx_patch_interpolated(void);
+    extern void patch_screen_transition_interpolated(void);
+    extern void patch_title_screen_scales(void);
+    extern void patch_interpolated_dialog(void);
+    extern void patch_interpolated_hud(void);
+    extern void patch_interpolated_paintings(void);
+    extern void patch_interpolated_bubble_particles(void);
+    extern void patch_interpolated_snow_particles(void);
+    mtx_patch_interpolated();
+    patch_screen_transition_interpolated();
+    patch_title_screen_scales();
+    patch_interpolated_dialog();
+    patch_interpolated_hud();
+    patch_interpolated_paintings();
+    patch_interpolated_bubble_particles();
+    patch_interpolated_snow_particles();
+}
+
 void produce_one_frame(void) {
     gfx_start_frame();
 
@@ -110,6 +129,11 @@ void produce_one_frame(void) {
     audio_api->play((u8 *)audio_buffer, 2 * num_audio_samples * 4);
 
     gfx_end_frame();
+
+    gfx_start_frame();
+    patch_interpolations();
+    send_display_list(gGfxSPTask);
+    gfx_end_frame();
 }
 
 void audio_shutdown(void) {
