Rainbow-OS: An Asteroids Game (and the Timer It Needed)

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.

Asteroids frame loop paced by the PIT timer
Asteroids frame loop paced by the PIT timer

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.

Nach oben scrollen