Author: gskjeret

  • Power of 2 – aka Fat & Binary – technical writeup

    So none of this was my idea, except for doing this little writeup. I made the Amiga part and some tooling. Idea and concept by Stone.

    WTF is this?

    The challenge we wanted to solve was to construct a single executable file that is both a valid C64 executable PRG file, and a valid Amiga executable, with each OS loading it and executing it natively.

    History and similar concepts

    As far as we can tell, no one did a fat binary for both C64 and Amiga before. These are the platforms where Offence are active, so that just made sense to us. But there have been several other similar productions, notably

    • A Statement on the Platform Wars (Revision 2024) – supports very many platforms, draws a trans flag by vertical color splits
    • Amor para Dos by L.I.A (Flashparty 2021) – Has different parts on each platform
    • Hello world! by RasterDream (2013) for DOS and C64, prints “Hello world”

    Research was done with Google search and is known to be incomplete. Anyway there aren’t that many true polyglot executables around. JAR files, javascript demos etc don’t count at all for this purpose, it has to be the OS loading the file as an executable.

    Another matter is floppies that work with different platforms, like some that will boot both on Amiga and Atari ST. As I understand it this is done on a magnetic level, so it’s a whole different ballgame. Several games shipped on such dual floppies but I don’t think it was ever explored in a demoscene prod. Fat floppy, anyone?

    We also have the theoretical possibility of making a shared executable between e.g. VIC 20 and C64, or PET, etc, where the same machine code runs on different platforms that share the same CPU. Other platforms might also be interesting like Atari 400/800/XL/XE. I have not looked into these at all, but I know VIC 20 and C64 load the PRG files in a very similar manner. So the divergence would be done in code, testing values in memory addresses and branching. But Amiga is completely different from the C64 with its hunk structure and different CPU, so the divergence has to be done by the loader in the OS.

    Finally, the same byte code means different things on different CPUs, but what if you find an instruction sequence that makes sense on both? This might be possible for a very small production, but extremely hard to do.

    Here’s a table for this:

    ClassShared artifactDivergence occurs atDifficulty
    Shared source Source code Compilation Routine engineering
    Shared floppy Floppy disk Disk drive subsystem Needs special hardware
    Shared CPU Instruction stream Runtime Custom chips and mem architecture
    Shared executable Executable image Loader This production
    Shared instruction stream Raw bytes CPU decode Godlike

    Why a writeup?

    Back in the day I never bothered learning the Amiga system stuff like the hunk structure and things like that. To be quite honest most of my productions (active period 1988-1992) were crunched with crap crunchers like Bytekiller and decompressed to an absolute address. If they even worked on other people’s machines it was mostly by accident or because the machine had the same config as mine. I’ve tried to do better in my current active period.

    So I decided to work through this prod and make a writeup, learning new stuff about both the Amiga and the C64. I’m not going to describe every single byte or every field in every data structure, but at least give an overview. This is a prod writeup, not a scientific article. Also I’ll not spend that much space on advertising my own ignorance wherever there are unknowns, just take it a a given 🙂 If you want more in depth Amiga stuff then I suggest the 1987 AmigaDOS manual, specifically the AmigaDOS Technical Reference Manual part.

    As for the actual demo effects I’m not going to write much, part of our shtick was to have them very similar-looking on the two platforms, although we didn’t manage completely identical. We certainly didn’t stretch either platform’s capabilities. The logo is drawn by Pal in PETSCII, so I had to include the C64 ROM font in the Amiga part.

    Basic file requirements for being executable

    Getting into the organization of the file.

    For C64, the first 2 bytes will determine the load address, IF the file is loaded with ,8,1. If it’s loaded just with ,8, it will load to $0801 and that won’t work for our purposes. In our example that address will be 0, so the rest of the file will be loaded at address 0 and up.

    For Amiga it’s a bit more complicated:

    • File must start with magic cookie 0x000003f3 to mark it as an exe
    • Then a library list pointer, usually zero
    • Then the hunk table (first the size and then a list of hunks).
      Header describes the hunks that follow, including size and requirements
      Then the actual hunk
      Then another magic cookie to mark the hunk end, 0x000003f2

    In general on the Amiga, if you try to execute a file that doesn’t tick all the boxes, you get an error 121: File is not an object module

    Tracing the loader flows

    So starting with the magic cookie required by Amiga, that would cause the C64 to load it at address 0. That could be a potential dealbreaker since some extremely central registers live at address 0 and 1.

    Address 0 is the DDR, which controls whether CPU pins are input or output. Sounds a bit dangerous. This register interacts with address 1 to control the fundamental memory configuration of the C64, and the values written here (the rest of the Amiga magic cookie – $03f3) will just prevent the character ROM from being mapped in. So we’re lucky here, if the combo was some invalid state the production would not be possible. And we’re not even using the built-in font, since we use topaz.8 for the C64 part.

    So the C64 has a few more addresses that must not be set to invalid values. Notably address $000a which would cause a VERIFY instead of LOAD if it’s not zero.
    After that there’s nothing of consequence until address $0029. Our approach here will be to create a minimal Amiga hunk table, which leaves a comfortable margin before reaching that address. That gives us a single Amiga code hunk, which will load into chipmem. Our hunk will contain all the rest of the executable including the C64 code, and start with a BRA.W instruction that skips past all the C64 code, and straight into the Amiga part. Surely this is a fairly brittle approach with regards to memory allocation and such. But hey, it only needs to work once!

    The rest of the C64 low memory area is filled with zeroes for the most part, or valid values where appropriate. Notable addresses are
    $90-$9d, $ae-$af, and the vectors at $0300-$0333. From $0334 we include the actual code for the C64 part.

    A note in the code says:
    ; Pointing the TXTTAB to a bunch of zeroes will provoke an error after loading is done, causing the CPU to jump to the address stored in the warm start vector at $300.
    ; $0300-$0303 - Basic warm start/reset vector points to the start of our program
        .byte <start, >start, <start, >start

    TXTTAB will normally point to $0801 which is where the basic program lives. According to my research, if it points to a block of 5 zeroes it will trigger a warm start AFTER loading is done. Apparently this trick is well known by the C64 community, and is used to let the user load a program that then starts by itself without having to type RUN. I’ll describe it here for the rest of us.

    • When the loader is done, it will check for a BASIC program in the address pointed at by the TXTTAB vector.
    • In this case it finds double zeroes, indicating no next line, no line number and an end of statement marker.
    • This provokes a warm start, jumping to the vector stored at $0300, usually a routine in ROM at address $E37B. Resets everything and presents the user with the READY prompt.
    • But we already patched the vector for this, so it calls our routine, the start of the C64 part, instead.

    On the C64 loading 16KB without a fastloader still takes some time, due to the crippled loading system it can transfer only around 400 bytes per second. Stone told me that because of the zeropage usage (basically we just run a bulldozer through the whole zeropage) most fastloaders would not work with this. But he managed to make a d64 version with a fastloader.

    After the c64 part we can include the Amiga binary part, and of course the offset of the BRA instruction has to be adjusted so it points to this. The C64 part may then not exceed 32K, as that is the max offset of BRA.
    And of course all the Amiga code will have to be PC-relative, since we have no reloc data and adjusting to an unknown size C64 binary blob would be too much hassle. That was less annoying that I thought it would be… Yay for the 68000!
    A final thing here is that the Amiga hunk start has to be longword-aligned (4 byte boundary) so we had to pad with $4e71 (nop) as the C64 part changed size. Looking now I see that we had 4 NOPs in there, which probably wasn’t needed 🙂 I think we ended up just adding another NOP whenever the Amiga part was out of alignment when we might have removed one instead. Chalk it up to hurry.
    Also the hunk end is the same (the hunk needs to be a whole number of longwords) so we did a zero pad:

    .rept (4 - ((* - AMIGA_HUNK_START) & 3)) & 3
    .byte 0
    .endrept

    The size of our PRG file was less than 16 KB, which loads nicely into the C64 RAM, but there is a limit (depending on memory bank configuration). I think 48K or so before you start overwriting stuff that will crash the loading. We never approached that limit in our project.

    The project includes a little python script to extract the first code hunk from the compiled executable that the Amiga chain with vasm produces, so we don’t include that whole exe file, rather just reproducing the needed parts of it.

    Here’s a summary of the file contents, showing how each platform will interpret the file according to its own loader, reach its own relevant code, and how it works to have both in the same file.

    C64
    addr
    File
    offset
    BytesC64Amiga
    N/A$000000 00Load address when using ,8,1HUNK_HEADER magic cookie part 1 (0x000003F3)
    $0000$000203 F3DDR and processor portHUNK_HEADER magic cookie part 2 (0x000003F3)
    $0002$000400 00 00 00UnusedResident library list pointer (NULL).
    $0006$000800 00 00 01UnusedHunk table size = 1.
    $000A$000C00 00 00 00$000A must be zero to avoid VERIFY mode.First hunk number = 0.
    $000E$001000 00 00 00UnusedLast hunk number = 0.
    $0012$001440 00 21 8AUnusedAlloc size for hunk 0 (0x218A longwords) with MEMF_CHIP attribute.
    $0016$001A00 00 03 E9UnusedHUNK_CODE.
    $001A$001E00 00 0F E4UnusedCode hunk length (0x0FE4 longwords).
    $001E$002060 00 1E EEUnusedBRA.W with displacement $1EEE.
    $0022$002423 … 23Fill bytesSkipped by the bra.w
    $0029$002B0A 00TXTTAB = $000A, forcing BASIC relink to fail.
    $002D$002F23 … 23Fill bytes
    $0090$009200 FF 00 00 40 FF 00 00Default IEC/KERNAL loader state
    $0098$009A00 00 03 00 00 80Default IEC/KERNAL loader state.
    $009E$00A023 … 23Fill bytes
    $00AE$00B0AE 02Load continuation pointer = $02AE.
    $02B0$00B223 … 23Previous vector write caused us to skip some bytes in memory!
    $0300$010234 03 34 03Warm-start/reset vectors patched to c64 start.
    $0304$0106(default vectors)Default KERNAL soft vectors.
    $0334$0136c64part.prgC64 program entry point (start)
    $1F06$1F084E 71 x 4Never seen by the C64NOP to adjust to 4-byte boundary adjustment (not needed)
    $1F0E$1F10Amiga codeAMIGA_CODE_START, start of Amiga code hunk extracted from exe
    $3FCE$3FB000 00 03 F2Amiga Hunk end, magic cookie value $000003F2

    Shared data

    The C64 part includes some lz packed data, containing the characters and colors for the PETSCII logo. In front of this is the scrolltext, encoded into topaz font offsets by another little python script.

    The C64 part includes these and uses them directly, but the Amiga part has to do a backwards memory search to find the two lz headers and depack the data, and after that it looks for the first $ff byte past that to get the start of the scrolltext. We thought this was a bit of a cool detail, rather than including them twice.

    And of course the Amiga part had to decode those topaz font offsets into C64 ROM font offsets – but that was fortunately not so hard. Yeah, I never got around to implementing lower case, gna gna.

    ; scroller byte in d0
    	cmp.b   #32,d0
    	blt.b	.numbersandspecial
    	cmp.b   #32+"Z"-"A",d0
    	bgt.b	.notupper
    	add.b   #32,d0
    .notupper
    	sub.b   #64,d0
    	bra    .render
    
    .numbersandspecial
    	add.b 	#$20,d0
    
    .render
    	lsl.w	#3,d0
    	lea	(a4,d0.w),a6 ; get character offset

    Fonts

    The Amiga scroller and “Basic fader” uses the C64 ROM font, while the C64 part uses topaz.8. Both were lz packed but obviously not shared between the parts.

    Music

    We looked into sharing note data for the music but had to abandon this due to insufficient octaves on the Amiga. No doubt this could be fixed with some effort, and additional Amigaklang instruments, but since our Amigaklang + LSP (micro mode) approach worked fine we just went with that. The C64 SID was the original and Response expertly turned it into a similar sounding Amiga tune.

    Additional lz packing

    The Amiga part has a lot of redundant data, REPT in the copperlist etc. Rather than fixing all those things I decided on a different approach: Create the main executable, extract that one code hunk and lz-pack it, and then create a new executable with just a depacker and that packed blob. This approach is also pretty brittle as it does no mem allocation and no BSS hunks are loaded. More on that in a bit…

    We couldn’t be arsed to do the same to the C64 part as the executable was already under our target size 16KB.

    Build system

    …is a shitshow. A part of the problem was that because we had two coders, each wanted to work with their own familiar tools. For me (on Amiga) that was vscode and its tasks in json, for Stone it was a makefile. Each time the other created a new dependency the other had to adapt theirs. In hindsight that was a mistake as we actually ended up with our builds somehow being binarily different (but both working and satisfying the conditions we set). And no one has any appetite for revisiting that.

    Testing on a real Amiga before the party I did notice that while the exe runs fine, if you start it again after exiting it won’t start up. So of course I fixed that by not allowing it to exit. Yeah yeah, gna gna.

    In fact on the party evening the orgas give us the dreaded message “There is a problem with the Amiga part” – So after some testing we found out that they have to cold start their Amiga in order to avoid a buggy display. Maybe we should have included some memory clearing routines here. Oh well!

    A problem with our approach is that since we don’t actually allocate the memory we use (normally you would do that by BSS hunks, but we don’t have any) we don’t actually have any guarantee that the memory we use is free, or exists. This should have been made more robust. There’s actually nothing preventing us from allocating the needed chipmem by normal library calls and failing gracefully if it’s not available.

    Last words

    In the end the approach proved viable, and we were able to release it at Black Valley 2026 in the oldschool demo compo. The most legendary of the Commodore machines working together, turning a mere theoretical possibility into a real practical polyglot demo. You could say that we abuse two operating systems with a single file.

    /Nosferatu

  • In Mono Where Available – tunes

    The tunes used in this musicdisk are LSP-compressed so here’s an archive of the originals.

    https://16bitblood.org/wp-content/uploads/2026/01/InMono-UsedTunes.zip

  • Cognitive Sovereignty

    For reasons we need not get to the bottom of, a bugged version was delivered to the GERP party orgas. Here is the working version.

    https://16bitblood.org/wp-content/uploads/2026/01/CognitiveSovereignty.zip

  • As Good As New

    Got around to uploading the original music by Jogeir:

    https://16bitblood.org/wp-content/uploads/2024/12/turning_fifty_release.zip

    Funny story, there was an extra little song in there meant for the credits part. But it wasn’t included in the release, so Jogeir used it as a base for the Magnar 50 tune.

    This version is sanitized and does not include that bit.

    /Nosferatu