A reconstruction of Sega's 1993 arcade game SegaSonic The Hedgehog (Sega System 32) into platform-agnostic C — not an emulator.
This repository stays the faithful reference: the game to play, built on it, goes on in segasonic-port.
Apart from the screenshots on this page, this repository contains no game data. The game's program
is here as C, decompiled from the original machine code (src/); as with
n64decomp/sm64, you bring your own ROM for everything else: the
tools verify it and extract the assets and the data the program reads, which the game loads when it
starts. No executable carries the ROM or anything from it.
Frames drawn by the game's own video library (runtime/src/video.c) while the
decompiled game plays the scenarios of tests/controls/.
- ROM set verification (MAME set
sonic, Japan rev. C), by CRC32 and SHA-1 of every file - NEC V60 disassembler, output identical to MAME's at all 786,432 byte offsets of the program ROM
- Recursive disassembly that follows branch tables, pointer tables and stored code pointers, cross-checked against real execution: 541 functions, about 26,000 instructions, no invalid or overlapping paths, and every executed address lands on a decoded instruction
- Asset extraction into plain resources, in folders by subject (see "The assets" below): the 6,138
sprite frames in 291 sheets, an animation family or an object a sheet (
sprites/sonic/walking.png, nine directions of sixteen frames), in the colors the game shows them with, showing what the board draws; 16 tile sheets in color; the program's data as text, a file a subject with a section a piece, each in a format of its own (palettes, the scripts that build the stages' maps, the characters' animations, floor blocks, object placements and spawn lists, restart points and paths, texts, fonts, the demos' recorded inputs), naming every sprite frame by its sheet (sonic/walking[3]); and the sound: the driver's tables as text (instruments, FM patches as an FM editor shows them, drum kits, pitch tables), the music and every sound effect and voice as a Standard MIDI File that plays at the game's speed, named by where the game plays it, and the 175 PCM samples as WAV. Everything but the code builds back from those files byte for byte (extract_assets.py --check) - The game built from its assets: the build makes the game's data from
assets/(the code, which the C replaces, comes from the ROM), so that what is changed there goes into the game: a sprite, a palette, a stage's spawn list, an instrument, a sample, a song (the songs may change length: the build lays them out anew) - System 32 board model in C (
runtime/): memory map, interrupt controller and timers, I/O chip, EEPROM, trackballs, palette and mixer registers, and the FD1149 (317-0213) level-load protection - Reference run of the original program on MAME's V60 core against that model: boots, shows the region warning and runs the attract mode for 10 minutes of game time
- V60 to C translation (the scaffold,
tools/recompile.py): the game runs as native C, and its work RAM, video RAM, sprite RAM, palette and mixer match the reference run at the end of every frame - Decompiler (
tools/decompile.py, which runs arcade-decomp, the decompiler and analysis tools this game shares with outrunners-decomp, intools/core/): one C function per original function, with no CPU left in it: registers become named parameters, results and typed local variables, the stack and the condition flags are gone, branches becomeif/while/do/switchwith||and&&, a jump through a table of code becomes aswitchon the index, and the game's variables, tables, structures (objects, players, and each object type's own variables) and the board's registers are read and written by name; addresses the code uses as values print as names (TEXT_CELL(14, 20),PALETTE_RAM + 0x83A0u,NAME_ADDRESS). Code two functions share is written once (the player's update runs into the state code of its animation, which takes anentry_point; each player state the update jumps to is a function of its own). The code nothing in the program reaches (about 14 KB: drawing routines, a packed picture decoder and an LZ unpacker, object types never spawned, earlier versions of live routines) is decompiled too, so the C holds the whole program. It matches the reference frame by frame in every scenario oftests/controls/ - Names: every function, variable, table, structure field and board register has one in
symbols.txt, with the evidence for it, and so do the parameters and results of the functions, the places the C's control variables are named after, and what each object type keeps in the object structure (OBJECT(current_object)->fire_front.path, per family of functions, wherever the code shows which type an object is: its own, one it gives a handler, one a typed link leads to); local variables have the namessymbols.txtgives them by the instructions that write them, or take the name of what they hold, the field they were read from or the parameter they are passed as. Every byte of the program ROM is code or named data. The records the code walks are structures: 201 in the game (181 in the program ROM: floor blocks and slopes, spawn lists, the chasers' paths, animations, texts and VRAM scripts, the ending's timed frames...; 20 in the work RAM: ranking records, palette cycles, sprite part lists...) and 13 in the sound program (instruments, register pairs, songs...), with 59 + 10 kinds of values the code walks one at a time (a text's characters, a list of sprite frames) and 48 + 9 tables of records read by index. The C reads the program's data by these names, never by its address. The original's quirks found along the way are noted next to the code they are in - Readable C: no
goto, a comment above every function (1,783), and what is left of the CPU counted bytools/noise_report.pyand explained under "What is left, and why" below - The game's source: the decompiled C is in
src/, and the game builds from it - Code coverage: the static analysis follows the game's object tables described in
symbols.txt, so the C comes from the ROM alone, and it finds all the code the reference runs. The 98 scenarios oftests/controls/(attract mode, games for one, two and three players, every stage, the ending, the test mode, and pokes that reach what the game itself never does) run 43,042 of its 47,575 instructions: 3,908 of the others are the code nothing in the program calls (symbols.txtsays what each piece is), anddocs/unreachable.txtsays, for each of the other 625, why no scenario runs it (dead code, a hardware fault the board model cannot produce, or a rare situation) - System 32 video library (
runtime/src/video.c): the four tilemaps (zoom, row scroll and row select, clip windows), the text and bitmap layers, the backdrop, the two sprite frame buffers with their list processor, and the mixer (priorities, blending, shadows, color offsets). It draws each frame from the board's memories into a 320 or 416 x 224 picture, and matches MAME's System 32 video pixel for pixel in every frame of the scenarios (check_scenarios.py --video) - Sound board model (
runtime/src/sound.c): the Z80's memory and ports, its interrupt controller, the ROM bank, the shared RAM; a reference run of the sound program on MAME's Z80 with ymfm's YM3438 and MAME's RF5C68 (s32ref --sound-wav), and a Z80 disassembler identical to MAME's - Sound: the Z80 sound driver decompiled into C (
src/sound/, written bytools/decompile_sound.pyfrom the sound ROM andsound_symbols.txt), running on the board's time, and the YM3438 and RF5C68 as libraries (runtime/src/ym3438.c,runtime/src/rf5c68.c) that make the same samples as ymfm and MAME. It matches the reference's samples and sound board memories frame by frame in every scenario (check_scenarios.py --sound). It reads like the game's C: its functions, parameters, local variables, variables, structures (the sequence players, the FM and PCM voices, the MIDI parts) and the chips' ports are named insound_symbols.txt, with the evidence for each name, and the bytes the Z80 keeps apart in a register pair (a count in B, a register number in C) are variables of their own - PC port (
ports/sdl2/): the game in a window, with its sound, the mouse, the keyboard and game controllers, on SDL2; its settings insonic.ini. Built and played on Windows; SDL2's own packages should build it on Linux and macOS (not tried yet)
Requirements: Python 3.10 or newer, CMake 3.20 or newer, a C11 / C++17 compiler (Visual Studio Build
Tools on Windows) and, for the game's window, the SDL2 development files: on Windows, unpack
SDL2-devel-2.x.x-VC.zip from SDL's releases into
build/downloads/sdl2/ (or point SDL2_DIR at it); elsewhere, the system's package (libsdl2-dev).
Without SDL2 only the headless builds are made.
The decompiler, the analysis tools and the asset formats are a project of their own,
arcade-decomp, which this repository keeps in tools/core/ as
a git submodule: clone it with the submodule (git submodule update --init in a clone made without it).
The game builds without it; extracting the assets and the decompiler need it:
git clone --recursive https://github.com/cheviman/segasonic-decomp-
Put your MAME
sonic.zip, or the files inside it, inrom/, and check it:python tools/verify_rom.py
-
Extract the assets into
assets/(the tile sheets, the sprites, the program's data, the sound driver's data, the music and the samples), and check that they build back into your ROM byte for byte:python extract_assets.py
python extract_assets.py --check
-
Build the game and the development tools into
build/bin/. No executable carries the ROM or anything from it: the build takes the assets out of your ROM set whenassets/does not hold them yet, and packs them intobuild/bin/segasonic.assets(tools/make_pack.py), which the game loads when it starts (runtime/src/game_data.c). The original code is in no file: the C replaces it. Change an asset and build again, and the change is in the game.python tools/make_pack.py --checkcompares what the pack puts in the board's memory with your ROM set, byte for byte, the code left out. The reference (s32ref), which runs the original code as MAME does, reads the ROM set fromrom/when it starts:cmake -S . -B build/cmakecmake --build build/cmake --config Release
-
Play:
build/bin/sonic
The executable can be copied anywhere with SDL2's library beside it (
SDL2.dllon Windows, which the build puts next to it) and the assets pack (segasonic.assets), which the game also finds in the folder where it keeps what it writes (below). With no pack, the first run makes it there from your ROM set:sonic.zipbeside the game (its.appon macOS), or the one you drop on the window it opens (the set's 18 files, each checked by its CRC32); from then on the game loads the pack and never opens the ROM set again. Built fromrom/,build/bin/has the pack already. The executable holds nothing of the game's; the pack is made from your ROM: it is yours, not one to share. On Linux and macOS the build needs the system's SDL2 (libsdl2-dev, Homebrew'ssdl2); with no ROM set it makes the executables alone.Or run the game without a window, here with the controls of a one-player game:
build/bin/sonic_game_headless --script tests/controls/coin_start.txt
It is the arcade game: put a coin in (5), then start (1). Player 1's controls:
| Control | Keys |
|---|---|
| Trackball | the mouse, or the arrow keys |
| Button | left mouse button, Z or space |
| Coins 1-3, starts 1-3 | 5, 6, 7 and 1, 2, 3 |
| Service, test | 9 and F2 (in the operator's menu, 9 moves the cursor and F2 picks) |
| Full screen | F11 or Alt+Enter |
| Smooth or sharp pixels | F3 |
| The mouse's ball rolls on, or stops with the mouse | F4 |
| The mouse slower, faster | F5, F6 |
| Quit | Escape |
The mouse turns a trackball as the arcade's does: the ball turns with the mouse and, when the mouse stops or is lifted, rolls on and slows down; the right button holds it. Each game controller is a player: the left stick is the trackball, A the button, Start the start and Back a coin.
The game keeps what it writes beside the executable on Windows, and elsewhere in the user's folder for it
(~/.local/share/SegaSonic on Linux, ~/Library/Application Support/SegaSonic on macOS). The window's
settings are in sonic.ini there, written with every setting and what it
does the first time the game runs: the mouse's speed and how the ball rolls on, the keys' and the
stick's speed, the window's scale, full screen, smooth pixels and the volume. The game reads the file again
whenever it is saved while the game runs, and the keys above write what they change into it. The game's own
settings and bookkeeping (the EEPROM) are kept in sonic.eeprom, there too.
src/ is written by the decompiler from your ROM and symbols.txt. The decompiler is arcade-decomp's,
in tools/core/: what is SegaSonic's (its structures, the registers its program shares, its sound chip)
is in tools/game_profile.py, and a change to the decompiler itself goes to arcade-decomp, checked on
both games, and comes here as a newer commit of the submodule
(git submodule update --remote tools/core). After changing your ROM, symbols.txt or the decompiler,
write the C again and check it:
python tools/decompile.pysrc/sound/ is written the same way from the sound ROM and sound_symbols.txt:
python tools/decompile_sound.pybuild/bin/s32ref runs the original program on MAME's V60 core against the same board model. Both
it and the game take the control scripts of tests/controls/ (format in
runtime/include/s32/script.h), and both write a hash of the board state after every frame. This runs
every script on both, compares them frame by frame and checks that the static analysis alone found all
the code the reference executed; --video and --sound compare every frame's picture and sound too,
and --jobs N runs N scripts at a time:
python tools/check_scenarios.pypython tools/decompile.py --check reports what the analysis found (functions, parameters and results,
the registers code reached through memory slots or interrupts would hand back), and --trace makes
every function report itself when it runs (--out DIR writes such a copy elsewhere; CMake builds it
with -DSONIC_DECOMPILED_DIR=DIR). build/decompile/index.txt lists every function with where its C is,
its callers and callees, what it mentions, and how much of it the scenarios ran. --variables FILE lists
every local variable with the instructions that write it, which is what symbols.txt's local lines
name them by, and --split-webs writes the C with every value of a register as a variable of its own,
to name them.
Other tools:
tools/disassemble.pywrites the listing,build/listing/sonic.s, with a report beside it.tools/recompile.pywrites the translation scaffold the decompiler grew from (one C statement per instruction) intobuild/generated/; CMake builds it with-DSEGASONIC_SCAFFOLD=ON, andcheck_scenarios.py --translatedchecks it.tools/coverage.pyclassifies every byte of the program ROM and lists what is still unexplained.tools/check_structure.py OUT_DIR [--sound]decompiles intoOUT_DIRand checks that the C'sif, loops,switchand control variables (tools/core/decompiler/structure.pyandgotos.py) do what the program's jumps do: it runs every function's blocks and its C on the same made-up outcomes of its tests and compares the blocks each runs, the code no scenario reaches too.tools/noise_report.py [--save]counts what is left of the CPU and of the decompiler in the C (gotos, names after a register, fields without a name, memory reached through computed addresses, results written back again before an early return, jumps out of a table, ROM addresses as numbers, functions without a comment) and compares it withtools/noise_baseline.txt: a count that goes up makes it fail.tools/memory_census.pyruns every scenario on a build of the game made with-DSONIC_MEMORY_CENSUS=ONand says, for each line ofsrc/that reads or writes memory through an address the code computes, which of the board's memories it reached (the ROM, the work RAM, video RAM, the sound board's RAM...): what typing the remaining pointers starts from.tools/make_walks.pywrites the per-stage scripts.tools/reference/compare_with_mame.pychecks our disassembler against MAME's.s32ref --watch ADDRESSprints a byte whenever it changes.s32ref --dump-frame N --dump-dir DIRsaves the board state at a frame, andtools/render_state.py DIRdraws its text layer and tilemap pages.s32ref --video-hashes FILEand--video-dump N --video-dir DIRdraw every frame with MAME's System 32 video (the hash of each frame, frames as BMP);sonic_game_headlesstakes the same options and draws them with the game's own video.check_scenarios.py --videocompares the two in every scenario.s32ref --sound-hashes FILE,--sound-log FILEand--sound-wav FILErun the sound board too, on MAME's Z80 and RF5C68 and ymfm's YM3438: per frame, a hash of the samples and of the sound board's memories; every write to the sound chips; everything mixed into a WAV file.tools/core/z80/disassembler.pydecodes the sound program;tools/reference/compare_z80_with_mame.pychecks it against MAME's Z80 disassembler at every byte.
rom/ your ROM (ignored by git)
assets.json asset manifest: positions and formats, no data
extract_assets.py extracts assets/ from your ROM, and checks they build back into it (--check)
sprite_palettes.json where in the program ROM each sprite frame's palette is (positions only), and
which frames the game draws opaque
tile_palettes.json the same for each tile
sprite_sheets.txt which frames make each sprite sheet, with the sheets' and their rows' names
data_formats.json the files of the program's data by subject, and each piece's format
(tools/formats/)
sound_formats.json the same for the sound program's data
src/ the game's C, decompiled from the program (tools/decompile.py writes it), in
files by subject: system/, video/, engine/, stage/, player/, objects/, scenes/
src/sound/ the sound driver's C, decompiled from the sound program
(tools/decompile_sound.py writes it)
symbols.txt names of functions, data, variables, board registers, structures and fields,
parameters and local variables, what pointers point at, and the layout of the
tables that hold code addresses (names only, no data); its comments explain how
each part of the game works, and its "comment" lines are what the C says above
each function
sound_symbols.txt the same for the sound program
runtime/ System 32 board model (memory map, interrupts, I/O, the sound board), the video
library, the YM3438 and RF5C68 libraries, and what the decompiled game and sound
driver use besides memory (src/game.c, src/sound_cpu.c, include/game/support.h,
include/sound/support.h), in C; part of the game
ports/headless/ runs the game with no screen or sound, for comparisons
ports/sdl2/ the game in a window, on SDL2: picture, sound, controls, settings (sonic.ini);
first_run.c, the window that asks for the ROM set when there is no assets pack
(its letters: font/, Kenney Mini, CC0, drawn by tools/make_font.py)
tests/controls/ control scripts: coins, buttons, trackball moves and pokes per frame
docs/unreachable.txt the code no scenario runs, and why
tools/sonic_rom.py ROM set definition and memory region layouts
tools/make_pack.py packs the assets for the game to load (segasonic.assets): each by name, where
the board's memory has it; --check compares it with the ROM set
runtime/src/game_data.c loads the assets pack into the board's memory, and the chips' mirrors
runtime/src/extract.c the first run: the player's ROM set (rom_set.c: a .zip, inflate.c, or a folder,
each file by its CRC32) into the assets pack, the pieces pack_layout.c lists
(make_pack.py --layout writes it: positions only)
tools/bisect_code.py finds the bytes of the code a scenario still reads as data (assets.json's
read_as_data takes them), against the reference at a frame
tools/core/ arcade-decomp, the decompiler and analysis tools shared with outrunners-decomp (a
git submodule; tools/game_profile.py tells it about SegaSonic, and tools/'s scripts
of the same names run its): v60/ (NEC V60 decoder and C translation of each
instruction), z80/ (Zilog Z80 decoder, analysis, lifting and timing: the sound
program), disassemble.py (recursive disassembler, listing and analysis report),
coverage.py (byte map of the program ROM), recompile.py (splits the program into
functions and translates them to C: the scaffold), decompile.py (decompiles the
program into readable C; decompiler/: lifting to IR, stack, data flow and
signatures, conditions, expressions, structure and the control variables that
take the gotos out, memory and pointer types, variables, the C printer),
decompile_sound.py (the sound program, with the same decompiler set up for the
Z80), check_structure.py, noise_report.py, check_scenarios.py, verify_rom.py,
render_state.py, memory_census.py, asset_formats.py (the board's data formats to
PNG and WAV and back), data_formats.py (the data as files and back, cut where
symbols.txt and sound_symbols.txt name a place, the code left out) and
formats/common.py (the data formats' helpers)
tools/game_profile.py what the core needs to know of SegaSonic: its ROM set (sonic_rom.py), its
structures, the registers the program shares, its board's sound chip
tools/formats/ SegaSonic's data formats, a module a family (palettes, scripts, animations, stages,
texts and the rest; the sound driver's tables and the music, as MIDI files), in
the core's formats package (tools/core/formats/common.py)
tools/palette_census.py runs the scenarios with the sprite and tile censuses on (sonic_game_headless
--sprite-census, --tile-census) and finds each frame's and tile's palette in the
program ROM: writes sprite_palettes.json and tile_palettes.json
tools/check_scenarios.py runs the control scripts on the reference and on the decompiled game
tools/check_structure.py checks that the C's control flow runs what the program's jumps run
tools/noise_report.py counts the leftovers in the C against tools/noise_baseline.txt
tools/reference/ MAME's V60 and Z80 cores and disassemblers, its System 32 video and RF5C68, and
ymfm's YM3438, built only into the reference tools (s32ref, the oracles), over
stand-ins for the parts of MAME they need
assets/, build/ generated on your machine (ignored by git)
python extract_assets.py writes them from your ROM into assets/, by subject:
sprites/ the sprite frames in sheets, a PNG and a JSON each (where each frame is in the sheet and goes
back in the sprite ROM): sonic/, ray/, mighty/ (the players' animations: walking, running,
tumble, climb...), stage_03/ ... stage_10/, stages_0b_0f/ (each stage's objects), objects/,
effects/, hud/, text/, eggman/, intro/, title/, map/, character_select/, ranking/,
ring_tally/, ending/
tiles/ the tile sheets, in color, with each tile's palette
data/ the program's data as text, a file a subject, a section a piece: system/ (the main program,
the attract demos, the test mode), video/ (fonts, palettes, the frame records), engine/,
objects/, player/ (sonic, ray, mighty and the players' own), stages/ (each stage: its spawn
lists, floor blocks, restart points, palettes, tilemaps and its objects' tables), scenes/
sound/ data/: the driver's tables (chips, tuning, instruments, commands) and music/, effects/, voices/,
a MIDI file each; samples/: the PCM samples, a WAV each
Edit them and build again: the game takes them. A piece of data keeps its size (the code reaches it at a
fixed address), but the songs may change length, and a sprite frame is cut from its sheet by its
rectangle. Every file says what it holds; layout.json and the sheets' JSON say where each piece goes.
The game's C lives in src/, in files by subject:
game.h ram.h table.c the prototypes (file by file); the variables, tables, board registers and
structures; the code the program reaches through memory slots, by address
system/ main.c power-on, the frame loop, the attract mode, the controls, the interrupts
coins.c coins, credits and the prompts outside a game
settings.c the EEPROM settings and the bookkeeping, with their checksums
sound.c the sound the game asks the Z80 for
test_mode.c the test mode
video/ video.c the video registers, palette RAM, the VRAM scripts that build the tilemaps
text.c fonts and text, digits, strings drawn with sprites
bitmap.c the bitmap layer
shapes.c lines, circles, polygons and fills on the bitmap layer
engine/ objects.c the object list and pools, spawning from the stage lists, what objects share
contacts.c contacts between objects and players
geometry.c between the map and the screen; square roots, angles, BCD
sprites.c animation and the sprite list
stage/ game_modes.c the game modes and what a stage frame runs
camera.c the stage view, the scroll, the camera, the fades
layers.c the animated palettes and the tilemap layers
events.c the scripted game modes
player/ player.c the player's update, movement, checks and contacts
states.c the states in play
scene_states.c the states in scenes
carried_states.c the states while the stage carries or holds a player
effects.c the HUD and the effects around the players
objects/ stage_03.c ... stage_10.c, stages_0b_0f.c each stage's objects
intro.c the opening and the how to play
eggman.c Eggman's booth, speeches and scene
rings.c rings
shared.c the objects several stages use: signs, scenery, walls, the chasers' zones
scenes/ title.c map.c character_select.c ranking.c ring_tally.c ending.c
sound/ sound.h main.c midi.c voices.c sequences.c the Z80 sound driver
Each file's header says what it holds. The original's linker laid the program out in modules, one after
another; symbols.txt's file lines give each stretch of it to its file (a stretch ends where a module
of the original ends: its tables after the code, or a byte that aligns the next routine), so a file takes
the stretches of its subject, such as a stage's objects from both code chips, and its functions are in the
order of their addresses. sound_symbols.txt does the same for src/sound/. What the C uses besides
memory is in runtime/include/game/support.h.
- Memory. The game's variables are lvalues over the work RAM (
stage,scroll_position), its tables read-only arrays over the program ROM (stage_camera_origins_x[stage]), and its two structures,struct objectandstruct player, are reached withOBJECT(address)andPLAYER(address): the game keeps 24-bit addresses in its variables, as the original does. Each object type's own variables are a view of the object (OBJECT(current_object)->tornado.path) where the code shows which type the object is: its own object, one it gives a handler (OBJECT(object)->saw_blade.stateafterOBJECT(object)->handler = saw_blade_update_ADDRESS), or one a link leads to thatsymbols.txttypes with the family (a button box'sparentis Eggman's scene object:OBJECT(LINKED(current_object, OBJECT(current_object)->parent))->eggman_scene.phase); elsewhere those bytes keep their generic names (var_80). The records the code walks through pointers are structures too: in the work RAM, the ranking records, the palette cycles, the sprite part lists; in the program ROM, read only, every record the code reads through a pointer, from the stage's floor blocks (FLOOR_BLOCK(block)->slope) and spawn lists to the chasers' paths, the ending's timed frames and the sound driver's register pairs. A record by its number from a pointer isSTRUCT(p)[i].field(the one after the pointer's,[1]; the one before,[-1]), and a table of records at a fixed place is read by its name (hud_gauge_positions_3p[kind].y), and one a byte offset the game keeps reaches, from where the offset counts (a floor type's attribute record,FLOOR_ATTRIBUTE_RECORD(floor_types_ADDRESS + OBJECT(o)->floor_attribute)->height). A field can be an array, in the record (shocked_palettes[kind].palettes[frame],crawler_walk_steps[variant].steps[step]) or after it, as an animation's frames follow its last frame's number (FRAME_ANIMATION(OBJECT(o)->animation)->frames[frame]). Values a pointer walks one at a time, a text's characters, a list of words, a script's bytes, are arrays of their kind (SPRITE_FRAME_NUMBERS(frame_list)[frame],VRAM_SCRIPT(script)[0]). What the code reads or writes through any other address it computes goes through the board's bus:mem_read16(p),mem_write8(io_cnt, x)(the board registers are addresses): the routines that fill, copy or draw into the board's memories wherever they are told, the memory tests. A part of a variable or field with no name of its own reads as a cast ((uint8_t)(OBJECT(o)->flags >> 8u)) and is written keeping the rest (x = (x & 0xFF00u) | ...); where the code reads or writes several fields at once (two words copied as one long, a halfword from a field's second byte on), the C reads those fields put together (OBJECT(o)->sprite_flags | (uint32_t)OBJECT(o)->sprite_frame << 16u) and writes each of them its bytes. A number the code divides by, counts or compares with has a name where symbols.txt gives it one (divx_pair(FRAMES_PER_HOUR, ...),initials == INITIALS_SEX). Where the code starts walking a list from an address that is no structure, the C says what that address is:hitter_list_ADDRESS - 0x68u, where a structure would have the list's head as its link field (hit_list_next, at 0x68). - Links. The game links its objects by the low half of their addresses:
LINKED(current_object, OBJECT(current_object)->child)is the objectchildnames, in the work RAM page. - Registers.
current_object,object_cursor,current_player... are the registers the whole program keeps (R20, R23, R18): global variables. Everything else a function keeps in registers is a local variable or a parameter. What a call hands back and nothing reads goes to a variable of the function kept for that, one for each register (ignored,ignored_2...): the call needs somewhere to write it, and never gets one pointer twice (animate(&ignored, &ignored_2, &at_last_step)when only the animation's last step matters). A value one routine leaves in a register and another one, several calls further on, reads has the name of what it holds where it comes from (run_sega_logorelies oninit_spritesleaving 0xFFFF in R0, itsend_mark): the C passes those values along explicitly so that it does what the original does; a register the original reads before anything sets it starts at 0.r14is the one register still called by its name: a global the game uses as scratch, which object handlers read where an earlier one left it. - Results. A function hands back one value as its return value, and more through pointers
(
uint32_t *p_depth); a caller's variable goes in and comes out. A result the function only passes on goes straight to the calls (check_player_step_against_floor(p_height_diff, p_cursor);), and one it copies back from the same variable at everyreturn, and uses little else, is read and set through its pointer (*p_dest += 2u;). Otherwise, or when some call hands it the same variable as another result, it has a variable of its own, written back through the pointer once, at the function's end, where its ways back meet; a way out from inside a loop, aswitchor a branch that cannot reach the end without a control variable writes it back before its ownreturn. - Comments. Every function has a comment above it that says what it does in the game's terms and the
C's names (the
commentlines ofsymbols.txtandsound_symbols.txt); code nothing calls starts with "Unused:". The rest ofsymbols.txt's comments keep the evidence: registers, addresses, the listing. - Control flow. Loops,
if,switch(a jump through a table of code is aswitchon the index) andreturncome from the original's branches; there is nogoto. Code several places jump to is a function of its own: each game mode (run_game_modejumps bygame_mode, and a mode that changes it runs the new one at once:run_game_mode(...); return;), each player state, the code two states share (player_hurt_fly). Where the original's jumps cross in a way blocks cannot (a loop entered at two places, a way out of two loops at once, two branches whose code joins halfway), aboolnamed after the place the jump goes to carries it:within_box_x = ...; if (!within_box_x) { ...; within_box_x = ...; } if (within_box_x) { ... }incheck_hit, which is how C writes a test with statements between its halves. A loop the code enters at two places gets a copy of the piece from one way in to the other, before it (the line drawing's odd and even halves), and a way out of a loop that runs into another one is written where it leaves, as if it were inside the loop. Three functions of the game have control variables (check_hit's andfill_span_list's tests, the sprite list's multi-part tail) and one of the sound driver (step_fm_vibrato's test).default: invalid_jump(address)is where a jump table's index would leave the table: the original checks nothing and would jump into whatever follows it; the C stops there. - Interrupts and the frame.
frame_wait()is where the game waits for the next frame and the board runs its events, andtake_interrupts()where the interrupts that raised are handled; the interrupt handlers are ordinary functions.halt()(only inint_defaultandint_vector_02) does nothing, as in MAME. - Unused code. The functions nothing in the program reaches are in the C beside the rest, in their
place by address; their entries in
symbols.txt(at the end) start with "unused:" and say what each one would do. With no caller to say what they take and hand back, they take every register they read before writing it, and hand back whatsymbols.txtsays (resultlines). The 00 bytes that align the routine after them are data, not code. A few of them use addresses of their own stack frame as values: there the V60's stack pointer is a global variable,stack_pointer, and the stack stays in work RAM, as on the board. An instruction the V60 would refuse (a bit operand in a register) isinvalid_operand(address), which stops the game, as MAME does. - The sound driver.
src/sound/is the Z80 program the same way. The sound chips are read and written throughport_in/port_outby the ports' names; its variables are names over the sound RAM (sound.h), its structures are reached withFM_VOICE(voice)->,SAMPLE_VOICE(voice)->,MIDI_PART(part)->, the records of its ROM withINSTRUMENT(instrument)->,SONG_RECORD(song)->and the like, and what it reads through any other address it computes goes through the board's bus (z80_read8,z80_write8: the bank window, whose contents depend on the bank register, and the RF5C68);z80_block(cycles)opens each stretch of code with the time it takes, so the chips' timers, the interrupt and the samples happen when they did on the board. Its unused routines are in the C too, as the game's are (the last section ofsound_symbols.txt): among them a driver for a MIDI port, an 8251 USART at ports F0 and F1 that the System 32 board does not have.
tools/noise_report.py counts these in src/ and src/sound/ against tools/noise_baseline.txt; none
of them can go without the C doing something the original does not:
| What | Count | Why it stays |
|---|---|---|
goto, functions or memory named after an address (sub_, unk_), functions without a comment, data read through its ROM address (ROM16(...): 1,137 of them before the program's data had names), numbers in the ROM's range left as numbers (below 0x10000 too, where an instruction names the address: 2 more), parameters and locals named after a register (98 before this round; the sound program's after the Z80's, de, a: 2 more), several fields read or written at once (AS16, AT8: 67), values the decompiler works out once named by a number (t1, stored1: 7), stack slots named after their offset (stack_4: 2) |
0 | |
Results written back again before an early return (*p_x = x; return;) |
274 | a function writes its results back once, at its end, where its ways back meet (1,500 copies at every return before); these are the ways out of a loop, a switch case or a branch that could only reach the end through a control variable |
Memory through a computed address (mem_read16, mem_write32) |
1199 | the board's memories and registers (video RAM, palette RAM, the I/O ports) and the generic routines that fill, copy and draw wherever they are told |
Partial writes of a variable ((x & ~0xFFFFu) | y) |
720 | the original writes the low byte or halfword of a register and keeps the rest |
default: invalid_jump(...) |
243 | the original never checks a jump table's index; the C stops where it would leave the table |
Object fields without a name (var_80) |
14 | all in init_object_fields_tail, which clears a new object: every object type uses those bytes (0x7F-0x9F) its own way, so where the engine clears them for any type they have no one name |
Functions with bool control variables |
4 | a test with statements between its halves, as C writes it (check_hit, fill_span_list, step_fm_vibrato), and the sprite list's multi-part tail, which only a 60-line copy would take away (see Control flow) |
Functions the board puts its own in place of (runtime/replaced.txt, BOARD_ORIGINAL) |
1 | check_rom_checksums, the MEMORY TEST's ROM part: it measures ROM chips, and the decompiled game has none (it finds no error, as a board with good chips does) |
Bytes of the code the game also reads as data (assets.json, read_as_data) |
999 | the original reads them through a null pointer or past a table's end: a structure or a text at a null pointer (the program's first 512 bytes, and the frames of its characters past text_frames), the map trail and marker of a stage with none, a rope bridge's sag. Each piece takes all a pointer or an index can reach there; the assets hold them, and no other byte of the code |
- Game code. The V60 machine code is decompiled to C (
src/), so the game runs natively with no CPU emulation. The game is that C and the assets: the build packs them (tools/make_pack.py) and the game loads the pack when it starts (runtime/src/game_data.c), each asset where the board's memory has it, so the program reads its data tables and level data, and the video and sound chips their graphics and samples, where the original reads them from its ROM chips. The original code is in no file and no executable, except the few bytes of it the game also reads as data (assets.json,read_as_data). Only the reference (s32ref), which runs the original code as MAME does, reads your ROM set, when it starts. - Video. The System 32 video hardware is a library (
runtime/src/video.c) that draws every frame from the board's memories, as the board does, into a 320 or 416 x 224 picture; a platform shows that picture as a texture, scaled and filtered as it likes. - Sound. The Z80 sound driver went through the same decompilation (
src/sound/); the YM3438 and RF5C68 chips are libraries that make the samples a platform plays. - Platforms. Each port only provides a window, input, audio output and the presentation of the
picture, through the board's hooks (
runtime/include/s32/machine.h);ports/sdl2/is the first.
- This project is licensed under the BSD 3-Clause License; see
LICENSE. - The C in
src/is the game's program decompiled into a new form: logic only, with names and comments written for this project. No ROM contents (graphics, sound, data tables, level data) and no extracted assets are distributed by this project; they come from your own ROM. The screenshots indocs/screenshots/show the game running from this code. runtime/,tools/core/v60/disassembler.pyandtools/core/z80/disassembler.pyfollow the behavior of MAME source files and, for the YM3438, of ymfm, andtools/reference/mame/holds copies of MAME files. Those are BSD-3-Clause; seetools/reference/mame/LICENSE.tools/reference/ymfm/holds a copy of ymfm (BSD-3-Clause; see itsLICENSE).- SegaSonic The Hedgehog is a trademark of SEGA. This project is not affiliated with or endorsed by SEGA.





