diff --git a/enhancements/moon64_60fps.patch b/enhancements/moon64_60fps.patch new file mode 100644 index 00000000..c072d013 --- /dev/null +++ b/enhancements/moon64_60fps.patch @@ -0,0 +1,1978 @@ +diff --git a/include/types.h b/include/types.h +index b3dc27e2..c46bdf01 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 802d97a8..1b0d6772 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 5b6775fe..2c11e254 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 bb72c869..75a49031 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; + +@@ -354,6 +355,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 be7ae4f0..8fe22cea 100644 +--- a/src/game/camera.c ++++ b/src/game/camera.c +@@ -485,6 +485,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 + */ +@@ -5541,6 +5545,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; + } +@@ -5703,6 +5708,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(); + } + } + +@@ -5898,6 +5904,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(); + } + } + +@@ -7268,6 +7275,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(); + } + + /** +@@ -7300,6 +7308,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); + } + +@@ -7309,6 +7318,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(); + } + + /** +@@ -7344,6 +7354,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(); + } + + /** +@@ -7362,6 +7373,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(); + } + + /** +@@ -7448,6 +7460,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); + } + +@@ -7458,6 +7471,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(); + } + + /** +@@ -10364,6 +10378,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 173ab8a7..b1abdc4f 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 060a12da..b866a914 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 c3c14a5c..d2212ef6 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 7a83b536..f4acc2de 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 05bafeb6..e3f47e6f 100644 +--- a/src/game/hud.c ++++ b/src/game/hud.c +@@ -82,6 +82,20 @@ void render_hud_texture(s32 x, s32 y, u32 w, u32 h, u8 *texture) { + gSPDisplayList(gDisplayListHead++, dl_hud_img_end); + } + ++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. + */ +@@ -134,6 +148,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)); + +@@ -141,7 +156,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 719bf87f..2f9d7911 100644 +--- a/src/game/ingame_menu.c ++++ b/src/game/ingame_menu.c +@@ -77,6 +77,47 @@ s8 gLastDialogResponse = 0; + u8 gMenuHoldKeyIndex = 0; + u8 gMenuHoldKeyTimer = 0; + s32 gDialogResponse = 0; ++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)); +@@ -540,6 +581,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); +@@ -548,6 +597,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); +@@ -658,7 +713,8 @@ u32 ensure_nonnegative(s16 value) { + return value; + } + +-void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 lowerBound) { ++void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 lowerBound) ++{ + UNUSED s32 pad[2]; + + u8 strChar; +@@ -688,9 +744,11 @@ void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 l + gSPDisplayList(gDisplayListHead++, dl_ia_text_begin); + strIdx = gDialogTextPos; + +- if (gDialogBoxState == DIALOG_STATE_HORIZONTAL) ++ if (gDialogBoxState == DIALOG_STATE_HORIZONTAL) { ++ sInterpolatedDialogOffset = gDialogScrollOffsetY + dialog->linesPerBox; ++ sInterpolatedDialogOffsetPos = gDisplayListHead; + create_dl_translation_matrix(MENU_MTX_NOPUSH, 0, (f32) gDialogScrollOffsetY, 0); +- ++ } + create_dl_translation_matrix(MENU_MTX_PUSH, X_VAL3, 2 - lineNum * Y_VAL3, 0); + + while (pageState == DIALOG_PAGE_STATE_NONE) { +@@ -715,7 +773,6 @@ void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 l + case DIALOG_CHAR_SPACE: + xMatrix++; + linePos++; +- + break; + case DIALOG_CHAR_SLASH: + xMatrix += 2; +@@ -747,7 +804,6 @@ void handle_dialog_text_and_pages(s8 colorMode, struct DialogEntry *dialog, s8 l + } + strIdx++; + } +- + gSPDisplayList(gDisplayListHead++, dl_ia_text_end); + + if (gDialogBoxState == DIALOG_STATE_VERTICAL) { +diff --git a/src/game/level_geo.c b/src/game/level_geo.c +index 4c98e705..abc51213 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 15818f70..190d30ca 100644 +--- a/src/game/object_helpers.c ++++ b/src/game/object_helpers.c +@@ -1550,6 +1550,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 6cae19c0..a304d4ae 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 c0111a27..6bf3d0ac 100644 +--- a/src/game/rendering_graph_node.c ++++ b/src/game/rendering_graph_node.c +@@ -40,6 +40,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 +@@ -53,6 +55,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 +@@ -62,6 +65,7 @@ struct GeoAnimState gGeoTempState; + u8 gCurAnimType; + u8 gCurAnimEnabled; + s16 gCurrAnimFrame; ++s16 gPrevAnimFrame; + f32 gCurAnimTranslationMultiplier; + u16 *gCurrAnimAttribute; + s16 *gCurAnimData; +@@ -130,6 +134,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. + */ +@@ -157,9 +201,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; + } + } +@@ -175,7 +224,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); +@@ -185,7 +234,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; +@@ -196,6 +247,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. + */ +@@ -242,7 +297,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; +@@ -251,9 +308,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); +@@ -305,6 +376,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. + */ +@@ -312,6 +416,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]); +@@ -322,12 +429,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; + } +@@ -344,13 +479,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); + } +@@ -369,13 +508,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); + } +@@ -393,12 +536,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); + } +@@ -417,12 +571,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); + } +@@ -441,21 +599,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); + } +@@ -504,13 +671,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); +@@ -535,61 +728,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); + } +@@ -621,6 +834,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); +@@ -639,8 +863,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; +@@ -648,6 +874,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) { +@@ -686,21 +913,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--; + } +@@ -797,31 +1060,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]; +@@ -832,9 +1165,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; +@@ -845,11 +1181,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; + } + } + +@@ -876,6 +1217,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); +@@ -891,6 +1234,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]; +@@ -899,6 +1250,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]); +@@ -906,12 +1264,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) { +@@ -926,6 +1287,7 @@ void geo_process_held_object(struct GraphNodeHeldObject *node) { + gCurAnimTranslationMultiplier = gGeoTempState.translationMultiplier; + gCurrAnimAttribute = gGeoTempState.attribute; + gCurAnimData = gGeoTempState.data; ++ gPrevAnimFrame = gGeoTempState.prevFrame; + gMatStackIndex--; + } + +@@ -1047,6 +1409,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(); + initialMatrix = alloc_display_list(sizeof(*initialMatrix)); +@@ -1056,7 +1419,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; + } + +@@ -1064,11 +1432,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 b49ddaf5..d6656af2 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 57297933..ad7117d5 100644 +--- a/src/menu/intro_geo.c ++++ b/src/menu/intro_geo.c +@@ -1,5 +1,6 @@ + #include + ++#include "engine/math_util.h" + #include "game/memory.h" + #include "game/segment2.h" + #include "game/segment7.h" +@@ -146,6 +147,18 @@ Gfx *geo_n64_screen(s32 sp50, struct GraphNode *sp54, UNUSED void *context) { + } + + ++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 +@@ -156,6 +169,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; +@@ -187,6 +202,11 @@ Gfx *geo_title_screen(s32 sp50, struct GraphNode *sp54, UNUSED void *context) { + 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 04674952..fa4eb33c 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 db4fa8a9..3668aba7 100644 +--- a/src/pc/gfx/gfx_sdl2.c ++++ b/src/pc/gfx/gfx_sdl2.c +@@ -60,7 +60,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 int frame_time = 1000 / FRAMERATE; ++static const int frame_time = 1000 / (2 * FRAMERATE); + + const SDL_Scancode windows_scancode_table[] = { + /* 0 1 2 3 4 5 6 7 */ +@@ -153,7 +153,11 @@ static inline void gfx_sdl_set_vsync(const bool enabled) { + if (enabled) { + // try to detect refresh rate + SDL_GL_SetSwapInterval(1); +- const int vblanks = test_vsync(); ++ int vblanks = test_vsync(); ++ if (vblanks & 1) ++ vblanks = 0; // not divisible by 60, fuck that ++ else ++ vblanks /= 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 a87d967f..712fce4c 100644 +--- a/src/pc/pc_main.c ++++ b/src/pc/pc_main.c +@@ -90,6 +90,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) { + moon_setup("Update"); + gfx_start_frame(); +@@ -118,6 +137,11 @@ void produce_one_frame(void) { + } + //printf("Audio samples before submitting: %d\n", audio_api->buffered()); + ++ gfx_start_frame(); ++ patch_interpolations(); ++ send_display_list(gGfxSPTask); ++ gfx_end_frame(); ++ + audio_api->play((u8 *)audio_buffer, 2 * num_audio_samples * 4); + + gfx_end_frame();