Beyond Oasis Recomp
Beyond Oasis Recomp is a native PC port of Beyond Oasis for the Sega Genesis that runs the game as C code.
- Type: PC port
- Game: Beyond Oasis
- Runs on Windows
- By OriginalSwayze
- Latest release v1.0, 2026-10-31
- Source: https://github.com/OriginalSwayze/beyond-oasis-recomp
README
Beyond Oasis Recomp
A native PC port of Beyond Oasis (USA, Sega Genesis) built from your own ROM.
No game data is included in this repository: every tool reads your ROM and
checks its SHA-1 (2944910c07c02eace98c17d78d07bef7859d386a).
It is a static recompilation, not a decompilation: at build time the game's 68000 machine code is translated into C from your ROM, and anything not yet covered runs on an interpreter.
Playing it
You need your own Beyond Oasis (USA) ROM. It is not included and never will be, and there is no ready-made .exe to download: the built game contains the game's own code, translated to C, so it can only be made on your PC from your copy. The builder does that for you:
- Download
Beyond-Oasis-Recomp-<version>.zipfrom Releases (not "Source code": that leaves out the third-party code the build needs). - Unzip it somewhere with a short path, for example your Desktop.
- Double-click Build Beyond Oasis.bat. If Windows asks whether to run it, choose Run (or More info, then Run anyway).
- Pick your ROM when asked. It has to be the USA release, unchanged: the builder checks its SHA-1 and tells you if it is a different one.
The builder then:
- downloads the build tools it needs into a
toolchainfolder next to it, after asking: Node.js, CMake, Ninja and LLVM-MinGW (Clang), a 260 MB download (about 1 GB unpacked), each from its publisher's own release and checked against a pinned SHA-256. Nothing is installed into Windows; delete the folder to remove them. Tools you already have are used instead. - recompiles your ROM to C and builds the game (a few minutes the first time)
- puts a Beyond Oasis shortcut on your desktop
The game is build/cmake/BeyondOasis.exe, with its saves, states and settings.ini next
to it. To update, unzip the new release over the old folder (your saves are kept) and run
the builder again. It needs 64-bit Windows 10 or 11.
To build it by hand instead, see Usage.
Status
| Part | State |
|---|---|
ROM verification (tools/setup.mjs) | done |
68000 decoder, code tracer, disassembly listing (tools/disasm.mjs) | done: ~128 KB of code in 11,075 blocks, with the code seen running in play (config/coverage.txt) |
| Runtime: memory map, VDP with widescreen renderer, pads, SRAM, settings | done |
| Z80 + YM2612 + PSG sound | done |
68000 → C recompiler (tools/recomp.mjs, built automatically by CMake) | done: 99.98% of 68000 time runs as native C |
Widescreen game patches (runtime/src/widescreen.c, config/patches.json) | done: gameplay at 16:9, other screens pillarboxed |
Save states and rewind (runtime/src/state.c, rewind.c) | done |
Launcher (runtime/src/launcher.c, foil.c, launcher_scene.c) | done: settings, ROM picker, foil steelbook, soundtrack player |
In-game menu (runtime/src/menu.c) | done: save/load slots with thumbnails, options, reset, quit |
Internal resolution 2-4x (runtime/src/hd.c) | done: per-layer Scale2x/Scale3x |
Smooth motion, 60-144 Hz+ (runtime/src/hd.c, motion.c) | done: moves every sprite by its object's movement between the game's 30 Hz ticks |
Display filters (runtime/src/filter.c) | done: scanlines, CRT, LCD |
Slowdown fix (runtime/src/machine.c) | done: 68000 at 150-300% |
Cheats (runtime/src/cheats.c) | done: infinite health and spirit, unbreakable weapons, attack power, move speed, RAM codes |
Testing
The native build must behave exactly like the interpreter. Two checks:
BeyondOasis.exe --headless 6000 --verify <rom> # every block vs. the interpreter: registers, flags, RAM, VDP, cycles
BeyondOasis.exe --headless 6000 --hash-log a.log <rom>
BeyondOasis.exe --headless 6000 --hash-log b.log --interpret <rom>
# a.log and b.log must be identical
Run both checks with --widescreen 0: the widescreen patches change the game on purpose.
Headless runs start from blank save RAM and never write save.srm, so tests play the same
whatever you have saved, and never touch your save.
With widescreen off, every patch falls back to the original code, so the results must match exactly.
Other headless options:
--press "FRAME:BUTTONS[:FRAMES],...": scripted input (U D L R A B C S), e.g.600:S,1500:R:300--shot F.png/--shots EVERY DIR: screenshots (PNG or BMP)--widescreen 0|1: override the setting for this run--watch ADDR,...: print RAM bytes (e.g.173Dfor $FF173D) whenever they change--trace-vram LO HI: log CPU writes and DMAs into a VRAM range, with the 68000 PC--dump F: save RAM, VRAM, CRAM, VSRAM and VDP registers--hide-layers MASK: hide plane A (1), plane B (2) or sprites (4)--state-check A B: snapshot before frame A, run to B, reload and replay; both runs must end identical--rewind-check K: record every frame, rewind K frames; must equal the snapshot taken then--launcher-shot F.png WIDTH TAB TIME: render the launcher to a PNG with tab TAB (0-3) open, TIME seconds into its animations--no-launcher: start the game directly this time--launcher-music-check SECONDS: run the soundtrack player without an audio device; prints the title's song, each song change and the level every 30 seconds--ost-check SECONDS: play every song for SECONDS and print its level per second (in tenths of its loudest second), then the menu sound effects' levels--menu-shot F.png W H: open the in-game menu over the last frame and render it at W x H--controls-shot: with--menu-shot, render the Controls page over the menu--input-check FILE: rebind a few keys and buttons (including clashes), save them to FILE, load them back and print every binding--save-to N,--slot N,--states-base PREFIX: save a slot at the end, pick the menu's slot, and keep test slots inPREFIXstates/instead of the real ones--hd-shot F.png SCALE: save the last frame at internal resolution SCALE (2-4), with the time it took--sprite-check EVERY DIR SCALE: every EVERY frames, compare the frame drawn layer by layer with the same frame drawn sprite by sprite (as smooth motion draws it); prints how many differ by more than 0.5% and saves the worst pair--smooth-check DIR SCALE: smooth motion over the last 8 frames at 120 Hz; prints how far each picture is between ticks and how many sprites follow objects, and saves the pictures--motion-check: from frame 300 on, check that every frame's sprites are found in a tick the game built; prints how many sprites follow an object and the largest move--filter N: use display filter N (0 off, 1 scanlines, 2 CRT, 3 LCD)--sharpen N: sharpening 0-3 for this run (shows in--filter-shot)--lighting N(0-2),--elements 0|1,--water 0|1,--footsteps 0|1,--texture-pack NAME,--dump-tiles 0|1,--scaler 0|1(edge, smooth),--audio N(0 original, 1 clean, 2 rich): the effects for this run (--filter-shotalso prints how long the finishing steps took)--outfit a,b,...,--outfit-anims 0,1,...: Ali's outfit colours and animations for this run (asoutfit_colorsandoutfit_animsinsettings.ini)--outfit-check F.png: at the end, save Ali's sprites 6x in his current colours and list the colour indices he uses--outfit-shot: with--menu-shot, render the outfit page over the menu--menu-tab TAB ROW: with--menu-shot, open tab TAB (0 game, 1 display, 2 effects, 3 more) with row ROW selected (shows its preview card)--save-preview: at the end, save the frame for the launcher's previews (preview_frame.cache)--png-check F.png: read a PNG with the built-in reader and print its size and a checksum of its pixels--objects: at the end, list every sprite on screen with the object that drew it (its list, slot, type and first words)--filter-shot F.png W H MODE: draw the last frame at W x H with filter MODE on the GPU (hidden window) and save it--volume N: volume 0-100 for this run--overclock PERCENT: run the 68000 at this speed (also below 100, to simulate a heavy scene); headless runs print the spare time, game ticks and late ticks--load N: load slot N (from the--states-basefolder) before the first frame--cheat health|spirit|weapons|double|quadruple|onehit|fast|veryfast: turn a built-in cheat on for this run (repeatable)--poke FFxxxx:VV[VV],...: hold RAM bytes or words every frame (applied after the built-in cheats)--wav F.wav,--pc-profile N(hottest code over the last N frames: cycles per native block, instructions for interpreted code)
Launcher
Before the game starts, a launcher presents the game like a collector's steelbook on a dark night over the dunes: stars twinkle, mist drifts low and embers rise. The case is dark brushed steel with the title logo in polished gold, the Gold Armlet embossed into it, and the four spirits set in sockets below as glowing orbs with their emblems: Dytto (water), Efreet (fire), Bow (plant) and Shade (shadow). A raised double frame of holographic foil runs round it. The light drifts slowly and leans towards the mouse, like tilting a real case: the foil's colours shift with it, a glint sweeps across, and sparkles flash at their own angles.
The settings sit on a tablet of the same steel, with a foil frame and one tab per spirit: DISPLAY, EFFECTS, GAME, MUSIC and CHEATS (the Gold Armlet's). The open tab's spirit glows in its socket on the case; its rows slide in, the highlight glides between rows, a changed value pulses, and the selected option's description shows under them.
The surfaces (runtime/src/foil.c) are height fields with a material per pixel (steel,
holographic foil, gold, embossed art, engraved recesses), built once per window size and
shaded every frame for the light on the renderer's worker threads.
The artwork comes from your ROM: on first start the launcher plays the intro silently
(about two seconds), captures the armlet scene and the title screen, and caches them in
launcher_art.cache next to the executable. The spirits' orbs and emblems are drawn from
shapes. Everything is drawn in software every frame (about 6 ms at 720p, 13 ms at 1080p;
larger windows are drawn at 1080p and scaled up).
Soundtrack player and sounds
The launcher plays the game's soundtrack, made live by your ROM's own sound driver on the
emulated Z80, YM2612 and PSG; no audio is stored anywhere (runtime/src/ost.c):
- A background thread runs the ROM from a reset (with blank save RAM, like the art capture) to the title screen without sound, noting the song the title asks for (song $04).
- It then parks the game (the 68000 loops on a
bra.s *in work RAM with interrupts masked) and calls the sound driver itself, as the game does:jsr $060000once a frame to play the music andjsr $060004with a command to start a song ($01nn) or a sound effect. The calls return to a hand-written routine at an address in the cartridge header. - The ROM's song table at $0638D4 has 34 songs ($00-$21). Looping songs play for three
minutes and fade out; jingles end when they go quiet (measured without the FM DAC's DC
offset). The player plays them all in order, repeats one, or shuffles (
ost_mode), and remembers the last song (ost_track). - The MUSIC tab shows the song, its time, a level meter and previous / play-stop / next buttons; the song playing also shows under the case on every tab.
- Every choice in the launcher makes the game's own menu sound: the cursor sound ($0256) when moving, the confirm sound ($0347) when changing or opening something, the cancel sound ($03AA) when closing the controls page, and the error sound ($0253) when Play is pressed without a ROM. They play even with the music off.
Keys: arrows select and change, Enter activates, Q / E (or Page Up / Down, LB / RB) switch
tabs, [ and ] change the song, M turns the music on and off, Esc quits; the mouse and a
controller work too. Pressing Play fades the music out; the game then starts from a reset
with your real save RAM. "Show launcher at startup" turns the launcher off;
show_launcher=1 in settings.ini brings it back.
Volume
"Volume" (launcher and pause menu, volume in settings.ini) is a slider from 0 to 100%
for the game's sound and the launcher music. Left/right move it in steps of 10%, and a
click on the track sets it there (in steps of 5%).
In-game menu
Esc pauses the game over a blurred, darkened copy of the last frame, with mist and embers drifting over it. The menu is a panel of the launcher's steel with "PAUSED" stamped into it in gold and a holographic frame, with one tab per spirit: GAME (resume, save and load the nine slots, rewind, reset, quit), DISPLAY, EFFECTS, CHEATS and MORE (volume, audio, Controls, Ali's outfit). Q / E, Page Up / Down or LB / RB switch tabs; the rows slide in, the highlight glides, and a changed value pulses.
The card on the right, framed in holographic foil, follows the selected row: the save slot's thumbnail and when it was
saved; for a picture option, a live before/after on the paused frame (runtime/src/preview.c):
the renderer keeps every frame's layers, so the frame is drawn again without the option and
with it (its value, or a typical one when it is off), joined by a dividing line that sweeps
slowly across. Detail options (internal resolution, scaler, sharpening) show 1:1 pixels
around the middle; scene-wide ones (lighting) show the whole frame. Every other
row explains what it changes. The launcher shows the same previews in a foil card over its case, on the
frame the game saved last (preview_frame.cache, written when the menu opens and on quit).
Every choice makes the game's own menu sound (cursor, confirm, cancel, error). The game
cannot be asked to play them while it is paused without changing it, so they are rendered
once from the ROM, the way the launcher's player does (menu_sounds.cache, about half a
second on the first start), and played straight into the audio queue.
All the enhancements are off by default (on a fresh install also widescreen, internal resolution, smooth motion and the sprite flicker fix), so the game looks and sounds as it did on the console until you turn something on.
Internal resolution
"Internal resolution" (launcher and pause menu, internal_scale in settings.ini,
default Native) redraws the game at 2x, 3x, 4x or 6x of its 320x224 picture (6x, 2544x1344,
is for 1440p and 4K screens: about 10 ms a picture with Edge, 15 with Smooth, on a fast CPU). The VDP keeps each
line's three layers (plane B, plane A with the window, sprites) with their colours and
priorities. hd.c scales each layer on its own with Scale2x or Scale3x (4x is 2x twice),
then composites them at that size with the VDP's own rules, priorities and
shadow/highlight included. 6x is 3x and then 2x.
These scalers only copy whole source pixels, so outlines and diagonals get smoothed while colours stay exact and layers never bleed into each other. Mid-frame palette changes and line scrolling still work, because every line was captured with its own colours. The art gains no new detail: it is the original art, drawn smoother at the window's size.
At 3x a frame takes about 4 ms (6-8 ms at 4x with widescreen) on one core. "Native" turns it off and shows the original pixels. The native picture is unchanged, as are save-state thumbnails and screenshots.
The last step to the window is "sharp bilinear" (filter.c): the picture is enlarged by
a whole number with hard pixel edges to just above the window's size, and only that last
small step is smoothed. A plain smooth stretch from 1272x672 to 1920x1080 softens every
edge; this keeps about 20% more edge contrast. On a 1080p or larger screen, 4x is the
sharpest choice, since it needs the smallest final step.
Smooth motion
The game moves its camera and characters once per tick, every other frame: its logic
runs at 30 Hz, in 2-pixel steps. "Smooth motion" (on by default, smooth_motion in
settings.ini) draws the moments between two ticks from the game's own data instead of
guessing from the picture:
- While the game builds its sprite list (
runtime/src/motion.c), the runtime notes for every sprite which object drew it and where the game placed that object on screen. Objects draw through three routines that first work out the object's screen position ($AB4A, $AECC, $B0C4) and small effects through one ($A98E); the V blank handler sends the finished list to the VDP ($1FBE), which ends a tick. The HUD, menus and text are drawn other ways, belong to no object and never move between ticks. - The VDP also records each line's scroll values and every sprite: its position and all of its pixels, including those hidden behind other sprites. The sprites shown are found in the tick they came from by their entries in the sprite list.
- Each picture is drawn a fraction
tof the way from the previous tick to the latest: plane A and B are shifted back by(1 - t)of their scroll change, and every sprite by(1 - t)of its own object's movement. All the pieces of a character belong to the same object, so they always move together, through animation changes too, and a character can never disappear, double or come apart. An object that is new, whose slot now holds a different kind of object, or that moved more than 48 pixels in one tick (a warp) is drawn where it is. - Sprites are stacked front to back by their position in the sprite list, exactly as
the hardware does (
--sprite-checkcompares this per-sprite drawing with the layer drawing; they match to within 0.07% of pixels, at overlapping edges). Every picture is drawn this way, also when nothing moves, so overlaps never switch between two ways of drawing. - The main loop runs at the display's refresh with vsync: every refresh runs a game frame if one is due (60 a second on any display) and draws the moment it is now. So 30 Hz steps become 60 Hz motion on a 60 Hz display and 144 Hz motion on a 144 Hz one.
- Scene cuts (scroll jumps over 24 pixels), column scrolling and sprite masking are drawn as they are, without gliding.
Motion is shown one tick (about 33 ms) late. Smooth motion works at any internal resolution; at Native it moves whole pixels. The game's own logic stays at 30 Hz: running it every frame would make the whole game run twice as fast, and halving every speed, timer and animation in the game to make up for it is a far bigger change.
A picture takes 1.5-3 ms at 4x (16:9), split across cores by a pool of worker threads started once. Measured live on a 144 Hz display: 144 pictures a second, 90% of them 6.7-7.2 ms apart (6.94 is perfect), the slowest 10.4 ms.
Display filters
"Filter" (launcher and pause menu, filter=off|scanlines|crt|lcd in settings.ini)
draws the picture with a screen look, on the GPU through SDL's renderer:
- Scanlines: a dark gap between the game's lines, lined up with its 224 lines at any window size (they fade out when a line is under three screen pixels tall).
- CRT: scanlines, red/green/blue aperture-grille stripes, a soft glow, a vignette and a gently curved picture.
- LCD: a thin grid between the game's pixels, like a handheld screen.
The scanlines, stripes, grid and vignette are one overlay texture, built once per
window size and multiplied over the picture. The glow is the picture drawn at 1/8 size
and added back stretched, which blurs it. The curve draws the picture and the overlay
as a bent 40x30 triangle mesh (SDL_RenderGeometry). Filters work with every internal
resolution and with smooth motion; screenshots and save thumbnails stay unfiltered.
Slowdown fix
The game runs its logic every other frame (30 Hz). Each tick runs the game