lauantai 7. marraskuuta 2026

Duke9500: Duke Nukem 3D on a Communicator, AdLib music included

After Doom9500 and Quake9500 the obvious next question was one line long: "Duke Nukem 3D for Nokia 9500. Doable?"

It was. Duke9500 is the game on the Communicator's 640×200 screen, with sound effects, the original music and a face on the Desk. As before, the code was written by Claude (Anthropic's AI). I built it on my Windows 2000 VM, ran it on the phone, sent back logs and complained. It took two days, and it fits in a SIS of about 400 KB, game data not included.

It also got me further than I expected before it broke, as you'll see in the section about the toilet.

Where the code comes from

Duke runs on Ken Silverman's Build engine, with 3D Realms' game code on top. Fabien Sanglard's Chocolate Duke3D made the pair portable, and Rockbox took that to small ARM devices. Duke9500 starts from Rockbox's tree at commit 46c888e, throws out SDL and Rockbox, and sits on the native layer already proven by Doom9500 and Quake9500. These are the new parts:

FileLinesWhat it does
display_epoc.c389The Build engine's video, key and timer driver
sys_epoc.c137Start-up, finding the game folder, the log
fx_epoc.c779Sound effects: the mixer
midi_epoc.c1203Music: MIDI sequencer and FM synth
epocsys.cpp952Native layer: window, framebuffer, blit loops, keys, exception handler
epocaudio.cpp345Audio thread feeding CMdaAudioOutputStream
armrt.c18964-bit helpers that egcc.lib lacks

33 of the original files changed too, for about 950 changed lines in a diff. That's much less than Quake needed, and it shows: Build is integer code from top to bottom, and an ARM with no FPU doesn't mind that at all.

320×200 art, doubled across. Duke has never had this wide steroid shoulders.

The compiler goes first

The SDK's compiler is GCC 2.9-psion-98r2, from the last century. The first build didn't get far:

(...\2.9-psion-98r2\cc1.exe 1002) Exception: STATUS_STACK_OVERFLOW
gcc: Internal compiler error: program cc1 got fatal signal 0

The compiler itself was overflowing its stack, recursing on functions full of inlined multiplies. gcc -Q names each function as it starts, so the last name printed is the culprit: hitscan first, then drawmapview. A batch file tried 15 optimisation flags one at a time. Only -fno-inline helped, and that's not something to wish on a renderer.

The way through was to give the compiler simpler code to choke on. The multiply macros now do their shift inside the assembly, so GCC sees one opaque result instead of chains of shifts and ORs. And the automap code, which doesn't need to be fast, calls ordinary functions instead of inlining anything. -O2 then found one more toy, in the code that drives bots:

player.c: In function `computergetinput':
internal error--insn does not satisfy its constraints:
(insn:HI 5662 9270 9273 (set (reg:SI 0 r0)
        (mult:SI (reg:SI 0 r0)
            (reg:SI 0 r0))) 42 {mulsi3}

That's GCC asking for r0 = r0 * r0, which the ARM's mul can't do. The bot's squares went into a small function of their own.

First run: instant panic

Then it built, and it died before drawing anything: Main, QUAKE-EXC, 12. That's the Quake9500 exception handler's name, still on the door. The log's last line had the answer:

Scanning directory '/.rockbox/duke3d/' for a GRP file like 'duke3d*.grp'.

Rockbox's getGameDir() ignored the folder it was told about and always answered with Rockbox's own. On the phone that folder doesn't exist, opendir failed, and readdir(NULL) did the rest. Fixing that exposed the next one: with a real game folder set, script includes went looking for D:/Duke3D/D:/Duke3D/DEFS.CON. The game quit politely with nothing on the screen, which was harder to spot than a panic.

After that fix the game ran: the first rooftop, on a Nokia 9500, with Duke's ship burning overhead.

The alien on the toilet

I played the first level, and I played it again, and each time the phone panicked at the same spot: dropping down the shaft from the roof. A crash you can repeat is a gift, but the log only said Crashed in: blit: pixels, the last marker anyone had set. So the crash handler learned to write out the stack, with the map file from the build alongside it. Among the stack's words sat what looked like the saved registers, and the address they gave pointed here, in ceilscan:

if (sec->visibility != 0)
    globvis = mulscale4(globvis, (uint8_t)(sec->visibility+16));

ldr   r2, =globvis          ; r2 = the address of globvis
smull r8, r2, r1, r3        ; the multiply's high word lands in r2 too
mov   r8, r8, lsr #4
orr   r8, r8, r2, lsl #28
mov   r7, r2                ; r7 "is" &globvis
str   r8, [r7, #0]          ; writes to a small number: page fault

So the bug was ours, born in the fix for the compiler crash. The new multiply macros had an output for the high word that nothing used after the assembly, and GCC 2.9 felt free to give that output a register still holding a live value. The line only runs for sectors with their own visibility setting, and the shaft was the first one I met. That's why it was always the same spot.

A small scanner went through the whole EXE: of 406 of these multiplies, two were really broken. The macros now use ip, declared as clobbered, and the scan of the next build came back clean. After that, I got to play on, and I reached the real goal of any Duke port:

Business phone, they said. High-end, they said.

Black screen on Quit

Menu, Quit, and the phone showed nothing but black. It wasn't a hang. The Plutonium version of the game shows an advert for TEN, the old online service, when you quit, and waits for a key. It fades the picture in by changing the palette. On a PC of 1996 that changes the screen at once. On the 9500's 16-bit screen it only changes a table, and nothing shows until the next blit, so the advert was drawn and waiting in perfect blackness. palto now blits at every step, so the game's other fades should show as well, and the advert is gone from Quit.

Sound: a small mixer

Duke's original audio library is thousands of lines of DOS code about Sound Blasters. Rather than port all of it, fx_epoc.c does its job in under 800 lines on the audio stream that already worked for Doom: 22050 Hz stereo, 16 voices, with the original pan table, pitch table, loop rules and priorities for who gets kicked out.

Sounds are parsed on the game thread when they start, and the audio thread only reads samples. It never calls the game, so finished voices wait for the game thread to collect them. That design is why the first test went like this: I chose the easiest skill, Duke said "Yeaaah, piece of cake", and the game froze. premap.c sits in a loop until that line of speech has ended:

while(Sound[globalskillsound].lock>=200) ;

On DOS a sound card interrupt changed that value behind the game's back. Here nobody did. The loop now does the collecting itself.



Music: an FM chip in software

Doom9500 plays its music as MP3s, but the memory card is only so big. Duke's music is MIDI, and the file that made it sound like anything in 1996, d3dtimbr.tmb, is already in the GRP: the instrument bank for AdLib and Sound Blaster FM chips. So midi_epoc.c is that chain in software:

  • A sequencer on the rules of the audio library's midi.c, with Duke's EMIDI loops and track selection.
  • Voices on the rules of al_midi.c: its pitch tables, volume calculation and voice allocation, up to 18 notes at once. It even keeps the place where the original seems to use the wrong variable, because that's how it sounded.
  • Two operators per voice like the OPL chip, with its waveforms, envelopes, feedback and key scaling, in integers only.

It isn't a chip-exact emulation, but it needs no extra files on the card, and it's the AdLib sound that belongs to this game. I was ready to give up some frames for it. I didn't notice any going.

Keys and the icon

Rockbox's key table was made for a D-pad, so Up also opened doors and Enter fired. The defaults now follow Doom9500. One trap: Duke treats a held Alt as "macro keys". With Chr's strafe toggle on, F-keys would have become chat messages and Chr+R (F4) would have tried to quit. So the Alt key reaches the game disguised as Scroll Lock, and strafe is bound to that.

The icons are made from the game's own art by a Python script: Duke's face from the game's tiles, on the red sky of the cover picture, at 64×50 and 25×20. The sunglasses survive even at the small size.

The launcher is the same recipe as in the earlier posts: a tiny app for the Desk that starts duke9500.exe and exits. It also checks for a duke3d*.grp the same way the game does, and tells you where to copy it if there isn't one.


About the pictures

These aren't photos or captures from the phone. As for the Doom and Quake posts, the real game code was compiled for the PC with a stub in place of the phone layer, and a script walked it around the level with god mode and all weapons. The frames are the 320×200 buffer the phone would get, doubled across and reduced to RGB565 like the 9500's screen. The PC build uses plain C for the multiplies instead of the ARM assembly.

What's missing

  • A frame rate. I haven't measured one, so there's no chart this time. It plays fast, faster than I'd expected, and Build's integer maths must be a big part of that. I have only played part of the first episode so far, too.
  • Multiplayer.
  • Screen shape. 320×200 doubled is wider than 4:3, and a wider field of view is still an open idea.

The controls: arrows or the rocker move and turn, the rocker's middle or Ctrl fires (and it's Enter in menus), Space opens, Shift runs, Alt strafes while held and Chr toggles strafing. A jumps, Z crouches, 1 to 9 and 0 pick weapons, Tab shows the map, Enter uses the selected item and [ ] choose one, Esc opens the menu. Chr with Q W E R T Y U I A S D F gives F1 to F12.

Install the SIS, then copy duke3d.grp from the shareware or the full game to \Duke3D\ on the memory card, or on C: or D:. The game uses the first of those folders that exists. Saves, duke3d.cfg and dukelog.txt land next to it.

Thanks to Ken Silverman for Build, to 3D Realms for the game and for releasing its source, to James R. Dose for the audio library this port learned from, to Fabien Sanglard for Chocolate Duke3D and to the Rockbox people for their port.

Duke's box asked for a 486 at 66 MHz and 16 MB of memory. The 9500 has an ARM at 150 MHz in a coat pocket, plays the AdLib soundtrack in software while it's at it, and has a keyboard for the cheat codes. Come get some.

I'll put the game pack here when it's finished.

Ei kommentteja: