Skip to content

Commit 483e779

Browse files
committed
snes hirom support added
1 parent c003624 commit 483e779

11 files changed

Lines changed: 341 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
run: zig build neo6502-graphics --summary all
8989

9090
- name: Build SNES examples
91-
run: zig build snes-hello snes-color-cycle snes-zig-logo snes-pi-test --summary all
91+
run: zig build snes-hello snes-color-cycle snes-zig-logo snes-pi-test snes-hirom-hello --summary all
9292

9393
- name: Build Apple IIe example
9494
run: zig build apple2-hello --summary all
@@ -113,7 +113,7 @@ jobs:
113113
pce-color-cycle.pce pce-color-cycle-banked.pce \
114114
mega65-hello.prg viciv.prg \
115115
graphics.neo \
116-
snes-hello.sfc snes-color-cycle.sfc snes-zig-logo.sfc snes-pi-test.sfc \
116+
snes-hello.sfc snes-color-cycle.sfc snes-zig-logo.sfc snes-pi-test.sfc snes-hirom-hello.sfc \
117117
hello.sys \
118118
sim-hello mos-sim; do
119119
test -f "zig-out/bin/$f" && echo "OK: $f" || echo "MISSING: $f"
@@ -172,8 +172,8 @@ jobs:
172172
for f in pce-color-cycle.pce pce-color-cycle-banked.pce; do
173173
smoke "$f"
174174
done
175-
# SNES
176-
for f in snes-hello.sfc snes-color-cycle.sfc snes-zig-logo.sfc snes-pi-test.sfc; do
175+
# SNES (LoROM + HiROM — mednafen detects map mode from header byte $FFD5)
176+
for f in snes-hello.sfc snes-color-cycle.sfc snes-zig-logo.sfc snes-pi-test.sfc snes-hirom-hello.sfc; do
177177
smoke "$f"
178178
done
179179
# Note: Atari 2600 (.a26) is NOT tested here — apt mednafen lacks the vcs module.

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Open an issue with: what went wrong, steps to reproduce, Zig version, OS.
88

99
## Pull Requests
1010

11-
- New examples must build with `zig build -Dsdk=...` and include a named step in the root `build.zig`.
11+
- New examples must build with `zig build` and include a named step in the root `build.zig`.
1212
- Follow Zig style — run `zig fmt` before committing.
1313
- PR title should summarize the change; description should explain why.
1414

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ zig build snes-hello
7474
zig build snes-color-cycle
7575
zig build snes-zig-logo
7676
zig build snes-pi-test
77+
zig build snes-hirom-hello
7778

7879
# mos-sim (6502 simulator)
7980
zig build sim-hello
@@ -127,6 +128,7 @@ Output files land in `zig-out/bin/`.
127128
| `snes-zig-logo` — Zig mark logo on BG1 with shimmer palette animation | ![](.github/snes-zig-logo.gif) |
128129
| `snes-color-cycle` — backdrop hue rotation (192-step colour wheel) | |
129130
| `snes-pi-test`~900 digits of π via Spigot algorithm, BG1 text (port of pi_snes by Sirmacho) | |
131+
| `snes-hirom-hello` — HiROM backdrop hello (map mode $21) | |
130132

131133
### Other platforms
132134

@@ -174,6 +176,7 @@ Output files land in `zig-out/bin/`.
174176
| `snes-color-cycle` | SNES LoROM | mosw65816 | `.sfc` |
175177
| `snes-zig-logo` | SNES LoROM | mosw65816 | `.sfc` |
176178
| `snes-pi-test` | SNES LoROM | mosw65816 | `.sfc` |
179+
| `snes-hirom-hello` | SNES HiROM | mosw65816 | `.sfc` |
177180
| `sim-hello` | mos-sim (6502 simulator) | mos6502 | binary |
178181
| `mega65-hello`, `mega65-plasma` | MEGA65 | mos45gs02 | `.prg` |
179182
| `mega65-viciv` | MEGA65 VICIV | mos45gs02 | `.prg` |

build.zig

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,16 @@ pub fn build(b: *std.Build) void {
785785
run_bininfo.addFileArg(exe.getEmittedBin());
786786
}
787787

788+
// ---- SNES HiROM hello ----
789+
{
790+
const step = b.step("snes-hirom-hello", "Build SNES HiROM hello example");
791+
const exe = addSnesHiRomExe(b, sdk_src, sdk_libs.snes orelse @panic("snes libs not built"), optimize, "snes-hirom-hello", "snes/hirom-hello/hirom-hello.zig");
792+
const install = b.addInstallArtifact(exe, .{ .dest_sub_path = "snes-hirom-hello.sfc" });
793+
step.dependOn(&install.step);
794+
b.getInstallStep().dependOn(&install.step);
795+
run_bininfo.addFileArg(exe.getEmittedBin());
796+
}
797+
788798
// ---- Atari 8-bit cartridge hello ----
789799
{
790800
const step = b.step("atari8-cart-hello", "Build Atari 8-bit standard cartridge hello example");
@@ -2150,6 +2160,73 @@ fn addSnesExe(
21502160
return exe;
21512161
}
21522162

2163+
fn addSnesHiRomExe(
2164+
b: *std.Build,
2165+
sdk_src: []const u8,
2166+
libs: sdk_mod.Libs,
2167+
opt: std.builtin.OptimizeMode,
2168+
name: []const u8,
2169+
root_src: []const u8,
2170+
) *std.Build.Step.Compile {
2171+
const target = b.resolveTargetQuery(.{ .cpu_arch = .mos, .os_tag = .snes });
2172+
2173+
const build_root = b.build_root.path orelse ".";
2174+
const wf = b.addWriteFiles();
2175+
const wrapper_ld = wf.add("snes-hirom-wrapper.ld", b.fmt(
2176+
\\SEARCH_DIR("{s}/mos-platform/common/ldscripts");
2177+
\\INCLUDE "{s}/snes/hirom.ld"
2178+
, .{ sdk_src, build_root }));
2179+
2180+
const exe = b.addExecutable(.{
2181+
.name = name,
2182+
.root_module = b.createModule(.{
2183+
.root_source_file = b.path(root_src),
2184+
.target = target,
2185+
.optimize = opt,
2186+
.sanitize_c = .off,
2187+
}),
2188+
});
2189+
exe.bundle_compiler_rt = false;
2190+
exe.lto = .full;
2191+
exe.forceUndefinedSymbol("__zig_call_main_section");
2192+
exe.forceUndefinedSymbol("main");
2193+
exe.root_module.addAssemblyFile(b.path("snes/crt0.s"));
2194+
if (libs.mem) |mem_obj| exe.root_module.addObject(mem_obj);
2195+
const snes_mod = b.createModule(.{
2196+
.root_source_file = b.path("snes/hardware.zig"),
2197+
.target = target,
2198+
.optimize = opt,
2199+
.sanitize_c = .off,
2200+
});
2201+
const sneslib_mod = b.createModule(.{
2202+
.root_source_file = b.path("snes/sneslib.zig"),
2203+
.target = target,
2204+
.optimize = opt,
2205+
.sanitize_c = .off,
2206+
});
2207+
sneslib_mod.addImport("snes", snes_mod);
2208+
exe.root_module.addImport("snes", snes_mod);
2209+
exe.root_module.addImport("sneslib", sneslib_mod);
2210+
exe.root_module.addImport("snes_header", b.createModule(.{
2211+
.root_source_file = b.path("snes/hirom_header.zig"),
2212+
.target = target,
2213+
.optimize = opt,
2214+
.sanitize_c = .off,
2215+
}));
2216+
exe.root_module.addImport("mos_panic", b.createModule(.{
2217+
.root_source_file = b.path("sdk/panic.zig"),
2218+
.target = target,
2219+
.optimize = opt,
2220+
.sanitize_c = .off,
2221+
}));
2222+
exe.root_module.linkLibrary(libs.crt);
2223+
exe.root_module.linkLibrary(libs.crt0);
2224+
exe.root_module.linkLibrary(libs.c);
2225+
exe.setLinkerScript(wrapper_ld);
2226+
2227+
return exe;
2228+
}
2229+
21532230
fn cx16HeaderMod(
21542231
b: *std.Build,
21552232
sdk_dep: *std.Build.Dependency,

snes/crt0.s

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,30 @@
1515
xce
1616
pea 0x0000
1717
pld
18-
phk
18+
lda #0x00 ; explicit DB=0: safe for both LoROM (bank $00) and HiROM (bank $C0+)
19+
pha
1920
plb
21+
22+
.extern vblank_flag
23+
.section .text.nmi_handler,"ax",@progbits
24+
.global nmi_handler
25+
nmi_handler:
26+
rep #0x30 ; 16-bit A, X, Y at runtime
27+
pha ; save A (16-bit)
28+
phx ; save X (16-bit)
29+
phy ; save Y (16-bit)
30+
sep #0x20 ; M=1: assembler and runtime now agree — all lda # below are 8-bit
31+
phb ; save data bank (8-bit, always)
32+
phd ; save direct page (16-bit, always)
33+
lda #0x00
34+
pha
35+
plb ; DB = 0
36+
lda #0x01
37+
sta vblank_flag ; signal VBlank to wait_vblank()
38+
pld ; restore direct page
39+
plb ; restore data bank
40+
rep #0x30 ; 16-bit for restoring A/X/Y
41+
ply
42+
plx
43+
pla
44+
rti

snes/hardware.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,45 @@ pub const VMADDH = reg(0x2117); // VRAM address high byte
4141
pub const VMDATAL = reg(0x2118); // VRAM data write low byte
4242
pub const VMDATAH = reg(0x2119); // VRAM data write high byte
4343

44+
// ---- PPU: Mode 7 ----
45+
pub const M7SEL = reg(0x211a); // Mode 7 settings (flip, repeat)
46+
pub const M7A = reg(0x211b); // Mode 7 matrix A (write twice: low, high)
47+
pub const M7B = reg(0x211c); // Mode 7 matrix B / multiplicand (write twice)
48+
pub const M7C = reg(0x211d); // Mode 7 matrix C (write twice)
49+
pub const M7D = reg(0x211e); // Mode 7 matrix D (write twice)
50+
pub const M7X = reg(0x211f); // Mode 7 center X (write twice)
51+
pub const M7Y = reg(0x2120); // Mode 7 center Y (write twice)
52+
4453
// ---- PPU: Palette (CGRAM) ----
4554
pub const CGADD = reg(0x2121); // CGRAM byte address (palette index × 2)
4655
pub const CGDATA = reg(0x2122); // CGRAM data write (two 8-bit writes per 15-bit BGR entry)
4756

57+
// ---- PPU: Window Masking ----
58+
pub const W12SEL = reg(0x2123); // Window mask settings for BG1/BG2
59+
pub const W34SEL = reg(0x2124); // Window mask settings for BG3/BG4
60+
pub const WOBJSEL = reg(0x2125); // Window mask settings for OBJ and color window
61+
pub const WH0 = reg(0x2126); // Window 1 left border
62+
pub const WH1 = reg(0x2127); // Window 1 right border
63+
pub const WH2 = reg(0x2128); // Window 2 left border
64+
pub const WH3 = reg(0x2129); // Window 2 right border
65+
pub const WBGLOG = reg(0x212a); // Window mask logic for BG1–BG4
66+
pub const WOBJLOG = reg(0x212b); // Window mask logic for OBJ and color window
67+
4868
// ---- PPU: Layer enable ----
4969
pub const TM = reg(0x212c); // Main screen layer enable
5070
pub const TS = reg(0x212d); // Sub screen layer enable
71+
pub const TMW = reg(0x212e); // Main screen window enable
72+
pub const TSW = reg(0x212f); // Sub screen window enable
73+
74+
// ---- PPU: Color Math ----
75+
pub const CGWSEL = reg(0x2130); // Color math control (clip/prevent, add/sub windows)
76+
pub const CGADSUB = reg(0x2131); // Color math add/subtract layer select
77+
pub const COLDATA = reg(0x2132); // Fixed color data (B/G/R + intensity)
78+
79+
// ---- PPU: Multiplication result (read) ----
80+
pub const MPYL = reg(0x2134); // Signed multiply result low (M7A × M7B)
81+
pub const MPYM = reg(0x2135); // Signed multiply result middle
82+
pub const MPYH = reg(0x2136); // Signed multiply result high
5183

5284
// ---- PPU: Status (read) ----
5385
pub const RDNMI = reg(0x4210); // V-blank NMI flag and CPU version
@@ -94,6 +126,9 @@ pub fn DASL(comptime n: u3) *volatile u8 {
94126
pub fn DASH(comptime n: u3) *volatile u8 {
95127
return reg(0x4306 + @as(u16, n) * 0x10);
96128
}
129+
pub fn DASB(comptime n: u3) *volatile u8 {
130+
return reg(0x4307 + @as(u16, n) * 0x10);
131+
}
97132

98133
/// Build a 15-bit BGR colour word (SNES format: 0bbbbbgggggrrrrr).
99134
pub fn color(r: u5, g: u5, b: u5) u16 {

snes/header.zig

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,21 @@ comptime {
1818
\\ .word 0xffff
1919
\\ .word 0x0000
2020
\\
21+
\\.extern nmi_handler
2122
\\.section .vectors,"a",@progbits
22-
\\ .word 0x0000
23-
\\ .word 0x0000
24-
\\ .word 0x0000
25-
\\ .word 0x0000
26-
\\ .word 0x0000
27-
\\ .word 0x0000
28-
\\ .word 0x0000
29-
\\ .word 0x0000
30-
\\ .word 0x0000
31-
\\ .word 0x0000
32-
\\ .word 0x0000
33-
\\ .word 0x0000
34-
\\ .word _start
35-
\\ .word 0x0000
23+
\\ .word 0x0000 /* $FFE4 native COP */
24+
\\ .word 0x0000 /* $FFE6 native BRK */
25+
\\ .word 0x0000 /* $FFE8 native ABORT */
26+
\\ .word nmi_handler /* $FFEA native NMI */
27+
\\ .word 0x0000 /* $FFEC (unused) */
28+
\\ .word 0x0000 /* $FFEE native IRQ */
29+
\\ .word 0x0000 /* $FFF0 emu COP */
30+
\\ .word 0x0000 /* $FFF2 (unused) */
31+
\\ .word 0x0000 /* $FFF4 emu ABORT */
32+
\\ .word 0x0000 /* $FFF6 (unused) */
33+
\\ .word 0x0000 /* $FFF8 emu NMI */
34+
\\ .word 0x0000 /* $FFFA (unused) */
35+
\\ .word _start /* $FFFC emu RESET */
36+
\\ .word 0x0000 /* $FFFE emu IRQ/BRK */
3637
);
3738
}

snes/hirom-hello/hirom-hello.zig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2024 Matheus C. França
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// SNES HiROM hello: fills the backdrop with Zig orange and spins forever.
5+
6+
pub const panic = @import("mos_panic");
7+
8+
const sneslib = @import("sneslib");
9+
comptime {
10+
_ = @import("snes_header");
11+
}
12+
13+
pub fn main() void {
14+
sneslib.ppu_off();
15+
// Zig orange: R=31 G=14 B=2 → 15-bit BGR $09DF
16+
sneslib.cgram_set(0, sneslib.color(31, 14, 2));
17+
sneslib.ppu_on();
18+
while (true) {}
19+
}

snes/hirom.ld

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* Copyright (c) 2024 Matheus C. França */
2+
/* SPDX-License-Identifier: Apache-2.0 */
3+
/* */
4+
/* SNES HiROM — single 32 KiB bank ($00:8000–$00:FFFF). */
5+
/* */
6+
/* Small HiROM ROMs keep code in the $8000-$FFFF window which */
7+
/* is accessible in banks $00-$3F even in HiROM mode. The map */
8+
/* mode byte in the ROM header ($FFD5 = $21) distinguishes this */
9+
/* from LoROM. For multi-bank ROMs the full 64 KiB per bank is */
10+
/* used, but a single-bank example fits here identically. */
11+
/* */
12+
/* Memory map (bank $00): */
13+
/* $0000–$00FF Direct page / ZP (WRAM mirror, first 256 B) */
14+
/* $0100–$01FF Hardware stack (WRAM mirror) */
15+
/* $0200–$1FFF WRAM (data / BSS / soft stack) */
16+
/* $2000–$5FFF Hardware I/O registers (not addressable here) */
17+
/* $8000–$FFFF ROM bank 0 (32 KiB) */
18+
19+
/* Imaginary (zero-page / direct-page) registers __rc0–__rc31. */
20+
__rc0 = 0x0000;
21+
INCLUDE imag-regs.ld
22+
ASSERT(__rc31 == 0x001f, "Inconsistent zero page map.")
23+
24+
MEMORY {
25+
/* Direct page: bytes after imaginary regs, up to end of page */
26+
zp (rw) : ORIGIN = __rc31 + 1, LENGTH = 0x100 - (__rc31 + 1)
27+
/* WRAM: $0200–$1FFF (stack page $0100–$01FF reserved for hardware stack) */
28+
ram (rw) : ORIGIN = 0x0200, LENGTH = 0x1e00
29+
/* ROM bank 0: full 32 KiB window */
30+
rom (rx) : ORIGIN = 0x8000, LENGTH = 0x8000
31+
}
32+
33+
REGION_ALIAS("c_readonly", rom)
34+
REGION_ALIAS("c_writeable", ram)
35+
36+
/* Soft stack grows down from the top of WRAM. */
37+
__stack = 0x2000;
38+
39+
SECTIONS {
40+
INCLUDE c.ld
41+
42+
/* SNES internal ROM header: 32 bytes at $FFC0–$FFDF. */
43+
.snes_header 0xffc0 : { KEEP(*(.snes_header)) } >rom
44+
45+
/* Interrupt vector table: 28 bytes at $FFE4–$FFFF.
46+
* The 4-byte gap $FFE0–$FFE3 between header and vectors is zero-filled. */
47+
.vectors 0xffe4 : { KEEP(*(.vectors)) } >rom
48+
}
49+
50+
OUTPUT_FORMAT {
51+
FULL(rom)
52+
}

snes/hirom_header.zig

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2024 Matheus C. França
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// SNES HiROM internal ROM header ($FFC0–$FFDF) and interrupt vector table
5+
// ($FFE4–$FFFF). Both sections are placed at fixed addresses by hirom.ld.
6+
//
7+
// Map mode $21 = HiROM. $31 = FastROM HiROM. Use $21 for compatibility.
8+
9+
comptime {
10+
asm (
11+
\\.section .snes_header,"a",@progbits
12+
\\ .ascii "ZIG SNES HIROM "
13+
\\ .byte 0x21 /* map mode: HiROM */
14+
\\ .byte 0x00 /* ROM type: ROM only */
15+
\\ .byte 0x05 /* ROM size: 1 Mbit */
16+
\\ .byte 0x00 /* SRAM size: 0 */
17+
\\ .byte 0x01 /* destination: NTSC */
18+
\\ .byte 0x00 /* fixed: $00 */
19+
\\ .byte 0x00 /* version: 1.0 */
20+
\\ .word 0xffff /* checksum complement */
21+
\\ .word 0x0000 /* checksum */
22+
\\
23+
\\.extern nmi_handler
24+
\\.section .vectors,"a",@progbits
25+
\\ .word 0x0000 /* $FFE4 native COP */
26+
\\ .word 0x0000 /* $FFE6 native BRK */
27+
\\ .word 0x0000 /* $FFE8 native ABORT */
28+
\\ .word nmi_handler /* $FFEA native NMI */
29+
\\ .word 0x0000 /* $FFEC (unused) */
30+
\\ .word 0x0000 /* $FFEE native IRQ */
31+
\\ .word 0x0000 /* $FFF0 emu COP */
32+
\\ .word 0x0000 /* $FFF2 (unused) */
33+
\\ .word 0x0000 /* $FFF4 emu ABORT */
34+
\\ .word 0x0000 /* $FFF6 (unused) */
35+
\\ .word 0x0000 /* $FFF8 emu NMI */
36+
\\ .word 0x0000 /* $FFFA (unused) */
37+
\\ .word _start /* $FFFC emu RESET */
38+
\\ .word 0x0000 /* $FFFE emu IRQ/BRK */
39+
);
40+
}

0 commit comments

Comments
 (0)