-
Notifications
You must be signed in to change notification settings - Fork 0
Writing a ROM
A ROM is just a GDScript file. It's your program — the thing that actually runs on the fake hardware. The CPU loads it, wires up references to everything, and calls your tick function every frame.
Only one ROM can run at a time. The project uses the custom "Rom File" field in the CPU node to identify which file is the active cartridge.
To switch between them, Click on the CPU node in the tree, and drag your file onto the "Rom File" field.

extends Node
class_name ROM
var ppu: PPU
var apu: APU
var cpu: CPU
func _enter_tree() -> void:
name = "ROM"
func tick(delta: float):
ppu.clear(Color.BLACK)
ppu.text(8, 8, "HELLO WORLD", Color.WHITE)That's a complete, working ROM. It clears the screen every frame and draws text.
extends Node — ROMs are Godot nodes, so they need this.
var ppu, var apu, var cpu — declare these and the CPU auto-fills them before anything runs. You don't need all three — only declare the ones your ROM actually uses. If the variable isn't there, the CPU skips it.
_enter_tree() setting name = "ROM" — important. The CPU finds your ROM in the scene tree by looking for a node named "ROM". If you skip this, it won't connect properly.
tick(delta: float) — called every frame while the CPU is running. delta is the time since the last frame in seconds, same as Godot's _process. All your game logic goes here.
Accumulate delta into a variable and use it to drive animations:
var t := 0.0
func tick(delta: float):
t += delta
ppu.clear(Color.BLACK)
var x = int(128 + sin(t) * 60)
ppu.circlefill(x, 128, 20, Color.WHITE)Enable channels in _enter_tree, then update them in tick:
func _enter_tree() -> void:
name = "ROM"
apu.pulse1_enabled = true
func tick(delta: float):
t += delta
apu.pulse1_freq = 220 + sin(t * 2.0) * 100if Input.is_action_just_pressed("ui_cancel"):
cpu.panic("USER QUIT", "Escape key was pressed")Forgetting ppu.clear() — if you don't clear the screen at the start of tick, every frame draws on top of the last one. Shapes will smear across the screen. Sometimes that's intentional, usually it isn't.
cpu.panic() is permanent — once called, the CPU stops forever. The only way back is restarting the project. Don't call it for recoverable errors.
Text is always uppercase — ppu.text() calls .to_upper() on whatever string you pass in. Lowercase works fine as input, it just renders as uppercase.
ppu.render() does nothing — it exists for compatibility but has no implementation. Don't rely on it.
noise_enabled isn't a register — you toggle it directly: apu.noise_enabled = true. The register map doesn't include it.
Software/ROM.gd is a good thing to read through. It shows:
- How to use
tfor animation (sin,costo drive positions and frequencies) - How to draw multiple things per frame (clear once, then draw everything)
- How to rotate geometry by hand (transform points with
Vector2.rotated, then draw lines between them) - How to make the noise channel fire in bursts (
apu.noise_enabled = (sin(t * 12.0) > 0.92)) - How to display live debug info with
ppu.text
Software/Template.gd is a blank slate — just the font test, easy to delete and replace with your own stuff.