CHAPTER 5 · TIME
THE HEARTBEAT
The same boxed copy of Colonization had to behave identically on a 16 MHz 386SX and a Pentium several times faster — with no operating system worth the name to referee. Its answer is a cooperative micro-kernel hanging off the reprogrammed system timer: an interrupt at 608.8 Hz, measured live, divided down by 2, by 5, by 3 — and at the bottom of the cascade, the untouched 1981 BIOS, kept on very nearly its ancient 18.2 Hz schedule by arithmetic.
The metronome in every PC
A real-mode PC has exactly one general-purpose timepiece: the 8253/8254 Programmable Interval Timer. Channel 0 counts down from a 16-bit reload value at 1,193,182 Hz — a clock rate the machine inherited in 1981 and never shook off — and every time the count expires it raises IRQ 0, reloads, and starts again. At power-up the BIOS leaves the reload at its maximum, 65,536 counts, which works out to 18.2065 Hz: one interrupt roughly every 55 ms. The firmware is built around that number. The BIOS's INT 08h handler advances the time-of-day count in the BIOS Data Area, runs the floppy-motor shut-off countdown, and IRETs, 18.2 times a second, forever.
But the divisor is three port writes away from being anything you like. Both games take the offer. Here is Colonization doing it, captured by the PIT model inside our emulator during an instrumented boot — the only channel-0 reload in the entire run, fired about 300 ms in:
07:58:27.85 DEBUG mddosem_hw::timer: PIT: mode set channel=0 mode=3 access=3 bcd=false 07:58:27.85 DEBUG mddosem_hw::timer: PIT: counter loaded channel=0 reload=07A8 mode=3
Reload 07A8 is 1,960, and 1,193,182 ÷ 1,960 = 608.8 Hz — the DOS tick retuned thirty-three-fold, in mode 3, once, at startup, and never touched again through title, load dialog and open gameplay. Civilization, three years earlier, plays the same trick to a different beat: it loads channel 0 with 4280 = 17,024 — 70.09 Hz, the VGA vertical refresh rate — with a proper mode command before each load, twenty times across its boot-and-intro trace. Civ's system tick is its frame clock.
One interrupt, four jobs
What runs 608.8 times a second? Not the game — the game's scheduler. The
handler Colonization installs at 1169:0010 is a cooperative
micro-kernel in perhaps a hundred bytes, and we disassembled it over IPC while it ran.
Every interrupt, it switches to a private ISR stack — so whatever stack the interrupted
code happened to be on cannot overflow — and increments a 32-bit tick counter at
2297:82E8. Then the dividing starts. On even ticks it advances
the engine clock and dispatches worker A: a far call through a pointer held in
RAM (1796:0065 in our capture), gated by a byte flag at
2297:92A4. Swap the pointer and the timer runs your service
three hundred-odd times a second — the slot cinematics use to install per-tick
services; in our capture it sat empty (a stale one of these RAM
vectors stars in one of Chapter 7's war stories). Every tenth tick, a countdown byte
fires worker B, still on the private stack. And every thirtieth tick comes the
elegant part: instead of calling anything, the handler executes
jmp far cs:[0Ch] — a tail-jump through the saved BIOS vector, kept at offset
0C of its own code segment, straight into the BIOS handler at
F000:FEA5.
A tail-jump, not a call: the original interrupt frame is still on the stack, so the
BIOS services its Data Area — the clock count, the floppy-motor countdown — sends its own
end-of-interrupt and IRETs on the game's behalf, none the wiser. On the twenty-nine ticks
in thirty that it keeps, the game's handler acknowledges the interrupt controller itself.
The BIOS never learns the timer was retuned; it simply gets called at very nearly the rate
it was built for, reconstructed by division. The hook is installed exactly once — a single
INT 21h AH=25h call in the whole run — and never unhooked, even though the
segment it occupies sits inside the image region Chapter 1's pager recycles.
Why build this? Because a 1994 DOS game needs services at several rates — a fine beat for animation and sound pacing, a coarser one for game logic, the BIOS's own for compatibility — and DOS offers exactly one timer and no scheduler. The cascade delivers all of them from a single interrupt, with priorities implicit in the division order, for the cost of a few countdown bytes. It is the smallest possible operating system: one input, a dispatch table you can rewire at runtime, and a strict contract with the firmware underneath.
The hook, observed live
None of the above requires trusting a disassembly listing. The interrupt vector table is the bottom kilobyte of guest memory — four bytes per vector — and our emulator's IPC debug channel can pause the guest and read it while the game runs. Here is the timer vector, watched across one Colonization session and one Civ session:
| What we read | Raw bytes | Decoded | Meaning |
|---|---|---|---|
| Colonization · IVT[8], during load (frame 33) | A5 FE 00 F0 | F000:FEA5 | still the BIOS default handler — no hook yet |
| Colonization · IVT[8], at the title (frame ≈601) | 10 00 69 11 | 1169:0010 | the game's own INT 08h handler, installed during init |
| Colonization · IVT[8], in-game (frame ≈1568) | 10 00 69 11 | 1169:0010 | still installed during play |
| Colonization · IVT[1C] | 70 01 00 F0 | F000:0170 | the polite user-tick hook, untouched |
| Colonization · BDA 0040:0040 | 00 | 0 | not in a motor-wait spin — the healthy state |
| Civilization · IVT[8], at the title (frame ≈1514) | 99 01 52 07 | 0752:0199 | Civ's own INT 08h handler |
(The decode is little-endian offset:segment — A5 FE 00 F0 reads back as
F000:FEA5.) The polite way to borrow the timer had existed since
the original PC: the BIOS's INT 08h handler calls out through
INT 1Ch, a user hook that is a do-nothing stub by default, put there
precisely so programs could ride the tick without touching the hardware vector. Both
games ignore it and seize INT 08h itself. They differ in manners after that:
Civilization's handler chains on to the BIOS every interrupt, so the BIOS Data Area keeps
advancing at Civ's chosen rate; Colonization rations the BIOS to every thirtieth beat,
reconstructing the old clock by arithmetic. Same vector, two philosophies — and both
depend utterly on the BIOS side staying alive, which is the next section's subject.
The cheapest timer in DOS
Byte 0040:0040 of the BIOS Data Area is the floppy-motor shut-off counter. The BIOS INT 08h handler decrements it once per tick, and when it reaches zero, switches the drive motor off — a courtesy so the floppy doesn't spin forever after an access. Both games noticed something lovely: that is a free hardware countdown timer, armed by a single byte write. Store 1 there, spin until it reads 0, and the BIOS itself has just timed you a delay of up to one tick — ≈55 ms — for zero bytes of timer code. No motor is running; nobody cares; the alarm still fires. We found the identical instruction pattern in both binaries by live disassembly — Civ's intro delay at 4195:06C1–06CE, and Colonization's post-audience spin at 145D:0032:
; Civ intro delay — live disassembly at 4195:06C1;
; Colonization's twin spin sits at 145D:0032
mov al, 1
mov es:[0440h], al ; ES = 0000 → BDA 0040:0040, the motor shut-off count
spin: cmp es:[0440h], al ; has the BIOS INT 08h handler decremented it yet?
je spin ; still 1 — keep waiting
Calibration: measure the machine, once
Neither game ever asks what CPU it is running on — there was no reliable way to ask. Instead, both measure. Civilization runs a startup loop at 0652:030E–0314 that counts PIT ticks across sixteen VGA retrace periods, polling the status port at 3DA: two independent crystal-driven references — the timer and the display — measured against each other, with the CPU's own speed cancelling out of the ratio. Colonization's calibration lives in overlay code and reads PIT channel 0 directly, via the latch command, to time a startup busy-loop. The engine is equally unsentimental about where results go: computed parameters get written straight into the instruction stream. The FAB decompressor of Chapters 2 and 3 ships with zeroed shift/mask fields at CS:045E–0464 in the on-disk EXE; at runtime the engine patches the operative values into its own code segment — self-modifying code as a configuration store.
Calibration loops carry hidden assumptions, and emulation finds them. Colonization's
startup measurement feeds an IDIV that stays in range only if the measured
divisor reaches 87,580; on an early build of our emulator crawling at 3.6 million
instructions per second it measured 4,033, the quotient overflowed, and Microsoft C
announced "run-time error R6003: integer divide by 0" — a misdiagnosis of its own
overflow, and a crash class we went on to meet in over forty DOS-era programs. Civ's
sixteen-retrace loop caught us too: our VGA held the vertical-sync status bit for 8% of
the frame where real hardware holds it for two scanlines in 449 — 0.45% — and Civ's
measurement came back massively inflated.
The deepest lesson was economic. Civ re-times the tick to 70 Hz and then
busy-waits on tick edges throughout its intro; on real hardware that costs nothing but
electricity. Our emulator initially ran every guest at a fixed "Pentium" budget of 25,200
cycles per millisecond — which made each 70 Hz wait roughly twenty times the emulated
work it would be on the 386-class machines the loop was sized for, and the intro crawled
at 3.4 fps, 6% of real time. The A/B that proved it: at --speed 386 the
same intro runs at 70.12 fps. The durable fix (bug MDD-00037) was DOSBox-X-style
adaptive cycles — a 3,000 cycles/ms seed plus ramp — which took it to 64–70 fps; and
"turbo" mode, which decouples CPU cycles from the PIT entirely, hangs these
intros outright, because a spin counting tick edges never sees one. That is this chapter
in one sentence: the games' time comes from crystals — the PIT's 1,193,182 Hz, the
VGA's refresh — never from counting CPU. Calibrate the CPU against the crystal and one
binary is correct on every machine of 1994; emulate the crystal side subtly wrong, and
the software does not slow down politely. It breaks.
Sources for this chapter — byte-level & live disassembly: the Colonization ISR at 1169:0010, the 0040:0040 spin sites and the FAB self-modifying constants, via mddosem's IPC debugger (bug ledger MDD-00036, MDD-00037); runtime: the 2026-07-18 instrumented boot (PIT reload capture, mid-run IVT/BDA reads) under mddosem v0.148.0; oracle: DOSBox-X source (INT8_Handler) for BIOS tick semantics — full credits in How We Know.