First-person shooter for the PlayStation 1, written in Rust.
DDA raycasting engine (Wolfenstein/Doom style), fixed-point math, textured walls, enemy sprites, SPU audio — all running on real PS1 hardware via RetroArch.
╔══════════════════════════════════════════════╗
║ H E L L G R I D — PS1 FPS ║
║ Survive the grid. Find the exit. Get out. ║
╚══════════════════════════════════════════════╝
In WSL (Ubuntu/Debian):
gcc-mipsel-linux-gnu— MIPS cross-compilerpython3+Pillow— texture conversionffmpeg— audio conversionrustupnightly withrust-srccomponent
Run the one-time setup:
bash build-wsl.sh setup# Full build: convert assets → compile Rust → create disc image
bash build-wsl.sh build
# Quick rebuild (skip asset conversion):
bash build-wsl.sh quick
# Just recompile Rust:
bash build-wsl.sh rustOutput: hellgrid.cue + hellgrid.bin
Load hellgrid.cue in RetroArch.
Recommended cores:
| Core | Notes |
|---|---|
| Beetle PSX HW | Best accuracy, texture filtering |
| PCSX ReARMed | Fast, good for lower-end devices |
| Mednafen PSX | Cycle-accurate |
| Button | Action |
|---|---|
| D-pad Up / Down | Move forward / backward |
| D-pad Left / Right | Turn |
| L1 / R1 | Strafe left / right |
| Cross (✕) | Shoot |
| START | Pause |
| START (on Game Over) | Restart |
Objective: Kill all enemies, find the golden exit tile, survive.
hellgrid/
├── src/
│ ├── main.rs — entry point, game loop
│ ├── hw.rs — PS1 MMIO register addresses
│ ├── math.rs — Q16.16 fixed-point, sin/cos table
│ ├── gpu.rs — GPU primitives, TIM loader, VRAM upload
│ ├── spu.rs — SPU audio, VAG upload, voice management
│ ├── input.rs — SIO0 controller polling
│ ├── level.rs — 24×24 tile map, collision types
│ ├── game.rs — player, enemies, game state, AI
│ ├── raycast.rs — DDA raycaster, wall/sprite/weapon render
│ ├── hud.rs — HP bar, ammo, score, overlays
│ └── assets_data_stub.rs — empty asset stubs (used before pipeline runs)
│
├── tools/
│ ├── prepare_assets.py — master asset pipeline (PNG→TIM, OGG→VAG)
│ ├── png2tim.py — PS1 TIM 4bpp converter
│ ├── wav2vag.py — PS1 ADPCM VAG encoder
│ ├── make_psexe.py — ELF binary → PS-EXE header
│ ├── make_iso.py — Mode2/2352 ISO 9660 disc builder
│ └── SYSTEM.CNF — PS1 boot config
│
├── assets/
│ ├── raw/
│ │ ├── textures/tiny-dungeon/Tiles/ — CC0 source tiles (16×16 px)
│ │ ├── sounds/sfx/ — CC0 sound effects (.ogg)
│ │ └── music/dungeon_ambient.ogg — CC0 ambient music
│ ├── tim/ — generated: converted textures
│ └── vag/ — generated: converted audio
│
├── crt0.s — MIPS startup: SP/GP init, BSS clear, jal main
├── psexe.ld — linker script: load at 0x80010000, 2MB RAM
├── Cargo.toml — no_std binary, release: opt-size + LTO
├── .cargo/config.toml — mipsel-sony-psx target, mipsel-linux-gnu-ld
├── Makefile — make all / assets / build / iso / run / clean
└── build-wsl.sh — full WSL pipeline script
- Renderer: DDA raycasting — one ray per screen column (320 rays)
- Math: Q16.16 fixed-point throughout; no floating point, no FPU
- Trig: 256-angle lookup table, quarter-wave (65 entries + symmetry)
- Textures: PS1 TIM 4bpp, 64×64px, loaded into VRAM via CPU→VRAM DMA
- Sprites: camera-space projection with Z-buffer occlusion
- Double buffering: draw buffer alternates VRAM Y=0 / Y=240
| Unit | Usage |
|---|---|
| GPU | PolyFT4 (textured quads), Tile, Fill, LineF2 |
| SPU | 24-voice ADPCM, DMA upload via FIFO |
| SIO0 | Digital pad polling |
| VRAM | 320×240×2 display buffers + 8 texture slots + CLUTs |
(0, 0)–(319, 239) display buffer 0 (15bpp)
(0, 240)–(319, 479) display buffer 1 (15bpp)
(320, 0)–(383, 63) wall_stone.tim 4bpp CLUT @ (0, 480)
(384, 0)–(447, 63) wall_metal.tim 4bpp CLUT @ (16, 480)
(448, 0)–(511, 63) wall_blood.tim 4bpp CLUT @ (32, 480)
(512, 0)–(575, 63) wall_panel.tim 4bpp CLUT @ (48, 480)
(576, 0)–(639, 63) floor.tim 4bpp CLUT @ (64, 480)
(640, 0)–(703, 63) ceiling.tim 4bpp CLUT @ (80, 480)
(704, 0)–(767, 63) enemy.tim 4bpp CLUT @ (96, 480)
(768, 0)–(831, 63) weapon.tim 4bpp CLUT @ (112,480)
PNG (16×16) ──[×4 nearest]──► 64×64 ──[4bpp quantize]──► TIM ──► embedded in EXE
OGG/WAV ──[ffmpeg → PCM]──► 22050 Hz mono ──[ADPCM encode]──► VAG ──► embedded in EXE
All assets are embedded as [u8; N] static arrays in src/assets_data.rs (generated). No file loading at runtime.
[profile.release]
opt-level = "s" # optimize for size
lto = true
panic = "abort"
overflow-checks = falseTarget: mipsel-sony-psx (Tier 3, requires -Z build-std=core).
All assets are CC0 (public domain):
| Asset | Source |
|---|---|
| Textures | Kenney Tiny Dungeon |
| Sound effects | 100-CC0-SFX pack |
| Music | Loopable Dungeon Ambience (CC0) |
make all — full build (assets + compile + ISO)
make assets — convert PNG/OGG to TIM/VAG
make build — compile Rust to ELF
make psexe — ELF → PS-EXE
make iso — PS-EXE → hellgrid.cue / hellgrid.bin
make run — launch in mednafen or pcsx-redux
make check — cargo check (no link)
make clean — remove build artifacts