GERLA.CC

behind the scenes · FIDATI.

Bugs and pieces of code

Bringing the same game to the Commodore 64 and the Sega Mega Drive means arguing with two very different pieces of hardware, and each one bites in its own way. No theory here: real problems met during development, and the small code fragment that solved each one. Every snippet is told in plain words.

Commodore 64 · SID audio chip

The phantom crackle

the background music crackles, a pop on every note

The SID gives each voice an envelope: when you "press the key" (the gate), the volume rises from zero. The obvious way to play a melody is to re-press the key on every note. But every re-attack restarts the volume from zero at once, and that abrupt jump in the wave is heard as a click. Over a continuous melody it becomes a burst of pops: the crackle. The fix is playing legato, like a violinist changing note without lifting the bow: the gate stays on and only the frequency changes. The key is re-pressed only after a real rest.

; melodia (voce 3): la nota nuova entra senza staccare l'arco lda tmp sta $d40e ; frequenza, byte basso lda tmp+1 sta $d40f ; frequenza, byte alto lda m1g ; il gate e' gia' acceso? bne mtb ; si': solo frequenza, niente pop lda #$59 ; no: attacco lento (56 ms), decay dolce sta $d413
game2.asm · music engine

moral: the bug wasn't in the notes but in the attacks. On 1982 hardware, the silence between two notes is a design choice, not a detail.

Commodore 64 · 6502 processor

The register that erases the clues

the blinking platforms are always solid, even when off

The 6502 has three registers to its name (A, X, Y), and every instruction that touches them also updates the flags, the clues that conditional branches rely on. Here the code reads a blinking platform's state from the grid, then restores the X and Y it had set aside. Except that restore overwrites the flags: the branch right after no longer decided on the value just read, but on the last register restored.

tay lda BS,y ; stato del blocco: 1 = acceso ldy tmp2 ; ripristino Y... ldx tmp3 ; ...e X, che pero' RISCRIVE i flag! ; bne hyes ; il baco: decideva sul valore di X cmp #1 ; il fix: ri-confronta A prima di saltare beq hyes ; solido solo se davvero acceso
game2.asm · timed platform collisions

moral: in assembly there are no local variables: every restore is an instruction like any other, with side effects like any other.

Commodore 64 · 64 KB for everything

The memory that overflows in silence

with the second world added, the game freezes at boot with no error

On the C64, code, graphics, levels and music share the same 64 KB, each block at its own fixed address. When the level data grew (six screens instead of three), it spilled into the character set. And the assembler said nothing: to it, bytes are just bytes. The game asked "how many blinking platforms are there?" and found a piece of font: 108. The real fix was giving each block its own area; the lesson was putting guards in the source: if a block grows past its boundary, the build fails with a clear message, today and every time.

; confini di memoria: se si sfora, la build FALLISCE (mai piu' in silenzio) .cerror * > $2000, "CODICE oltre $2000: invade il charset" .cerror * > INPUT, "LDATA oltre l'area input script" .cerror * > $4400, "TESTI oltre $4400: invadono ldata"
game2.asm · memory map

moral: the worst bug isn't the one that crashes: it's the one that corrupts quietly and far away. The defense is making the build explode, not the game.

Mega Drive · two processors

The purple screen

the ROM with the new audio won't start: solid purple screen

The Mega Drive has two brains: the 68000 (the game) and a Z80 (usually, the audio). Here the audio is driven directly by the 68000, so the Z80 must be asked for the bus: "stop, I'm taking over". There is, however, a rule written in small print: a Z80 held in reset never grants the bus. The code waited for the grant before releasing the reset: an infinite wait, before the screen was even on. The purple was the background color of a game that never started.

static void z80_stop(void) { uint16_t n = 16384; /* prima si toglie il RESET: uno Z80 in reset non concede mai il bus */ Z80_BUSREQ = 0x100; /* bus al 68000, per sempre */ Z80_RESET = 0x100; /* PRIMA di aspettare! */ while ((Z80_BUSREQ & 0x100) && --n) {} }
audio.c · audio init

moral: and since that day every hardware wait has a counter: worst case the game starts without audio, never a purple screen.

Mega Drive · tile graphics

The stripe that wouldn't crumble

the fake floor crumbles, but a stripe of the block stays on screen

On the Mega Drive the screen is a mosaic of 8×8 tiles, and each game block takes four of them. The put16 function draws a block by writing four consecutive tiles: number base, base+1, base+2, base+3. To erase, instinct says "draw block zero". But put16(0) writes tiles 0, 1, 2 and 3, and tiles 1 to 3 are pieces of the first block. Hence the stripe.

static void put16(/* ... */ uint16_t base, uint16_t pal) { plane_cell(PLANE_A, col, row, (pal<<13)|(base)); plane_cell(PLANE_A, col+1, row, (pal<<13)|(base+1)); /* ...con base=0 scrive le tile 1-3: pezzi di blocco! */ } static void clear16(uint16_t col, uint16_t row) /* il fix */ { plane_cell(PLANE_A, col, row, 0); /* quattro zeri veri: vuoto, non "blocco numero zero" */
main.c · level drawing

moral: "erase" and "draw nothing" look like the same thing. On a tile-based console, they are not.

Mega Drive · method

Testing a console without the console

how do you verify audio code if you can't see inside the chip?

The trick holding the whole port together: the exact same C source is compiled twice. Once for the Mega Drive's 68000, where writes go to the chips' real registers; once for the development machine, where the same writes end up in a log. On the log, a test program checks everything: that the bass plays legato, that effects last the right number of frames, that channels shut off. If the tests pass, the ROM ships that same code, not a copy.

#ifdef AUDIO_TEST /* collaudo su PC */ #define YMW(part,reg,val) test_ym(part,reg,val) /* -> log */ #else /* ROM vera */ #define YM_A0 (*(volatile uint8_t*)0xA04000) /* -> chip */ #endif
audio.c · dual target

moral: the same idea holds for the game physics: the Mega Drive logic is verified frame by frame, bit by bit, against the original. The ear judges the timbre; math judges everything else.

← Back to FIDATI. The devlog