- C 91.8%
- Python 7.5%
- GLSL 0.5%
- Makefile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| notes | ||
| patched | ||
| recomp | ||
| tools | ||
| .gitignore | ||
| README.md | ||
re-hmbtn
A native port of Harvest Moon: Back to Nature (PlayStation, USA, SLUS-01115), built by statically recompiling the game's MIPS code to C and running it on a small PS1 runtime. No emulator CPU is involved: every game function becomes a C function compiled for your machine (Apple Silicon macOS today; Linux should work, untested).
This repository contains no game data. You need your own disc dump.
Status
| Area | State |
|---|---|
| Boot, title screen, menus | working |
| New game: name, birthday, farm and dog naming | working |
| 3D farm, cutscenes | working |
| Memory cards (save files) | working: two slots, stored as raw .mcd images |
| Keyboard and gamepad input (Xbox, PlayStation, Switch Pro, ...; 2 players) | working |
| Sound: SPU music and effects (ADSR, reverb, noise, pitch modulation), CD-XA audio | working |
| GPU renderer (Metal/Vulkan), 1x–8x internal resolution | working |
| PC keyboard/mouse layout with rebinding (game build: de-hmbtn) | working |
| Widescreen, zoom, precise geometry, frames interpolated up to the display's refresh rate (game build: de-hmbtn) | working |
Only the start of the game has been exercised. Untested areas (minigames, festivals, ...) may reach a kernel call or GPU feature that is not implemented yet. The runtime stops with a message naming the missing function or address rather than misbehaving silently.
Requirements
- A raw 2352-byte/sector dump of the USA disc (
.bin, or the.cuenaming it), to extract the game files from. The expected Redump SHA-1 iscb26381fa1336bdd15f07469e59f1143ccf37b03. A.isothat is really a.binis fine; a 2048-byte ISO is not (it lacks the XA audio). - clang (C11), make, Python 3, pkg-config
- SDL3 (
brew install sdl3on macOS)
Build and run
# 1. extract the game's files from your disc (writes assets/)
python3 tools/extract.py /path/to/HARVESTMOON.bin
# 2. recompile to C and build the runtime (writes recomp/gen/ and recomp/build/hmbtn)
cd recomp
make
# 3. play (reads ../assets/; no disc needed)
./build/hmbtn
The binary extracts too: hmbtn --extract game.bin [--assets DIR], or simply the first start
with a disc image and no assets (hmbtn game.bin, in a window or with --settings-menu): it
shows "Preparing game files... N%", writes assets/ next to the binary and plays from it;
later starts need no disc. Both extractors write the same files (manifest included).
Game data: the assets
The game's data are named files in assets/ (runtime/rt_assets.h), the original bytes:
- the disc files:
SYSTEM.CNF, the executableSLUS_011.15it boots,A_FILE.HDT, the XA audioSTR/01..04.XA(raw sectors), and the ISO 9660 sectors the game's CdSearchFile reads (disc/volume.bin: volume descriptors, path tables, root directory;disc/str_dir.bin); A_FILE.BIN, the archive the game reads by byte offset, cut into 1704 regions, every byte in exactly one:overlay/ovl11.bin(GF_FARM),area/39_beach_summer/{header,textures, schedule,resident}.bin,cutscene/012.bin,sound/05_vab_body.bin,message/bank0.bin,icons/items.bin, ... andunknown/<offset>.binfor the 0.01% no table or loader names;manifest.json: every file with its disc sector or archive offset, size, kind and SHA-256.
The table of them (runtime/rt_assets_table.h, names, positions, sizes and hashes only) is
compiled in. tools/asset_catalog.py disc.bin --write regenerates it: it cuts the archive with
the game's own offset tables (areas, cutscenes, sound, the field banks) and the offsets its
code hard-codes, and with --reads LOG... checks CdRead logs (HM_CD_LOG=file: every read
with the asset it starts in and its callers) against the regions. Both extractors check
every file against its hash, so a damaged image or another region's disc is refused.
runtime/rt_data.h serves the CD drive the disc's raw sectors from the disc image or the
assets (rt_data_sector): each sector comes from the asset at that disc position, so the
game's own CD library finds and reads the same data with the same timing. Sector numbers stay
those of the USA disc.
The assets directory is --assets DIR, else assets/ in the working directory, next to the
binary, or one or two directories above it. The disc image argument is then optional (a
missing one is ignored). Setting data=disc plays from the disc image instead; headless runs
without --settings-menu do too when given a disc, so tests see the original game (--assets DIR makes them read the assets). With no usable data the runtime stops with a message saying
what to give it. When both sources are there the setting can change while the game runs: the
drive reads its next sector from the new source.
A game build may link a native CD library instead of the original (runtime/psyq/libcd.c,
de-hmbtn): the emulated CD-ROM controller (cdrom.c) is then not used. Its setting
loading=fast completes reads at once. Likewise a native SPU library (runtime/psyq/libspu.c)
drives the sound generator directly, without the SPU's registers (spu_regs.c), and a native
GPU library (runtime/psyq/libgpu.c) hands the GPU its commands directly, without the GPU's
registers and DMA channel 2 (gpu_regs.c).
What is heard comes from one of two renderers (spu.h). The sound generator's voice model
(spu.c: everything the game reads back) always runs, and its own 44.1 kHz mix is the
Original output. Setting audio=enhanced (the default in a game with its settings menu;
headless test runs always hear Original) renders the same voices again in spu_hq.c: at the
playback device's rate, windowed-sinc resampling per voice, continuous envelopes, ramped
volumes, release tails for re-keyed notes, the PS1 reverb at the output rate in float, the XA
input resampled from its own rate, a soft limiter at the end. It changes nothing the game sees.
Save files are created as hmbtn_slot1.mcd / hmbtn_slot2.mcd in the working
directory. They are standard 128 KB card images and can be exchanged with emulators. A game
build may link the native memory-card libraries (runtime/psyq/libmcrd.c, libcard.c,
de-hmbtn), which work on these images directly: the kernel's card functions and the bu:
device are then not used (calling one stops the runtime). The serial-port card stays for code
that talks to it directly (LIBMCX, PocketStation commands).
Graphics
In a window the game is drawn by the GPU renderer (SDL3 GPU API: Metal on macOS, Vulkan on
Linux) at an internal resolution of 1x–8x the PS1's, with the original textures. If no GPU
device is available it falls back to the software renderer. The software renderer always
runs as well: it is the exact picture used for screenshots, --headless and tests. The
picture keeps its 4:3 shape in any window or fullscreen, centred with black bars.
Settings are kept in hmbtn_settings.ini in the working directory (key=value lines):
| Key | Values | Default |
|---|---|---|
graphics |
hd (GPU renderer at scale), original (the exact 320×240 picture) |
hd |
scale |
1–8 | 4 |
aspect |
4:3, 16:9, 16:10, 21:9: widescreen for game views that support it (HD; the de-hmbtn build's field) |
4:3 |
zoom |
60–100 (%): the world of such views drawn smaller, HUD unchanged (HD) | 100 |
text |
k8x12 (the game's letters redrawn with the free k8x12 pixel font, game palettes), pixel (the game's glyphs through the MMPX pixel-art scaler), original (HD, internal scale ≥ 2) |
k8x12 |
filter |
smooth, sharp (scaling to the window) |
smooth |
display |
window, fullscreen |
window |
vsync |
off, on |
off |
data |
files (the assets), disc (the disc image); changes take effect at once |
files |
saves |
files (a game build's own save files, e.g. de-hmbtn's saves/), card (the game's memory-card screens) |
files |
geometry |
precise (3D polygons at the GTE's unrounded vertex positions, textures perspective-correct), original (integer vertices, affine textures, as the PS1) (HD, with the game's settings menu) |
precise |
controls |
pc (keyboard and mouse drive actions; needs a game build that translates them, de-hmbtn), ps1 (the keyboard table below) |
pc |
loading |
fast (CD reads complete at once), original (the drive's timing); with a game build's native CD library (de-hmbtn), with the game's settings menu |
fast |
framerate |
display (frames interpolated at the display's refresh rate, e.g. 100, 120, 144 Hz; best with vsync=on), 60 (interpolated, one picture per vblank), 30 (the game's own 30 frames per second) (HD, with the game's settings menu) |
display |
audio |
enhanced (the voices rendered again at the playback device's rate, spu_hq.c), original (the sound generator's own 44.1 kHz mix); with the game's settings menu, else always original |
enhanced |
timing |
original (the PS1's CPU time: a game frame whose work overruns its 2 vblanks lags), smooth (no slowdown: a frame's work stops taking emulated time at its budget; waits take theirs); with a game build's native LIBETC (de-hmbtn), with the game's settings menu, else always original |
original |
bind.<action> |
keys of an action in the PC layout (below) | the PC layout |
A game build can offer an in-game menu for them (de-hmbtn: SETTINGS on the title screen, Select on the Start status screen); changes apply immediately and are saved.
Precise geometry removes the PS1's vertex snapping ("wobble") and texture warping from the
3D world without changing what the game computes. While it is on, the GTE also keeps each
projected vertex's screen position before rounding (moved by half a pixel, so it rounds to
the PS1's own coordinate) and its view depth. When game code stores an SXY register into a
GPU packet (swc2; decompiled code calls psx_gte_store_sxy after its store), the runtime
records them for that RAM word, outside guest memory. When the GPU renderer draws a polygon
from the DMA list whose vertex words still hold the recorded values (within 8 vblanks), it
uses those positions and depths: unrounded coordinates, and perspective-correct texture
coordinates (shaders: w per vertex, flag 64). Polygons with any vertex not recorded this
way (sprites, 2D screens, anything built from integers) are drawn exactly as before. Zoom
and widescreen apply to both alike; the software picture, game RAM and timing never change.
It needs a game build with its settings menu (--settings-menu in tests), like other
PC-only features.
Frame interpolation shows the field's motion smoothly although the game draws a new frame
only every other vblank (30 per second). While it is on, the GPU renderer keeps every draw
into the framebuffer of the frame being built, with what each vertex belongs to: decompiled
draw code names it (psx_gte_store_sxy_key: the model vertex an SXY was projected from;
psx_gte_tag_sprite: a sprite's animation and anchor point), recorded beside the packet word
like precise geometry's positions. When the game shows that frame (GP1 display start), the
renderer copies the VRAM it was textured from (at native resolution, taken before the first
write over texels it read) and pairs each vertex with the same thing in the frame shown
before (the nearest one with that key, within 32 pixels). Parts of the world without a
partner, or drawn without keys, move like the paired model vertices around them (a fitted
affine motion); everything else (HUD, text, menus, fades) is drawn as in the newest frame. A
picture for presentation time t (in vblanks) replays the game frame shown at t with each
vertex part of the way back to its place in the frame before (positions and depth w
interpolated, no extrapolation). Frames shown more than 6 vblanks apart (loading, pauses),
cuts (fewer than half of the keyed vertices paired) and frames the renderer cannot replay
exactly (drawn into otherwise than by draws, textures rewritten between draws, not starting
with a clear) are shown as they are. Pictures are one game frame behind the game; at the
display rate the frontend shows a picture at every display refresh while it waits for the
next vblank (one more vblank and a half behind, as the audio clock releases vblanks in
bursts). Game RAM, timing and the software picture never change; it is on in the field only
and needs a game build with its settings menu (--settings-menu in tests).
Controls
Gamepads are picked up automatically, including when connected after launch: wired, USB or Bluetooth. Anything SDL recognises works (Xbox, DualShock/DualSense, Switch Pro, 8BitDo, ...). The first pad connected is player 1, the second is player 2. Keyboard and player-1 pad can be used together. Buttons are mapped by position:
| Xbox pad | PS1 button |
|---|---|
| A (bottom) | ✕ |
| B (right) | ○ |
| X (left) | □ |
| Y (top) | △ |
| D-pad or left stick | D-pad |
| LB / RB | L1 / R1 |
| LT / RT | L2 / R2 |
| Menu (☰) | Start |
| View (⧉) | Select |
Keyboard, PS1 layout (controls=ps1, and always in this repository's own build):
| Key | PS1 button |
|---|---|
| Arrow keys | D-pad |
| Enter | Start |
| Right Shift | Select |
| Z / X / S / A | ✕ / ○ / △ / □ |
| Q / W | L1 / R1 |
| E / R | L2 / R2 |
| F12 | screenshot (screenshot.ppm, plus screenshot_gpu.ppm from the GPU renderer) |
| - / = | zoom out / in (setting zoom, steps of 10 %) |
| Esc | asks "Quit the game?" with the game paused: Yes (Enter / ✕) quits; No, Esc or ○ goes back to the game. Closing the window quits at once |
PC layout
With controls=pc and a game build that translates actions (rt_input.h: the weak
rt_input_game_layout() returns 1; de-hmbtn's src/ui/pc_controls.c), keys, mouse
buttons and the wheel drive actions instead of PS1 buttons; gamepads keep the table above.
The game build turns actions into buttons once per game frame, by context (field or
menus), and decides what Esc does (pause menu, back, or the quit prompt on the title).
Actions and their defaults (runtime/rt_input.c):
| Context | Action (bind. key): default keys |
|---|---|
| field and menus | up W, Up; down S, Down; left A, Left; right D, Right |
| field | run Left Shift, Right Shift; interact E, Mouse2; tool Space, Mouse1; menu Return, Keypad Enter; pause Escape; rucksack I, Tab; map M; tool_next WheelDown, R; tool_prev WheelUp, Q; item_next F; item_prev G; whistle_dog Z; whistle_horse X |
| menus | confirm Return, Keypad Enter, E, Space, Mouse1; back Escape, Backspace, Mouse2; info F; square C; page_left Q; page_right R; menu_l2 Z; menu_r2 X; menu_start Tab; menu_select M; scroll_up WheelUp; scroll_down WheelDown |
| everywhere | screenshot F12; zoom_out -, Keypad -; zoom_in =, Keypad + |
In the settings file: bind.run=Left Shift,Right Shift, with SDL scancode names
(Return, Escape, Left Shift, Keypad +, ...), the short forms LShift, RShift,
LCtrl, RCtrl, LAlt, RAlt, Enter, Esc, KpEnter, KpMinus, KpPlus, and
Mouse1..Mouse5 (left, right, middle, back, forward), WheelUp, WheelDown; an
empty value unbinds the action. An input bound to two actions of overlapping contexts is
a conflict: the first binding counts, the later one stays in the file but is ignored and
logged (controls: E is bound to both interact and tool; ignored for tool). The same key
may serve one action in the field and another in menus; hotkeys clash with everything.
Command-line options
| Option | Effect |
|---|---|
--assets DIR |
the game's files (default: assets/ here or near the binary); the first start with a disc image and no assets (window or --settings-menu) extracts them there (default: assets/ next to the binary) |
--extract |
only extract the assets from the disc image given (into --assets DIR, default assets/), then exit |
--maps DIR |
the game's map files, for a game build that reads them (de-hmbtn; default: maps/ beside the assets directory as found, else maps/ here) |
--export-maps |
a game build that implements it (de-hmbtn): write the map files anew from the game files (--maps DIR), then exit |
--headless |
no window |
--fast |
do not pace to 60 Hz |
--frames N |
exit after N frames |
--screenshot out.ppm |
save the displayed frame on exit |
--vram out.ppm |
save all 1024×512 of VRAM on exit |
--wav out.wav |
record the audio output (stereo, at the rate of the renderer in effect when it starts: 44.1 kHz Original, 48 kHz or the device's Enhanced) |
--script "F:MASK/DUR,..." |
scripted input: hold button mask MASK (hex, PS1 bit order) from frame F for DUR frames |
--trace[=N] |
log CD commands, controller buttons, kernel calls (N≥2), interrupts and DMA (N≥3) |
--renderer software|gpu |
force a renderer (default: GPU in a window, software headless); gpu with --headless renders offscreen |
--scale N |
GPU internal scale 1–8 for this run |
--filter nearest|linear |
scaling filter for this run |
--hd-text k8x12|pixel|original |
the text setting for this run |
--gpu-driver metal|vulkan |
GPU API (default: SDL's choice) |
--gpu-screenshot out.ppm |
save the GPU renderer's frame (internal resolution) on exit |
--gpu-debug |
GPU API validation |
--perf |
log timing once per second: emulation cost, game frames/s, pictures shown/s, and with framerate=display how evenly the presentation times follow the wall clock |
--settings PATH |
settings file (default hmbtn_settings.ini; headless runs use none) |
--settings-menu |
let the game offer its settings menu without a window (tests) |
--audio original|enhanced |
the audio setting for this run |
--timing original|smooth |
the timing setting for this run |
--export-music DIR [--song N] |
a game build that implements it (de-hmbtn): write the game's songs to DIR, headless (default --audio enhanced) |
Example that reaches the new-game dialogue without a window:
./build/hmbtn game.bin --headless --fast --frames 1300 \
--script "300:8/40,500:4000/40,700:8/40,850:10/6,900:4000/10" --screenshot shot.ppm
python3 tools/ppm2png.py shot.ppm shot.png
How it works
disc ──extract.py──► assets/ ──psxrecomp.py──► recomp/gen/*.c ──clang──► hmbtn
runtime/*.c ┘
recomp/psxrecomp.pytranslates R3000A (MIPS-I) machine code to C.- Branch delay slots and jump tables are handled.
- Function bodies come from following control flow. Entry points come from
Ghidra/REA function lists (
recomp/config/functions.json), plus call targets, data and code pointers, and code that follows a return. - The main EXE and the 13 code overlays from
A_FILE.BINare each recompiled against their load address (recomp/config/segments.json).
recomp/runtime/stands in for the console:core.c: RAM, overlay detection, indirect calls, virtual time, interrupt deliverybios.c: high-level replacement for the kernel calls the game makes (events, interrupt chains, memory card files and sectors, pads, setjmp, string helpers)io.c: interrupt controller, DMA, root counters, controller/memory card serial portgpu.c: the GPU at the command level (gpu.h): drawing commands rasterized into VRAM, image transfers, the displaygpu_regs.c: the GPU's GP0/GP1 registers and DMA channel 2 overgpu.c, for builds that run the original LIBGPUgpu_hw.c: GPU renderer; replays the primitivesgpu.chands over at the internal scale (shaders inruntime/shaders/, embedded bymake shaders); frame interpolationrt_settings.c: settings (graphics, data, saves, controls), settings filert_data.c: game data source: the disc's raw sectors from the disc image or the assetsrt_assets.c,rt_assets_table.h: the asset table (names, disc positions, sizes, hashes)rt_extract.c:--extract: the assets from a disc image, checked against the tablert_input.c: PC-layout actions, bindings, conflicts, key capture for rebindinggte.c: the geometry coprocessor; precise geometry's unrounded verticescdrom.c: CD-ROM controller reading sectors from the game data source (rt_data.c); XA-ADPCM streams play at real-time sector rate into the SPU's CD input (xa_audio.c). Builds that link the native LIBCD do not use itxa_audio.c: XA-ADPCM sector decoder and resampler for the SPU's CD inputpsyq/: native PsyQ libraries a game build may link instead of the original ones (libcd.c,libpad.c,libmcrd.c,libcard.c,libspu.c,libgpu.c; hooks inpsyq.h)spu.c: the sound generator (parameters inspu.h): 24 ADPCM voices with ADSR, pitch modulation, noise, sweep volumes and reverb, CD input, mixed at 44.1 kHz (the Original output)spu_hq.c: the Enhanced output: the same voices rendered at the device's rate in floatspu_regs.c: the SPU's register file and DMA channel over the generator, for builds that run the original LIBSPUmain.c: SDL3 window, audio output, keyboard and mouse, frame pacing
- Overlays share load addresses. When the game loads one from disc, the runtime identifies it by its ID word and a fingerprint, then dispatches to the matching recompiled code.
Testing the recompiler
recomp/test/difftest.py runs thousands of game functions from random CPU states twice:
once in an independent instruction-stepping interpreter (mipsref.py), once as
recompiled C (harness.c). It then compares all registers and the full RAM.
cd recomp/test && python3 difftest.py 3 20000
Last run: 5,650 cases across 2,129 functions, 0 mismatches.
Other tools
tools/hmtex.py: decodes the game's0x0Dtexture packs (list,scan, andvramrendering to PNG).patched/hmbtn_ring8_seeds.ppf: patch for the original disc. Seed bags sow a ring of 8 tiles instead of 3×3, so no crop ends up unreachable in the middle. It is a one-byte change to the seed sowing pattern at0x8004ACA0, with the sector's EDC/ECC rebuilt.notes/afile_map.json,notes/hooks.json: notes on the disc layout and the code addresses relevant to widescreen, frame rate and input.
Legal
This project contains no code or data from the game. The disc image, the assets,
and the generated C in recomp/gen/ (a translation of the game's code) are produced
locally from your own copy and are excluded by .gitignore.