Näytetään tekstit, joissa on tunniste communicator. Näytä kaikki tekstit
Näytetään tekstit, joissa on tunniste communicator. Näytä kaikki tekstit

maanantai 9. marraskuuta 2026

CommDoom: the long fight for a fast Doom on the Nokia 9210 Communicator

Doom9500 took a day. I said so in that post, and I also said that it wasn't really a day: it was a day plus a month of earlier suffering on a different phone. This is the post about the suffering.

CommDoom is PrBoom 2.2.6 on the Nokia 9210 Communicator: a real 640×200 picture instead of a doubled 320×200, with sound effects, the screen melt, a launcher, and about 3.7 MB of RAM to do it all in. If you read the Doom9500 post, this is the port it calls Doomunicator. That was the working name until I noticed that it was too long for the Desk.

As with the others, 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. The difference is that this time neither of us knew anything yet. Almost every rule that made the later ports quick was found here, usually by getting it wrong first.

Where it starts

The 9210 already has a Doom. Hannu Viitala and Markus Mertama's CDoom showed in 2001 that it could be done, and WildPalm sold a commercial one. I wanted the fastest one that could be made, with sound, which CDoom never had.

Claude picked PrBoom 2.2.6 as the engine, because it's the last version before PrBoom's renderer was rewritten for truecolour, consoles and filters. It still has the classic 8-bit renderer, with years of optimisation in it. CDoom's platform code turned out to be plain SDL on top of Hannu's SDL port for EPOC, so the engine could be swapped underneath it. It sounded easy. These are the main files that had to be written anyway:

FileLinesWhat it does
epocsnd.cpp1241Sound thread, one kept-alive player per effect, the chaingun
r_drawasm.c232ARM assembly: wall columns, floor spans, fixed-point maths
winsrt.c220Compiler helpers for the emulator build, which has no C runtime
armrt.c17164-bit helpers that this SDK's egcc.lib lacks
o2.bat79Sneaks -O2 past a build system that doesn't allow it
Launcher\*600The app on the Desk

Ten builds to link

The first emulator build took ten attempts. A selection from the list:

  • ftd_conf.h not found. The SDK's own assert.h includes it. It belongs to a Nokia-internal "field trial diagnostics" build and was never shipped. That's very likely why CDoom doesn't use assert.h anywhere. PrBoom does, so it got an empty stub.
  • io.h not found. The emulator build is compiled with Visual C++ and defines _WIN32, but its C library is a POSIX-style EPOC one. PrBoom's Windows code had to be fenced off, file by file.
  • __alldiv, __allmul, __chkstk unresolved. There's no compiler runtime either, so 64-bit maths doesn't link. Claude wrote those helpers in naked assembly from memory, which is what winsrt.c is. Remember that one.
  • signal unresolved. This libc doesn't have it, and at -O0 GCC 2.9 keeps unused static functions, so even a handler nobody installs needs its whole body removed.
  • __divdi3 unresolved, on the ARM this time, followed by __ucmpdi2. This SDK's egcc.lib has none of libgcc's 64-bit helpers, so armrt.c has the whole family, and FixedDiv was rewritten with 32-bit operations only.

The crash that wasn't

It linked. It ran, and it was playable, and after about five seconds it closed itself and File Manager came up. No panic, no error, nothing in the log. It happened every time, at the same moment, whatever I did.

The five seconds are also the length of Doom's title screen, so the first suspect was the first 3D level. winsrt.c did have a bug: its _chkstk left big stack frames four bytes short. That was a real bug and a good fix, and the crash stayed. Then the 6 KB of clip arrays on the stack went, and the crash stayed. -nosound: stayed. A stack report said we had a full megabyte, and it was the wall clock that mattered, not the game's tics. Stage logging narrowed it to the event pump in the SDL driver, and the event log showed that every handler returned. So nothing was crashing. Then the timestamps on the log came in (the emulator's ticks, about a dozen per second):

71 event begin type=10
71   focus group now=10
72   our group id   =5
...
73 DirectUpdate: NOT focused, focus group=12
74   waiting for focus (1s sleep), focus group=12
89   waiting for focus (1s sleep), focus group=12
100   waiting for focus (1s sleep), focus group=12
111   waiting for focus (1s sleep), focus group=12
123   waiting for focus (1s sleep), focus group=12

The game was alive the whole time. Doom isn't a registered application, and about five seconds after launch the Series 80 shell (Shell0) took the foreground and started File Manager (Fileman). SDL's video driver then politely waited for focus to come back, one second at a time, forever. On the screen that looks exactly like a clean exit.

The fix is two lines in the window setup, plus staying in front afterwards:

EPOC_WsWindowGroup.SetOrdinalPosition(0, ECoeWinPriorityAlwaysAtFront);
EPOC_WsWindowGroup.EnableReceiptOfFocus(ETrue);

My bug report: "Wow it actually stays up!" That AlwaysAtFront line is still there in Doom9500, Quake9500 and Duke9500.

"No application installed"

Then the phone. It refused:

Sovellusta ei ole asennettu tiedostolle MMC\DOOMUNICATOR.EXE.
Tätä tiedostoa ei voi avata.

"The application isn't installed for this file. It can't be opened." My first 9210i couldn't start even the original CDoom, so that unit was faulty, and I got another. The new one started Hannu's CDoom.exe straight from File Manager, and it gave ours the same message as before. There was no log either, so main was never reached.

A comparison of the two EXEs answered it. Their headers were the same kind. The difference was in the imports: Hannu's has five DLLs, ours had two more, and one of them was MEDIACLIENTAUDIOSTREAM.DLL. That was the streaming audio library our SDL sound backend used, a backend I'd spent days on in the emulator, because the emulator has that DLL. The phone's ROM doesn't. And when an EXE can't load, File Manager reports it as "no application installed".

Claude made a variant without sound, with only Hannu's five imports. It's still called doomnosnd.exe today, even though it has had sound for a long time. I put it on the phone and reported in capitals: "BUT IT RUNS CLAUDE. It's a giant milestone already!"

3.7 megabytes

A freshly restarted 9210 has about 3.7 MB of free memory, and that's for everything including the program itself. Claude's estimate of what the emulator build needed was 7.5 to 9 MB. So before the first real test, the game went on a diet:

  • MAX_SCREENWIDTH 1600 went to 320 (later 640). A single visplane has arrays that size, so each one shrank from 6.4 KB to about 1.3 KB, and about 15 other arrays shrank with them.
  • The zone went from a fixed 6 MB to whatever free RAM allows, minus a reserve. The first log from the phone had a 1.7 MB zone, later about 2 MB.
  • The screen melt and the translucency map were switched off, the melt being two extra screens. The melt came back later.
  • The stack went from 500 KB to 64 KB, and the sound mixer's volume table from int to short.

The first log showed a whole session from title screen to E1M1 and a clean exit, with no zone errors and 1.1 MB left unused. All that worry, and then it had room to spare.

Speed, the first round

That log also gave the first number: about 9 fps in a level. The next builds added proper profiling, one line per second, and the renderer turned out to take about two thirds of each frame. This is a later example at 640×200, when the log had grown some details:

PROF 8.7 fps | ms/frame: total=105 logic=8 render=78 blit=16 other=1 | per frame: colpix=82112 cols=1098 spanpix=54358 spans=441

Here's what makes the 9210 special. Its SDK compiles everything at -O0, with the optimisation flag commented out in the toolchain's own scripts. The project file can't set compiler flags. So at first, every global variable in the innermost loop was read from memory again for every pixel, and everything that followed was about that:

  • Hand-tuned loops first, with everything in register locals, and then ARM assembly for the wall columns, floor spans and the blit. The blit went from about 19 ms to about 8.
  • Low detail, half the columns, which got E1M1 to 22–27 fps at 320×200 and later became automatic: it drops down when frames go over 120 ms and returns under 55.
  • Process priority. The SDL startup code ran the game at Low priority with the thread at MuchLess, which made it stall whenever anything else wanted to run. It's Foreground and Normal now.

Then I asked for the whole screen. Native 640×200 costs a lot, and it brought the numbers back down to 8–9 fps on high detail in a big hall and 15 or more on low. But it's a real 640×200 picture, and walls have detail you don't see in a doubled image. My report from the phone: "it's MUCH faster than Hannu's port, even with sounds!"

Sound with one voice

With the streaming library gone, the only thing proven on the phone was the plain player that Cinema uses: CMdaAudioPlayerUtility, which plays a file. It also accepts a WAV in memory, so each Doom effect got a 44-byte WAV header and a player of its own, in a separate thread that has an active scheduler. The first test:

"There IS sound, but with some 1-2-second lag."

The sound log explained why, and it told us something worse:

  • Creating a player takes 0.3 to 1.3 seconds. That was all of the lag.
  • The errors said -14, KErrInUse, every time a new sound started. The phone plays one sound at a time. A new one cuts off the old one.

So the design turned around. The sound thread keeps players alive, keyed by effect, and only creates each one once. The ten most common are created at startup, so the first shot doesn't lag. With one voice something has to lose, so each effect has a priority class: your own weapons beat your other sounds, doors and items, and those beat monsters. "Pistol audio now starts almost at the same time I press fire."

Then the fights. A player cut off by the system is unusable until it's rebuilt, and that's a second of media server CPU, in the middle of a battle. Stopping the old sound ourselves first keeps the player valid, so the rebuild storms ended there. The last sound experiment was music, through the player and Doom's MUS converted to MIDI. The 9210's media server has no MIDI codec: -5, KErrNotSupported. So the game has no music, and it would have had to share that one voice anyway. "Perfect, dropping the music was a good call," I said.

The key clicks are a sound too, and they compete for that one voice. There's no API in this SDK to turn them off, so I turned them off in the phone's profile settings.

"Muisti ei riitä"

"Not enough memory." I could play once. The second run failed, even right after a reboot. The Switcher showed about 870 KB free after quitting, and three to five minutes later all of it came back. As a sanity check I ran Hannu's CDoom and watched it: it gave all the RAM back at once.

Claude wrote SysDump, a little tool that lists every process, thread and memory chunk on the phone, so I could run it before and after. The answer was that the C library's first write to stdout or stderr opens a console for the program. And Doom printed things, as Doom does, right down to the ENDOOM screen on exit. That console then kept the whole process alive for minutes after the game had quit, with its memory. CDoom never touched stdout. On the phone we don't either now, and after that fix I wrote: "Bravo Claude, just bravo!"

The launcher, the 16 MB card and the bunny

A launcher came next: an app for the Desk that starts the game and exits, to give back its memory. The picture on it is mine, made with Doom's own font:

It also got an Args button for a reason. Options used to live in a doomargs.txt, but PC Suite converts .txt files on their way to the phone, and my 16 MB MMC is so old that card readers can't read it. Now the launcher writes the file itself, on the phone. That meant I could type -file psxsound.wad, and the PlayStation sound effects played. They're twice as long as Doom's, so anything over 1.85 seconds gets cut and faded to fit the 20 KB buffers, and every next sound cuts them off anyway. That itch is what Doom9500 later scratched properly.

Then some smaller things:

  • The detail key. It was L, which meant that the IDBEHOLDL and IDCLEV cheats couldn't be typed. The letters g, j, x and z aren't in any cheat, so it's Z now.
  • The bunny. I reached Inferno's ending, and the bunny scroll was "veery sluggish", at about a second per frame. It redrew two full-screen pictures through the general stretching code every frame. Now each picture is rendered once into a buffer, and each frame is just two memcpys per line. "Works perrfectly!"
  • The melt. It needs a transposed copy of two screens, and at 640×200 PrBoom's way of doing that would ask the zone for 128 KB of contiguous memory at the worst moment. Now it's written straight into two buffers that exist only during the melt. If there's no memory for them, you simply don't get a melt, instead of an error.


The melt across the whole 640 pixels. I wasn't giving this one up.

A package from WildPalm

Around this time I asked WildPalm if I could buy the source of their Doom 9210. Nigel dug it up from old hard drives and gave me a development snapshot for free, which was very decent of him.

Claude went through it. The snapshot is from a later Series 60 retarget, so the 9210 video code is only there as commented-out leftovers. Their sound API has the same shape as ours (load by effect id, replay by id, stop by id), which was reassuring: we'd arrived at the same place independently. But the bigger news came from my own comparison on the phone. WildPalm's port doubles 320 pixels to 640, and it runs at about the same speed as our real 640. I wrote: "Our CommDoom really is the best port there is." To be fair, we have twenty years of hindsight, no deadline, and I'm not trying to sell anything.

Then I asked if there was anything left to optimise.

The last drops

Claude had already told me that what's left was small. "Heck, let's try squeezing the last drops," I said, and we spent a whole day on it. The test spot was the start hall of E1M1 on high detail, with an A/B key at first, to swap the wall code while standing still:

StepRenderFrame rate
Where the day started109.7 ms6.7 fps
Wall loop rewritten: register locals, texture lookup inlined, calls the assembly directly101.3 ms7.1 fps
FixedMul and FixedDiv in assembly, everywhere95.0 ms7.6 fps
SlopeDiv in assembly, r_segs.c at -O288.9 ms7.9 fps
All six renderer files at -O283.3 ms8.3 fps

That's a quarter more frames, on a game that had already been through all the above. On low detail the same spot went from about 8.1 to about 11.2 fps, once r_draw.c joined in.

FixedMul is the satisfying one. At -O0 the C version is a real function call with 64-bit library helpers, and in assembly it's four instructions:

AsmFixedMul:
    smull   r2, r3, r0, r1
    mov     r0, r2, lsr #16
    orr     r0, r0, r3, lsl #16
    mov     pc, lr

Doom's demos are recordings of key presses, and they only stay in sync if every calculation gives the very same result. So these routines have to be exact to the last bit. The SDK's ARM compiler doesn't run on my Windows 10 host, so nothing could be run there. Instead, Claude simulated the assembly bit by bit in a PowerShell script and compared it with the C version: 6109 cases for FixedDiv and 8143 for SlopeDiv. That caught one difference, for INT_MIN, which now goes to the C version.

The -O2 part took some cunning. The project file can't set flags, and the toolchain's global -O2 had once glitched the walls. But a build leaves its object files in a known folder. So o2.bat recompiles chosen files with the makefile's own command line plus -O2 -fno-strict-aliasing and drops the objects over the old ones, and the next abld build sees that they're up to date and only links. We did it in groups, and I watched for broken walls. For the 19 game logic files the test suite was free: let a title demo play, and if the marine walks into a wall, something's wrong. It played correctly, and the old wall glitch never came back, so it must have come from a file we never touched this time.

The -O2 experiment also saved us a lot of work. The plan was to write the whole wall loop in assembly next. But compiling that file at -O2 only took the loop from 36.5 to 33.8 ms, which showed that most of the rest is waiting for memory: texture columns, tables and pixels 640 bytes apart. Hand-written assembly wouldn't have changed that, so we didn't write it.

Here's the whole project in one picture. Every point comes from a log the phone wrote.

The drop on Sep 21 isn't a step backwards: it's me asking for the whole screen. Blue is the average of a whole run at 320×200. Green and orange are one identical frame, the first view of E1M1, which the logs let us pick out by its exact pixel counts, so those two lines compare like with like. October 3 also starts below where Sep 21 ended. That evening's numbers rest on only a few seconds of log, and we never tracked down the difference.

The barrels are too fat

While I stood in that hall for the measurements, I finally looked at it. The barrels were about twice as wide as they should be. And when I walked up to the dead marine in front of the door, the corpse vanished at about two metres, every time.

Claude asked whether anything had changed in the sprites, and that made me a bit jumpy, but it wasn't that day's code at all. PrBoom's vertical scale assumes a 4:3 monitor. On a 640×200 screen with square pixels, the horizontal scale comes out twice the vertical one, so the whole world is twice as wide as it should be. Every doubled port on this screen looks like that, and after a while you stop noticing. The explanation got the reply: "That is sooooooo true!"

Top: wide. Bottom: true proportions, on J. Compare the little barrel at the far end of the corridor. The price is a much narrower view up and down, and the near barrel is already half off the screen.

The corpse was the same mismatch causing a real bug. PrBoom's sprite height clip tests whether a sprite is above or below the view, and it used the horizontal scale for that vertical test. On a 4:3 monitor those are the same number. Here, close objects on the floor were thrown away while still visible:

yscale = FixedDiv(projectiony, tz);
if (thing->z > viewz + FixedDiv(centeryfrac, yscale) ||
    gzt      < viewz - FixedDiv(centeryfrac-viewheight, yscale))

True proportions went on the J key, with wide as the default, because honestly: "The default proportion is wide, it looks awesome".

The chaingun

I'd complained about this one back on the first day of sound: "Chaingun doesn't sound like chaingun, but pistol." It went on the list and stayed there until the very end, when I recorded it from the emulator and sent the WAV to Claude.

Both drawn from my recordings, 1.4 seconds each. Top: the old way. Bottom: the new rattle, in the version with the final shot.

The top half shows what was wrong. The gun fires every 114 ms, and restarting a player takes about 80 ms before any sound comes out. So you get about 25 ms of each shot, and then the next shot stops it. The long shot at the end is simply the last one, the only one allowed to finish.

One voice can't overlap shots, but nothing stops it from playing a sound that already has overlapping shots in it. So at startup the sound thread mixes eight pistol shots exactly four game tics apart into one loop. The tails of the last shots wrap around to the start, which makes it seamless:

for (TInt s = 0; s < shots; s++)
    {
    TInt j = i - s * period;            // shot s starts at s*period; this is its sample under i ...
    if (j < 0)
        j += len;
    for ( ; j < n; j += len)            // ... and every later wrap of a shot longer than the loop
        sum += (TInt)pcm[j] - 128;
    }

The first shot starts the loop with SetRepeats, and later shots only say "still firing". There's no restart, so there's no gap. It rattled at last. It also carried on, in my words, "waaaaay too long, like 3 seconds" after I let go.

The stop was a timer in the sound thread, measured in the system's ticks. It should have stopped after a third of a second. We never found out why it didn't, since the next idea made the question go away: why should the sound thread guess? A held chaingun fires exactly every four tics, so the game itself knows. Once a fifth tic passes with no shot, the firing has stopped:

if (epoc_loop_on &&
    (gametic - epoc_loop_tic > 4 || gametic < epoc_loop_tic || I_GetTime_RealTime() - epoc_loop_real > 17) &&
    EpocSndStopLoop())
  epoc_loop_on = 0;

Now it stopped on time, but it stopped dead, usually in the middle of a shot. So Claude had the normal pistol shot play right after the stop, to ring out like the last shot does in real Doom. That's the lower half of the picture, and you can see the problem. Switching to another player leaves 140 to 160 ms of silence, longer than one beat of the gun. The volume was a good match and the decay was right, but the gun now skipped a beat at the end of every burst. "The hitch bothers. Let's go back to the hard stop." So we did, and a comment in the code says why. Everything has its price on one voice.

Out of curiosity, I asked how WildPalm handled the chaingun. They don't, at all. Each shot is just the pistol sound, like in the original game. So how does that sound fine? Their sound engine, WildSnd, ships separately, and besides its DLLs its package installs a plugin into the phone's media server itself, linked against the server's internal audio libraries. As far as I can tell from those imports, it does its own mixing inside the server, where the one-voice rule doesn't apply. WildPalm solved back then the problem we'd spent a month working around. The SDK does contain the headers for such plugins. Their documentation page says: "The document is not available yet."

About the pictures

The game pictures aren't from the phone. They come from the PC build of the Doom9500 fork made for that post: the same renderer, compiled for a PC, walked through id's demo by a script. I reduced the colours to the 9210's 4096. The waveforms are from my two recordings.

What's missing

  • Music. There's no MIDI codec on the phone, and only one voice to play it with.
  • Sounds at the same time. Fixing that for real means writing a media server plugin with no documentation.
  • The translucency map is still off, to save 64 KB and startup time.
  • Multiplayer. The netcode was left out, since as I told Claude, the 9210 uses HSCSD, which was decommissioned over a decade ago.
  • Final proof on the phone of the rattle's last version. I judged its sound in the emulator, where I could record it.

The controls are Doom's own: arrows move and turn, Ctrl fires, Space opens doors, Shift runs and Alt strafes. Z cycles the detail between High, Low and Auto, J toggles the proportions and = toggles the numbers overlay.

Install the SIS, then copy prboom.wad and your doom.wad, doom2.wad or the shareware doom1.wad to D:\Doom\ on the memory card. CommDoom is GPL like the code it's built on, so the source goes out with the SIS.

Thanks to id Software for Doom and for releasing its source, to the PrBoom team, to Hannu Viitala and Markus Mertama for CDoom and the SDL port it stands on, to Kenneth Guy, whose 9210 game showed what assembly can do on this CPU, and to Nigel at WildPalm for digging through old hard drives for me.

In October my 9210 started shutting itself off at random, and it missed one of my alarms, so it has retired. CommDoom doesn't run on the 9500 that replaced it: it starts and draws a few pixels at the top of the screen. So it's a monument to one phone, which I think is fine. Everything that was learned here went on to Doom9500, Quake9500 and Duke9500, and they all took days instead of a month. But this is the one where I walked up to a dead marine on a Nokia 9210, at 640 pixels wide, on 3.7 megabytes, and the marine stayed there.

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

sunnuntai 1. marraskuuta 2026

NES9500: a NES emulator for the Nokia 9500 Communicator, from scratch

The Nokia 9500 has been my main phone since the 9210 started shutting itself down at random. It has a 640×200 screen, a full keyboard and a 150 MHz ARM9 running Symbian OS 7.0s with Series 80 v2.0 on top. In other words, it's a pocket computer that badly needed a NES emulator.

There wasn't a good one, so I made one: NES9500. The code was written by Claude (Anthropic's AI). I built it on my Windows 2000 VM with the S80 v2.0 SDK, tested it on the phone, sent back logs and screenshots, and complained. A week later, it plays Super Mario Bros. 3, Blaster Master and Battletoads at full speed with sound, and the whole thing fits in a 42 KB SIS file.

This post is about how it works. It gets geeky.

A core from scratch

NES9500 isn't a port. It doesn't contain code from FCEUX, Nestopia, InfoNES or anything else; every line was written for this phone. The SDK's compiler is GCC 2.95, so everything is C89, with declarations at the top of each block like it's 1999. That's fitting, since it nearly is.

FileLinesWhat it does
cpu.c4126502 interpreter: one big switch over all 256 opcodes, unofficial ones included
ppu.c637Scanline renderer using loopy's scroll model, writing straight into the framebuffer, already scaled
apu.c539Two pulses, triangle, noise and DMC, non-linear mixing, 22050 Hz mono
nes.c569Memory map, iNES loader, mappers, frame loop, save states
nesmain.c1253ROM list, menus, settings, key config, frame pacing
epocsys.cpp971Native Symbian layer: framebuffer, keys, menus, files
epocaudio.cpp345Audio thread feeding CMdaAudioOutputStream

The native layer comes from my earlier Doom9500 and Quake9500 ports. It skips the Symbian UI framework entirely: it grabs the screen's raw memory address through the HAL (RGB565, 1280 bytes per line), puts its own window group in front so the system keeps quiet, and draws there directly. Menus are drawn with the system font straight into the framebuffer, so the emulator carries no font data at all.

Supported mappers: 0 (NROM), 1 (MMC1), 2 (UxROM), 3 (CNROM), 4 (MMC3), 7 (AxROM), 11 (Color Dreams), 66 (GxROM) and 71 (Camerica). That covers SMB 1–3, both Zeldas, Metroid, Mega Man 1–6, Castlevania 1–2, Contra, Kirby, Battletoads, Duck Tales and a few hundred others.


SMB3 uses MMC3, the mapper with a scanline counter that games use for split screens, like the status bar at the bottom here.

Squeezing 240 lines into 200

The NES draws 256×240, and a TV shows roughly lines 8–231. The 9500 has 200 rows, which doesn't fit. There are two options in the menu:

  • Fit all 224 (the default) drops every ninth line. You see the whole picture, slightly squashed. A precomputed table maps each NES line to a screen row or to −1, and lines that map to −1 aren't drawn at all.
  • Crop to 200 shows the middle 200 lines 1:1. Text is sharper, but status bars at the edges get cut.

Left: fit. Right: crop. Look at the WORLD 1 text in the status bar: crop keeps it sharp but cuts off the bottom of the bar.

Horizontally there are four sizes: 1× (256 px, the true shape when combined with "fit"), 1.5× (384, the default), 2× (512) and full width (640). Scaling happens while each line is written out, so there's no second pass.

The same frame at 1×, 1.5×, 2× and full width.

Whatever the picture doesn't cover gets a wallpaper: background.bmp in the NES folder, or <game>.bmp next to a ROM to give that game its own. The decoder handles uncompressed 1/4/8/24/32-bit BMPs, so whatever Paint saves works. A 5-pixel black border keeps the game picture from bleeding into the wallpaper.

My wallpaper: a living room with a CRT, a lamp and too many VHS tapes. At 1.5× the game covers the TV and the shelves frame it.

The speed hunt

The first build ran straight away and played Blaster Master with sound, which genuinely surprised me. It wasn't fast enough, though, and we went through three rounds of optimisation, each one driven by numbers from the phone's log file.

Round 1 wrote two pixels per 32-bit store, built a per-line sprite list only when OAM changes instead of scanning all 64 sprites on every line, and folded the NMI into the IRQ word so the CPU checks one variable per instruction instead of two. Result: 58–62 fps emulated, about 36 frames drawn per second.

Round 2 went after the CPU. Every opcode fetch now goes through a cached window onto the current ROM bank:

#define FETCH(a) ({ \
    const u32 fa_ = (a); \
    u32 fv_; \
    if (fa_ - pcwin < 0x2000) \
        fv_ = pcbase[fa_]; \
    else if (fa_ >= 0x8000) \
    { \
        pcwin = fa_ & 0xe000; \
        pcbase = cart.prgbank[(fa_ >> 13) & 3] - pcwin; \
        fv_ = pcbase[fa_]; \
    } \
    else \
        fv_ = nes_read(fa_); \
    fv_; })

The unsigned subtraction folds both bounds checks into one compare, and pcbase is pre-offset so the hit path is a single load. It's a GCC statement expression, which GCC 2.95 already supported. Round 2 also added a log line that turned out to be the most useful thing in the project:

Frame time: drawn 21-29 ms, skipped 0.2-4 ms

A frame where the picture is drawn costs about 25 ms. A frame where it's skipped costs a couple. So the 6502 is cheap; the time goes into drawing. The CPU work had been aimed at the wrong target all along.

Round 3 therefore rebuilt the drawing. The PPU no longer draws a line whenever the CPU crosses a scanline. It catches up lazily: nes_frame() only records how far the beam has got, and lines are drawn in a batch when something visible is about to change (a write to a PPU register, a mapper bank switch, OAM DMA) or at the end of the frame:

#define PPU_SYNC() do { if (ppu_done <= ppu_now && ppu_done < 240) ppu_sync(); } while (0)

Most games touch the PPU only during vblank, so most frames are drawn in one tight loop that doesn't fight the CPU emulation for the 9500's small cache. Each line is built in a cached buffer and then blasted to screen memory four words at a time, because the 9500's screen memory is slow to write and much prefers burst stores:

"1:\n\t"
"ldmia %1!, {r4, r5, r6, r7}\n\t"
"stmia %0!, {r4, r5, r6, r7}\n\t"
"subs %2, %2, #1\n\t"
"bne 1b\n\t"

Games always run at the correct 60 Hz (or 50 for PAL), and automatic frameskip decides how many frames are actually drawn. Blaster Master typically draws 30–45 frames per second and Battletoads 23–41, and it feels smooth. The emulator sleeps only when it's a whole 16 ms system tick ahead, since Symbian's timer can't do anything finer.

A Symbian bug found along the way

The input code polls a window server event request (iEventStatus) instead of blocking on it. It turns out that if you check the status without calling User::WaitForRequest, the completion signal stays on the thread's semaphore. They pile up, and every later wait in the program spins straight through them. NES9500 now consumes the signal properly.

Sound

The APU runs in step with the CPU and writes 22050 Hz mono samples into a ring buffer. A separate audio thread pulls from the ring into CMdaAudioOutputStream. The first test logged 1 underflow in 4617 chunks. The channels are mixed with the NES's non-linear lookup tables, and there's a DC filter and a low-pass, so it sounds like a NES and not like five square waves added together.

The volume has 8 steps that halve at the quiet end: 1, 2, 4, 8, 16, 32, 48 and 64 out of 64, roughly 6 dB apart. The original four steps bottomed out at 25%, and at 25% the 9500's speaker makes it clear to everyone on the bus that you're playing Mario.

PAL, NTSC and Finnish ROM dumps

I'm in Finland, so a lot of my carts are PAL. PAL runs 312 lines per frame, 106.5625 CPU cycles per line, and has its own APU period tables. NES9500 picks the region from the iNES header or from the file name: (E), (Europe), (Scandinavia) and so on mean PAL. You can override it in the menu.

Save states the lazy way

Everything the emulated machine knows lives in one struct, NesState, with no pointers in it: CPU, PPU, APU, mapper registers, RAM, SRAM and CHR RAM. A save state is a memcpy. After loading one, mapper_sync() rebuilds the bank pointers from the mapper registers. Chr+S saves, Chr+L loads, and a small square flashes in the corner (green for saved, blue for loaded, red for no state). Chr+F toggles fast forward.

Keys, turbo, and 16 sprites per line

The Communicator keyboard has plenty of keys. The defaults are WASD, the arrow keys or the rocker for the D-pad, Z/K/Ctrl/rocker press for A, X/L for B, Enter or Space for Start, and Tab or Shift for Select. I and O are turbo A and B, with fast, medium and slow speeds (30, 15 or 10 presses per second).

Every button can be remapped from the menu. The emulator keeps a 256-entry table from hardware scan code to pad bits, and turbo uses bits 0x100 and 0x200, above the eight real NES buttons, so they get masked off before the game sees them. Remapping waits for the key to be released before returning, so the key you just assigned doesn't fire into the game. The table is saved in nes9500.cfg as plain scancode=bits pairs.

A real NES can show 8 sprites per line, and games flicker sprites to get around that. NES9500 shows up to 16 by default, which removes most of the flicker. "Sprites per line: 8" restores the real behaviour for the few games that deliberately hide things behind the limit.

The feature that didn't make it

The 9500's screen is wide, and a NES nametable holds more picture than the 256 pixels you see. So we tried a "wide view" that drew 64 extra background pixels on each side from the offscreen part of the nametable.

It failed for a fundamental reason: games write each new column of scenery into the nametable just before it scrolls into view. The area beyond the visible edge isn't the upcoming level; it's leftovers. In Blaster Master the extra columns flickered and showed scenery from random places. After a couple of fixes that made it only somewhat less bad, I removed the whole thing.



A real screenshot from the phone: Blaster Master, taken while chasing the wide-view bug. The other images in this post come from the same emulator core running on a PC with the phone's 640×200 layout, because it's easier to get the timing right with scripted input.

The launcher and the icon

The emulator itself is a plain .exe. To get an icon on the Desk, there's a tiny launcher app. It draws a black screen, starts nes9500.exe from a CIdle callback right after startup, and exits. If the emulator can't be found, it stays open with an error and Start/Exit buttons.

Something worth knowing if you make S80 v2 apps: the AIF Builder offers 24×24, 32×32 and 48×48 icons, but the 9500 actually uses 64×50 and 25×20. I checked the AIF files of the built-in apps: 33 have a 25×20 icon and 24 have a 64×50 one. You can skip the AIF Builder entirely and put the icons on the AIF line in the .mmp:

AIF NES9500.aif . NES9500Aif.rss c16,1 appicon_64x50.bmp appicon_64x50_mask.bmp appicon_25x20.bmp appicon_25x20_mask.bmp

In the masks, black means icon and white means transparent. I drew the icon with green corner pixels to mark where it should be transparent, and a small Python script turns exactly that green into the mask. The black tile stays, with rounded corners.

What's missing

  • MMC2/MMC4 (Punch-Out!!, Fire Emblem)
  • MMC5 (Castlevania III)
  • The Konami VRC mappers and the Famicom Disk System
  • ARM assembly for the 6502 core, if I ever want more speed. Honestly, it's already fine.

Games go in D:\NES\ on the memory card (or C:\NES\). Battery saves and save states sit next to each ROM, and nes9500log.txt records what the emulator thinks of your phone, including the 6502 program counter if something crashes.

Not bad for a 2004 phone with a 150 MHz CPU. Next up: Punch-Out!!.



maanantai 26. lokakuuta 2009

My-Symbian.com-vetoomus / My-Symbian.com petition

ceroberts75 My-Symbian.com-foorumilla teki vetoomuksen sivuston puolesta Nokialle luettavaksi, eli kaikki jotka ovat sivuston puolesta, kannattaa kirjoittaa koska Nokian touhussa ei mielestäni ole mitään järkeä kohdella sivustoa kuin roskaa, kaiken sen jälkeen mitä ylläpitäjä on tehnyt Nokian ja Symbianin hyväksi. Kirjoitin itsekin vetoomuksen, mutta kommentistani taisi tulla liian pitkä, koska se ei näy allekirjoituksissa.


http://www.ipetitions.com/petition/my-symbian/index.html


ceroberts75 on the My-Symbian.com forum created a petition on behalf of My-Symbian.com to be read by Nokia, so everyone who is on the site's side, I ask you to sign the petition because there's really no sense in Nokia's way in which it treats the site and the administrator. Especially after what the site has done for Nokia and Symbian - promoted every Symbian device out there, but Nokia just ignores it. I signed the petition but my comment was probably to large so it doesn't show in the signatures. But here it is.


"This is truly the best Symbian site there is, and I really can't understand why Nokia treats the site like cr*p. Michal is always the last one who gets the review phones (or not at all), like the N900, which were given to 300 people at the Maemo Summit, but there's no unit left for him although he's been constantly asking Nokia for months if he could get one. 

My-Symbian has been on the net before Symbian was even born, and has ALWAYS promoted Nokia's products from the GEOS Communicator times, and has brought thousands of new customers for Nokia and has kept a thousand more as Nokia users. Somehow Nokia doesn't see it and leaves the site out of link lists (like in the case of Nokia Conversations), and the Maemo Marketing Manager doesn't even respond to his e-mails regarding a better N900 review phone than the one Michal had to obtain unofficially, with broken/ missing radio chip!?

Nokia acts very immaturely towards My-Symbian and Michal, and I'd like to hear a reason for this."



http://www.ipetitions.com/petition/my-symbian/index.html

sunnuntai 27. heinäkuuta 2008

Sihdissä Nokia 9210(i)/ Nokia 9210(i) in sight

Nyt on sen verran hulluksi mennyt tuo kännyköiden vaihtelu että pitää vähän vaihdella lisää. Nokia 9210 tai sen i-malli on siis hankinnassa, vaikka onkin älyttömän vanha puhelinmalli. Käytin sitä joskus nuoruusvuosinani, ja haluaisin vielä kerran saada sen henkilökohtaiseksi data-avustajalaitteekseni. Oli se sen verran hyvä kännykkämalli, kun sille tehtiin sen kultavuosina raivokkaasti erilaisia ohjelmia, mm. kaksi Doom-versiotakin (Hannu Viitalan ja Wildpalmin).

Pääsen myös jatkamaan puolitoista vuotta sitten stopanneita OPL-projektejakin. OPL-ohjelmointi on hauskaa, toisin kuin C++:n. Ai niin, C++:sta tulikin mieleen, että Hannu Viitalan sivuillahan on SDL-julkaisu Nokia 9210:lle, joka toimii (toisin kuin Nokia 9300:n ja 9500:n sekä 7710:n versiot). Tarjoa Nokia 9210:aasi minulle, jos sinulla on sellainen ja haluat päästä siitä eroon. Mailia vain osoitteeseen sambonenpoistatämäosa@gmail!!!PISTE!!!com


All this changing phone model too often has gone so crazy that I must practice it some more. I'm currently looking for Nokia 9210 or its i model although its a really old model. I used it in my younger years, and would like to get it to act as my personal data assistant once more. It was such a good phone model because in its golden years it got made all these wonderful applications, and even two Doom versions (Hannu Viitala's and Wildpalm's).

I can also continue my OPL projects which ended some year and a half ago. OPL programming is fun, unlike C++. Oh yes, C++ reminds me the fact that in Hannu Viitala's site there is the SDL release for Nokia 9210 which actually works (unlike Nokia 9300's and 9500's or 7710's versions).