The Amiga Comes to Life!
This week was a massive milestone. The European Amiga version of Kult now runs almost perfectly under ScummVM — it boots, plays through, and looks exactly the way it did on real hardware. Getting there took twelve intensive commits(for now) that dragged the port all the way from “the engine knows the Amiga platform exists” to “you can actually sit down, click around, and play it.”
The absolute best news from the start: the Amiga build didn’t require a completely separate engine. It runs on the same chunky, one-byte-per-pixel, 16-colour pipeline as EGA — the Amiga’s planar source data just gets converted into it. Almost everything that differs comes down to the palette, the way static data is stored, and a handful of filenames. That single insight shaped my entire week.
Sharing the EGA Path
The first major step was to stop the graphics code from asking “is this strictly EGA?” and start asking “is this an EGA-like renderer?”
I added an isEgaLikeRenderer() helper and routed the sprite, portrait, and transition code through it instead of comparing directly against the EGA render mode. With that simple seam in place, the upcoming Amiga renderer could reuse all of those paths completely unchanged! There was zero behavioural change for EGA or CGA, but we now had a clean foundation to hang the new platform on.
On top of that, I implemented the AmigaRenderer itself: a custom 12-bit palette over the same planar graphics, selected purely by platform. It came with its own cursor decoder for the SOURI.BIN hardware sprites, plus the title screen and its fade-in ramp wired directly into the boot path.
Finding the Data
The Amiga release doesn’t lay its files out like the DOS versions. Instead of a compressed PXI module, it keeps the static resources uncompressed inside the KULT executable itself. Furthermore, it ships per-language text files rather than the generic ...I.BIN names.
I added a dedicated loadAmigaStaticData() function and routed the resource loading accordingly, ensuring the engine pulls text and data from the exact right offsets for each language.
Colour, At Last
Early on, the rooms were wearing the wrong palette entirely. The fix was to compose the room palette from the zone’s palette_index and apply it as soon as the zone loads. This means intro and special screens (which can bypass the usual zone refresh) are no longer left stuck on the previous title palette.
The title screen also had its own bizarre palette bug. It was reading a table at one offset in the KULT executable that turned out to be a brightness fade-lookup ramp, not actual RGB data — which is exactly why the whole title initially rendered in spooky shades of red! Pointing it at the title’s actual 16-colour palette brought the gorgeous artwork fully back to life.
Before:

After:

Decoding the Sprites
The sprite banks were the trickiest reverse-engineering puzzle of the week. They share the EGA record layout, but with two fun twists:
-
The record size is stored big-endian.
-
The width/height byte pair is swapped.
The pixels themselves are word-planar — one big-endian word per four-pixel column. I added appendFromStreamAmiga() to decode them, a shared placeholder for any missing sprites, and successfully loaded the merged Amiga banks (SPRIT/PUZZL/A/B).
Scripts and Threads
Two subtler bugs came next:
-
The Padding Bug: The Amiga script blob inserts
0x00/0xAApadding for word alignment right in the middle of instructions. The engine didn’t expect this and was aborting scripts early (for instance, the Twins’ serpent-jaw redraw would just randomly stop). Instructing the engine to skip that padding fixed it instantly. -
The Racing Thread: I also had to explicitly skip
animateGausson Amiga, because it runs on the timer thread and blitting from there directly races the main render thread.
The Ending and Final Polish
The endgame sequence needed several specific fixes to shine:
-
Saucer Animation: The vblank flush originally only pushed the chunky screen buffer for EGA, meaning the in-place saucer take-off animation never reached the Amiga screen. Now, it correctly flushes for any EGA-like renderer.
-
End Screen Noise: The engine was trying to decode the planar Amiga
PRES.BINwith the CGA splash loader, turning the ending into a band of static noise. Loading it properly and restoring the title palette fixed the climax. -
Infinite Loops: The non-US end path used to loop forever on an empty busy-loop, causing the host OS to report a frozen, “not responding” application. Now it cleanly pumps the event loop, holds the end screen until the player clicks or presses a key, and then returns gracefully to the launcher.
-
Portrait Pacing: Portrait animations were pacing off a busy-wait loop that collapses to nearly zero on fast modern CPUs, making them run at lightspeed. They are now properly paced by wall-clock time.
-
UI Borders: The rectangular UI boxes finally had their borders corrected and the right color as well.
Next Week
With the European Amiga version playing through cleanly, next week I’ll turn my full attention to the US variant. I’ll be checking exactly how its assets and text differ to bring it up to the exact same standard. The hardest groundwork is done; now it’s all about making the second variant just as rock-solid as the first!
These are some versions from before I find the right palette:



