With a 256-color graphical console in place, Rainbow-OS was finally ready for
the obligatory rite of passage for any hobby OS: a game. The new
asteroids shell command launches a full vector-style Asteroids clone
— rotate, thrust, fire, split rocks, score, lives, game over — running
at 800×600. Getting there meant first giving the kernel two things it had
quietly lived without.

The kernel had no sense of time
Until now Rainbow-OS had no timer at all. The Programmable Interval Timer
(IRQ0) was masked, and there was no tick counter anywhere — the system only
ever woke up for keystrokes. A real-time game needs a steady heartbeat, so the
first addition is a small PIT driver that programs channel 0 to fire 100 times a
second:
outb(0x43, 0x36); /* channel 0, mode 3 */
outb(0x40, divisor & 0xFF); /* 1193182 / 100 Hz */
outb(0x40, divisor >> 8);
register_interrupt_handler(32, timer_handler); /* ISR 32 = IRQ0 */
pic_irq_unmask(0);
__asm__ volatile("sti");
The handler just increments a counter; timer_ticks() exposes it.
This is also the first time interrupts are enabled globally — previously the
kernel only flipped them on briefly inside the keyboard’s blocking read.
The keyboard only knew about presses
The PS/2 driver is interrupt-driven with a ring buffer, but it only ever
queued key-press events — perfect for a shell, useless for a game
where you hold a key to keep turning. So the IRQ handler now also tracks a
held-key state table, set on make codes and cleared on break
codes, exposed as keyboard_is_down(scancode) (plus an
_ext_ variant for the arrow keys). The existing ring buffer is
untouched, so the shell and editor behave exactly as before.
Drawing without a floating-point unit
The target is a 486 with no guaranteed FPU and no SSE, so all the game’s math
is fixed-point integer. Rotation uses a precomputed 256-entry
sine table scaled by 16384; cosine is just the table read 64 entries ahead.
Rotating a vector is two multiplies and a shift:
nx = (x * COS(angle) - y * SIN(angle)) >> 14;
ny = (x * SIN(angle) + y * COS(angle)) >> 14;
Positions and velocities carry an 8-bit fraction, the ship accelerates along
its heading with a little drag, and everything wraps around the screen edges.
Rendering: a back buffer and one blit
Each frame is drawn into a 480 KB RAM back buffer — plain array
writes, no slow bank-switching — and then pushed to video memory in a single
pass with a new svga_blit() helper. The back buffer lives in
.bss, so it costs nothing in the kernel image on disk. Asteroids are
jagged polygons drawn with a Bresenham line routine, bullets are little squares,
and the score/lives HUD is drawn with the same VGA ROM font the console uses.
The loop
The game advances one frame every three timer ticks (about 33 FPS),
hlt-ing between frames to stay idle. Each frame it polls the held-key
state for rotate / thrust / fire, steps the physics, checks collisions
(integer distance-squared against radius-squared), splits any rock a bullet hits
(large → two medium → two small), and respawns a fresh wave when the
field is cleared. Ram a rock and you lose a life; run out and you get a GAME OVER
screen. Quit with Q and you drop straight back to a clean shell
prompt — with your final score printed for good measure.
It is a small thing, but a satisfying one: a 486, a custom bootloader, a
from-scratch kernel, and now a playable game rendered pixel by pixel.
