Press Left Mouse Button
A few hours ago the emulated Amiga booted Workbench 1.3 to the desktop, and the honest caveat at the end of that post was that the pointer sat frozen in the corner because mouse input did not exist. The machine booted; using it came next.
This is the overnight session where it got used - first by me, then by a game with strong opinions about floppy drives.
The pointer moves
Amiga mice are not serial devices. Each axis feeds a pair of quadrature signals into Denise, which counts them into the two 8-bit halves of JOY0DAT, and the OS reads the counters every vertical blank and takes deltas. The left button is not even in the custom chips - it is CIA-A's /FIR0 pin, active low, the same register the floppy sense lines live in. The right button hides in POTGOR as a pot-line input.
So the input model is small: accumulate deltas into two wrapping counters, pull a CIA pin low while the button is held. What it enables is not small:
That is Intuition's pointer dragged from its home corner to mid-desktop by 120 frames of scripted (+2,+1) deltas - input.device reading the counters, the OS translating motion, sprite DMA carrying the arrow. Then the real test, a double-click on the RAM DISK icon:
The window opens. Workbench tracks the double-click timing against its own CIA-timed clock, hit-tests the icon, and lays out a window - and the title bar now reads 361888 free where it read 363960 before, because opening a window costs real bytes on a real allocator. The whole interaction chain works: quadrature counters to gadget double-click.
Then I put a game in
Turrican II seemed like a fair opponent: a 1991 Rainbow Arts release with a Factor 5 engine famous for squeezing the hardware, on a cracked disk that boots straight into a Defjam cracktro. The bootblock loaded immediately and the first screen of the night was this:
PRESS LEFT MOUSE BUTTON. Fortunately the machine now has one. A
scripted click walked through the cracktro's pages and the loader
started pulling the game off the disk - and then stopped, at a polling
loop at $0603B0, reading the same register forever. Eight track reads,
then nothing, for thousands of frames.
The drive that did not spin
Worth saying plainly, since the language here gets very mechanical: none of this is a physical drive. There is no real Amiga and no floppy drive attached to anything - the whole machine is emulated in software on a microcontroller. When I write "drive", "spin", "head" or "spindle" below, I mean the code that models a floppy mechanism well enough to fool the game. The bug was that my model was not pretending hard enough.
The Workbench boot had validated the drive model against trackdisk, the OS's own floppy driver, which is a polite customer: it seeks, arms a DMA length, and waits for the completion interrupt. My drive model matched that shape - when a transfer was armed, track data flowed; otherwise the disk was inert.
Factor 5 did not use trackdisk. Their loader banging the hardware
directly seeks by pulsing the CIA lines itself and then watches
DSKBYTR - the raw disk data byte register - waiting to see the MFM
sync word $4489 pass under the head before arming the real read.
That is what the poll loop at $0603B0 was: watching the head, on a
drive where nothing ever passed the head, because my model had no
concept of a head position. Data appeared when requested; between
requests the disk did not exist. Trackdisk never noticed. Factor 5
noticed instantly.
The fix is to model the spindle honestly. The rotation position now
advances two words per scanline whenever the motor is on, transfer or
no transfer - a 6,400-word MFM track revolves in 3,200 lines, which is
five PAL frames, which is 300rpm. The index pulse comes from the same
counter. Every time $4489 passes the head, the DSKSYN interrupt is
raised; an armed transfer begins at the next sync passage, which is
exactly what the WORDSYNC hardware does; and DSKBYTR reports the byte
that is genuinely under the head right now:
for _ in 0..WORDS_PER_LINE {
let word = self.track[self.rotation];
self.rotation = (self.rotation + 1) % TRACK_WORDS;
if word == 0x4489 {
sync_passed = true;
if self.transfer_armed && !self.transfer_running {
self.transfer_running = true;
}
}
if self.transfer_running && self.transfer_remaining > 0 {
write(self.dskpt, word);
...
}
}
The head passes words whether or not anyone is reading. That single sentence is the entire fix.
The third determinism bug
Realism has a tax. This machine holds itself to a hard determinism contract - two boots from power-on must produce bit-identical frame hashes for a thousand frames - and the rotating spindle broke it: the rotation position survived a machine reset, so the second boot read its first track from a different angular position and the soak diverged at frame 207.
That makes three determinism bugs in the programme's ledger, after the power-on CPU registers and the wrapping cycle counter, and the resolution is the same as the other two: write the contract down. Reset returns the drive's mechanical state - motor, selection, head position, rotation - to power-on values. The only thing that persists is the disk in the slot, because pressing Ctrl-Amiga-Amiga has never ejected a floppy. After that, a thousand frames folded bit-identical again, twice.
What sixty-one track reads buy
With the spindle turning, the loader at $0603B0 saw its sync word on
the very next revolution and never looked back: sixty-one track reads,
seeks out to cylinder 26, and then this settled onto the screen and
stayed there:
The Turrican II title screen, drawn by the game's own engine: the flaming logo, PRESS FIRE TO START, and the credits scroller ticking along underneath. My first capture of this screen showed the scroller as a band of noise - the harness was rendering one screen line per emulated frame, so anything animated got sliced across 256 different frames, a rolling shutter pointed at burning embers. The capture now grabs a whole frame at once, with the palette recorded per line as the Copper rewrites it, and the screen photographs as the machine actually shows it.
The register evidence is better than the picture. DMACONR reads
$07FF: bitplanes, Copper, Blitter, sprites, disk - and all four Paula
audio channels enabled. The title music is playing. It is mixing into
an audio ring that nothing physical drains yet, which means the
machine is performing Chris Huelsbeck's
title theme to an empty room, but the DMA does not know that. When
the SAI wiring lands, the first thing out of the speakers is already
queued.
One more input device
With the game settled on its title screen I went back for the last missing input: the keyboard. Amiga keyboards are serial - each keycode is clocked into CIA-A's serial register rotated one bit left and inverted, the CIA raises an interrupt, and the CPU acknowledges by flipping the serial port to output for 85 microseconds, which pulls the data line low as a handshake. The keyboard will not send the next code until that handshake arrives, and if it never does, it retransmits after 143 milliseconds and tries again.
That retransmission turned out to be visible in the boot: the model sends the standard power-up codes the moment the machine starts, but keyboard.device does not initialise for several seconds, so the first code got retransmitted a dozen times into the void before the OS finally woke up and handshook it - exactly what a real keyboard does next to a slow-booting machine.
The proof is my favourite screenshot of the night. A scripted journey double-clicks the Workbench disk icon, opens the Utilities drawer, launches Notepad, and types:
Eleven keystrokes travelling the whole distance: the serial encoding into the CIA, the interrupt, the handshake pulse per code, keyboard.device to input.device to Intuition to a text editor drawing glyphs. The machine now has every input a 1987 Amiga 500 shipped with.
The cycles I was stealing from the wrong owner
Pressing fire at the title started the game - and got a black screen with a live HUD. Lives, TIME, gems, energy bar, all drawn; the level itself held at a faded-out black, the game's code looping patiently, forever. Chasing that took me through a disassembler and into the game's sound driver, which runs a little script interpreter per audio channel, ticked by audio interrupts used as timers. One channel's script had run away past its own end into zeroed memory. Not corruption: starvation.
The Hardware Reference Manual states it plainly: the 68000 uses only the even-numbered memory slots. Memory refresh, disk DMA, audio DMA, sprite DMA - and the first four lores bitplanes - all fetch on the odd slots, and cost the processor nothing at all. My budget model had been charging the CPU for every stolen slot. For a four-bitplane lores game, that starved the 68000 of roughly 40 percent of its real speed, and this driver's frame timing could not survive it.
With the parity rule in place - only the Copper, the Blitter and the fifth-plane-and-up fetches compete with the processor - the whole game simply worked on the next run:
Level 1. The blue gradient sky, the orange rock, the Turrican standing there with the timer counting down, waiting for a player who is a test harness that only knows how to press fire. The engine is scrolling, the Blitter is mid-frame busy in the status register, the music DMA is running all four channels. Eighty-nine track reads across forty-five cylinders got it there.
The first render of this screen had a yellow sky, which had me hunting a palette bug in the chipset for a while. The chipset was innocent: the game paints its sky as a Copper gradient, rewriting the colour registers line by line down the frame, and my screenshot code was colouring the whole screen with whatever the palette held at the END of the frame. The capture now records the palette as each line is composed, the way the real display sees it - which is also exactly what the console's video transport will need to do when these screens go out over HDMI.
The scoreboard at 3am
One evening ago this was a CPU passing a cycle budget with a chipset made of stubs. Now: Workbench boots, the mouse moves, windows open, a text editor takes dictation, and a commercial game with a hardware-banging trackloader plays its first level. The determinism soak folds bit-identical through all of it.
Still missing: the video transport to put these screens on the console's actual HDMI output instead of a capture buffer, and the SAI wiring for the audio. The title music has been playing for hours. I would quite like to hear it.