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.
- Type: PC port
- Game: Phantasy Star IV: The End of the Millennium
- Runs on Windows, Linux and Android
- By MotaviaWorks
- Latest release v0.1.1-alpha, 2026-11-07
- Source: https://github.com/MotaviaWorks/PSIVRecomp
README
PSIV Recomp
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)
| Folder | What | Source |
|---|---|---|
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
| Build | Package for testers | |
|---|---|---|
| Windows | build.bat (static C runtime: no redistributable needed) | python tools/package.py windows build\native-Release dist |
| Linux | cmake -S . -B build/linux -G Ninja && ninja -C build/linux (system SDL3) | below |
| Linux, portable | SDL3 built static from source, then -DPSIV_PORTABLE=ON -DPSIV_TOOLS=OFF -DCMAKE_PREFIX_PATH=<its prefix> | python3 tools/package.py linux <build> dist |
| Android | tools/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()inruntime/main_sdl.c). The game boots only when Start game is chosen.psiv --playskips it (for frontends and shortcuts); the game-frame testing hooks (--menu,--test-keys,--test-click,--test-rumble,--capture) skip it too, unless a--menu-scriptdrives the launcher. - First run: with no
rom/psiv.bin, a dump in the home folder orrom/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 asrom/psiv.bin. Testing hooks:--test-pick FILEanswers the dialog,--no-dialogssends 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.cbinds 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.mdis the README that goes into every package.
Controls
| Mega Drive | Keyboard | Gamepad |
|---|---|---|
| D-pad | arrows | D-pad / left stick |
| A / B / C | Z / X / C | West / South / East |
| Start | Enter | Start |
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_MAPinalgol.c, positions in driver ticks from the song's start, found withpython tools/algol_sections.py 8F, which lists a song bar by bar).algol.iniin the working folder overrides the instrument of any patch (PSIV_ALGOL_LOG=1lists them) and the drums of any song (song 8F = rockor= standard);psiv_headless --algolrenders with it. -
SNES instrument banks -- paused (2026-11-06): not in
psiv.inifor now;PSIV_MUSIC_BANK=ROMturns it on for development andpsiv_headless --algol-bank ROMstill renders it (runtime/snesbank.c, docs/IDEAS.md). -
First-person dungeons -- paused (2026-11-05): not in the menu or
psiv.iniuntil it plays well;PSIV_FIRST_PERSON=1turns it on for development, andpsiv_headless --fpview Sstill 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 Stakes 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.wavfor Meet Them Head-On (PSIV_MUSIC_LOG=1prints 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 inmusic/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 ; dBThe 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 DIRrenders 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.pycounts which effects the test bot's battles ask for and what asks;tools/sfx_study.pysets an original and its replacement side by side as spectrograms, with their loudness). Your samples maps sound IDs to your own WAVs insfx/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 DIRwrites every synthesized sound to WAV files;--sfx-synth/--sfx DIRplay 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-plainshows the margins the old way, for comparison;PSIV_WIDEMAP_LOG=1logs 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. Needsthird_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