Skip to main content

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.

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:
  • Recording — called from game logic code (C) to capture transform state.
  • Playback — called from the rendering pipeline (C++) to produce blended matrices.
Both layers are declared in src/port/interpolation/FrameInterpolation.h.

How It Works

Game Logic Tick (20 fps)           Render Loop (60 fps)
─────────────────────────          ──────────────────────────────────────
FrameInterpolation_StartRecord()
  ... game updates actors ...   →  FrameInterpolation_Interpolate(0.0f)
  RecordOpenChild(actor, 0)        FrameInterpolation_Interpolate(0.33f)
  RecordMatrixTranslate(...)       FrameInterpolation_Interpolate(0.66f)
  RecordCloseChild()
FrameInterpolation_StopRecord()
Each call to 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()

std::unordered_map<Mtx*, MtxF> FrameInterpolation_Interpolate(float step);
Blends every recorded matrix between the previous frame’s value and the current frame’s value. Returns a map keyed by the original Mtx* pointer; each value is the interpolated MtxF to substitute during rendering.
step
float
required
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.
// Inside the render loop — produce three interpolated frames per game tick
for (int i = 0; i < frameCount; i++) {
    float step = (float)i / (float)frameCount;
    auto replacements = FrameInterpolation_Interpolate(step);
    GameEngine::RunCommands(gDisplayList, { replacements });
}

C Recording API

Frame Control

FrameInterpolation_StartRecord()

void FrameInterpolation_StartRecord(void);
Marks the beginning of a game logic tick’s recording session. All subsequent 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()

void FrameInterpolation_StopRecord(void);
Finalises the current tick’s recording. After this point the render loop may call FrameInterpolation_Interpolate() to consume the recorded data. Calling any Record* function between StopRecord and the next StartRecord has no effect.

FrameInterpolation_ShouldInterpolateFrame()

void FrameInterpolation_ShouldInterpolateFrame(bool shouldInterpolate);
Enables or disables interpolation for the current frame. When 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.
// @port Skip interpolation for this frame (e.g. level transition)
FrameInterpolation_ShouldInterpolateFrame(false);

// ... render the frame ...

// @port Re-enable interpolation for subsequent frames
FrameInterpolation_ShouldInterpolateFrame(true);
Always re-enable interpolation after disabling it. Leaving it disabled permanently will result in stuttery 20fps rendering even at high target frame rates.

Debug / Diagnostic

FrameInterpolation_RecordMarker()

void FrameInterpolation_RecordMarker(const char* file, int line);
Inserts a named source-location marker into the recording timeline. Used by the interpolation debugger to correlate recorded entries with their originating source lines. Pass __FILE__ and __LINE__ for automatic tagging.
FrameInterpolation_RecordMarker(__FILE__, __LINE__);

Transform Hierarchy

The hierarchy system lets the interpolation engine track which actor or object “owns” a given matrix, so the same physical Mtx* address can be reused across ticks for different objects without confusion.

FrameInterpolation_RecordOpenChild()

void FrameInterpolation_RecordOpenChild(const void* a, int b);
Opens a new node in the transform hierarchy, identifying it by the pointer 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:
// @port: Tag the transform with this actor's identity.
FrameInterpolation_RecordOpenChild(shot, 0);

// ... matrix operations for this actor ...

// @port Pop the transform id.
FrameInterpolation_RecordCloseChild();
a
const void*
required
Identity pointer for this node — typically the actor or object struct pointer.
b
int
required
Integer discriminator to disambiguate multiple nodes with the same parent pointer (e.g. multiple bones of the same skeleton). Usually 0.

FrameInterpolation_RecordCloseChild()

void FrameInterpolation_RecordCloseChild(void);
Closes the most recently opened hierarchy node. Must be paired with every RecordOpenChild() call. Unbalanced open/close calls will cause incorrect matrix attribution and visual corruption.
Treat RecordOpenChild / RecordCloseChild like a stack-based push/pop. For deeply nested skeletons, open a node for the skeleton, then open child nodes for each limb, closing each limb’s node before opening the next.

Camera

FrameInterpolation_DontInterpolateCamera()

void FrameInterpolation_DontInterpolateCamera(void);
Marks the camera matrix recorded in the current tick as non-interpolatable. The camera will snap to its new position rather than blending. Use this during cutscene camera cuts or when the camera teleports.

FrameInterpolation_GetCameraEpoch()

int FrameInterpolation_GetCameraEpoch(void);
Returns a monotonically increasing counter that increments each time 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()

void FrameInterpolation_RecordActorPosRotMatrix(void);
Records the current model-view matrix as an actor position/rotation matrix within the active hierarchy node. Call this immediately after positioning an actor’s transform so interpolation can blend its world-space location and orientation.

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 bare Matrix_* calls in ported code.

FrameInterpolation_RecordMatrixPush()

void FrameInterpolation_RecordMatrixPush(Matrix** mtx);
Records a matrix stack push. mtx should point to the stack pointer variable so the interpolation system can track the stack depth.

FrameInterpolation_RecordMatrixPop()

void FrameInterpolation_RecordMatrixPop(Matrix** mtx);
Records a matrix stack pop. Must be paired with every RecordMatrixPush().

FrameInterpolation_RecordMatrixMult()

void FrameInterpolation_RecordMatrixMult(Matrix* matrix, MtxF* mf, u8 mode);
Records a matrix multiplication. mode is the standard MTXMODE_NEW / MTXMODE_APPLY flag.
matrix
Matrix*
required
Destination matrix on the stack.
mf
MtxF*
required
Source floating-point matrix.
mode
u8
required
MTXMODE_NEW to replace, MTXMODE_APPLY to multiply.

FrameInterpolation_RecordMatrixTranslate()

void FrameInterpolation_RecordMatrixTranslate(Matrix* matrix,
    f32 x, f32 y, f32 z, u8 mode);
Records a translation transform. Equivalent to Matrix_Translate(x, y, z, mode) but also logs the operation for interpolation.

FrameInterpolation_RecordMatrixScale()

void FrameInterpolation_RecordMatrixScale(Matrix* matrix,
    f32 x, f32 y, f32 z, u8 mode);
Records a scale transform.

FrameInterpolation_RecordMatrixRotate1Coord()

void FrameInterpolation_RecordMatrixRotate1Coord(Matrix* matrix,
    u32 coord, f32 value, u8 mode);
Records a single-axis rotation. coord selects the axis: 0 = X, 1 = Y, 2 = Z. value is the angle in radians.
coord
u32
required
Axis selector: 0 for X, 1 for Y, 2 for Z.
value
f32
required
Rotation angle in radians.

FrameInterpolation_RecordMatrixRotateAxis()

void FrameInterpolation_RecordMatrixRotateAxis(f32 angle, Vec3f* axis, u8 mode);
Records a rotation around an arbitrary axis. axis must be a unit vector.

FrameInterpolation_RecordMatrixReplaceRotation()

void FrameInterpolation_RecordMatrixReplaceRotation(MtxF* mf);
Replaces only the rotation component of the current matrix with the rotation embedded in mf, preserving translation and scale. Useful for aiming a bone toward a target without affecting its position.

Matrix Conversion

FrameInterpolation_RecordMatrixMtxFToMtx()

void FrameInterpolation_RecordMatrixMtxFToMtx(MtxF* src, Mtx* dest);
Converts a floating-point matrix src to the N64 fixed-point Mtx format at dest and registers the pair so the interpolation system can produce blended replacements.

FrameInterpolation_RecordMatrixToMtx()

void FrameInterpolation_RecordMatrixToMtx(Mtx* dest, char* file, s32 line);
Converts the top of the matrix stack to N64 Mtx format at dest and logs the source location for debugging. Pass __FILE__ and __LINE__.

FrameInterpolation_RecordSkinMatrixMtxFToMtx()

void FrameInterpolation_RecordSkinMatrixMtxFToMtx(MtxF* src, Mtx* dest);
Variant of 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()

void FrameInterpolation_RecordMatrixMultVec3f(Matrix* matrix,
    Vec3f src, Vec3f dest);
Records the result of transforming 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()

void FrameInterpolation_RecordMatrixMultVec3fNoTranslate(Matrix* matrix,
    Vec3f src, Vec3f dest);
Same as above but ignores the translation component of 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.
// @port: Tag the transform with this actor's identity.
FrameInterpolation_RecordOpenChild(shot, 0);

    // ... matrix operations ...

// @port Pop the transform id.
FrameInterpolation_RecordCloseChild();

// @port Skip interpolation for this frame (e.g. a hard cut or teleport)
FrameInterpolation_ShouldInterpolateFrame(false);

// @port Re-enable interpolation for subsequent frames
FrameInterpolation_ShouldInterpolateFrame(true);
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.
uint32_t fps = GameEngine::GetInterpolationFPS();
// e.g. 60, 120, or the display refresh rate
To query this from C code, use the C-linkage wrapper:
uint32_t fps = GameEngine_GetInterpolationFrameCount();
Mod code that spawns timed particle effects or audio should use GetInterpolationFPS() rather than hardcoding 60 so the effect timing scales correctly at higher frame rates.

Build docs developers (and LLMs) love