tutorial · game boy advance
It compiles, it runs, it doesn't work
When you write for a bare machine, with no operating system and no standard library, "it compiles and runs" doesn't mean "it works". It only means nobody complained. These are five real faults met while writing VECTOR for the Game Boy Advance: none of the five produced an error, and only for some of them was there anyone who could have warned me, had I asked.
One variable in three never changed
For a while the game was mute. The sound code was right, the registers were the right ones. The fault was in the linker script: the .data section was missing, the one for variables that start from a value.
Why it bites hard on this machine in particular: the cartridge sits at 0x08000000 and is read-only; writable memory is elsewhere. An initialized variable has to live in two places: the starting value inside the cartridge, the real variable in RAM. At boot, someone copies the first into the second. If the section isn't there, that someone copies nothing and the variable lives in ROM: it reads perfectly, and writes fall into the void without a murmur.
The fix lives in the linker script: you declare the section with two different addresses, the one where the variable lives and the one its value comes from, and you copy at boot.
moral: the linker never complained. It put everything in ROM and the program ran. A fault that doesn't stop the boot costs more than one that does, because it sends you looking in the wrong place for days.
The branch written twice
The closing screen never appeared. The code was this, and it's the most insidious fault I met in this project:
It's perfectly valid C. The first branch catches the condition and does nothing; the second, the one holding the work, is never executed. No error, and with the usual options not even a warning.
Here, though, there is someone who warns, and it just has to be switched on. GCC has an option made exactly for this: it looks at if / else if chains and reports when the same condition appears twice, because the second branch is dead code by construction.
There was a second fault layered on top, and it's worth telling because it's typical of screen transitions. The brightness registers were written only by the game branch. Moving from one room to the next happens inside a white flash, which is the old trick for hiding a load: leaving the game right there, the registers stayed frozen on their last value. White screen forever. Now, outside the game, they're reset once per frame.
moral: when the machine's state is written by a single branch of the program, that branch becomes mandatory. And sooner or later you'll leave through another door.
Two things in the same place
Sprites take their art from a store of tiles, and each sprite declares which slot it starts at. Those slots were sums of lengths written by hand: the character takes this much, then the cube starts there, then the beam starts there.
A wrong sum raises no error: it gives you two things in the same place. On screen you see one of them showing pieces of the other. It happened with the laser beam, which growing from two frames to four ended up inside the platform's tiles.
moral: any number that depends on every number written above it is a time bomb. The defence isn't being careful: it's letting whoever prepares the data compute the positions, and having it stop with the names of the two blocks treading on each other.
The ivy that grew in the clean rooms
Every room has a table of plants: fourteen slots, and the rooms that have none leave them empty. But "empty" isn't a data type, it's an agreement, and the two sides of the agreement disagreed. Whoever wrote the table marked an empty slot with -1. Whoever read it skipped slots below -100.
Taken on its own, each of the two is reasonable. Put together they mean that -1 isn't empty: it's a good value. So every tidy room, the ones in the first half of the game where the concrete is clean, quietly drew itself fourteen creepers, all in the same spot, stacked against the left wall.
The compiler can do nothing here, and that's why this fault is on the list. There's no type error: -1 is as valid a short as any other, and < -100 is a legitimate comparison. The constraint that was broken doesn't exist in the language, it exists only in the head of whoever wrote the two lines, and several months passed between them.
The cure isn't remembering better: it's taking the agreement out of the middle. Either the agreed value is decided in one place only, and both sides use that, or there is no agreed value at all and the table carries how many slots are filled, which is a number and not a convention.
But the part that cost me most time isn't the fault: it's that you couldn't see it. In a game whose second half is an abandoned facility overrun with ivy, a bit of extra ivy doesn't shout that something is wrong. At a glance it even looked like it belonged. A fault that produces something plausible costs more than one producing something absurd, and it has to be hunted the same way as all the others: by reading the table in memory instead of looking at the wall.
moral: an agreed value is a contract nobody enforces. If the language can't check it, sooner or later the two sides drift apart, and you only notice if the result is absurd enough to be noticed.
It runs out, and it runs out quietly
The rooms, once there were six of them, didn't fit any more. The ceiling on this machine isn't the size of video memory: it's that the map entry gives ten bits to the tile number. Ten bits are 1024 numbers, so a background can name at most 1024 different tiles, however much memory you have. The rooms wanted sixteen hundred.
The cause wasn't the amount of art: it was the grain. The wall blocks had a noise grain unique to each position, so every block produced four tiles of its own and none could be reused. The cure was making it repeat every four blocks: at sixty-four pixels of period the repetition has to be hunted for to be noticed, and meanwhile identical tiles merge into one. From 1600 to 900.
And the fault, once more, doesn't announce itself: if you overflow, the map entry hasn't got the bits for the number you're handing it, and the wrong tile shows up on screen. No warning, no halt, just a wall wearing a piece of something else.
The fault that does complain
For balance it's worth closing with the one that doesn't keep quiet. This machine's processor cannot divide: there is no instruction for it. When you write a / b with an arbitrary b, the compiler neither errs nor invents: it plants a call to a library routine. And since the standard library isn't here, the program stops linking, with a clean unresolved reference and the name of the missing routine.
That's why in VECTOR every cycle, every cadence and every ramp is a power of two or a counter that wraps: a bit shift the processor can do, a division it cannot. It's a constraint that feels like an inconvenience at first and after a while becomes a way of thinking.
moral: when something complains, be grateful. The faults that stop you immediately are the cheapest ones you'll ever get.