msx2go

module
v0.0.0-...-076377d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: Apache-2.0

README

msx2go

msx2go turns an MSX game — a cartridge ROM or a floppy image — into a standalone Go program. Not an emulator with a ROM inside it: the game's machine code is translated to Go source ahead of time, its data sits beside the code as named byte slices, and a machine model (VDP, PSG, SCC, BIOS, disk) gives that code a world to run in. The output is a self-contained Go module; build it and you have a native program that plays the game, with a window, keys and sound — or headless, for scripting and verification.

msx2go -rom salamander.rom -out ./salamander
cd salamander
go mod tidy
go build -tags msxdata ./cmd/salamander-play
./salamander-play

Verified titles, checked frame-by-frame against openMSX running the real machine ROMs: King's Valley, Salamander, Space Manbow (retail and the FRS re-release), King's Valley II, King's Valley Plus (floppy), Breaker (floppy).

Requirements

  • Go 1.26 or later. Install with:

    go install github.com/brunoga/msx2go/cmd/msx2go@latest
    go install github.com/brunoga/msx2go/cmd/msxrun@latest
    
  • For the windowed build: whatever Ebitengine needs on your platform (on Linux: X11/GL development libraries; nothing at run time). The headless build needs nothing.

  • A game image you have the right to use. msx2go ships no ROMs.

Converting a cartridge

msx2go -rom game.rom -out ./gamego

msx2go detects the mapper (flat, Konami, Konami+SCC, ASCII-8/16) and the machine (MSX1/MSX2) from the image, traces every instruction reachable from the cartridge's INIT, interprets the game for a while to see what else runs (-sweep, default 20000 frames), and writes a Go module. Useful flags:

flag what it does
-rom file the cartridge image
-out dir where the module goes; without it, just report on the image
-name n short name; commands are built as cmd/<n> and cmd/<n>-play
-module path the go.mod module path (default example.com/<name>)
-machine m msx1, msx2 or msx2plus; default is detected
-mapper m override mapper detection
-base a where the cartridge maps (default 4000h)
-sweep n frames to interpret before translating; 0 = static trace only
-tape f a recorded input tape to drive the sweep deeper into the game
-config f extra entry points and do-not-decode ranges, for the rare image that needs them
-whole keep the whole image instead of pruning bytes the translation covers
-interpret emit an interpreter-only module: same machine, same harness, no translation. The build to reach for when a translation is suspected of diverging

Converting a floppy

msx2go -dsk game.dsk -run GAME.BAS -out ./gamego

A disk has no cartridge header. What it has is a BASIC loader, and msx2go runs it: the tokenised program is interpreted — BLOAD, LOAD, SCREEN, SET PAGE, POKE, the loader vocabulary — files land in RAM and VRAM exactly as they would on the machine, and the program the loader ends by starting is the game. -run names the BASIC program to begin with when the disk has more than one and no AUTOEXEC.BAS.

The conversion boots the disk once and records what the boot revealed: the program's shape (does it settle into an interrupt handler, or is the loop the program itself), the machine (a boot that programs the V9938 is an MSX2 program), and the region of RAM the loader filled with code. That region is snapshotted, traced from the game's interrupt hooks, and translated; its SHA-1 goes into the module. At run time the same loader runs again, and the moment it finishes the loaded bytes are hashed: a match turns the translation on, a mismatch — an edited floppy — leaves the machine interpreting, which is always correct and merely slower.

If the game writes its floppy (a high-score file, a level editor), the changed image is written back beside the save state on exit.

Games on more than one floppy

Name them together, in order, and the first is the one that boots:

msx2go -dsk disk1.dsk,disk2.dsk,disk3.dsk -out ./gamego

All of them go into the generated program, so "insert disk 2" is a keypress -- F12 puts the next floppy in the drive -- rather than a file to go and find. msxrun -dsk a.dsk,b.dsk takes the same list.

The machine has drives as well as disks, and they are not the same thing: three floppies and one drive is a game that asks you to swap, while two drives hold two floppies at once and never ask. Both work. BDOS's select-drive and current-drive calls answer honestly, a file control block naming drive A or B gets that drive, and absolute sector reads use the drive in L. Which floppy is in which drive is saved in a snapshot, because it is part of where a player got to.

The output module

gamego/
  go.mod
  cmd/<name>/         the headless harness: frames, digests, PNGs
  cmd/<name>-play/    the windowed harness: keys, sound, snapshots
  z80/
    rom_gen.go        generated: the translation, a label per instruction
    rom_meta.go       generated: name, machine, mapper, hashes
    data_gen.go       generated: the game's bytes  (build tag `msxdata`)
    *.go              the runtime: machine, VDP, PSG, SCC, BIOS, disk

Build with -tags msxdata and the game's data is compiled in; the binary is the game. Build without the tag and the binary looks for <name>.dat beside itself instead (<name> -extract game.dat writes it), so a binary can be shared separately from the data it needs.

Everything the module imports is inside it. It builds with no network and no reference back to msx2go.

Playing

go build -tags msxdata ./cmd/<name>-play
./<name>-play

Arrows and space are the joystick and trigger A; Z is also trigger A, shift and X trigger B. The rest of the MSX keyboard is there too — letters, digits, function keys, RETURN, ESC, TAB — for games that read keys by name. Harness keys: F9 saves a snapshot, F10 restores it, F11 toggles fullscreen.

flag what it does
-scale n window scale
-fullscreen start fullscreen
-border draw the hardware border (default on)
-speed x run faster or slower than real time
-cpu x processor speed as a multiple of a stock MSX; the default 1 reproduces the slowdown the game was tuned around
-hz n 50 or 60
-state dir where snapshots go
-rectape f record every frame's input to a tape
-data f where to find <name>.dat for a build without msxdata
-disk f where a changed floppy is written on exit (floppy builds)

The headless harness

go build -tags msxdata ./cmd/<name>
./<name> -frames 3000 -digest 500 -png last.png

It runs frames and reports. -digest n prints a hash of VRAM, work RAM and the sound registers every n frames — the equality test between two builds. -png writes the last frame; -vramspan from:to:file appends VRAM and the VDP registers each frame for vramcmp; -tape replays input; -savestate/-loadstate snapshot and resume; -extract writes the data sidecar; -learn records every address that had to run interpreted, which feeds the discovery loop:

The discovery loop

A translation covers the code that was seen. To grow it:

./<name> -frames 5000 -learn round.txt     # play, record what interpreted
grep -v '^#' round.txt >> sites.txt        # feed it back
msx2go -rom game.rom -out ./gamego        # regenerate: translated next time

msx2go -discover n automates up to n rounds of that for cartridges, and -tape drives the runs deeper than an attract mode goes. An address never fed back still works — it falls back to the interpreter at run time and is written down. The loop is done when a long, varied run reports nothing new.

-explore n maps by force instead of play: it boots the machine, then forks it at every conditional branch and runs both arms, at most n instructions in total. Every fork is a real machine state, so dynamic jumps — dispatchers, threaded code — have concrete targets, and because only code coverage is wanted, each branch arm is forked once and the work stays linear. What it finds goes to explored.txt beside sites.txt: explored addresses seed the tracer but are candidates, not observations — a forced arm can walk into data — so pruning never believes them, and the -interpret twin comparison remains the test of truth. On Breaker, one 72-second exploration reaches within a few percent of what three interactive learn rounds found.

For a floppy, sites.txt in the output directory is picked up the same way. The main thread crosses into the translation at call boundaries: when the interpreter executes a call whose target is a translated label, the routine runs translated and hands back when the stack rises past the call — the interpreter's own stopping rule, so code that reads its return address off the stack still reads the truth. A bridged run that outlives the frame's cycle budget hands back at its next return, which is a deoptimisation, never a divergence. On Breaker this takes the share of instructions running translated from 8% to 99%, with the picture sequence identical to the interpreter-only build frame for frame.

msxrun: the workbench

go build ./cmd/msxrun
msxrun -rom game.rom -frames 600 -digest 600
msxrun -dsk game.dsk -run GAME.BAS -frames 600

msxrun interprets an image directly — no generation step — with the same machine the generated modules use. It exists to answer questions: most of its several dozen flags are instruments (watch a memory range, log VDP commands, trace BIOS calls, dump VRAM at a frame) grown while verifying the titles above, and msxrun -h describes each. The three worth knowing: -frames, -digest, -vramdump.

vramcmp renders a reference VRAM dump and a -vramspan capture through the same rasteriser and passes if any frame in the span shows the identical picture — the tool for "does it draw what the real machine draws".

Verification

The claim "behaves like the cartridge" is checked, not assumed:

  • The -interpret twin. Generate the same game twice, once translated and once interpreter-only, and compare -digest output over thousands of frames. Any divergence is a translation bug with a frame number attached.
  • The reference machine. ref/ drives openMSX headless over its control channel and digests work RAM at every interrupt, VRAM at every frame, for comparison against the real BIOS ROMs. See ref/README.md.
  • go test ./... covers the machine model itself.

What is modelled

  • Z80, with an explicitly modelled stack — cartridge code treats return addresses as data, and the translation preserves that.
  • VDP: TMS9918 and V9938 — tile and bitmap screens 0–8, the command engine, sprites in both modes, mid-frame register changes (split screens, line interrupts), vertical scroll, and the interleaved VRAM of screens 7/8. The V9958's extras (YJK screens 10–12) are not modelled yet, so msx2plus covers only titles that stay within V9938 features.
  • Sound: AY-3-8910 PSG and the Konami SCC, synthesised on the machine's clock.
  • BIOS and sub-ROM: implemented as documented routines, not copied code — each entry written from the datasheet and measured against a real machine when the documentation ran out. The character set is C-BIOS's (BSD-licensed), not a copy of any Microsoft ROM. An entry no verified game has needed yet reports itself loudly rather than guessing.
  • Disk: FAT12 floppies, the BASIC loader vocabulary, the disk BIOS and BDOS calls the verified titles use, write-back for games that save.
  • Timing: instructions cost cycles; a handler that overruns the frame loses its next interrupt, which is what the hardware does and what the games were tuned around. -cpu relaxes it deliberately.

Honest limitations

  • Coverage follows verification. The BIOS and machine model implement what the verified titles exercise, measured against real hardware; other games will work to the extent they use the same surface, and a missing routine names itself at run time rather than failing silently.
  • Floppy main threads run interpreted (see above). The interpreter is hundreds of times faster than the hardware, so this costs nothing you can see or hear.
  • No cassette, no MSX-DOS 2, no mouse yet, no Kanji ROM, no V9958.
  • PAL/NTSC is a -hz switch, not a per-machine model.

msx2go contains no copyrighted ROM code or data, and asks you to supply the game image. What the tool generates from that image contains the game's code (as translated source) and data: a generated module, and any binary built from it with -tags msxdata, embodies the original game and carries its copyright. Keep the output as private as the image it came from, unless you hold the rights.

msx2go itself is licensed under the Apache License 2.0 (see LICENSE). The built-in character set is from C-BIOS, BSD-licensed; its notice is in the NOTICE file and travels with every generated module. openMSX is used only as a development reference and is not part of msx2go or its output.

More

  • PLAN.md — the design: why translation, the output's shape, milestones.
  • docs/bios.md — how BIOS entries are implemented and measured.
  • games/*/README.md — per-title verification notes.
  • ref/README.md — the openMSX reference rig.

Directories

Path Synopsis
cmd
msx2go command
Command msx2go turns an MSX cartridge ROM into Go source: a static recompilation of its code, the data it reads, and a machine to run it against.
Command msx2go turns an MSX cartridge ROM into Go source: a static recompilation of its code, the data it reads, and a machine to run it against.
msxrun command
msxrun runs a cartridge in the interpreter and reports what it did.
msxrun runs a cartridge in the interpreter and reports what it did.
vramcmp command
Command vramcmp asks whether a machine showed the same picture as the reference, within a window of frames.
Command vramcmp asks whether a machine showed the same picture as the reference, within a window of frames.
internal
dis
Package dis decodes Z80 instructions.
Package dis decodes Z80 instructions.
emit
Package emit turns traced instructions into Go.
Package emit turns traced instructions into Go.
emit/runtime
Package z80 is the machine that statically translated cartridge code runs against.
Package z80 is the machine that statically translated cartridge code runs against.
trace
Package trace finds the code in a cartridge.
Package trace finds the code in a cartridge.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL