The Sega Saturn is system #43 in the sigilOS RetroPie launcher. It's the most architecturally significant addition yet: the Saturn required a new CPU core from scratch. Every other system added so far has reused an existing CPU — the 6502, the Z80, the 68000, the MIPS. The Saturn's Hitachi SH-2 is none of those. It's a 32-bit RISC with fixed-width 16-bit instructions, a big-endian register file, and a delay-slot branching model that took careful implementation to get right. uart=50 PASS.
The SH-2 core
cores/sh2.sg implements the Hitachi SH-2 instruction set in the same pattern as the other CPU cores in the launcher — a step function (sh2_step) that reads one instruction from the program counter, decodes it, and updates the register file and memory. The SH-2's fixed 16-bit instruction width makes the fetch loop clean: every instruction is exactly two bytes, there are no variable-length prefixes.
Register file
The SH-2 has 16 general-purpose registers (R0–R15) plus a set of control registers:
- PC — program counter
- SR — status register (T bit at bit 0 — the conditional branch bit)
- PR — procedure register (return address for BSR/JSR)
- GBR — global base register (for GBR-relative addressing)
- MACH / MACL — multiply-accumulate high/low (for MAC instructions, future increment)
Delay slots
The SH-2's branch instructions — BRA, BSR, BT-S, BF-S, JMP, JSR, RTS — all have a delay slot: the instruction immediately following the branch executes before the branch takes effect. This is the SH-2's most implementation-sensitive feature. Get it wrong and the branch target is right but the post-branch state is corrupted.
sh2.sg implements delay slots with two state fields: dly_active (bool) and dly_tgt (u32 — the branch target). At the top of sh2_step, before instruction fetch, the delay slot is checked:
if dly_active:
dly_active = 0
pc = dly_tgt
return # branch takes effect after the delay instruction executed
A delay-slot branch sets dly_active = 1 and dly_tgt = target but does not modify PC. The current instruction (the one in the delay slot) then executes at the current PC. On the next sh2_step call, the check at the top fires, PC becomes dly_tgt, and the branch has taken effect. The instruction in the delay slot ran once between those two steps — exactly the SH-2's specified behaviour.
Instruction coverage
The first increment covers the subset needed to execute a meaningful boot sequence and frame render loop:
- MOV: immediate, register, all memory addressing modes (@Rm, @-Rn, @Rm+, @(d,Rm), @(d,GBR))
- Arithmetic: ADD/ADDI/SUB, AND/OR/XOR/NOT, DT (decrement and test — the SH-2's loop primitive)
- Comparisons: CMP/EQ, CMP/GE, CMP/GT (sets T bit)
- Shifts: SHLL/SHLR (×1), SHAR, SHLL2/SHLR2 (×2), SHLL16/SHLR16 (×16)
- Branches: BRA/BSR (PC-relative), BT/BF (T-bit conditional, no delay slot), BT-S/BF-S (with delay slot), JMP/JSR (register indirect), RTS (return via PR)
- System: STS/LDS PR (move PR to/from register), LDC/STC SR, TRAPA (trap — treated as NOP for now)
The Saturn bus and VDP2
cores/saturn.sg is linked before sh2.sg, which means its definitions of sh2_mem_r32, sh2_mem_w32 (and the 8/16-bit variants) take precedence via first-def-wins. The Saturn's memory map:
| Address | Region | Size |
|---|---|---|
$00000000 | Boot ROM | 512KB |
$05E00000 | VDP2 register shadow | 2KB |
$06000000 | Work RAM-L | 2MB |
Reads and writes outside these ranges return 0 / discard silently — a safe default for a first increment where most of the Saturn's peripherals (VDP1, SCSP audio, SH-2 DMA, CD block) are not yet modelled.
VDP2 backdrop
The Saturn's VDP2 is the background processor — scroll planes, rotation backgrounds, colour arithmetic. For a first-increment core, the backdrop colour is the right starting point: it's always present, it requires no tilemap data, and it exercises the VDP2 register shadow correctly.
The backdrop colour register lives at VDP2 offset $E0 (absolute $05E000E0). It stores an RGB555 value: 5 bits each of red, green, and blue packed into 16 bits. sat_backdrop_argb unpacks this to ARGB32 with the standard 5-bit scale (each channel × 8 + channel >> 2 for rounding). sat_render fills the 320×224 framebuffer with this colour for every pixel.
Boot sequence
core_load copies the 512KB ROM image into sat_brom, clears work RAM and the VDP2 shadow to zero, then calls sh2_reset. The SH-2's reset vector is two 32-bit words at the start of the ROM: $00000000 holds the initial stack pointer (loaded into R15), and $00000004 holds the initial PC. sh2_reset reads these two values and sets the register file accordingly — the same pattern as every other console that stores its reset vector in ROM.
Test coverage
tests/saturn_core.sg (uart=50) verifies:
- Core ABI:
core_id,core_fb(320×224) - RAM bus: byte/halfword/word read-write round-trips in work RAM
- 32-bit bus: word-aligned read-write at the work RAM base
- VDP2 backdrop: write RGB555 to
$05E000E0, read back viasat_backdrop_argb, verify ARGB32 sat_render: fill 320×224, sample pixel at (0,0) and (319,223) — both match backdropsh2_reset: vectors loaded from ROM[0]/ROM[4] → R15/PC correctly- MOV #42 / ADD #5: two-step sh2_step sequence → R0 = 47