Starship runs the original N64 game logic at 20 fps (the native VI rate) while rendering at 60 fps or higher. The frame interpolation system bridges this gap by recording every matrix transformation that occurs during a game logic tick, then interpolating between the previous tick’s matrices and the current tick’s matrices for each intermediate rendered frame. The result is fluid motion without altering game physics or timing. The API is split into two layers:Documentation Index
Fetch the complete documentation index at: https://mintlify.com/HarbourMasters/Starship/llms.txt
Use this file to discover all available pages before exploring further.
- Recording — called from game logic code (C) to capture transform state.
- Playback — called from the rendering pipeline (C++) to produce blended matrices.
src/port/interpolation/FrameInterpolation.h.
How It Works
FrameInterpolation_Interpolate(step) produces a map of Mtx* → MtxF replacements. These are passed to GameEngine::RunCommands() so Fast3D substitutes interpolated matrices into the display list instead of the raw N64 matrices.
C++ Playback API
FrameInterpolation_Interpolate()
Mtx* pointer; each value is the interpolated MtxF to substitute during rendering.
Blend factor in
[0.0, 1.0]. 0.0 returns the previous tick’s matrices; 1.0 returns the current tick’s matrices. The render loop typically passes evenly-spaced steps such as 0.0, 0.33, and 0.66 across three sub-frames.C Recording API
Frame Control
FrameInterpolation_StartRecord()
Record* calls are associated with this tick until FrameInterpolation_StopRecord() is called. Call this once per game tick, before any actor or matrix updates.
FrameInterpolation_StopRecord()
FrameInterpolation_Interpolate() to consume the recorded data. Calling any Record* function between StopRecord and the next StartRecord has no effect.
FrameInterpolation_ShouldInterpolateFrame()
false, the render loop will use the raw unblended matrices instead of interpolating.
Disable interpolation for hard cuts, teleports, cutscene camera jumps, and screen transitions where blending the previous frame would produce visual glitches.
Debug / Diagnostic
FrameInterpolation_RecordMarker()
__FILE__ and __LINE__ for automatic tagging.
Transform Hierarchy
The hierarchy system lets the interpolation engine track which actor or object “owns” a given matrix, so the same physicalMtx* address can be reused across ticks for different objects without confusion.
FrameInterpolation_RecordOpenChild()
a and an integer discriminator b. Every Record* call made after this — until the matching RecordCloseChild() — is associated with this node.
In ported actor code the convention is to pass the actor pointer and 0:
Identity pointer for this node — typically the actor or object struct pointer.
Integer discriminator to disambiguate multiple nodes with the same parent pointer (e.g. multiple bones of the same skeleton). Usually
0.FrameInterpolation_RecordCloseChild()
RecordOpenChild() call. Unbalanced open/close calls will cause incorrect matrix attribution and visual corruption.
Camera
FrameInterpolation_DontInterpolateCamera()
FrameInterpolation_GetCameraEpoch()
DontInterpolateCamera() is called. Mod code can compare epochs across ticks to detect whether the camera snapped, and react accordingly (e.g. skip post-processing effects that depend on continuous camera motion).
Actor Position / Rotation
FrameInterpolation_RecordActorPosRotMatrix()
Matrix Stack Recording
These functions mirror the standard N64 matrix stack operations but additionally log the transformation for later interpolation. Use them as drop-in replacements for the bareMatrix_* calls in ported code.
FrameInterpolation_RecordMatrixPush()
mtx should point to the stack pointer variable so the interpolation system can track the stack depth.
FrameInterpolation_RecordMatrixPop()
RecordMatrixPush().
FrameInterpolation_RecordMatrixMult()
mode is the standard MTXMODE_NEW / MTXMODE_APPLY flag.
Destination matrix on the stack.
Source floating-point matrix.
MTXMODE_NEW to replace, MTXMODE_APPLY to multiply.FrameInterpolation_RecordMatrixTranslate()
Matrix_Translate(x, y, z, mode) but also logs the operation for interpolation.
FrameInterpolation_RecordMatrixScale()
FrameInterpolation_RecordMatrixRotate1Coord()
coord selects the axis: 0 = X, 1 = Y, 2 = Z. value is the angle in radians.
Axis selector:
0 for X, 1 for Y, 2 for Z.Rotation angle in radians.
FrameInterpolation_RecordMatrixRotateAxis()
axis must be a unit vector.
FrameInterpolation_RecordMatrixReplaceRotation()
mf, preserving translation and scale. Useful for aiming a bone toward a target without affecting its position.
Matrix Conversion
FrameInterpolation_RecordMatrixMtxFToMtx()
src to the N64 fixed-point Mtx format at dest and registers the pair so the interpolation system can produce blended replacements.
FrameInterpolation_RecordMatrixToMtx()
Mtx format at dest and logs the source location for debugging. Pass __FILE__ and __LINE__.
FrameInterpolation_RecordSkinMatrixMtxFToMtx()
RecordMatrixMtxFToMtx() for skinned mesh deformation matrices. Skin matrices are stored separately from the rigid-body hierarchy so they can be interpolated independently.
Vector Transforms
FrameInterpolation_RecordMatrixMultVec3f()
src by matrix (with translation) into dest. Logs the output for interpolation so derived positions (e.g. effect spawn points) also move smoothly.
FrameInterpolation_RecordMatrixMultVec3fNoTranslate()
matrix. Use for direction vectors and normals.
Annotation Conventions
The ported source code uses// @port comments to flag every interpolation call that was added by the port team. These tags serve as a quick grep target when auditing interpolation coverage.
When porting new actors or effects, add
// @port comments on every interpolation call you introduce. This makes future review and debugging significantly easier.GetInterpolationFPS() and CVars
GameEngine::GetInterpolationFPS() drives the interpolation step count. Internally it reads the gInterpolationFPS CVar (default 60) but yields to gMatchRefreshRate and gVsyncEnabled when those options are active.
GetInterpolationFPS() rather than hardcoding 60 so the effect timing scales correctly at higher frame rates.