How N64 Graphics Microcode Shaders Are Translated for Modern APIs in Lighthouse

Lighthouse translates N64 graphics microcode by parsing display list commands into an intermediate representation, then executing equivalent GLSL/HLSL shaders through libultraship to replicate the original RSP/RDP fixed-function pipeline on modern GPUs.

Lighthouse is a modern source port that brings classic Nintendo 64 titles to contemporary platforms. A core challenge in this effort involves bridging the gap between the N64's specialized Reality Signal Processor (RSP) microcode and today's graphics APIs. This article examines how the Lighthouse codebase—specifically its interaction with the libultraship submodule—accomplishes this translation from low-level microcode to cross-platform shader execution.

Understanding the N64 Graphics Pipeline Architecture

The Nintendo 64 rendered graphics through a tightly coupled pair of processors: the RSP (Reality Signal Processor) and RDP (Reality Display Processor). Games submitted work to these processors via microcode—small programs that interpreted graphics commands. Common variants include F3DEX, F3DEX2, and L3DEX, each offering different feature sets and performance characteristics.

Microcode operated on display lists: sequential command buffers containing opcodes for vertex transformation, triangle rasterization, texture binding, and color combining. These commands used the Graphics Binary Interface (GBI)—a standardized set of macros defined in lib/ultralib/include/PR/gbi.h.

Modern GPUs lack native support for this architecture. Lighthouse solves this by emulating the RSP's behavior in software, then mapping the resulting graphics state to shader programs that run on OpenGL, Vulkan, or DirectX.

Stage 1: Microcode Loading and Task Initialization

The translation process begins when a game prepares to render a frame. In src/core1/graphics_thread.c, Lighthouse sets up an OSTask structure that encapsulates the microcode binary and its associated data.

void thread5_startF3DEXTask(struct ucode_task_data_s *task_data) {
    // Microcode boot pointers are stripped in the port
    sGfxTask.t.data_ptr = (void*)task_data->data_ptr;
    sGfxTask.t.data_size = ((u8 *)task_data->data_ptr_end - 
                           (u8 *)task_data->data_ptr) & ~7;
    
    osSpTaskLoad(&sGfxTask);
    osSpTaskStartGo(&sGfxTask);
}

The OSTask definition lives in lib/ultralib/include/PR/ucode.h. This structure carries the microcode type, entry points, and stack configuration. Lighthouse supports multiple microcode variants through dedicated entry points:

  • thread5_startF3DEXTask() — Standard F3DEX
  • thread5_startF3DEX2Task() — Enhanced F3DEX2
  • thread5_startL3DEXTask() — Line-drawing variant

Each path validates the task against expected microcode signatures before proceeding to display list processing.

Stage 2: Display List Parsing and Command Extraction

Once initialized, the microcode's display list must be interpreted. The function core1_15B30_addF3DEXTaskData() in src/core1/display_list.c records the list boundaries and assigns a task type identifier.

void core1_15B30_addF3DEXTaskData(Gfx *start, Gfx *end, s32 flags) {
    task_data->task_type = UCODE_TASK_TYPE_F3DEX;
    // Store pointers for subsequent traversal
    task_data->data_ptr = start;
    task_data->data_ptr_end = end;
}

The Gfx type represents individual GBI commands—packed 64-bit structures where high bits encode the opcode and remaining bits hold operands. Common opcodes include:

Opcode Purpose
gsDPSetCombineMode Configure color combiner
gsSPVertex Load vertex buffer
gsSPTexture Bind and configure texture
gsDPSetRenderMode Set blending and z-buffering
gsSPSetGeometryMode Enable lighting, fog, culling

The display list walker decodes these opcodes sequentially, accumulating state changes without immediate execution. This deferred approach allows Lighthouse to batch operations and optimize for modern GPU submission patterns.

Stage 3: Port-Layer State Conversion

Raw GBI commands are not directly consumable by modern graphics APIs. The port layer—centered in src/port/Game.cpp and src/port/Patches/GraphicsPatches.cpp—transforms accumulated state into platform-agnostic rendering primitives.

GraphicsPatches.cpp performs several critical conversions:

  • Segment address resolution — N64 textures referenced via segment-relative addresses are mapped to physical GPU texture objects
  • Vertex format normalization — RSP vertex data (position, color, texture coordinates) is repacked into VBO-compatible layouts
  • Pipeline state assembly — Combiner modes, render modes, and geometry flags are consolidated into descriptor structures
void GraphicsPatches::applyCombiner(const RenderCommand &cmd) {
    // Map N64 combine mode to shader permutation
    auto shader = shaderManager.getCombinerShader(cmd.combineMode);
    
    // Upload uniforms derived from parsed GBI state
    shader->setUniform("uPrimColor", cmd.primColor);
    shader->setUniform("uEnvColor", cmd.envColor);
    shader->setUniform("uCombineMode", cmd.combineMode);
    
    // Bind vertex buffer and issue draw
    glBindVertexArray(cmd.vao);
    shader->bind();
    glDrawArrays(GL_TRIANGLES, 0, cmd.vertexCount);
}

The RenderCommand structure serves as the bridge format—decoupled from both GBI specifics and underlying API requirements.

Stage 4: Shader Execution in libultraship

The actual GPU work is delegated to libultraship, a submodule providing cross-platform rendering infrastructure. Its shader system in src/fast/shaders/ implements N64-equivalent functionality through standard GLSL (and HLSL on DirectX targets).

Vertex Shader Emulation

The vertex stage replicates RSP transformation, lighting, and texture coordinate generation:

// libultraship/src/fast/shaders/vertex.glsl
#version 330 core

uniform mat4 uProjection;
uniform mat4 uModelView;
uniform vec4 uPrimColor;
uniform vec4 uEnvColor;
uniform int uGeometryMode;

in vec3 aPosition;
in vec4 aColor;
in vec2 aTexCoord;

out vec4 vColor;
out vec2 vTexCoord;

void main() {
    // Transform to clip space
    gl_Position = uProjection * uModelView * vec4(aPosition, 1.0);
    
    // Apply lighting if enabled (GEOMETRY_MODE_LIGHTING)
    vec4 litColor = aColor;
    if ((uGeometryMode & 0x00020000) != 0) {
        litColor = calculateLighting(aPosition, aColor);
    }
    
    vColor = litColor;
    vTexCoord = aTexCoord;
}

Fragment Shader Emulation

The fragment stage implements the N64's iconic color combiner—a fixed-function unit that blends multiple color sources through configurable operations:

// libultraship/src/fast/shaders/fragment.glsl
#version 330 core

uniform int uCombineMode;
uniform sampler2D uTexture0;
uniform sampler2D uTexture1;
uniform vec4 uPrimColor;
uniform vec4 uEnvColor;
uniform vec4 uFogColor;
uniform float uFogMultiplier;
uniform float uFogOffset;

in vec4 vColor;
in vec2 vTexCoord;
in float vFog;

out vec4 fragColor;

void main() {
    vec4 texel0 = texture(uTexture0, vTexCoord);
    vec4 texel1 = texture(uTexture1, vTexCoord);
    
    // Decode combine mode and execute equivalent blend
    vec4 combined;
    switch (uCombineMode) {
        case 0x00000000: // G_CC_MODULATEI
            combined = vColor * texel0;
            break;
        case 0x00000001: // G_CC_MODULATEIA
            combined = vColor * texel0;
            break;
        // ... additional cases for all N64 combiner modes
    }
    
    // Apply fog if enabled
    float fogAlpha = clamp((vFog * uFogMultiplier) + uFogOffset, 0.0, 1.0);
    combined.rgb = mix(combined.rgb, uFogColor.rgb, fogAlpha);
    
    fragColor = combined;
}

These shaders are compiled once at startup and cached by combine mode, minimizing runtime overhead.

Geometry Pipeline Integration

Model data extraction occurs in src/port/Resource/Importers/ModelFactory.cpp, which processes G_VTX commands to build efficient vertex arrays:

  1. Extract position, normal, color, and texture coordinate data from RSP vertex buffers
  2. Dequantize fixed-point values to floating-point
  3. Generate index buffers for indexed drawing where possible
  4. Upload to GPU-managed VBOs

The resulting geometry feeds directly into the shader pipeline described above.

Performance Considerations in Microcode Translation

Lighthouse optimizes the translation path through several strategies:

  • Command batching — Multiple GBI commands with compatible state are merged into single draw calls
  • Texture atlasing — Small N64 textures are packed into larger GPU textures to reduce binding overhead
  • Uniform buffer objects — Per-frame constants (projection matrices, lighting parameters) are updated once rather than per-draw
  • Shader permutation caching — Unique combine modes map to pre-compiled shader variants, avoiding runtime compilation stalls

Key Source Files Reference

File Line Numbers Responsibility
src/core1/graphics_thread.c Task setup functions Microcode task initialization and RSP emulation kickoff
src/core1/display_list.c core1_15B30_addF3DEXTaskData() Display list boundary recording and task type tagging
src/port/Game.cpp Main loop dispatch High-level coordination between emulation and rendering
src/port/Patches/GraphicsPatches.cpp Pipeline conversion functions GBI-to-GPU state transformation
src/port/Resource/Importers/ModelFactory.cpp Vertex processing Geometry extraction and VBO generation
lib/ultralib/include/PR/gbi.h GBI macro definitions Opcode constants and command structure layouts
lib/ultralib/include/PR/ucode.h OSTask structure Microcode task metadata
libultraship/src/fast/shaders/vertex.glsl Vertex shader source RSP transformation and lighting emulation
libultraship/src/fast/shaders/fragment.glsl Fragment shader source Color combiner and fog emulation

Summary

Lighthouse translates N64 graphics microcode through a four-stage pipeline:

  • Microcode tasks are loaded from game code and validated in graphics_thread.c
  • Display lists are parsed into intermediate commands by display_list.c
  • Port-layer conversion transforms GBI state to GPU-compatible structures in GraphicsPatches.cpp
  • Shader execution via libultraship runs GLSL/HLSL programs that replicate RSP/RDP behavior

This architecture preserves authentic N64 rendering while achieving modern performance through batching, caching, and GPU-efficient data flows.

Frequently Asked Questions

What N64 microcode variants does Lighthouse support?

Lighthouse supports F3DEX, F3DEX2, L3DEX, and S2DEX through dedicated task entry points in graphics_thread.c. Each variant uses specialized parsing logic that accounts for differences in vertex layout, command encoding, and feature availability. The codebase detects microcode type via task metadata and routes to appropriate handlers.

Why use shaders instead of fixed-function OpenGL?

Modern graphics APIs have removed fixed-function pipelines entirely. Vulkan and DirectX 12 require programmable shaders for all rendering. Even OpenGL 3.3+ core profiles deprecate legacy combiner and transformation state. Lighthouse's shader-based approach ensures cross-platform compatibility and future-proofing while enabling accurate emulation of N64-specific behaviors like the color combiner's complex blend modes.

How accurate is the translated output compared to original N64 hardware?

The translation achieves pixel-accurate results for most content because libultraship shaders implement N64 hardware behaviors mathematically rather than approximating them. The color combiner, lighting equations, and fog calculations match documented RDP specifications. Some edge cases—particularly around texture filter modes and VI (video interface) post-processing—may show minor differences due to hardware-specific behavior not fully captured in public documentation.

Can the shader system be extended for enhancements like widescreen or higher resolution?

Yes. The modular shader architecture in libultraship allows custom uniform injection and vertex shader modification without altering the core microcode translation. Lighthouse and related ports commonly use this to implement widescreen aspect ratio correction, internal resolution scaling, and post-processing effects. The separation between GBI parsing and final rendering means enhancements apply transparently across all supported microcode types.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →