PSIVRecomp

PSIVRecomp is a native port of Phantasy Star IV: The End of the Millennium for Windows, Linux, and Android with enhanced graphics and sound options.

README

PSIV Recomp

The port's icon: a four-point star

A native port of Phantasy Star IV: The End of the Millennium (Mega Drive / Genesis, USA) built by static recompilation. Work in progress: see docs/PLAN.md for the architecture and milestones and docs/IDEAS.md for planned enhancements.

This repository contains no game data. You need your own dump of the USA cartridge (GM MK-1307-00).

How it's made: built by PsiLok with Claude (Anthropic), which wrote most of the code; the design, direction, testing, music and art are PsiLok's. Commits made with Claude say so in a Co-Authored-By line.

Setup

Requires Python 3.10+, and git with submodules.

git submodule update --init
python tools/prepare_rom.py "path/to/your/dump.smd"   # .smd, .bin, .md or .gen
python tools/build_disasm.py                           # rebuild + verify, writes build/disasm/ps4.lst
python tools/validate_decoder.py                       # decoder vs every instruction in the game
python tools/resolve.py                                # function map + indirect-jump targets -> analysis/
python tools/emit.py                                   # recompile to C -> build/gen/
build.bat                                              # Windows (VS Build Tools); elsewhere: cmake -S . -B build/native && cmake --build build/native
build\native-Release\psiv.exe                          # play (run from the project folder)
Third-party code (fetched into third_party/, not committed)
FolderWhatSource
SDL3-3.4.20/Window, input, audio (Windows only; Linux uses the system SDL3)SDL3-devel-3.4.20-VC.zip from github.com/libsdl-org/SDL releases
ymfm/src/YM2612 FM chip (BSD-3)github.com/aaronsgiles/ymfm, commit in ymfm/COMMIT: src/ymfm.h, ymfm_fm.h/.ipp, ymfm_opn.h/.cpp, ymfm_adpcm.h/.cpp, ymfm_ssg.h/.cpp, ymfm_misc.h/.cpp
chips/Z80 CPU (zlib)chips/z80.h from github.com/floooh/chips, commit in chips/COMMIT

The PSG is implemented in runtime/sound.c. Android and the portable Linux build also need the SDL3 3.4.20 source (SDL3-3.4.20.tar.gz, same releases page), kept outside the repo. The modern text's font, Noto Sans (SIL OFL 1.1), is committed in res/fonts/ with its licence.

Platforms and packages
BuildPackage for testers
Windowsbuild.bat (static C runtime: no redistributable needed)python tools/package.py windows build\native-Release dist
Linuxcmake -S . -B build/linux -G Ninja && ninja -C build/linux (system SDL3)below
Linux, portableSDL3 built static from source, then -DPSIV_PORTABLE=ON -DPSIV_TOOLS=OFF -DCMAKE_PREFIX_PATH=<its prefix>python3 tools/package.py linux <build> dist
Androidtools/build_android.sh <SDL3 source> on Linux (SDK, NDK r30, build-tools 36, JDK 17; no Gradle)the .apk it makes
  • The launcher: the app opens on it -- Start game, the mode (Enhanced / Classic, a switch), Settings (the same five pages as in the game), Quit -- over a drifting field of stars under the port's emblem, the icon's four-point star drawn at full size from the same shape (launcher() in runtime/main_sdl.c). The game boots only when Start game is chosen. psiv --play skips it (for frontends and shortcuts); the game-frame testing hooks (--menu, --test-keys, --test-click, --test-rumble, --capture) skip it too, unless a --menu-script drives the launcher.
  • First run: with no rom/psiv.bin, a dump in the home folder or rom/ installs itself at startup; otherwise Start game asks for it in a file dialog (Android: the system picker), and cancelling returns to the launcher. .smd, raw, or either inside a .zip (runtime/unzip.c); checked by SHA-256 and kept as rom/psiv.bin. Testing hooks: --test-pick FILE answers the dialog, --no-dialogs sends message boxes to the log.
  • Home folder: next to the program when writable (the zip, the tarball), else SDL_GetPrefPath (%APPDATA%\PSIV Recomp, ~/.local/share/psiv-recomp); Android: Android/data/com.psilok.psiv/files; a dev build: the project root.
  • Portable Linux: libstdc++ and SDL3 are linked in, so it needs only glibc; runtime/glibc_compat.c binds the functions newer glibcs re-versioned to their old versions, so it runs on glibc 2.35 (Ubuntu 22.04) while being built on a current distro. Check: objdump -T psiv | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1.
  • Android: libmain.so (the port) under SDL3's activity (android/java), landscape, an on-screen pad (runtime/touch.c) that hides when a gamepad or key is used; Back opens the menu. The APK is signed with ~/.psiv-keys/psiv-alpha.jks (made on first build, password beside it): keep that key, since Android only installs updates signed with the same one.
  • macOS: not packaged yet. The code is ready for it (an app bundle's home is the pref path); it needs a Mac to build and sign.
  • docs/TESTERS.md is the README that goes into every package.
Controls
Mega DriveKeyboardGamepad
D-padarrowsD-pad / left stick
A / B / CZ / X / CWest / South / East
StartEnterStart

The app opens on the launcher: Start game, Enhanced or Classic, Settings, Quit (arrows or the stick, Enter or C; Left / Right flips the mode). Hold Tab to fast-forward, F10 pause, F11 fullscreen, F12 screenshot (screenshots/). Esc (or the pad's Back/Select) opens the system menu: resume, settings, quit. Settings are on five pages -- Game, Display, Sound, Battles, Controls -- turned with Tab / Page Up / Page Down, the shoulder buttons, or a click or tap on the page's name; the line under the list says what the highlighted setting does. Battery saves go to saves/psiv.srm; psiv --save FILE plays another, an emulator's .srm too (BizHawk, Gens, Kega Fusion), written back in its own layout.

Rewind (Backspace, the left shoulder, or "Go back 10 seconds" in the Esc menu; Settings > Game > Rewind; PsiLok's design): the picture and the sound fade to white, and fade back in on the game as it was about ten seconds before -- one state kept every ten seconds, the last two minutes of them; again while it fades back in, ten seconds more. A note in the corner says how far it went. A state is the whole machine (the 68000 and its RAM, the VDP, the Z80, the YM2612 and the PSG, the bus) and the game's own C stack: the recompiled game keeps its place in the C call chain, so it runs on a stack of its own (runtime/coro.h: a routine in assembly per platform, x86-64 Windows and System V, AArch64) and comes back to the program's stack at each frame, where its stack is copied. The battery save stays as it is. psiv_headless --rewind-test S:R keeps a state at logical frame S and puts it back at R: the game then goes on from S byte for byte as it did (RAM, VRAM, CRAM, VSRAM, registers, and the chips' sound, sample for sample; checked on Windows, Linux and the Odin 2).

Rumble (Settings > Controls > Rumble: off / light / normal / strong; pad.rumble): the Mega Drive had none, so the port reads the moments from the game's memory (runtime/rumble.c, reads only, so the game is unchanged): a blow landing, felt with the target's hit reaction (heavier when it's the party's hurt, stronger the more HP it takes when the game has worked that out yet), a critical hit with its white flash, an enemy destroyed, a party member falling, the final bosses' deaths, the big techniques, and the field's doors, stone slabs, elevators, breaking ice and barriers, explosions and a ship's engines, each shaped to the length of its sound. It plays on a gamepad's two motors; on a phone, or a handheld whose controls can't rumble, on the device's vibration motor, a short pulse per effect. Mode: Classic leaves it as set (nothing on screen or in the sound changes). psiv_headless --rumble-log lists the effects as they come (PSIV_RUMBLE_SOUNDS=1: every sound request too); psiv --test-rumble F attaches a virtual gamepad that logs what it's asked to play.

Enhancements

Settings in the Esc menu (saved to psiv.ini). Mode: Classic is the original game, unchanged; Enhanced turns on the additions:

  • Battle depth (F9: off / subtle / strong): battles drawn as layers at different depths under a gently moving camera that leans in on attacks. Sprites on a party member (a blow landing, a glow in a caster's hand) move with the party, and sprites that touch move as one, so no picture comes apart.

  • Battle fog (F8: off / light / heavy): drifting mist between the enemies and the party, tinted from each battle's background.

  • Battle weather (off / light / heavy), by where the battle is (the port works out which of the game's 32 battle backgrounds it uses): rain or a storm in some of Motavia's grassland, beach and sea battles -- darker skies, lightning, the rain and the thunder heard -- snow on Dezolis, mist clouds drifting over Rykros, its towers and the carnivorous trees, and rocks floating in the Air Castle and at the Edge. Drawn at three depths under the battle camera: behind the enemies, between them and the party, and in front of everyone (runtime/weather.c; the sounds are synthesized like the port's sound effects). Off in vehicle battles.

  • Battle effects (off / subtle / full): the game's own attack and technique sprites light the scene in their colours, sparks and a flash where hits land, embers rising from fire, a lens flare at bright moments.

  • Music (original chip / Algol): Algol is the port's own modern sound chip (runtime/algol.c). The game's music driver still runs and still writes the YM2612 and the PSG; Algol decodes those writes into notes (pitch, instrument, loudness, pan) and plays them on new instruments -- unison saw leads, brass, strings, a bass with a sub, a driven guitar, plucks, FM bells and keys, an organ, soft square leads -- each of the game's FM patches voiced by the instrument closest to its sound, with a synthesized drum kit in place of the drum samples (voices and effects sampled in the game are played cleanly from your ROM), reverb, chorus and a stereo delay. Parts the game doubles on several channels are merged. Sound effects keep the original chips, on the channels they borrow. The tense songs -- the title, Meet Them Head-On and the battle and boss themes -- get hard rock drums: a kick with a beater knock, a snare with a crack, deeper toms, brighter cymbals, a room and a punchy bus compressor, and a hi-hat on the eighths locked to the song's own backbeat. In the louder sections a ride takes over from the hat, with a crash on the way in: Meet Them Head-On's melody, the title's break, the second half of Defeat at a Blow, the middle of The King of Terrors (RIDE_MAP in algol.c, positions in driver ticks from the song's start, found with python tools/algol_sections.py 8F, which lists a song bar by bar). algol.ini in the working folder overrides the instrument of any patch (PSIV_ALGOL_LOG=1 lists them) and the drums of any song (song 8F = rock or = standard); psiv_headless --algol renders with it.

  • SNES instrument banks -- paused (2026-11-06): not in psiv.ini for now; PSIV_MUSIC_BANK=ROM turns it on for development and psiv_headless --algol-bank ROM still renders it (runtime/snesbank.c, docs/IDEAS.md).

  • First-person dungeons -- paused (2026-11-05): not in the menu or psiv.ini until it plays well; PSIV_FIRST_PERSON=1 turns it on for development, and psiv_headless --fpview S still renders it. What it is: the dungeons seen from the leader's eyes, like the mazes of Phantasy Star I, built live from the game's own map and tiles (runtime/fpview.c): the walls are its solid cells faced with the wall fronts it draws in its 3/4 view, the floor is the map itself in perspective, and its palettes and animations carry over. Up walks forward, left / right turn that way, down turns back (each held key keeps its direction); menus and dialogue are drawn on top and take the pad as usual. A battle that starts there is fought in that view: the corridor behind the enemies, the party seen through the battle menus. People, chests and the game's other objects stand in the corridor as pictures turned to you, drawn from the game's own object list and art, near or far (the game itself only draws what is on its screen); people show the side of them that faces you, and a thing set into a wall (the Machine Center's elevator doors) is painted onto it. What the game draws over the party in its 3/4 view -- a pillar's shaft, an arch over a passage, a wall's top edge -- stands up from the floor line beneath it at the art's own scale: the Soldier's Temple's pillars rise at full height beside its walls, its arch hangs over the doorway. Doors open as in the game (each door changes the map, and the view follows it: the Machine Center's elevator slides open). Towns and the planets' surfaces keep the game's view. psiv_headless --fpview S takes shots of it (PSIV_FPVIEW_ANYWHERE=1: in towns too, for testing). Inspired by Gen1Recomp's 3D mode.

  • Click to walk (Controls page: on / off): a click on the field sends the party there, by the shortest way the game's own collision allows (runtime/walkto.c): the d-pad is held a step at a time, as you would. Click a person, a chest or a door to walk up to it, face it and press C (talk, open, examine); click a wall, water or a mountain to get as near as you can; click a cave or a town's entrance to go in. It works in first person too (the floor or the person under the cursor). A marker shows where you're going; your own keys take over at once, a right click stops. While text is up, a click is C (next) and a right click B. In battles (the modern menus) a click picks what it is on -- Command / Macro / Run, a command, a technique, skill or item (on any page), an enemy or a member of the party as the target -- by walking the game's own cursor there a press at a time and pressing C; a right click is B (back). psiv_headless --click F:X,Y[,R] clicks frame pixel (X, Y) at logical frame F (R: the right button).

  • Your music (on / off): audio files of your own -- a remix, say -- play in place of the game's songs. Put WAVs (16/24-bit or float, any rate) in music/ (git-ignored), named by the song's ID: music/8F.wav for Meet Them Head-On (PSIV_MUSIC_LOG=1 prints each song's ID and name as it starts: 8F battles, 95 bosses, AA Zio, A0 Dark Force, 96 vehicle battles, AC the title, 8C the Motavia field). Or name them in music/music.ini, which also sets the loop (the WAV's own loop markers count too), where a file starts (by default at its first sound) and its level:

    8F = Meet Them Head-On (my remix).wav
    8F loop 19.76 212.08      ; at 212.08 s, back to 19.76 s
    8F gain -2                ; dB
    

    The game's sound effects keep playing on the chips; the file starts with its song, follows the game's fade-outs and pauses, and stops when the music does. Over Algol or the original chip alike. psiv_headless --custom-music DIR renders with it.

  • Sound effects (modern / your samples / original): modern is the port's own set, synthesized at startup (runtime/sfxsynth.c), so nothing is shipped: blade hits (a short swing, a slice, a ringing edge, a thump; hits in quick succession overlap), the slasher's throw (the spin flying off and coming back, timed to its hits, with Doppler), a menu-cursor tick and a two-note chime for choices, and the battle set: the 21 effects battles ask for most -- claws, staves, Wren's gun, misses; enemies' blows, swipes, bites and grinding attacks, and their destruction; Zan's wind, Foi's fire, Tsu's lightning, Gra's warp (a pulse the game repeats every 8 frames), Brose's surge, fire breath; casts, buffs, Moonshade's glimmer, and the healing bells of Res, its cast and Recovery -- several variants each, picked at random. Each keeps its original's length, so it fits the animation it was timed to, and sits 1 dB under the original's loudness (tools/sfx_census.py counts which effects the test bot's battles ask for and what asks; tools/sfx_study.py sets an original and its replacement side by side as spectrograms, with their loudness). Your samples maps sound IDs to your own WAVs in sfx/sfx.ini (a file, or a folder played at random); IDs it doesn't map stay modern. Samples stay out of git. psiv_headless --sfx-dump DIR writes every synthesized sound to WAV files; --sfx-synth / --sfx DIR play them in headless runs.

  • 16:9 widescreen (on / off; 400 pixels wide, 424 with the 4:3 TV shape): the field and battles show more to the left and right. The game only keeps what a 320-pixel screen needs in video memory, so the field's margins are drawn from the map it keeps in RAM instead (runtime/widemap.c: checked against the screen every frame, the planes' own content when they disagree, the edge chunks repeated past a map's edge); objects are no longer dropped near the old edges; battle backgrounds are mirrored outward. The title, intro and other screens built for 320 pixels keep their shape. psiv_headless --wide-plain shows the margins the old way, for comparison; PSIV_WIDEMAP_LOG=1 logs how the screen and the map agree.

  • Text (modern / original): dialogue, menus and battle windows redrawn with a TrueType font (enhance.text_font, Segoe UI by default) at full resolution, in the game's own positions, colours and timing. Text is recognised from what the game draws (its 8x8 window font and the 8x16 dialogue font, read from the ROM), so the game itself is untouched. Needs third_party/stb/stb_truetype.h (public domain).

  • Side panels (Widescreen: Side panels): in exploration the game keeps its original view and the 16:9 margins show the party -- a card per member (portrait, level, HP / TP / EXP bars, status) and meseta with the inventory. Portraits, item names and level tables come from your ROM at startup; nothing from the game ships with the port.

  • Battle menus (modern / original): the battle windows replaced by menus drawn at full resolution -- a party bar with portraits and HP / TP bars (and each member's chosen action), the commands spelled out (Attack, Technique, Skill, Item, Defend) with a line on what each does, technique / skill / item lists with costs or uses left and a description of the one highlighted, a marker and name over the enemy being targeted. The whole party stays on screen in their places all battle long (the original shows only the member choosing, the one acting, or the one an enemy attacks): an attacker's place empties while they strike; a caster, an archer and the one an enemy attacks step under that enemy as in the original, so every attack and technique plays exactly as the game draws it, with whoever's place they step into standing behind them; and the game's messages move to the top of the screen. The cards run in the order the party stands. Whoever is choosing has a gold aura, an ally being targeted a cyan one; the commands sit in a strip under the party and the lists beside or above it, so no menu covers anyone. The game still runs its own menus: the port mirrors their state from RAM, so the controls are the original ones. Names come from your ROM; the descriptions are the port's own (runtime/battledesc.c). Vehicle battles keep the original windows. Needs the modern-text font.

Planned (see docs/IDEAS.md): the rest of the sound set synthesized (rarer techniques and enemy attacks, the field: doors, chests, stairs).

Headless runs

`psiv_headle