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.
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.
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.
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.
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.
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.
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