Kether static recomp
Kether static recomp is a Windows port of Kether for the Philips CD-i that translates the game's 68000 program into C.
- Type: PC port
- Game: Kether
- Runs on Windows
- By tools81
- Latest release v1.0.0, 2026-11-10
- Source: https://github.com/tools81/kether-static-recomp
README
Kether: CD-i static recompilation
A native Windows build of Kether (Philips CD-i, USA disc, English/French).
The game's 68000 program (cdi_appl) is translated ahead of time into C and
runs on the CD-i runtime below.
Installing a release
Download from Releases.
You need your own copy of the disc as an image (Kether (USA) (En,Fr).bin
and its .cue, raw 2352-byte sectors); it is not included.
Kether-<version>-Setup.exe: installs for the current user, without administrator rights (default%USERPROFILE%\Games\Kether), and asks for the folder that holds the disc image. The location goes intokether.ini.Kether-<version>-portable.zip: unzip anywhere and run. Put the disc image inrom\, or pick it when the game asks.
Windows 10 or later, 64-bit. Nothing else needs installing: the C runtime is linked in, the shaders are compiled into the executables, and Direct3D 11 is part of Windows.
Portable. Everything the game writes stays in its own folder: saves,
quicksaves, settings and the log in saves\, the disc location and update
setting in kether.ini. Nothing goes to AppData or the registry (apart from
the installer's uninstall entry). The game is always portable; the shortcuts
pass --portable anyway. To move the game, move the folder.
Disc location. The game looks for the disc image in this order: the
path given on the command line, rom= in kether.ini (a folder, a .cue or
a .bin), then rom\ next to the executable or the working directory. If
none has it, a file dialog asks for it and remembers the answer in
kether.ini.
Updates. At start the game asks GitHub for the latest release. When it
is newer, it offers to download the portable zip, closes, copies the new
files over its folder (keeping saves\ and kether.ini) and starts again.
kether.ini: update_check=1 asks first (default), auto installs without
asking, 0 turns it off. --no-update skips it for one run; --version
prints the version.
Making a release. package.bat builds both editions and writes
dist\Kether-<version>-portable.zip, dist\Kether-<version>-Setup.exe
(needs Inno Setup 6:
winget install JRSoftware.InnoSetup) and dist\SHA256SUMS.txt. The
version comes from VERSION; package.bat -NoBuild packages the current
executables. The installer script is installer\kether.iss. The disc image
is never packaged.
Building and playing
The repository has the tools and runtime, not the translated game code:
build.bat generates gen\ from your disc image.
- Put
Kether (USA) (En,Fr).bin/.cueinrom/. - Run
build.bat(needs Python 3, CMake 3.19+ and Visual Studio 2022). - Run
kether.exe(original picture) orkether_hd.exe(Cartoon HD upscaling). Both builds have the same game and the same gameplay features; they differ only in the picture filters.
| Key | Action |
|---|---|
| Arrow keys (Shift = faster) | move the pointer / steer the ship |
| Z, Space, A | button 1 (select, fire) |
| X, Ctrl, S | button 2 |
| C, Enter | both buttons |
| F2 | quicksave to the current slot |
| F3 | next quicksave slot (0-9) |
| F4 | load the current slot |
| F5 | invincibility on/off |
| F6 | run-ahead: 1, 2, 3 frames, off |
| F7 | 60 fps sprite interpolation on/off |
| Tab (hold) | fast forward |
| F9 / F10 / F11 (or Alt+Enter) | aspect ratio / filter / fullscreen |
| Esc | quit |
An Xbox controller works too (left stick or d-pad, A = button 1, B = button 2).
Saves (the game's NVRAM file), quicksaves (quicksave_N.sav), settings and
logs go to saves\ next to the executable. The window title shows the
filter, run-ahead setting and current slot. Hotkeys confirm with a message at
the bottom of the picture.
Player features
- Quicksaves: ten slots holding a snapshot of the whole machine (CPU,
RAM, OS, CD, video, audio and the game's NVRAM file), kept in
saves\quicksave_0.sav..._9.sav(about 0.9 MB each), so they survive restarts. Loading restores the exact moment: playback continues bit for bit as it did after the save. Saves made with a build whose state layout differs are refused ("incompatible save"), not loaded. - Invincibility (F5): the player's ship takes no damage and no lives
are lost. A failed space segment, such as crashing or losing the ship you
chase, is simply replayed, so the game never ends. Enemies and everything
else behave normally, and energy refills still work. See
runtime/cheat.c. - Run-ahead (F6, default 1 frame): lower input lag. After each real frame the machine is snapshotted, the next N frames are run with the current input and the last one is shown, then the snapshot is restored. Sound comes from the real frames only. The real timeline is unchanged: a session with run-ahead produces the same audio, bit for bit, as one without.
- Sprite interpolation (F7, default on): Kether draws its gameplay
picture only every 4-7 fields (10-12 frames per second). The runtime looks
ahead to the game's next picture (snapshot, hidden fields, restore, so no
added delay), finds the objects in both pictures and draws every field in
between with each object moved and scaled part of the way, giving 60 fps
motion. Details are in
runtime/frameinterp.c. Only scenes the object model explains are interpolated: objects over a backdrop, with a steady background, few objects, and most of them found again in the next picture. Menus, maps, maze rooms, films and screens being loaded are shown exactly as the game drew them. - Cartoon HD / Cartoon HD+ (
kether_hd.exe, F10): GPU upscalers. Cartoon HD follows edges on the game's 384x240 pixel grid and turns staircases into smooth, sharp contours with flat colour regions. Cartoon HD+ also flattens noise inside regions, draws dark ink lines on strong edges and livens the colours. The HD build keeps the original filters on F10 as well, and stores its settings insettings_hd.ini.kether.exehas only the original filters.
Command-line equivalents: --runahead N, --frameinterp 0|1,
--invincible, --slot N (current slot), --load-slot N (start from a
quicksave), --filter N.
State of the port
Tested headless: boot, Philips and Infogrames logos, title menu, language switch,
new game, NVRAM save, the intro film and the first level in flight, and four
40,000-field fuzzed sessions. A scripted 12,000-field session (about 3⅓
minutes, into gameplay) produces bit-identical frames and audio with
--interp, which runs everything in the reference interpreter.
What Kether needed beyond the template
-
Disc: every file on the disc, the program included, has file number 1, so
cdi_extract.pynow skips only.rtf/.rtrstreams. -
Code discovery (
tools/analyze.py): Ultra C code is reached mostly through function pointers. The analyzer now also uses- every code-relative iref, including the
jmp abs.ltrampoline table called withjsr d16(a6); lea x(pc),aN ... jsr/jmp (aN);moveq #N,dK / cmp.l dK,d0andmovea.wswitch tables;- computed jumps into unrolled loops
(
lea x(pc),aN / adda.w dK,aN / jmp (aN)); - speculative decoding of gaps and of
lea/peatargets. A candidate is accepted only if all of its reachable code decodes cleanly without overlapping known instructions.
Coverage is 89 KB of the 94.6 KB code area; the rest is tables and strings.
- every code-relative iref, including the
-
Missed code doesn't crash. A jump to an undiscovered address inside the module runs in the interpreter, which hands back at the next translated entry. It is logged as
undiscovered code ...; none occur in the tested sessions. -
Self-modifying code. Kether patches the instructions in one sprite loop (
10f38c–10f3a6: it flipsmove.w/move.band insertsext/neg).game.jsoninterp_rangesmakes the interpreter run that loop from the live bytes. The runtime compares the code area with what was translated once per field and reports any other change. -
UCM drawmaps (
SS_DM= 0x51):DM_Creatreturns a drawmap descriptor in a0, with the pixel data address at +4. -
CDFM:
- a PCL with no buffer and unbounded size discards its sectors;
SS_Abortends the play with an error status and code0x060A, and sends the PCB signal;SS_Pause/SS_Contare implemented.
-
Graphics cursor:
GC_Blnkis 5 andGC_Orgis 6. -
DC_NOP(display control function0x10): fills a region of a line control table with NOPs. Kether uses it to remove the cockpit from the display program before planet intros. Without it, the cockpit buffer showed as coloured noise, or part of the picture stayed black, while the next cockpit image streamed in. -
A second self-modifying routine: the maze sprite drawer patches its coordinate loop (
10d510-10d52a) to mirror sprites, so that loop is ininterp_rangestoo. Without that, maze sprites were placed wrongly and the game eventually crashed.
How the player features are built
- Snapshots (
runtime/state.c): translated code always unwinds to the dispatcher when it yields, so between frames the whole machine is the CPU registers, RAM and the runtime modules' state, and each module saves and loads its part. RAM is allocated with Windows write-watch, so a run-ahead snapshot copies only the 4 KB pages written since the last one. To make this possible:- a blocking
F$Sleepnow returns to the dispatcher at each field and continues afterwards; - NVRAM files live in memory and are written through to
saves\. Run-ahead frames never write to disk, because the real frame repeats the write. F$Timecounts emulated time from boot.
- a blocking
- Frame driver (
runtime/frontend.c): runs one field at a time and handles run-ahead, the lookahead for interpolation, hotkeys and on-screen messages. Run-ahead frames skip pixel composition, which has no effect on the machine, so run-ahead 1 costs about 0.6 ms per field. - Invincibility uses a new
game.jsonfeature,"hooks". A hook is a C function called just before the instruction at an address, in translated and interpreted code alike. Kether's energy routine (10ac26) adds a signed amount to the player's energy (0x141BBE, 0-300, 13-segment bar). The hook drops negative amounts while the cheat is on.
Measured cost per field on this machine (headless, gameplay):
| Configuration | Time per field |
|---|---|
| Plain | 3.0 ms |
| Run-ahead 1 | 3.5 ms |
| Run-ahead 1 + interpolation | 7.4 ms |
A field lasts 16.7 ms.
Known differences
- The game sets all four audio attenuation paths (including the cross paths) to 0 dB, so left and right are summed. The Philips/Infogrames jingles clip on up to 0.5% of samples; the game's music doesn't.
- The game builds a 280-line display and starts it 20 lines down. The runtime shows the 240 NTSC lines, as an NTSC player does.
- Sprite interpolation is a reconstruction. Objects are told apart by colour-table ramp and matched between pictures, so objects of the same material that touch move as one piece. A large object that gets covered stays behind the nearer one until the next picture. Occasionally a small fragment is matched wrongly and shows as a brief speck for a few fields. Palette-cycling lights inside objects update once per game frame in the in-between fields. F7 turns interpolation off.
- Cartoon HD can bring out colour fringes that are present in the source images (for example dark specks along the edge of the Philips logo).
CD-i static recompilation template
This is a starting point for turning a Philips CD-i game into a native Windows program. The game's 68000 code is translated ahead of time into C. A runtime provides the parts of the CD-i the code expects: the CD-RTOS (OS-9) kernel, the CD file manager, the video decoder (with a Direct3D 11 presenter at any resolution) and the ADPCM audio processor.
It was extracted from the Link: The Faces of Evil project. That build is bit-identical to a pure-interpreter run of the same game, and this template reproduces it exactly.
Starting a new game
- Copy this folder to a new project folder.
- Put the disc image (
.bin+.cue, raw 2352-byte sectors) inrom/. - Edit
game.json: settitle,exe_name, and the modules to recompile (see below). - Run
build.bat. It extracts the program modules from the disc, recompiles them intogen/, builds with MSVC and copies<exe_name>.exeto the project root. - Run the game, and use the debugging workflow below for anything that breaks.
Requirements: Windows 10+, Visual Studio 2022 (C compiler), CMake 3.19+ and Python 3.8+. No third-party libraries are needed.
game.json
{
"title": "My CD-i Game",
"exe_name": "mygame",
"modules": [
{"file": "auto", "base": "0x100000", "extra_entries": [], "a5_table": "auto"},
{"file": "cdi_bumper", "base": "0xE0000", "a5_table": "auto"}
]
}
- The first module is the program the player boots.
"auto"reads its name from the disc label's application identifier (extracted/_app.txt). - Other modules are programs the game loads at run time with F$Load or
F$Fork, for example the Philips logo "bumper". Each one is recompiled for its
baseaddress, and the runtime places it there. Ranges must not overlap and must stay below0x140000, where the heap starts. Program modules the game loads that are not listed still run, slowly, in the built-in interpreter. extra_entriesare code addresses the analyzer can't discover, as absolute hex values (base + offset). The analyzer already finds:- call and branch targets;
- switch tables;
- MPW A5 jump tables;
- Microware
move.l #off,dN / jsr (pc,dN.l)calls; - F$Icpt handlers;
lea/peafunction pointers that start withlinkormovem.
a5_table:"auto"detects programs built with Apple MPW-style toolchains, whose calls go throughjsr N(a5).interp_ranges:[["0xLO", "0xHI"], ...]lists address ranges (HI exclusive) that always run in the interpreter. Use it for self-modifying code. When the game patches its own code, the runtime logsself-modifying code: X-Y changedwith the range to add.hooks:{"0xADDR": "c_function"}calls a nativevoid c_function(void)(defined in the runtime) just before the instruction at ADDR runs, in translated and interpreted code alike. The hook may read and change the 68k registers and memory. Kether uses one for its invincibility cheat (runtime/cheat.c).
To see what the analyzer found, run python tools/analyze.py [module index].
It writes a disassembly with recovered function names, from MacsBug symbols if
present, to build/listing_<module>.asm.
Layout
| Path | Contents |
|---|---|
tools/cdi_extract.py | Green Book filesystem reader. Writes program files and _app.txt (--all also dumps the real-time streams). |
tools/m68k.py | MC68000 decoder |
tools/os9mod.py | OS-9 module header, idata and irefs |
tools/analyze.py | Code discovery (recursive descent plus the patterns above) |
tools/gen.py | 68000→C generator, flag liveness, dispatch tables, gen/game_config.h |
tools/config.py | game.json loader |
tools/gen_tests.py | Per-instruction differential tests |
tools/bmp2png.py | Converts frame dumps to PNG |
runtime/cpu.c | Dispatcher and run loop |
runtime/interp.c | 68000 interpreter, for RAM code and the --interp reference mode |
runtime/alu.c | Shifts, division and BCD with exact flags |
runtime/os9.c | OS-9 kernel: memory, processes (fork/wait/exit), signals, alarms, paths, NVRAM, CSD |
runtime/cdfm.c | CD file manager: reads, and real-time SS_Play with PCB/PCL/CIL |
runtime/video.c | UCM display control (FCT/LCT), MCD212-style renderer, cursor, pointer |
runtime/audio.c | ADPCM levels A/B/C, sound maps with gapless SM_Out, attenuation |
runtime/render_d3d11.c | GPU presenter: aspect modes and sharp/smooth/CRT filters |
runtime/host_win32.c | Window, input (keyboard and XInput), waveOut, pacing, scripted and fuzz input |
runtime/state.c | Machine snapshots: write-watch RAM tracking, run-ahead, quicksave files |
runtime/frontend.c | Frame driver: run-ahead, interpolation lookahead, hotkeys, on-screen messages |
runtime/frameinterp.c | Sprite interpolation to 60 fps |
runtime/cheat.c | Invincibility (Kether-specific game.json hook) |
test/ | Instruction differential tester and ALU reference vectors |
docs/CDI_NOTES.md | CD-i facts and API codes worked out during the first port |
Debugging workflow
- Find what fails. Unimplemented OS calls and SetStat/GetStat codes are
logged to
saves/recomp.log. Fatal errors also show a 68k register dump and a stack backtrace with function names. - Debug options:
--trace/--trace3log syscalls with callers, signals and disc activity.--headless --frames N --dump Kruns fast without a window and writes every Kth frame as a BMP.--script "field:key:dur,..."scripts input (keys1 2 L R U D).--fuzz SEEDdrives the game with random input.--wav out.wavrecords the audio.--watch ADDR[,ADDR...](hex) logs 16-bit words in RAM whenever they change, once per field. Useful for game variables such as the pointer position.--pcslogs where the CPU is every 30 fields. A game stuck in a wait loop shows up as the same PC.--hashlogs a per-field hash of RAM and registers, and writessaves/ramdump.binat--frames N. Diff two runs to find the first field where they diverge.--load-slot Nstarts from a quicksave, which is the quickest way to test one scene repeatedly.--ramdump-every Nwrites the 256 KB data area (0x140000) tosaves/ram_<field>.binevery N fields; comparing dumps over time finds game variables (that is how Kether's energy was found).--dump-phase Pshifts--dumpso that frames line up between runs (for example run-ahead 2 with--dump-phase 2).--screenshot Nsaves the presented GPU picture at field N, to check filters.--interp-debuglogs the interpolator's decisions.- Kether only:
--debug-state Nsets the level state machine's state once (11 = this level's maze, 12 = leaving it, 3 = the space segment).--debug-segment-wincounts every finished segment or map as won. Both work throughgame.jsonhooks inruntime/cheat.c, and both are meant for runs without run-ahead. - Script keys
S,O,N,I,Fpress F2, F4, F3, F5 and F7 at the given field, which lets headless runs test quicksaves. - A host crash logs a symbolized stack trace to
saves/recomp.log. The Release build writes a.pdbfor this.
- Check the translation. Run the same headless session twice, once with
--interp, which executes everything in the interpreter, and compare the frame dumps and WAVs. Headless runs are deterministic: emulated time is counted in instructions, and the clock uses a fixed epoch. Any difference points to a translation bug. Start both runs from the same NVRAM state: deletesaves/nvr_*next to the executable you run. - Find the first wrong instruction. Use
--hashto find the field, then build the lockstep tracer withcmake --build build\cmake --config Release --target game_bt. Runkether_bt.exe --btrace LO,HI(instruction counts, from the--hashlog) once normally and once with--interp. Each run writesbtrace.txtwith PC, instruction count and registers at every block entry. The first differing line, ignoring the flags column, pins down the block. - Unit-level checks.
python tools/gen_tests.pyandpython test/gen_alu_vectors.pygener