Harvest Moon: Back to Nature (PS1) static recompilation runtime
  • C 91.8%
  • Python 7.5%
  • GLSL 0.5%
  • Makefile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-23 15:37:14 +07:00
notes Game data as named assets: assets/ (tools/extract.py, hmbtn --extract, or the first start with a disc image) holds the disc files and A_FILE.BIN cut into 1704 named regions, the original bytes, checked against the compiled-in table rt_assets_table.h (tools/asset_catalog.py: names, positions, sizes, SHA-256); rt_data serves the CD drive each sector from the asset at that disc position (no ISO synthesis, no A_FILE/NN.bin); --assets replaces --data; HM_CD_LOG names every read; the recompiler reads assets/; notes/ keeps the disc layout notes 2026-10-22 01:26:46 +07:00
patched Harvest Moon: Back to Nature static recompilation 2026-10-20 02:01:37 +07:00
recomp Windows (MinGW-w64) and Linux portability: plat.h (mkdir, rename over an existing file, localtime_r, realpath, backslash paths, backtrace only on glibc/macOS), 4 KB RAM alignment on PE, portable rm_tree (no nftw), extraction creates missing parent directories, binary file mode and LF output of the generators, no clock_gettime (winpthread) on Windows 2026-10-23 15:37:14 +07:00
tools Windows (MinGW-w64) and Linux portability: plat.h (mkdir, rename over an existing file, localtime_r, realpath, backslash paths, backtrace only on glibc/macOS), 4 KB RAM alignment on PE, portable rm_tree (no nftw), extraction creates missing parent directories, binary file mode and LF output of the generators, no clock_gettime (winpthread) on Windows 2026-10-23 15:37:14 +07:00
.gitignore Game data as named assets: assets/ (tools/extract.py, hmbtn --extract, or the first start with a disc image) holds the disc files and A_FILE.BIN cut into 1704 named regions, the original bytes, checked against the compiled-in table rt_assets_table.h (tools/asset_catalog.py: names, positions, sizes, SHA-256); rt_data serves the CD drive each sector from the asset at that disc position (no ISO synthesis, no A_FILE/NN.bin); --assets replaces --data; HM_CD_LOG names every read; the recompiler reads assets/; notes/ keeps the disc layout notes 2026-10-22 01:26:46 +07:00
README.md Timing setting: timing=original|smooth (No slowdown), --timing: psx_work for work (a game build's HM_CYCLES, native LIBC/LIBGPU/LIBCARD/LIBMCRD loop costs) stops taking emulated time once a game frame begun at VSync(2) reaches 1/16 vblank before its second vblank; waits (psx_tick), DMA/register time and interrupt handlers always take theirs; a frame's other waiting VSync ends the budget; 8 vblanks held back release it; --trace logs held and late frames 2026-10-22 16:13:01 +07:00

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 .cue naming it), to extract the game files from. The expected Redump SHA-1 is cb26381fa1336bdd15f07469e59f1143ccf37b03. A .iso that is really a .bin is fine; a 2048-byte ISO is not (it lacks the XA audio).
  • clang (C11), make, Python 3, pkg-config
  • SDL3 (brew install sdl3 on 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 executable SLUS_011.15 it boots, A_FILE.HDT, the XA audio STR/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, ... and unknown/<offset>.bin for 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.py translates 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.BIN are 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 delivery
    • bios.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 port
    • gpu.c: the GPU at the command level (gpu.h): drawing commands rasterized into VRAM, image transfers, the display
    • gpu_regs.c: the GPU's GP0/GP1 registers and DMA channel 2 over gpu.c, for builds that run the original LIBGPU
    • gpu_hw.c: GPU renderer; replays the primitives gpu.c hands over at the internal scale (shaders in runtime/shaders/, embedded by make shaders); frame interpolation
    • rt_settings.c: settings (graphics, data, saves, controls), settings file
    • rt_data.c: game data source: the disc's raw sectors from the disc image or the assets
    • rt_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 table
    • rt_input.c: PC-layout actions, bindings, conflicts, key capture for rebinding
    • gte.c: the geometry coprocessor; precise geometry's unrounded vertices
    • cdrom.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 it
    • xa_audio.c: XA-ADPCM sector decoder and resampler for the SPU's CD input
    • psyq/: 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 in psyq.h)
    • spu.c: the sound generator (parameters in spu.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 float
    • spu_regs.c: the SPU's register file and DMA channel over the generator, for builds that run the original LIBSPU
    • main.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's 0x0D texture packs (list, scan, and vram rendering 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 at 0x8004ACA0, 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.

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.