DMA Transfers

The dma package provides low-level data transfer functions for the N64. The N64's CPU cannot directly access cartridge ROM - all data must be copied into RDRAM via DMA (Direct Memory Access). This package also covers SRAM access for save data, MIO0 decompression, memory pools, and segment address translation.

Cartridge ROM to RDRAM

CartToRDRAM

Copies data from cartridge ROM into an RDRAM buffer:

func CartToRDRAM(romOffset uint32, dst []byte) error
  • romOffset - byte offset into the cartridge ROM (relative to 0x10000000)
  • dst - destination slice in RDRAM, determines the transfer size
buf := make([]byte, 4096)
err := dma.CartToRDRAM(0x100000, buf)
if err != nil {
    panic(err)
}

Internally, this creates a peripheral device at the ROM address and performs a read via the N64's PI (Parallel Interface). After the transfer, it invalidates the CPU data cache for the destination region to ensure coherency.

This is an N64-only function (build tag n64). On other platforms, use the standard file I/O or embedded filesystem instead.

SRAM access

SRAM is the N64's battery-backed save memory, mapped at physical address 0x08000000. The standard SRAM size is 32KB.

SRAMRead

Reads data from SRAM into a buffer:

func SRAMRead(offset uint32, dst []byte) error
saveData := make([]byte, 256)
err := dma.SRAMRead(0, saveData)

SRAMWrite

Writes data from a buffer to SRAM:

func SRAMWrite(offset uint32, src []byte) error
err := dma.SRAMWrite(0, saveData)

Both functions access SRAM through the PI peripheral interface. The offset is relative to the start of SRAM (0x08000000). These are N64-only functions.

MIO0 decompression

DecompressMIO0

Decompresses MIO0-compressed data, the standard compression format used in many N64 games (including Super Mario 64):

func DecompressMIO0(src []byte) ([]byte, error)
compressed := make([]byte, compressedSize)
dma.CartToRDRAM(romOffset, compressed)

decompressed, err := dma.DecompressMIO0(compressed)
if err != nil {
    panic(err)
}

The MIO0 format structure:

OffsetSizeDescription
04 bytesMagic: "MIO0"
44 bytesDecompressed size (big-endian)
84 bytesOffset to compressed data
124 bytesOffset to uncompressed data
16+variableLayout bits, compressed data, uncompressed data

The algorithm uses layout bits to decide whether each output byte is a literal copy (from the uncompressed stream) or a back-reference (length + offset pair from the compressed stream). Back-references copy 3-18 bytes from previously decompressed output.

Returns ErrInvalidMIO0 if the magic bytes are wrong or the data is truncated.

Memory pools

Pool

A linear memory allocator for RDRAM that allocates from both ends:

type Pool struct { /* ... */ }

See Memory Pools for the full Pool API including NewPool, AllocHead, AllocTail, ResetTail, and Available.

Segment table

SegmentTable

The N64 uses 16 segment registers (0x00-0x0F) to translate segmented addresses in display lists to physical RDRAM addresses. A segment address encodes the segment number in bits 28-31 and the offset in bits 0-27.

type SegmentTable struct {
    bases [16]uint32
}

NewSegmentTable

Creates a segment table with all segments set to 0:

st := dma.NewSegmentTable()

Set / Get

Set or query a segment's base address:

st.Set(6, 0x80200000)       // segment 6 maps to RDRAM 0x80200000
base := st.Get(6)            // returns 0x80200000

Segment numbers must be 0-15. Out-of-range values are silently ignored (Set) or return 0 (Get).

Resolve

Translates a segmented address to a physical RDRAM address:

physAddr := st.Resolve(0x06001000)
// segment 6 base + offset 0x001000

The segment number is extracted from bits 24-27, the offset from bits 0-23. The result is bases[segment] + offset.

MakeSegAddr

Creates a segmented address from a segment number and offset:

addr := dma.MakeSegAddr(6, 0x1000)
// returns 0x06001000

Typical usage pattern

A common N64 game initialization sequence:

// Set up segment table
segments := dma.NewSegmentTable()

// Load level data from cartridge
levelData := make([]byte, levelSize)
dma.CartToRDRAM(levelROMOffset, levelData)

// If data is compressed
decompressed, err := dma.DecompressMIO0(levelData)

// Set up memory pool for the level
pool := dma.NewPool(poolBase, poolSize)

// Allocate permanent data from the head
modelAddr, _ := pool.AllocHead(modelSize)

// Set segment to point at the loaded data
segments.Set(6, modelAddr)

// Allocate per-frame display list from the tail
dlAddr, _ := pool.AllocTail(dlSize)

// At frame end, reset tail allocations
pool.ResetTail()