No description
  • Zig 99.3%
  • Shell 0.4%
  • Nix 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 32c9ce50c2
All checks were successful
test / nix (push) Successful in 8m35s
test / test (push) Successful in 48m35s
test / docs (push) Successful in 8m1s
the README says cards go in slots now, and one thing it said was untrue
The paragraph that had to go was the densest sentence in the file: "Every slot
has a card in it now: 1 is the parallel printer card, 2 the serial card, 3 the
Videoterm or an UltraTerm, 4 the mouse or a Mockingboard, 5 the clock, 6 the
Disk II and 7 the block device." Every one of those is still where that kind of
card goes when nobody says otherwise, and none of them is where it has to go.

The sentence after it was simply wrong, and had been since the a2audit work:
"any address in $c100-$cfff that no fitted card answers comes from the internal
ROM whatever INTCXROM says". An empty slot floats -- there is a test called "an
empty slot floats rather than answering from the machine's own ROM" -- and this
paragraph had not caught up.

"Writing the machine down" is rewritten around `.slots`, since the config file
no longer has a field per card, and gains the schema and why the examples are
not validated against it here. The per-card bullets say "by default" where they
used to say a slot as though it were the only one. The layout table gains
`src/slots.zig` and `.ziggy-schema`.

CLAUDE.md gains the paragraph that matters for anyone working on this next: the
apparatus is in one file, it used to be in two, and that duplication is how a
Mockingboard came to work on a IIe and be silent on a II Plus. Do not put it
back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aieuyb2KR2DsKyZWPJpas6
2026-09-07 16:43:58 -05:00
.forgejo/workflows functional tests: what the corpus, one instruction at a time, cannot see 2026-09-06 09:52:31 -05:00
completions --slot N=CARD, and a IIe boots a disk from slot 5 2026-09-07 15:14:19 -05:00
corpus the disks are not the cost, and the WOZ runner had been running in Debug 2026-09-04 00:29:52 -05:00
examples a config file describes a card where it sits, and nowhere else 2026-09-07 15:30:45 -05:00
functional the other two of Dormann's tests, assembled with aigrette65 2026-09-06 14:02:01 -05:00
LICENSES put the generated files in the public domain 2026-09-01 17:35:52 -05:00
src a config file describes a card where it sits, and nowhere else 2026-09-07 15:30:45 -05:00
tools a .ziggy-schema, so an editor can check a config while it is being written 2026-09-07 16:31:04 -05:00
woz a II Plus's cards move into the seven slots, and two of a kind fit 2026-09-07 13:48:19 -05:00
.gitignore say what the 3.5-inch drive will and will not do 2026-09-02 16:46:14 -05:00
.ziggy-schema a .ziggy-schema, so an editor can check a config while it is being written 2026-09-07 16:31:04 -05:00
build.zig a .ziggy-schema, so an editor can check a config while it is being written 2026-09-07 16:31:04 -05:00
build.zig.zon write a machine down instead of typing it out 2026-09-02 18:05:42 -05:00
build.zig.zon.nix write a machine down instead of typing it out 2026-09-02 18:05:42 -05:00
CLAUDE.md the README says cards go in slots now, and one thing it said was untrue 2026-09-07 16:43:58 -05:00
flake.lock mb-audit runs, and the 6522's counters turn out never to have counted 2026-09-06 15:27:35 -05:00
flake.nix mb-audit runs, and the 6522's counters turn out never to have counted 2026-09-06 15:27:35 -05:00
package.nix take the dependency copy from zon2nix instead of doing it here 2026-09-01 17:30:22 -05:00
README.md the README says cards go in slots now, and one thing it said was untrue 2026-09-07 16:43:58 -05:00
REUSE.toml a .ziggy-schema, so an editor can check a config while it is being written 2026-09-07 16:31:04 -05:00

zig65

Cycle accurate 6502, 65C02 and 65816 cores written in Zig, and the machines that were built around them: an Apple-1, an Apple II, a II Plus, a IIe, a IIc, an Apple III and an Apple IIgs, each with its memory, its video, its disk drives and the cards that went in its slots. One program runs all of them, in a terminal window: --machine says which, and every machine and every processor is compiled in, so choosing one is a flag rather than a rebuild.

They boot. A IIe comes up in Applesoft, reads a .dsk through the Disk II's own PROM and puts eighty columns of green text on the screen; a IIgs boots ProDOS off a 3.5-inch disk or a hard disk, formats a floppy, and runs the Apple II System Utilities in eighty columns with its mouse working; an Apple III runs its power-on diagnostics and boots SOS from the drive in its case. Everything a machine asks for is answered by something that was worked out from the hardware rather than guessed at, and where the manuals and the machine disagreed, How it was found out says which won.

It is a library as much as a program: the cores and the machines are exported for other code to embed, not only for the terminal front end here. merle65, a Merlin-syntax assembler for the three processors, embeds zig65 as an oracle — running the Apple II versions of the real Merlin to settle what those assemblers' manuals leave unsaid — and to carry out Merlin's USR pseudo-opcode, which hands its line off to a user-written 6502 routine that the emulator runs to generate the output. And the IIgs here builds its own Mega II, the chip that is an Apple IIe underneath, out of the very eight-bit IIe the other machines run. As a library has the detail.

The processors come first, because nothing above them is worth anything if they are wrong. All 256 opcodes of each are implemented: on the NMOS parts the 151 documented ones and the 105 that are not, and on the CMOS parts everything they added and everything they do differently, decimal mode included. There are five in all, since each family differs within itself. The CMOS side has the plain 65C02, Rockwell's with its bit instructions, and Western Design's with those and two more. The NMOS side has the 6502 and Ricoh's RP2A03, the processor in a NES, which is the same chip with the decimal correction left off the die: the D flag is still a flag and still rides the stack, but ADC and SBC add in binary whatever it says. Correctness is not taken on trust. Every opcode is run against the SingleStepTests corpus, 10,000 cases apiece recorded from hardware, and each case is checked three ways: the registers and memory it ended with, the number of cycles it took, and the address, value and direction of every single access it made on the bus.

That last check is the one that matters most and is the easiest to skip. A 6502 performs reads whose results it throws away, and writes the value it is about to replace back before replacing it. None of that shows up in the final state, so an emulator can pass a state-only comparison while touching entirely the wrong addresses — which on an Apple II is the difference between reading a byte and flipping a soft switch under it.

$ zig build corpus
2560000 tests: 2560000 succeeded, 0 failed, 0 skipped, 1278890 decimal
bus traces:   every cycle matches
cycle counts: all 2560000 cases match

$ zig build corpus -Dprocessor=W65C02
2540000 tests: 2540000 succeeded, 0 failed, 0 skipped, 1270184 decimal
bus traces:   every cycle matches but 9975, which are the decimal correction
              of ADC and SBC against a constant and are the recording
              rather than the chip: see known_difference
cycle counts: all 2540000 cases match

All five processors pass every case: every register, every flag, every cycle count. Ricoh's is checked against the corpus recorded from that chip rather than against the 6502's, since half of the cases have the decimal flag set and every one of those has a different answer on it. The exception in the CMOS runs is one cycle of ADC # and SBC # in decimal, and it is worth explaining rather than hiding. Those instructions cost the CMOS parts an extra cycle there, which they spend putting the answer right, and something is on the address bus while they do. Every other addressing mode has an operand address to re-read, and does, and so does this. Against a constant there is none — and what the recordings show instead is the same address in every single case of an opcode: $0056 for ADC on one set, $0059 on another, $0000 for SBC on both. An address that does not vary with the operands, the registers or the counter, and that differs between recordings of the same instruction, is the bench that made the recording rather than the chip. The runner names those cycles and counts them separately, in known_difference, rather than either failing on them or quietly passing.

The two Western Design instructions are not in that total: 2,540,000 rather than 2,560,000, because the corpus files for WAI and STP are empty.

What the corpus cannot see

The corpus is exhaustive one instruction at a time and, by construction, blind to everything that happens between two of them. Each case sets a machine up, runs a single opcode and compares; nothing in two and a half million files ever runs a second instruction. So a core could pass all of it and still carry a flag wrongly from one instruction into the next, or lose the stack across a chain of calls, and the corpus would have nothing to say.

Klaus Dormann's functional tests are the other shape, and they fill in exactly that. Each is an image that computes: it works through every documented opcode and addressing mode, checks each result against a value the assembler worked out, and jumps to a trap if anything disagrees. Reach the end and the last instruction is jmp * at a known address, so a run has two outcomes and where the machine stops says which.

$ zig build functional
6502_functional_test.bin
  every documented NMOS opcode and addressing mode, on a 6502
  PASS at $3469, after 30646176 instructions and 96241366 cycles

65C02_extended_opcodes_test.bin
  what the 65C02 added, with Rockwell's bit instructions, on a W65C02
  PASS at $24F1, after 21986985 instructions and 66907071 cycles

6502_decimal_test
  decimal mode, against results worked out in binary, on a 6502
  PASS at $024B, after 14464187 instructions and 46089507 cycles

6502_interrupt_test
  IRQ, NMI and BRK, through a register wired to the pins, on a 6502
  PASS at $06F5, after 1049 instructions and 3015 cycles

4 of 4 passed

Sixty-seven million instructions of it. Three of the four are worth a note.

The 65C02 image is fussier about its chip than the file name suggests: it is assembled with both wdc_op and rkwl_wdc_op set, so it wants BBR, BBS, RMB and SMB as well as WAI and STP. A plain 65C02 has none of the Rockwell bit instructions and does nothing at those encodings, which is why the image is run against the W65C02 core rather than the 65C02 one — given the plain part it settles at $0730 after five hundred instructions, on the first BBS it tries.

The decimal test is Bruce Clark's, and it is the one that does not say how it went by where it stops. It stops in the same place either way and leaves the answer in ERROR, at $0B. As published it is set to the NMOS part and checks the accumulator and the carry but not N, V or Z, which are genuinely undefined on a 6502 after a decimal add; the corpus is what checks those on the parts that define them.

The interrupt test needs hardware. There is no way for a program to raise its own IRQ, and BRK is a different path through the processor that would prove nothing about the pins, so the test writes to a feedback register and expects something to pull the lines. functional/src/feedback.zig is that something: flat memory with a register at $BFFC, bit 0 to IRQ and bit 1 to NMI, latching what is written to it with bit 7 masked off. IRQ is a level and NMI an edge, and the test checks both — pin one of them to "never asserted" and it stops at $0434 after 26 instructions or at $05D0 after 558, rather than reaching $06F5.

Assembling the two Dormann does not ship

Only two of the four come as binaries. Dormann assembles with Frank Kingswood's as65, a DOS program, and says in as many words: "and no, I will not switch to another assembler."

He does not have to. aigrette65 is a reimplementation of as65's dialect, written against the original as an oracle, so the .a65 sources assemble unaltered. It is a dependency of the functional/ subproject like the tests themselves, which means zig build functional assembles them on the way past and what gets run is what Dormann published rather than something committed here and drifting.

Unlike the corpus these are small and quick, so they run in CI.

Running

Both front ends draw into a terminal, and how much of the machine you get to see depends on which terminal. Graphics want one that speaks the Kitty graphics protocolGhostty, kitty and WezTerm all do — because that is what lets the machine's own pixels be sent as a picture: the whole 560 by 192 of a Double Hi-Res screen, or the 640 by 200 of a IIgs's Super Hi-Res, scaled into the pane by the terminal.

A terminal without it still shows everything, in the upper half block: one cell carries two stacked dots, which is exactly right for a Lo-Res screen and an honest approximation of the others. Text is unaffected either way, and so is everything that is not a picture. There is more on both, and on why the pixels are worth the trouble, under Reading the screen.

One program, every machine

--machine says which Apple to be: flat, i, ii, ii+, iie, iic, iii, iii+ or iigs, defaulting to iie. --processor says what is in its socket: 6502, RP2A03, 65C02, R65C02 or W65C02, defaulting to the one the machine was sold with — a 65C02 in a IIc and in a IIe whose ROM is the 1985 enhanced firmware, a 6502 in everything else. Neither needs a rebuild.

zig65 --rom APPLE2E.ROM --disk-rom 341-0027-a.p5 --disk1 game.dsk
zig65 --machine iic --rom "Apple IIc ROM FF.bin" --disk1 game.dsk
zig65 --machine iii --rom 342-0031.bin --disk1 sos_13_sys_utils.dsk
zig65 --machine iigs --rom "Apple IIGS ROM 01.bin" --hard-disk system.2mg

These were build options once — -Dmachine and -Dprocessor — and a IIgs was a second executable beside the first. The memory model is a comptime parameter, so a machine cannot be chosen with an if: it can only be picked out of a set that was all compiled. So the debugger is generic over the machine, every pairing of the seven eight-bit models with the five processors is instantiated, and the switch at the front picks one. What that costs is the executable — 22MB against 6.6, or 14.9 for the two it replaces — and what it buys is a program that can be handed to somebody without asking them first which Apple they meant.

A IIgs gets a different front end within the same program: a window with the machine's screen in it and nothing else. There are no debugger panes, because the sixteen-bit core has no debugger around it yet.

The one build option left is zig build corpus -Dprocessor=…, which is not about what gets built at all: it says which chip's recordings to check, and there are two and a half million of them.

Every machine, and what it takes to start one

Each of these is a whole machine rather than a mode, and each has at least one whole-machine test behind the status below: power on with Apple's own ROM, get to a prompt, type a program, run it, read the screen back. Those tests are in src/system_tests.zig, they need the ROMs and disks named here, and they skip rather than fail when a file is absent — so the suite passes on a machine that has none of Apple's code on it.

--machine what it is system ROM it wants status
flat 64K of RAM and no I/O none of Apple's; --rom still wants an image complete, and not an Apple
i Apple-1, 1976 wozmon.bin, 256 bytes boots; loads BASIC from tape
ii Apple II, 1977 apple2o.rom, 12K boots to the Monitor and Integer BASIC
ii+ Apple II Plus, 1979 APPLE2.ROM, 20K boots to Applesoft; the same seven slots a IIe has
iie Apple IIe, 1983 and the 1985 enhanced APPLE2E.ROM 32K, or apple2e-unenhanced.rom 16K boots to Applesoft; boots disks; any card in any slot
iic Apple IIc, all five ROM revisions one of the five below boots to Applesoft and off 5.25, 3.5 and UniDisk
iii Apple III and III Plus, 1980 342-0031.bin, 4K boots SOS, Business BASIC, a ProFile, and Apple's own II emulation
iigs Apple IIgs, 1986 Apple IIGS ROM 01, 128K, or a ROM 3, 256K boots 5.25, 3.5 and a UniDisk on the port; reads, writes and formats

No ROM is built in and none is shipped here, so --rom is required. What each machine wants is its own system ROM and nothing else; everything further down is a card, and a card is only needed for what it does. The hashes are in The ROMs below, and every file named here is catalogued in ~/dev/Apple ROMs with what it was identified as by being run.

flat — sixty-four kilobytes and nothing else

Not an Apple: RAM at every address, no I/O, no video. It is what a bare 6502 program wants, and it is what the SingleStepTests corpus and Klaus Dormann's functional tests run against, so it is exercised harder than anything else here.

There is no Apple ROM to give it, but --rom is still how something gets into memory: the file is laid down so its last byte is at $ffff, so a 64K image is the whole address space and its reset vector is wherever the image says. That is exactly how the functional tests are loaded.

i — the Apple-1

A 6502, the Woz Monitor in 256 bytes of PROM, and a terminal on the other side of a PIA where a video display would be. --rom wozmon.bin and it comes up printing \ and waiting. alt+b is RESET and alt+c is CLEAR SCREEN, the two buttons the board had.

The one card it could take is the Apple Cassette Interface: --aci-rom, 256 bytes at $c100, entered with C100R. With it, --cassette-in plays a WAV in and the machine loads Apple-1 BASIC off tape — a whole-machine test does that and then types a program at BASIC and runs it.

ii — the 1977 Apple II

--rom apple2o.rom, 12K of Integer BASIC and the old Monitor. It comes up at the Monitor's *, and E000G enters Integer BASIC.

--memory populates the three rows of the board the ways Apple did — 4K, 8K, 12K, 16K, 20K, 24K, 32K, 36K or 48K, the five standard sets of memory select blocks (Apple Computer, Inc., 1978, pp. 133134) — and --revision 0 makes it one of the first boards, before the colour-burst killer, the Hi-Res half-dot delay and the power-up reset. The dead test ROM checks all nine memory configurations: see Testing the memory with no memory.

A disk needs a card: --disk-rom with the Disk II P5 PROM.

ii+ — the Apple II Plus

--rom APPLE2.ROM. The 20K dump is from a machine that had a Disk II card in it, so it carries the card's boot PROM at $c600 and the machine starts from a disk — give it --disk1, or watch it wait for one exactly as a real one did. The 12K at the top is the system ROM proper and comes up in Applesoft on its own.

--memory takes 16K, 32K or 48K here, which is what a II Plus could be built in: its 16K plugs were soldered (Sather, 1983, Appendix G).

iie — the Apple IIe

The default machine, and the one most of the cards below are fitted to. Two firmwares, and it works out which from the ROM rather than being told:

  • APPLE2E.ROM, 32K, the 1985 enhanced firmware. Two 16K banks differing only at $c000-$cfff. It says which IIe it is at $fbc0, so the machine fits the 65C02 and the MouseText character generator that came with it, and the title reads Apple //e.
  • apple2e-unenhanced.rom, 16K, the 1983 CD and EF ROMs as one image. $EA at $fbc0: a 6502 and the original character generator, and the title reads Apple ][.

Both boot to Applesoft and take a program at the keyboard. A disk wants --disk-rom 341-0027-a.p5, the sixteen-sector P5 PROM; a thirteen-sector disk wants the thirteen-sector PROM given to the same switch, since what the card does is whatever its PROM says. --video-rom overrides which character generator is fitted.

This is the machine a2audit is booted on — the language card, the auxiliary memory soft switches and the $Cxxx decoding all pass: see Auditing the machine itself.

iic — the Apple IIc, all five of them

--machine iic takes any of the five ROM revisions and does not need to be told which: a 16K image is the original, and a 32K one is two banks that $C028 switches between (Apple Computer, Inc., 1989b, Appendix F.2). The version byte at $FBBF says which revision, and what the machine fits follows from it. There is no --disk-rom: the drive is soldered into the case and its driver is in the machine's own ROM.

revision file what it adds status
$FF Apple IIc ROM FF - 342-0272-A.bin, 16K the original, 1984 reset into Applesoft, boots 5.25
$00 Apple IIc ROM 00 - 342-0033-A - 1985.bin, 32K UniDisk 3.5 support boots 5.25 and a UniDisk 3.5
$03 Apple IIc ROM 03 - 341-0445-A.bin, 32K the Memory Expansion machine boots 5.25, memory card works
$04 Apple IIc ROM 04 - 341-0445-B.bin, 32K the last of them boots 5.25, memory card works
$05 Apple IIc ROM 05 - IIc Plus.bin, 32K the IIc Plus: 4 MHz, 3.5-inch drive in the case boots its own drive, and one on the back

A IIc with the $FF ROM and no disk appears to hang, and that is what the hardware did: the firmware starts the drive and waits for a disk that is not there. Control-Reset is the way out, and warm starts into BASIC.

--memory-expansion SIZE fits the Apple IIc Memory Expansion Card in the connector inside the case — 256K, 512K, 768K or a megabyte. It needs no firmware because what drives it is in the machine's own second ROM bank, which is why it means nothing to the $FF machine.

An 800K image given to --disk1 goes to whichever 3.5-inch drive the machine has: the one in a IIc Plus's case, or the UniDisk 3.5 on a $00 machine's port.

iii — the Apple III and III Plus

--rom 342-0031.bin, the 4K "Sara" ROM at $f000: the power-on diagnostics, the disk core routines SOS's drivers call, and the boot. --machine iii+ is the same chip in the same socket; a III Plus is a III with a clock that worked, and --clock fits it.

With no disk in the drive it runs its diagnostics and prints RETRY, which is what the machine did. With one it boots SOS. Whole-machine tests take it through the SOS System Utilities, Business BASIC with a program typed at it, Apple's own Apple II emulation booting a DOS 3.3 disk inside it, and a ProFile catalogued from Business BASIC — --hard-disk puts one on its interface card in slot 4, where SOS's .PROFILE driver looks.

The drive and its controller are on the board, so a disk needs no card. What is not here is anything to put in the four slots.

iigs — the Apple IIgs

A different front end within the same program — a window with the machine's screen in it and nothing else, because the sixteen-bit core has no debugger around it yet. zig build gs -- --rom ROM, or --machine iigs.

The ROM is two banks for a ROM 00 or ROM 01 (128K) or four for a ROM 3 (256K), and the file's layout is worked out rather than assumed: the banks are not always in the same order in the file, so the firmware bank is found by its marks. Apple IIGS ROM 01 - 342-0077-B.bin is what the tests use.

Both drives are built in, so no card and no disk ROM. It boots a 5.25-inch disk into Applesoft, boots an 800K disk from the 3.5-inch drive, writes a file to one and reads it back, and formats a blank one into a volume that it can then read — each with a test of its own.

A UniDisk 3.5 goes on the same socket, and --unidisk PATH puts one there with --unidisk2 for the one daisy-chained behind it. It is the one machine where that has to be said rather than worked out, because it is the one with drives of its own on that socket; a IIc reaches its UniDisk through --disk1 by the size of the image. A IIgs given one takes its empty 3.5-inch drives off the cable, because they sit in front of it — "Apple 3.5 drives first, followed by UniDisk 3.5 drives, followed by Disk II drives" (Apple Computer, Inc., 1987a, Figure 6-1, p. 110) — and the startup goes to the first device on the port and stops there. A pair of drives nobody owns is a pair of drives the machine tries to boot from.

Starting a machine

There is no ROM built in, so --rom is required unless a config supplies one. The file is loaded so that its last byte lands at $ffff, which is where the processor looks for its vectors. An image larger than the space it goes into keeps its tail, which is what the common 32K IIe dump wants: it is two 16K banks that differ only at $c000-$cfff, and the second is the one with the firmware in it.

A real Apple IIe ROM boots to Applesoft, and it comes up running, so just type. --halted starts it stopped at the reset vector instead, with nothing executed yet, which is what a debugger is sometimes wanted for; alt+r sets it going.

zig build run -- --rom apple2.rom

The debugger shows the registers and flags, a page of memory, and the Apple II text screen. The screen is decoded the way the machine draws it: the page is interleaved, three rows eight apart sharing each 128-byte block, and a byte is not ASCII but a character in its low bits with normal, inverse or flashing in its high bits.

A IIe draws eighty columns as well, and PR#3 turns them on. The second forty columns are not extra display memory but the auxiliary bank: the same addresses in the other half of the machine, read alternately with the main ones so that auxiliary supplies the even columns and main the odd. The screen is drawn from both halves directly and takes no notice of the switches that decide which half the processor is talking to, which is the point of them being separate. Which of the two text pages is shown follows PAGE2, except that with 80STORE on PAGE2 stops meaning that at all and the display stays on the first page.

Eighty columns and a border need a good deal more of the terminal, so when there is not room for the screen beside the debugger's own panes they move underneath it rather than the screen being cut off. A hundred and sixty columns or so fits everything side by side.

The keyboard belongs to the machine. Every key an Apple II, II Plus or IIe had goes straight through to it, control keys included, so ^C stops a running program and ^G rings the bell rather than doing anything to the debugger. A program polls $c000 for what was typed and clears the strobe at $c010, as it would on the real thing. The debugger keeps alt for itself, which is the one combination none of those keyboards could produce.

key
alt+space run one instruction, and stop a running processor
alt+r run
alt+p pause
alt+b reset
alt++ / alt+- double or halve the speed
alt+0 back to the speed the machine was built to run at
alt+page up / alt+page down move the memory view
alt+o / alt+s hold the Open Apple or Solid Apple key
alt+t wind the tape back and play it again
alt+1 / alt+2 open the disk picker for a drive: a disk from the library, a blank one, or a typed path
alt+shift+1 / alt+shift+2 put a new blank formatted disk straight in
alt+g write a picture of the screen
alt+x switch between the machine's character generator and the terminal's own letters
alt+m switch between a monochrome monitor and a colour one
alt+q quit, whatever else is on screen

A II Plus keyboard could not produce lowercase and its model folds it away, so typing at one gets the uppercase a real one would have sent. A IIe had both and keeps them.

The ROMs

There is no ROM built in and none is included here: an Apple II is Apple's code in Apple's chips, and what this is is the machine around it. Every ROM below is one that was in a machine or on a card that was sold, and each is identified by what it is rather than by what a file happened to be called, since the same dump travels under a dozen names. The hash is the whole file.

A machine needs one system ROM. Everything else is a card, and a card is only needed for what it does.

what it is bytes sha256
Apple-1 Woz Monitor. --rom --machine i. 256 bytes; reset prints \\ and waits. 256 e5af0d1c4057bd8e0ef5cb069c208ff7cc0984a7dff53b12c5cf119de8cb5c25
Apple-1 Cassette Interface PROM. --aci-rom --machine i. 256 bytes at $c100; C100R enters it and it prompts *. 256 1b9fcf6f3e9e77b8956181f399dfb7905eb7cc947b658a6c202f6b123518a0b7
Apple II system ROM: Integer BASIC and the old Monitor. --rom --machine ii. Comes up at the Monitor's *; E000G enters Integer BASIC. 12288 68d9db6bb4c305d40c3fa89fa0f2d7b7f71516a9431e3138859f23bc5bddb2d1
Apple II Plus system ROM, dumped from a machine with a Disk II card. --rom --machine ii+. The 20K carries the card's boot PROM at $c600, and the machine boots it, so it wants a disk in --disk1; with none it waits for one as a real one did. The 12K at the top is the system ROM proper, and comes up in Applesoft on its own. 20480 92c4bef609920842ea472d21b661a0d35dbda6cd90963b8b734a205e22d84108
Apple IIe system ROM, the 1985 enhanced firmware. --rom, and --machine iie is the default. Two 16K banks differing only at $c000-$cfff. Says which IIe it is at $fbc0, so the machine fits the 65C02 and the MouseText character generator that came with it, and the title reads Apple //e. 32768 c17bc38c75ba96c33a30c688a1efd60144811073423533fe7f8453cdd9457aab
Apple IIe original firmware, the 1983 CD and EF ROMs as one image. --rom --machine iie. $EA at $fbc0: a 6502 and the original character generator are fitted, and the title reads Apple ][. 16384 1fb812584c6633fa16b77b20915986ed1178d1e6fc07a647f7ee8d4e6ab9d40b
Apple IIc system ROM, 342-0272-A. --rom --machine iic. The first one, and the one this was written against. 16384 3ca1c8a27bf111aa0f4dc9372c5e6404457adb9a458ce32ae4b03a7e989e0c76
Apple III system ROM, 342-0031, the "Sara" ROM. --rom --machine iii, and --machine iii+: the III Plus has the same chip in the same socket. 4K at $f000: the power-on diagnostics, the disk core routines SOS's drivers call, and the boot. With no disk in the drive it prints RETRY. 4096 370be5f0c1b57b606d6c7312f99e5f6064bf405b22430ab53d9bec9bcc0f7520
Disk II boot PROM, P5, sixteen sector. --disk-rom. The 256 bytes the machine runs when it starts from a disk. Not needed on a IIc, whose drive is soldered down and whose driver is in its own ROM. 256 de1e3e035878bab43d0af8fe38f5839c527e9548647036598ee6fe7ec74d2a7d
Super Serial Card firmware, 341-0065-A. --serial-rom. Page seven of the 2K answers at $c200; the whole of it is the $c800 window. 2048 4fa1323ba31f4bd562486838397b7744257c126806766e360876720daca6e2aa
Applied Engineering RamFactor firmware, revision 1.4. --ramfactor-rom. Eight kilobytes in two banks: a slot page for each of the seven slots, the resident firmware, and the partition manager the resident firmware copies into the machine and runs. Revisions 1.0, 1.1 and 1.3 are also here and all four behave the same. 8192 3369e5e9838db52df7924e79e65451267953172390d30308a28c688a886989ce

The three later IIc ROMs run too, and --machine iic takes any of them without being told which: a 16K image is the original, and a 32K one is two banks that $C028 switches between (Apple Computer, Inc., 1989b, Appendix F.2). Each of them boots a five-and-a-quarter disk and there is a system test apiece to say so.

a later IIc ROM bytes sha256
342-0033-A, the 1985 revision 32768 678d5c9d374664d2ee94ba9c9564d26a917b31116b07c46df30a3e82c640885b
341-0445-A, with memory expansion 32768 48318d174d7df008c12efe7a26e9c83147499141933b8041d94639fb767466c1
341-0445-B, the last of them 32768 8ad5e6c4ed15d09b62183965b6c04762610d4b26510afc1896bdb4ecc55da883

--memory-expansion SIZE puts an Apple IIc Memory Expansion Card in the connector inside the case, holding 256K, 512K, 768K or a megabyte. It needs no firmware, because what drives it is in the machine's own ROM — in the second bank, which is why it wants one of the later ROMs and means nothing to the first. Four registers at $c0c0 are the whole of it: a twenty-bit address counter that steps itself, and one byte wide door onto the memory (Apple Computer, Inc., 1987b, p. 7).

The UniDisk 3.5 is here now, and a $00 machine boots one. With nothing on the port each of the later ROMs looks for a drive, finds an IWM with nothing 3.5-inch on it, gives up and falls through to the drive in the case — which is what a IIc with an empty port does.

The first piece of it is src/cbus.zig. A UniDisk is not a drive the machine works directly but one with a processor of its own, reached in packets over the disk port, and the firmware that does the reaching is the Protocol Converter at $C500 — which, from above, "appears to be a block device" (Apple Computer, Inc., n.d.-b, p. 20). That file is the packets and nothing else: the sync run and the two marks, the five header bytes in the order the firmware sends them, the leftover bytes and then the groups of seven behind their gathered high bits, and the checksum riding in two bytes with ones between its bits. It touches no shift register and counts no cycle, which is deliberate — the part that has to meet "32 cycles/byte and the timing is critical" will have something already known to be right underneath it.

Every constant in it was found in the bytes of the ROM at the address Apple's listing gives, rather than read off a scan whose hex columns are the worst case for OCR. But the thing that made it right was a packet a real machine sent.

An empty disk port reads as a write-protected drive, and a write-protected drive is indistinguishable, on the one line the firmware looks at, from an intelligent one saying it is ready. So a IIc with nothing attached goes ahead and transmits — and thirty-two bytes handed to the data register during an ordinary power-on turn out to be a whole InitCMD packet, encoded by Apple's own firmware. Holding the file against it found two things wrong that no test written against the file itself could have found, because an encoder and a decoder that agree with each other agree just as well when both are wrong.

The first was the leftover bytes. A packet's contents go across as however many bytes are left over and then whole groups of seven, and this had the leftovers at the end of the caller's buffer rather than at its front. What settles it is that the first byte of the captured packet is $05, which is InitCMD — the call the reset code makes to force a reset of the bus — and a packet with the leftovers at the other end would have had that command buried in the middle of seven zeros instead.

The second was the checksum, which is neither over the packet as sent nor over the contents as given but over one of each: the seven header bytes with their high bits on, and the contents raw, before any encoding was done to them. The captured packet's header exclusive-ors to $82 and its contents to $07, and $82 ^ $07 is $85, which is what its two checksum bytes carry. Reading the whole packet off the wire instead gives $05, and would have rejected a packet a real machine sent.

The order the gathered high bits sit in is the one thing that captured packet cannot settle, every byte of its contents having a high bit of nought. It is settled instead by reading the firmware from both ends, which is better than reading it twice from one: sending, at bank 1 $c965, an ASL on each byte followed by ROL on the gathered one carries earlier bits further up, so the first byte of a group ends highest; receiving, at $ca39, one ASL of the gathered byte per data byte hands them back out in that same order. A test pins it and fails if it is reversed.

The calls it answers are now all of the ones a block device has. READ BLOCK, WRITE BLOCK, FORMAT, INIT and STATUS were what Apple's own firmware asks for — every command it issues across the tests here was logged, and it never asks for anything else — but a host that knows about SmartPort will, so:

  • STATUS $03 returns the Device Information Block: the general status byte, the size again, the name in a field of sixteen, and what the device is. "Several device types and subtypes are assigned to existing SmartPort devices", and $01/$00 is the UniDisk 3.5 (Apple Computer, Inc., 1987a, p. 124).
  • STATUS $05 is the UniDisk's own, "more detailed information about the cause of a read or write error". Nothing here fails the way its error byte describes — there is no surface to mis-read and no track to seek wrongly — so it reports none, which is the truth about this drive rather than a stub.
  • CONTROL resets the device on $00, ejects the disk on $04, and answers BADCTL to $01 and $02, which are the control block a UniDisk has not got and the newline mode only a character device has.
  • OPEN, CLOSE, READ and WRITE answer BADCMD. They are how a character device is worked, and answering them with a status of nought — which is what every unimplemented call used to do — would have a caller believe a stream had been opened.

That last change caught something on the way in. INIT had been relying on the same silent success, so making the unknown calls say BADCMD stopped the drive answering the one call the firmware uses to find out what is on the bus. The tests found it immediately, which is the argument for not having a case that quietly agrees to anything.

src/unidisk.zig is the other end of that bus: the drive itself, taking bytes in until a packet is whole, reading it, and framing an answer. It is wired to the port now, and a IIc with one plugged in gets through a whole exchange — Apple's firmware frames a packet, the drive reads it as the Init call it is, answers, and the firmware accepts the answer and carries on about its business. A system test holds the machine to that.

Getting there was three measurements, none of which could have been guessed. The sense line is not just a way of saying a drive is present: it has to fall, twice. Having sent a packet the firmware spins until it goes down — that is the drive saying the packet arrived — and having taken the answer it spins on it going down again. Miss either and the machine hangs with its screen never drawn, which is what it did.

The second is that the fall is a level and not a pulse. The firmware polls that line in more than one place, so a line that drops once and comes straight back up has its fall taken by whichever poll happens to come first, and the machine hangs at the next one. It stays down until the host takes REQ away, REQ being phase zero of the port.

The third the machine simply told us. The firmware finds what is on the bus by asking — Init to unit one, then unit two, and on up, taking the first silence as the end of the chain — so a drive that answers to every address it sees is an endless chain of drives. It ran to seventy-one of them before the address was checked.

It reads blocks now, and a IIc reads its boot block off one and runs it. The firmware frames a READ BLOCK — the command, a count, the buffer and three bytes of block number, the unit being carried by the packet's own destination rather than repeated in its contents — and the drive hands back five hundred and twelve bytes, which arrive where the firmware asked for them and match the disk to the byte. The machine then executes what it read, and that code goes on asking for the volume directory and the file it names, twenty-eight blocks deep, which is ProDOS loading itself off an emulated intelligent drive.

Two things had to be got right to reach that, and the second was not in this part of the machine at all.

The wire layout of READ BLOCK is the manual's parameter list minus the unit number, and being one byte out reads the second block the boot block asks for as block $c50000, which is nothing. Two captured packets settle it: 01 03 00 08 00 00 00 is block nought to $0800, the boot block, and 01 03 00 0c 02 00 00 is block two to $0c00, a ProDOS boot block fetching the volume directory.

The other was that a drive with nothing in it was reading as silence. Apple says a IIc "attempts to boot from the built-in disk drive. If this fails, it attempts to boot from the first device attached to the Protocol Converter", and the way it fails is a counter — DEC $03 at bank 0 $c644, which hands over to $c500 at nought. That counter only comes round when the read loop above it finds a byte, so a silent drive holds the machine there for ever and nothing on the port is ever reachable. A real drive with no disk in it is not silent: the head hears the amplifier, which is the same thing it hears over unrecorded surface, and the WOZ reference calls that a position that "continuously output[s] random data". Making an empty drive read that way is what let the machine get to its port, and it made two tests fail that had asserted the old silence — which is the usual way round here, an emulator got more accurate and the tests that described the old behaviour were the bugs.

And it boots. A IIc with the 32K ROM starts, fails over from the drive in its case to the Protocol Converter, finds the drive on the port, reads its boot block, runs it, and loads ProDOS a block at a time until the System Disk puts up its menu — none of whose text is anywhere in the ROM.

The last thing in the way was a single byte, and it is worth setting down because it took a whole boot to show itself. The drive knew a packet had ended when it saw $c8, the end mark. But every byte of a packet's contents travels with its top bit set, so a content byte of $48 goes across as $c8 and is indistinguishable from the mark. A booting machine reads block after block into ascending buffers — $0800, $0c00, $0e00, and up — and the read that would have gone to $4800 carries $48 as its buffer's high byte. The drive cut the packet short there, could not decode the fragment, and said nothing; the firmware retried, some six thousand times, and then gave up with UNABLE TO LOAD PRODOS.

What is wrong with watching for the mark is that a packet already says how long it is. Its header carries a count of leftover bytes and a count of whole groups of seven, and those give the length exactly, so the drive now reads that many and stops. The same disk that would not boot boots, and so does another.

Everything else that was suspected turned out to be innocent, which is worth recording so nobody suspects it twice: the checksum arithmetic was right, the data arrived byte for byte, no request was ever dropped or refused, and neither disk was at fault. What found it in the end was counting: six thousand and thirty-one packets assembled where twenty-eight were expected.

Putting a disk in one takes no new option. An eight hundred kilobyte image handed to --disk1 already goes to whichever 3.5-inch drive the machine has, and this is now one of them: a IIc Plus takes it in the drive in its case, and a IIc with the 32K ROM takes it on the port. The two are never both there, which is what lets the same option mean the right thing on either — a IIc Plus has its own drives on that connector already, and the machines that take a UniDisk have no drive in the case.

And it writes. Reading a block is one packet each way; writing is three, and the shape of it was read off a IIc saving a file rather than guessed at. The command names the block and goes out with $80 in its type byte; the five hundred and twelve bytes follow in a packet of their own with $82; the drive answers. A system test boots the machine, leaves the menu for BASIC, saves a program, and then finds the file and the bytes Applesoft tokenised it into in the image the drive was handed — not in the machine's own catalogue, which would be the machine vouching for itself. A disk whose tab is across answers $2B NOWRITE instead, which is what lets a program say so rather than watch a write vanish.

The same trace showed the firmware asking for STATUS, which it never does while booting, and that call is now answered properly rather than with an empty packet. Status code nought returns "four bytes", the first a general status byte and the next three "the size in 512-byte blocks", low byte first. Two of that byte's bits run the opposite way to the rest — "3 — 0 = format allowed" and "2 — 0 = medium write protected" — which is the manual's own doing and was read off the page rather than its OCR, that being exactly the kind of thing a scan gets wrong. The codes a UniDisk does not answer are refused with $21 BADCTL rather than guessed at, as the manual says they should be.

Whether those two inverted bits were read right is not left to argument: with STATUS answered, the machine still saves a file, which it would not do if it had been told the disk was protected.

FORMAT is there too, and does exactly what the manual says and no more — "prepares all blocks on the recording medium of a block device for reading and writing", with "bitmaps and catalogs are not written by this call". So it lays the medium down and stops; what makes a volume of it is the host writing a directory afterwards, block by block, the ordinary way. A protected disk is not formatted either, and says NOWRITE rather than letting it happen quietly.

And there are two drives on the port, because a UniDisk has a connector on its back for the next one along and the firmware finds out how many there are by asking — Init to unit one, then to unit two, and on until nothing answers. So they all hear every byte and each answers only when its own address is on the packet.

The one part of that worth thinking about rather than delegating is the sense line, which is shared and which a drive pulls down. A drive that is busy therefore takes the line down for the whole chain, and the line is only up when every drive on it is ready — which is why the chain asks all of them rather than stopping at the first, an idle drive being perfectly capable of raising a line that a working one is holding down.

With two of them the machine can boot from one and lay the other down, which is what makes FORMAT provable through a machine rather than only in a unit test: a IIc boots the System Disk off the first drive, takes its Utilities to Format a Disk, points them at slot five drive two, and what comes back is a ProDOS volume of sixteen hundred blocks carrying the name that was typed — while the disk it booted from, writable throughout, is untouched.

And the other machine with a SmartPort has one too. An Apple IIgs takes a UniDisk 3.5 on the socket on its back and boots ProDOS off it, which took three things being got right and each of them was got wrong first.

What makes a drive on that cable answer at all is not a latch. It is a combination of the phase lines, and Apple chose one that "never occur[s] under normal Disk II operation" so that "a Disk II device and other bus residents [can] stay out of each other's way": phase 3 and phase 1 both up is a bus enable, phase 0 and phase 2 both up is a bus reset (Apple Computer, Inc., 1987a, p. 157). Both are pointless on a five-and-a-quarter drive, being two opposing magnets pulling a head in two directions, which is exactly why they were picked. Neither of them had been modelled: what stood in for the enable was the second of the port's two drive selects, which a IIc happens to use and which is not what arbitrates.

The machine's own firmware has all three of them side by side, each after LDX #$60 so the indexed addresses land on the port: LDA $c081,X / LDA $c085,X is the reset, LDA $c083,X / LDA $c087,X is the enable, and four more addresses put every line down again (342-0033-A, bank 1, $ca60, $ca80, $ca8a).

That the select is not what decides was settled by the disagreement between the two machines. The book names one line — "The Disk II device is enabled by the disk port signal /ENABLE2. The SmartPort must activate the /ENABLE2 line to communicate with intelligent bus residents" (p. 158) — and a IIc does use it, which is what a machine with its own drive on the first select would do. A IIgs, traced through its own startup, selects the second to go looking for an Apple 3.5 drive and then goes back to the first before it raises phase 1 and phase 3 and starts the handshake. So the select is whichever one the host has spare. What keeps a Disk II at the end of the chain out of it is the same page read the other way round: each resident gates the select onward "so that whenever any SmartPort bus resident is enabled (PHASE1 and PHASE3), any Disk II at the end of the chain is disabled".

The second thing was the drive's own name. A drive here was born as unit one, which is where a IIc starts counting, and a IIgs does not: it has devices of its own to number first and offers the drive on the port the number three. The packet it sends is a IIc's Init to the byte save for that one destination byte, and a drive born as unit one hears it, decides it is somebody else's, and says nothing — after which the machine reports that there is nothing on the port. The manual says plainly that the number is the host's to give: "the host then sends the ID definition command. Whenever a device receives this command (with Enable), it assigns the unit number embedded in the command string as its own unit number. Thereafter it will not respond to any command string with a unit number other than that given it" (p. 157). So a drive now has no number until it is told one, and takes the one in the first Init it is allowed to hear; the drive behind it is deaf until the one in front has finished, which is how the numbering walks down a real chain — "upon completing the ID definition command, the bus resident reenables the phase 3 line, allowing the next resident to receive its ID definition command" (p. 158).

The third was how the chain says where it stops. A IIc needs no telling: it asks the next address, hears nothing, and stops. A IIgs, told there was another drive behind this one and then finding none, abandoned the port altogether. The last drive is supposed to say so — it "returns an exception, indicating that it is the last bus resident" (p. 158), a real one working it out by sensing a line that every intelligent device grounds — and $28 NODRIVE is among the errors the call is documented to return (Apple Computer, Inc., n.d.-b, p. 39). Saying that is what makes the machine go on and read a boot block.

One thing it does not do is boot a UniDisk with Apple 3.5 drives in front of it, and that is not a fault either. Apple's ordering is "Apple 3.5 drives first, followed by UniDisk 3.5 drives, followed by Disk II drives" (Figure 6-1, p. 110); the startup goes to the first device on the port and stops there, so an empty Apple 3.5 drive ahead of a good disk is an empty drive the machine tries to boot. Both are tests: one machine with drives of its own, which finds the UniDisk, numbers it three, and boots nothing; and one without, which boots it. --unidisk takes the empty drives off the cable for exactly that reason, since an empty drive that is not there is not a drive.

The IIc Plus comes up. That machine has a chip no other IIc has — the multidrive interface glue, a custom part and two kilobytes of static RAM buffering the 3.5-inch drive — and its firmware leaves $ce00 empty for it and builds the vectors it jumps through in the chip's own RAM. Without the glue nothing runs at all: the vector reads as zeros and the machine lands on $0000. With it, in src/mig.zig, ROM 5 reads the disk in its case and boots ProDOS off it.

Getting there took the other half of what the glue is for. A IIc Plus talks to that drive as a 3.5-inch drive, which means the phase lines stop being magnets and become an address, with a fourth bit — SEL — beside them. A IIgs takes SEL from bit 7 of $c031; a IIc Plus has no such register, and the line is one of the glue's own latched outputs, put down at $ce40 and up at $ce60. Without it the firmware asks a 3.5-inch question and is answered by the five-and-a-quarter drive, which is why an empty machine came up and a machine with a disk in it hung: the answer it waited on was that disk's write-protect tab.

The last piece was the buffer itself. A sector does not go straight to memory: the firmware fills it thirty-two bytes at a time with LDX #$e0 / STA $cd21,X, and $cd21 + $ff is $ce20, so every thirty-second byte lands one past the window — which is the next page, and reaching it is what carries the page on. Throw that byte away and each sector loses one in thirty-two, fails its checksum and is read again for ever. Keep it and the disk reads.

What then stopped it was not the drive at all but the memory expansion interface. Twice on its way up the firmware sizes that memory, once walking the address counter forward until the middle byte carries and once walking it back until the high byte reaches nought, and it will not leave either loop until the counter moves. No value an undriven bus could give satisfies both tests, which is the proof that the counter is not on the card: it is on the board, and it answers whether or not anybody fitted memory behind it. Model it that way and a IIc Plus boots all the way to the System Utilities the disk starts — the same place a IIgs reaches from the same image — with or without memory fitted.

The disk port now has a notion of an outside, and it turned out not to be a latch at all. A IIc Plus lists four places it will start from, and two of them are 3.5-inch drives that differ only in where they are: the "internal 3.5-inch drive at $C500 (port 5, drive 1)" and the "external UniDisk 3.5 or Apple 3.5 drive at $C500 (port 5, drive 2)" (Apple Computer, Inc., 1989b, p. 86). The port is the kind of drive and the drive number is whose — and the drive number is the IWM's own, so the drive on the back is reached by selecting drive two and changing nothing else. The glue chip's pins say the same from the other side: it carries INTEN*, "a latched output bit to enable internal drive", against ENB2, which "follows EN2X* or ROMEN1* if addressing mapped device space to select external drives" (Table 11-15, p. 328) — one enable each, the second being the IWM's ENABLE2 qualified by the address decode so that it is driven only during an access that really is the port's. A system test boots a IIc Plus to the System Utilities with the only disk in the drive on the back and the drive in the case left empty.

A five-and-a-quarter drive on the back is still out of reach, and the reason is worth setting down, because looking for it in the wrong place costs a day. That one does want 3.5 DRIVE*, the "latched output bit to select 5.25-inch or 3.5-inch drives" (Table 11-15, p. 328), because a IIc Plus has one drive built into it and it is always a 3.5-inch one, so the kind never changes and the machine comes up pointed at the drive it has. Three guesses at what writes that latch have now been measured and none of them survived. It is not the address being executed from: ROMEN1* marks an access to $C100-$DFFF, and the port-6 driver straddles the boundary — its head stepping and its read loop run inside that space while the very instruction that starts the motor runs outside it, so no split by address can separate one driver from the other. It is not the port address either: with a five-and-a-quarter disk in the machine and the firmware spinning on the boot PROM, $C0Ex is the only page it ever touches, never $C0Dx. And it is not a write as against a read on the SEL pair: making those writes latch the kind breaks the 3.5-inch path in both polarities. With a five-and-a-quarter disk present the firmware still touches only the five glue addresses that are already spoken for, and never the three that are not.

The same unplaced latches have a second consequence, and it is a good deal easier to reproduce than a five-and-a-quarter boot. A IIc Plus boots from the 3.5-inch drive on the back perfectly well, which a system test proves. But once ProDOS is running, pointing its System Utilities at slot 5 drive 2 reads the disk in the drive in the case: put a volume named PLACEHOLDER on the back and ask to format drive 2, and the program offers to destroy /UTILITIES, which is the disk in the internal drive. Booting and block access take different routes to the same drive, and only the first of them is right here. INTEN*, the "latched output bit to enable internal drive" (Table 11-15, p. 328), is one of the outputs nothing has been able to place, so the suspicion is that this is the same gap seen from another side; that is a hypothesis and not a finding.

The obvious form of it has been tried and does not hold. The writes on the SEL pair are the only accesses the firmware makes that are not already spoken for, so they were made to drive the drive select, both ways about: one polarity changes nothing and the program still offers to destroy the disk in the case, and the other leaves the drive on the back unreadable, so that no volume is found on it and no format ever finishes. Watching the machine says why the answer is not there anyway. By the time the program asks, the firmware has selected drive two two hundred and twenty-five times and is sitting on it; it is the firmware itself that then selects drive one, five times over, and reads it. So it is not that a selection is being ignored — the firmware is choosing the internal drive on purpose, having been told something that made it. Until it is settled a IIc Plus cannot be made to format a disk, because formatting on this machine necessarily addresses drive 2.

It was not INTEN*, and it was not the Protocol Converter either, though both had to be ruled out to get here. The drive select was arriving the other way about, and the machine said so plainly once it was asked the right question.

ProDOS keeps the device it last touched in DEVNUM at $bf30, as the slot in bits six to four with bit seven set for drive two. Boot a IIc Plus with its only disk in the first of the port's two drives and ProDOS calls what it booted $d0 — slot five, drive two. Put the same disk in the second and it calls it $50, drive one. The manual is unambiguous about which of those is which: "internal 3.5-inch drive at $C500 (port 5, drive 1)" against "external UniDisk 3.5 or Apple 3.5 drive at $C500 (port 5, drive 2)" (Apple Computer, Inc., 1989b, p. 86). So the drive in the case is the one this port selects second, and everything here had the two the wrong way round.

That explains what looked like a fault in ProDOS and was not. Asked to format slot five drive two it read the disk in the case, and it was right to: drive two was the disk in the case, as far as anything here had wired it. It explains the traffic as well — nearly all the work went to the second of the two drives during an ordinary boot, which is exactly what a machine booting from the drive in its case should do, and which made no sense at all while the second was thought to be the drive on the back.

Very likely this is the glue chip's doing: INTEN* is a "latched output bit to enable internal drive", nothing has been able to place it, so what reaches the drives is the select on its own and it arrives reversed. Turning it round is what makes drive nought mean the drive in the case, which is what a front end and every test here assume. The two boot tests are the check on it: one puts its only disk in the drive in the case and the other in the drive on the back, and only with the select the right way about do both of their names describe what they actually do.

A IIc Plus will now format a disk, and gets most of the way through one. What was in the way was not the machine at all but the blank being offered to it.

A disk with nothing on it was filled here with $ff and called self-sync, and that is wrong twice over: self-sync is what the gaps on a formatted disk hold, and a blank that hands out $ff for ever gives a firmware hunting an address mark nothing to give up on. What an unrecorded surface really reads as is noise — the head hears the amplifier, exactly as it does between two tracks on a five-and-a-quarter disk, which is what fakeBit has always made there. A IIgs copes with either and formats regardless. A IIc Plus, whose hunt for a D5 at bank 1 $e0cd has no counter in it, sat in that loop for ever. Given noise it gives up on reading and lays the surface down instead, and the tests that make blanks now make them the way a drive would find them.

What it lays down is half a disk: eight hundred blocks of sixteen hundred, in an alternating pattern twelve long, which is one track's sectors on the outermost zone. It writes one side of every track and not the other.

Where that happens is now known exactly. Every one of the million and a half bytes goes to side nought, and it is not that the firmware never asks for the other one — it chooses the upper head four hundred and nineteen times. The head is chosen by reading register one or three, which selects that head as a side effect, and those two registers differ in nothing but the SEL bit. During the writing the lines sit on register one for seven hundred and thirty-one thousand of the writes, and twelve hundred and fifty-nine sense reads land there while they do — each one quietly pulling the head back to the lower side before a byte goes down.

So the question is not what selects a head but whether reading the sense line should still be selecting one at a moment when the machine is writing, and that wants settling against what the drive really does rather than against what would make this test pass. HDSEL, the glue chip's "latched output bit used for 3.5-inch drives", was the obvious suspect and is not the answer: driving the head from the spare writes on the SEL pair stops the format finishing at all.

Reading the source that settles it points at this model rather than at the machine. Inside Macintosh prints the table of which state-control lines address which disk register, and the two in question are RDDATA0, "read data, lower head", and RDDATA1, "read data, upper head" — the same four lines with SEL the other way about (Apple Computer, Inc., 1985c). Apple describes them as where the data comes from, and says nothing about reading one leaving the drive on that head afterwards. This models it as a latch that a read sets, which is a stronger claim than the table makes and is very likely an over-reading of the article that was followed alongside it. A head that is addressed rather than latched would be chosen by SEL at the moment of each access, reading or writing, and could not be taken away by a stray sense read while the machine was writing.

That was tried, with the table open, and it does not hold as stated. Choosing the head from SEL at the moment of each write scatters the bytes across both sides and the format stops finishing at all, because SEL does not sit still while a write goes on: the lines were measured alternating between two registers throughout. So the drive must be holding the head somewhere across a write rather than taking it from the lines byte by byte, and the latch here is closer to right than the table alone suggests.

What is left is narrower and stranger than it looked. The head is held, the firmware does ask for the upper one, and something puts it back before any byte is written. Where that happens is known to the instruction; why a real drive would not do the same is not. The test stays out of the tree until it is.

That last point has since been read from both ends, because it is the one that decides whether the latch is reachable at all. Statically, no instruction in either bank of the ROM names an address falling in any of the three unused registers; what looks at first like a reference is a table of ascending bytes in one place and a run of high-ASCII message text in another, each being disassembled as though it were code. Dynamically, a IIc Plus booted with a five-and-a-quarter-inch disk in the drive touches the three exactly nought times. So the latch is not written through a register of its own, and where it is written from is still unknown.

Reading the pins closely did settle one thing about the chip. Every address line it has is in its pinout — A5, A6 and A7 to pick the register, A9, A10 and A11 to place it (Figure 11-13, p. 327) — and A8 is not among them, so it cannot tell $ce00 from $cf00 and answers across both pages. The ROM agrees from the other side: bank 1 leaves five hundred and twelve zero bytes there, two pages of hole rather than one, because both belong to the chip. The window is modelled that wide now.

The four-megahertz accelerator is still to come, and what is worth saying is why it is not a switch. It is a cache. The machine has a 65C02 clocked at four megahertz beside two eight-kilobyte static RAMs, one holding data and one holding a tag for every byte of it, and a gate array between them — the CGGA — which gives the processor its fast clock only while the tag says the byte it wants is already there. On a miss, or with the accelerator disabled, the array synchronises the processor to the rest of the machine, fetches the byte from system RAM, fills the cache behind it, and runs at the 1.023 MHz every other IIc runs at (Apple Computer, Inc., 1989b, p. 329). Which parts of the address space may be cached is itself programmable, and the state it comes up in is a map of the machine's own compromises: the firmware for ports 1, 2, 5 and 6 and the code for the speaker and the paddles are marked uncacheable and stay slow, because speeding up ports 5 and 6 stops the disk drives working and speeding up the paddles stops them reading at all; ports 3, 4 and 7 may run fast (p. 626).

So the speed is not a setting that can be read back. The manual says so — "there is no way to determine the state of the system's speed at any given time" (p. 626) — and what this emulator runs at is the speed a miss gives, which is the speed the machine falls back to anyway.

The firmware does run, all the same. Reset reaches it through a thunk at $c7c7 that flips the ROM bank and jumps to $fd00 in the second one, and what it finds there borrows the glue chip's RAM for scratch, which is one more thing that would not work without that chip. The hardware it drives is write-only: $c05a takes $a5 to unlock the array and $5a four times over to lock it again, $c05a and $c05b take $08 to disable and enable it, and $c05c through $c05f take four bytes copied out of a table at $fe8c saying what may be cached. Nothing in either bank ever loads from any of those six addresses. They are borrowed from the switches every other IIc keeps there — $c05a and $c05b are its vertical-blanking interrupt controls, and $c05e and $c05f its double-high-resolution switch, which is why the manual warns that running a command changes the state of double hi-res, and why its own sample code saves that switch and puts it back (p. 626).

There is a Read Accelerator call in spite of all that, and how it answers is the giveaway: it does not ask the chip. The firmware keeps a two-byte shadow of everything it has written in the glue chip's RAM at $ce02, sets or clears a bit of it beside every knock, and hands that back (Apple Computer, Inc., 1989b, p. 631).

Nothing else needs a ROM. The block device in slot 7, the clock card, the mouse card and the Mockingboard all answer with firmware written for this project, which is possible because what each of those cards has to do is published and none of it is Apple's code.

zig65 --rom APPLE2E.ROM --disk-rom 341-0027-a.p5 --disk1 game.dsk
zig65 --machine iic --rom "Apple IIc ROM FF - 342-0272-A.bin" --disk1 game.dsk
zig65 --machine iigs --rom "Apple IIGS ROM 01 - 342-0077-B.bin" --hard-disk system.2mg

The Apple IIgs ROMs

--machine iigs loads one of these and boots it, and zig build bootgs runs the same ROM without a screen to see how far it gets. Which one you have matters more than it should, though, because identifying a IIgs ROM is not what you would guess.

A IIgs system ROM covers the top banks of the address space: two of them for the 128K ROM 00 and ROM 01, four for a 256K ROM 3. The banks are not always in the same order in the file. ROM 01 has the high bank first and ROM 00 has it last, which is already enough to rule out going by the size. Worse, two dumps of the same part disagree: the copies of 341-0748 and 341-0749 that circulate with "(new)" in the name are byte for byte the other ones with the two halves exchanged — same content, different arrangement, different hash. So the order cannot be taken from the size, the name or the part number.

It can be taken from the image. Bank $ff holds the Apple II firmware, which has not moved in decades: its reset vector is $fa62, the Monitor's reset entry; $fbb3 and $fbc0 are $06 and $e0, the bytes a IIe answers with when a program asks what machine it is on; and $fded is 6c 36 00, COUT jumping through the output vector as on every machine in the family. Four marks at fixed addresses will not turn up together by accident. Whichever end of the image has them is bank $ff, and the rest run in order from there.

iigsrom.identify does that, and is checked against all fifteen IIgs images to hand — including the swapped pairs and the 256K ones. So the machine takes whichever of them you have and works out the arrangement itself; there is no switch for it and nothing to get wrong.

what it is bytes order in the file sha256
ROM 00, 342-0077-A. The first one shipped. 131072 $fe then $ff 9a36f55562d483e11c777a73bb76bb3b01a9ca34ea867f22d2c3277cdca16176
ROM 01, 342-0077-B. The common one. 131072 $ff then $fe eeacf455cc642f9e7e36353ecf67eea65b4a9d725606400f50a0987006eed5f3
ROM 3, 341-0748. Banks $fe and $ff of the 256K ROM 3, as a file of its own. 131072 $ff then $fe 3dc560f51f2a0b3c61472163e212d36c8321f43203b343abafe289072dc4472b
the same dump with its halves exchanged, which circulates as "(new)" 131072 $fe then $ff 31947bb32cd6c564be93fd251e0900f45a9561874bc00307d4978c3db08c9a66
ROM 3, 341-0728. Its other half, banks $fc and $fd. No firmware in it, so the test above does not apply and identify refuses it — correctly: it is half a ROM. 131072 $fc then $fd b327c945a15a11b4a54ca1fb30f5974dc10f6322e280579e1cce51f775cfa588
ROM 4, the Mark Twain. The machine Apple did not ship, and a whole 256K in one file. 262144 $fc up to $ff 565d8ca073e5c46367ca56d4b12827020e76e6a0bf6c2300da0989161aa34ad9

Writing the machine down

A machine that gets used often is a long line of switches, and most of them do not change from one run to the next. --config reads them from a file instead:

zig65 --config examples/apple2e.ziggy
zig65 --config examples/iigs.ziggy
.machine = "iie",
.rom = "roms/APPLE2E.ROM",
.monitor = "colour",

.slots = [
    .{
        .slot = 6,
        .card = "disk",
        .rom = "roms/341-0027-a.p5",
        .drives = 2,
        .disk1 = "disks/game.dsk",
    },
    .{ .slot = 4, .card = "mouse" },
]

A field is named after the switch it replaces with the dashes turned into underscores, so anything in --help can be written in the file. Everything is optional, and a switch on the command line beats the file — which is what makes a config a starting point rather than a straitjacket:

zig65 --config examples/apple2e.ziggy --slot 5=disk

A card is described where it sits, and nowhere else. That is the one place the file does not mirror a switch, and it is deliberate: a machine can have two Disk II controllers with different disks in them, and there is nowhere for one .disk_rom at the top of a file to have meant both. So a card's firmware, its drives and what is in them are written beside the slot the card is in.

Naming a card in a slot is what puts it there. A slot holds one card, and two in one slot is refused with a message saying what is already in it, rather than one of them quietly winning — which is what used to happen, decided by the order of a chain of else ifs that nobody could see.

One file serves both programs, and each takes the settings that apply to it. A IIgs and a IIc have their drives built in, so a disk goes in .disk1 on those and in a controller's entry on a machine with slots; a IIgs's only card is the block device in slot 7, since its serial and disk ports are choices in the Control Panel rather than cards.

One setting has no switch behind it, because it is a list rather than a value: .library is the disks the alt+1/alt+2 picker offers by name. Each entry has a name to show, the path to the file, and a writable flag saying whether the machine may write to it — absent, it is read only, which is what a library of originals wants. It is what makes flipping between a game's several disks a matter of arrowing down a list, and a boot disk kept read only can sit in it beside the character disk that must be written to:

.machine = "iie",
.library = [
    .{ .name = "Wizardry Boot", .path = "disks/wiz-boot.woz" },
    .{ .name = "Scenario Disk A", .path = "disks/wiz-a.dsk", .writable = true },
    .{ .name = "Character Disk", .path = "disks/wiz-chars.dsk", .writable = true },
],

The format is Ziggy, which is a small typed language for exactly this. What that buys over a hand-rolled reader is the part nobody enjoys writing: it knows the difference between a number and a string, it refuses a field that is not a setting rather than quietly doing nothing with it for the rest of the day, and it says where:

zig65: machine.ziggy:2:1: no such setting
zig65: machine.ziggy:5:11: the wrong kind of value for this setting

An editor can check a file while it is being written, because the format has a schema language of its own and .ziggy-schema at the top of this repository is that schema. The Ziggy language server walks up from a document looking for a file called exactly that, so it covers both examples and anything else in the tree; copy it beside your own config, or into any directory above it. Two tests keep it in step with what the program actually reads: it has to parse, and it and the Zig struct behind it have to describe the same settings, checked both ways.

One caveat worth recording, since it cost an afternoon to find: Ziggy's current releases require a development Zig, and this project is on 0.16. The commit pinned here, 0b61581, is the last one that declares minimum_zig_version = "0.16.0" — the one immediately before "bump up zig version". tools/config.zig is the whole of the reading.

That has one consequence worth knowing about the schema. The validator in this Ziggy has no notion of a field that may be left out: every ?bytes is a field a document must still mention, so checking the shipped examples against the schema here reports forty-eight missing settings they simply do not use. Upstream fixed exactly that — "fields of outer type ?, {:} and [] can be omitted" — and the release with the fix was pinned and tried: its own build.zig calls addPassthruArgs, which is Zig 0.17, and every version carrying the fix carries that requirement too. The file is still worth shipping, because what reads it is the editor's own Ziggy rather than this one, and those are ahead of this.

Disks

The Disk II goes in slot 6, and it needs two things: its own ROM, which is a separate 256 byte chip from the machine's and is what the machine hands over to when it starts from a disk, and something to put in the drives.

zig build run -- --rom apple2e.rom --disk-rom disk2.rom --disk1 game.dsk

--drives N says how many drives are on the card, 0, 1 or 2; the card only ever had two connectors, and two is the default. --disk1 and --disk2 put a disk in at the start, and alt+1 and alt+2 change one while the machine is running. They open a picker: a list of the disks named in the config file's library (see below), then a blank disk and a typed path, and — when there is already a disk in the drive — a way to eject it. A dot marks whichever of the library's disks is the one already in that drive, so a stack of them that go in and out of the same slot does not have to be kept track of by memory. The arrow keys move the highlight, return chooses, and escape closes it. A game spread across several floppies that go in the one drive is a keystroke apart this way rather than a retyped path each time. alt+shift+1 and alt+shift+2 skip the picker and put a new blank formatted disk straight in — a DOS 3.3 disk laid out the way INIT leaves one, volume 254, with no DOS on it and every sector free — which is scratch space to catalog or write to; nothing on disk backs it, so ejecting it loses it. The status line shows what is in each drive and which track its head is on, which is the quickest way to tell a machine that is reading a disk from one that is hunting for something that is not on it.

A .dsk or .do file is read in DOS sector order and a .po in ProDOS order. Nothing inside a bare disk image says which it is, so the name is all there is to go on. All three are the same 143,360 bytes: 35 tracks of 16 sectors of 256, with no header.

A 2IMG file says. Sixty-four bytes in front of the data give the mark 2IMG, which order the sectors are in, whether the write-protect tab was on, the DOS volume number if anybody wrote one down, and where in the file the disk itself starts — which is what makes the header a wrapper rather than a fixed prefix, since a writer may leave room for a comment in front of the disk. Everywhere a path is taken — either drive of either machine, and the hard disk — the wrapper is taken off if it is there and the file is read as a bare image if it is not, so nothing has to know which kind it was handed. src/twomg.zig is the whole of it, and it hands back a slice of the caller's own bytes rather than a copy: a disk written back to its file is written whole, and the header comes along unchanged. The header is believed over the name, which is the entire point of the format — a DOS-order image called .po reads correctly, and this is tested by making exactly that file and booting it.

One field is not believed: the data length. Sweet16, whose creator code WOOF is in a good many 800K images, writes a zero there and puts the size in the block count, so a reader that takes the length at its word hands the drive an empty disk — which is what this one did until a shelf of Print Shop disks turned up wrapped that way. A zero length now means "the block count says", and a zero block count "the rest of the file"; a stated length is still used as stated.

Recordings of the surface

A .dsk says what sectors are on a disk. A .nib says what the drive's head saw, which is not the same thing: address fields with the wrong prologue, sectors with no address field, a thirteenth sector hidden in the gaps, a track carrying two sector 3s that read differently. It is thirty-five tracks of 6,656 nibbles and nothing else — 232,960 bytes, and no header, so the size is what says a file is one. Writing works and saves back as nibbles.

A revolution does not hold 6,656 nibbles, though. A Disk II turns at 300 rpm, a revolution is 200 milliseconds, and a bit cell is four processor cycles, so about 51,150 bits go past the head each time round — 51,200 is the WOZ specification's figure for a track of unknown length, and it is the length a disk laid out here from a sector image is given, 6,400 nibbles. It used to be the nibble file's 6,656, which made every such disk turn at 288 rpm and every load from one run four per cent long; the Wizardry boot in Where the time goes was three quarters of a second of that. A nibble file's 6,656-byte track is taken at its word and read with a bit cell one thirty-second shorter, which is what the drive does with any disk written on a slowed drive, and what keeps a .nib turning at speed too.

WOZ: the bits themselves

WOZ goes one further and writes down what is actually on the surface: the bit cells, in order, as many of them as that track happens to hold. It came out of the Applesauce project in 2018, and it exists because everything above it throws away what copy protection was built on.

zig65 --rom apple2e.rom --disk-rom disk2.rom --disk1 "DOS 3.3 System Master.woz"

What the other formats throw away is timing and shape. A track is not a fixed number of bytes — it is however many bit cells went past in one revolution, which depends on how fast the disk was turning. A self-sync byte is ten bits long rather than eight. Some disks have a track between two tracks, or a track two tracks wide, or a run of bits that reads differently every time because nothing is recorded there and the drive's amplifier is listening to its own noise. None of that fits in a file of sectors, and a file of nibbles keeps only the second-hand version of it.

Reading it properly meant the drive had to change. It used to hold a disk as thirty-five tracks of 6,656 bytes and hand one over per read; it now holds each track as the bits it is, with its own length, and the controller assembles them the way the real one does — a shift register that takes a bit every four microseconds and says a byte is ready when its top bit comes round. That is a better model of the hardware for every disk, not only for WOZ, and it made something visible that had been quietly wrong all along.

The gaps had to become real self-sync bytes. A gap written as plain $ff is eight bits, so a reader that starts mid-byte stays mid-byte for ever and every field on the disk reads as a smear of two. Real formatters write $ff followed by two extra zero bits, and those two bits are what push a lost reader back into step within a byte or two. The old byte-at-a-time drive could not tell the difference and did not need to; the moment the shifter became real, disks laid out here stopped being readable until the encoder wrote proper self-sync. That is the sort of thing only the bit-level model can show you.

A head that has just been put down is not in step with the disk, so the first bytes it assembles are two neighbours smeared together. That is correct, and the tests now say so: what they check is not that every byte is a legal nibble — it should not be — but that the marks a sector begins with turn up, which is what the ROM is looking for too.

Verified against the WOZ reference images, which the format's authors publish for exactly this and order by difficulty. zig build woz runs them, in woz/, which is its own project for the same reason corpus/ is: the disks are other people's software, so they are fetched by the package manager rather than committed, and nothing that merely builds this has to pull six megabytes of them.

$ zig build woz

where to begin
  DOS 3.3 System Master.woz  [WOZ 1.0]   35 tracks, 104 positions (69 between), 665 marks
  DOS 3.3 System Master.woz  [WOZ 2.0]   35 tracks, 104 positions (69 between), 665 marks
cross-track synchronisation
  Take 1 (Baudville).woz  [WOZ 1.0]      35 tracks, 104 positions (69 between), 630 marks

The same title twice over is not a duplicate. The set ships most of its disks in both versions of the format, which makes it a test of the reader as well as of the drive: the disk is the same and only the file around it changed.

By default it reads: every image opened, its CRC checked, its chunks walked, and every track it holds put under a real drive head and read the way the machine reads it. That exercises the WOZ reader, the bit-level drive, the shifter, self-sync, the quarter-track map and the noise over unrecorded surface, and it needs nothing but the disks — which is what lets it run anywhere.

Booting them needs Apple's own ROMs, and this project ships none of Apple's code, so that is what --rom and --disk-rom are for:

zig build woz -- --rom APPLE2E.ROM --disk-rom 341-0027-a.p5 \
                 --disk-rom-13 DISK2-13sector.rom

Booting all forty-five takes about a minute and a half — it took the better part of an hour until the runner was found to have been building in Debug, see the benchmark notes — and --only SUBSTRING runs the one disk whose name matches, while --instructions N puts a shorter ceiling on how long each is given to get somewhere.

Each disk then carries what it did last time, and the step fails either way round: a disk that stops booting is a regression, and a disk that starts booting fails too, because the table is then out of date and that is news worth being told rather than swallowed. It is the same bargain the corpus runner makes with its known differences — the point is to notice a change, not to see a row of ticks.

What "boots" means there is coarse and is reported as what it is: the machine left the boot ROM and put something on the screen. The set's own criteria — "if you get to the main menu", "if you can start playing a game" — are not things a program can see.

Their readme is the scoresheet:

what it tests disks
where to begin DOS 3.3 System Master, The Apple at Play run
DOS 3.2 System Master runs, given the thirteen-sector PROM
copy protection Bouncing Kamungas, Commando, Planetfall, Rescue Raiders, Sammy Lightfoot, Stargate run
cross-track synchronisation Blazing Paddles, Take 1 run
a track two tracks wide Hard Hat Mack runs
half tracks The Bilestoad runs
soft-switch trickery Dino Eggs, Crisis Mountain, Miner 2049er II run
fake bits The Print Shop Companion runs
the lifespan of the data latch First Math Adventures runs
offset data streams Stickybear Town Builder, Wings of Fury run
non-standard bit timing Border Zone runs
flux rather than bits ProDOS User's Disk runs

Every disk in the set that is a disk to boot, boots. That is forty-five files rather than twenty-one, because most of these titles are shipped in both versions of the format and the same disk read out of a WOZ 1 and a WOZ 2 is two tests rather than one: the disk is identical and only the file around it changed. Three of the forty-five are not there to boot at all — second sides, which hold the game rather than the loader — and the set's last two files are 3.5-inch images, which want an 800K drive on an IWM rather than a Disk II and are refused by name rather than quietly misread.

Two of them are worth writing down because each looked like a failure and was not.

Wings of Fury's second side has no address fields anywhere on it, and that is the disk rather than the reader: track 0 holds sixteen distinct byte values in a repeating pattern, which is a side written by the game's own loader and owing DOS nothing. The runner used to treat a disk with no fields as this project having failed to read it, which is the right rule for a sectored disk and the wrong one here, so the table now says which disks have no fields — and checks that too, because a disk written down as having none that suddenly has some is also news.

DOS 3.2 in a WOZ 1 cannot say that it wants the other PROM. Thirteen sectors a track needs the earlier boot PROM, and which one a disk wants is a field that arrived with version 2 of the INFO chunk; a version 1 file simply has not got it. So where the file cannot say, the disk is asked instead: a disk with no sixteen-sector address fields at all and plenty of the thirteen-sector kind is a thirteen-sector disk, whatever its header does or does not manage to mention.

How long a bit cell is comes from the disk and not from the drive, which is not obvious and is the whole reason WOZ carries the number. Four microseconds was the rule, so people broke it on purpose: slow a drive down before writing and the bits come off a normal drive faster than they went on, and a bit copier that writes everything back at four produces a disk that reads differently from the one it copied. Border Zone is written at 3.5, says so in its INFO chunk, and is now read at 3.5.

Border Zone boots because of it, and two others came along: Planetfall and Miner 2049er II. Getting there needed something else fixed first, which is worth setting out because it is the least comfortable thing in this drive.

The shifter and the data register are two things. The shifter never stops: a bit goes in every bit time, and a zero shifted into an empty shifter leaves it empty, so a run of zeroes costs nothing and the next one bit begins a byte — which is why every legal nibble has its high bit set, and why the two extra zeroes in a self-sync byte put a lost reader back in step. When the shifter finishes a byte it hands it to the data register and starts the next one on that boundary, and the processor reads the data register.

Keeping those apart is what lets the head go on turning while a byte waits to be collected. A processor too slow to come back loses the byte, which is what a real one does; it does not lose its place, which a real one does not. The first attempt at this held the finished byte in the shifter itself, which meant choosing between dropping the bits that went past — losing alignment, and with it every disk in the set — and stopping the head, which kept alignment but cost Take 1, a cross-track disk measuring exactly the thing a stopped head breaks. Two registers need neither compromise.

The drive does not stop when software tells it to. The last of the twenty-one to boot, Bouncing Kamungas, sat for ever in a two-instruction read loop it had loaded to $0556, waiting on a byte from a drive this emulator had obediently stopped. Real hardware does not stop there. Sather, chapter 9: "The drive off/on signal is routed through one half of an NE556 timer. The effect of this timer is to delay drive turn-off until one second after a reference to $c088,x. This gives the drive apparent momentum, keeping it running after it is turned off."

That second is the reason the light on a real drive lingers after a CATALOG has finished printing, and the reason back-to-back accesses do not each pay for a spin-up. It is also a thing to read the disk through: turn the motor off and carry on reading, and what comes back is a drive winding down. The one thing it does not survive is reset — "Pressing RESET causes the delay timer to clear and turns off the drive almost immediately" — which is what already stopped the drive when a boot going nowhere was interrupted, and now has to keep doing it without the second getting in the way.

A second is not a fixed number of cycles, so it is not stored as one. It is 31,250 nibbles, a nibble being eight four-microsecond bits, and the same figure that scales how fast the disk turns scales the coast: on a IIgs, counting cycles nearly three times as fast, a second is still a second.

A IIgs also gets to decide whether it wants the second at all. What the card had soldered on, the IWM puts in bit 2 of its mode register — "when the current disk drive is deselected, the drive will remain enabled for 1 second if this bit is set" — so the card and the disk port ask the drives for the same thing and differ only in what they pass for that bit.

DOS 3.2 is worth a note, because it looked like a failure and was not. Thirteen sectors a track is a different encoding and a different boot PROM — five and three rather than six and two, and address fields marked $d5 $aa $b5 — and handing it the sixteen-sector PROM is handing it a card that cannot read it. It was not a guess which one it wanted: a WOZ says so in its INFO chunk, and the format's reference is explicit that the field is there to "assist emulators in determining which P5 ROM variant to load". So --disk-rom-13 supplies the earlier PROM and the disk picks it for itself.

Asking about the write protect tab loses the byte being read, and that is not a wart. The tab's answer goes into the same register that has been assembling a byte, so a program that asks part way through a nibble destroys what was there — and when reading resumes, the next byte begins wherever the head is rather than on the boundary the last one ended at. That is the only lever the hardware offers for reading the surface out of step with the bytes written on it, and the timing bits between nibbles then come through as data. It is what the "offset data streams" pair are testing.

The cross-track pair is the one the readme makes a point of: "they both do a crazy cross track check when they boot up. If you get to the main menu of these, then consider yourself a winner." Of Hard Hat Mack it says "being able to start playing a game means that you passed the checks". Both need the head's position to be carried across a track change in proportion — a disk written in one pass has its tracks lined up, and that alignment is the thing being checked.

Reading the register does not empty it. The set is explicit — "when a program reads data from the data latch, the implementation should not be clearing the latch" — and for a long time this one did, which left First Math Adventures a row that booted without the thing it tests being right.

What gives a read loop something to wait on is not the read but the hold running out. Sather, chapter 9: "When QA becomes set, the sequencer holds the data register long enough for a 6502 program to detect the valid byte with a seven MPU cycle polling loop. Then the data register is cleared and the next byte is shifted in from the disk." The loop it is built around is lda $c08c,x and bpl, four cycles and three. Once those seven microseconds are up the register follows the shifter again, so what a loop sees between bytes is a part-built byte with its top bit clear — which is exactly what bpl is branching on. It never needed the read to clear anything.

One refinement is the difference between working and nearly working: the hold is not counted down while the shifter is empty. Zeroes off the surface are a gap rather than the start of the next byte, so there is nothing coming to replace what the register is holding and it goes on offering it. A program reading across a gap is handed the last real byte rather than a hole where one used to be.

Seven microseconds are seven microseconds and not two bits. On a disk written at 3.5 microseconds a bit, more bits go past inside the hold, so the hold is counted in eighths of a microsecond — which is the unit bit_timing already measures a bit cell in.

What this costs is that sampling the register once a byte time no longer catches every byte: a sample lands inside the hold about a quarter of the time and otherwise finds the register part way through building the next one. That is not a loss. It is what a real one does, and it is why the ROM polls every seven cycles rather than every thirty-two.

Each of the last few took the same check to find, and it is worth writing down because it separates two questions that look identical from outside: the stream the drive hands the firmware is compared byte for byte against what a plain shifter reads off the same bits. Where they agree, nothing is being lost or misaligned on the way in, and whatever is wrong is somewhere else. That is what ruled the drive out for Bouncing Kamungas, which sat on track 0 reading two hundred thousand bytes without ever stepping the head — the bytes were right, and the fault was that they should have stopped coming a second after the motor did and instead stopped at once.

There is a stronger check than a plain shifter, and it is the card's own. The Disk II had a second 256-byte PROM at P6, wired up as what Sather calls "a little 2 MHz computer": four of its data outputs come back round to its address inputs through flip-flops, which is what makes it a state machine, and the other four address lines are the read pulse, the data register's top bit, and the two soft switches. Running it is running the hardware's own answer to what a stream of bits means. zig build woz -- --sequencer-rom PATH reads every track twice, once through this project's drive and once through that PROM, and compares the two byte for byte.

They agree on all but 2,742 of 6,658,281 bytes over 1,067 tracks, which is about two and a half bytes a track: the head coming down in the middle of somebody else's byte, which both readings recover from within a few bytes and neither is wrong about. A further 452 tracks are left alone rather than compared, because they have unrecorded stretches that the drive is supposed to invent bits over — a track that does not read the same way twice cannot be compared with anything, and pretending otherwise would only blunt the check.

Getting the PROM to say anything at all took reading the chapter rather than guessing at it. The four sequencing bits are not a nibble: "it uses a 256-byte ROM with four of its data outputs (D4—D7) connected through flip-flops to four of its address inputs (A5, A0, A6, and A7)". Every arrangement that looks sensible — the state as the low nibble, as the high nibble, permuted either way — produces a table that reads like a state machine and decodes nothing. The contents confirm the real one on sight: every one of the sixteen states in the quarter where $c08d has been touched and $c08f has not holds $0a, which is go to state zero and shift the write protect signal in, and that is exactly what asking about the tab does.

A position with no track on it is not silent. The head hears the amplifier's own noise, and the WOZ reference is explicit that an empty position should be "a synthetic length of 51,200 bits" that "continuously output[s] random data" — so a program that steps between two tracks to see what is there gets a stream of bytes that never mean anything, rather than a data register stuck at zero. The difference is the whole point of stepping there.

The head moves in quarter tracks, which is the other half of what a protected disk needs. The four magnets sit a half track apart, so one on its own can only place the head on a half track; leaving two adjacent ones energised at once pulls it to the point between them. That is not an accident of the mechanism — data written there is data an ordinary copier, which steps in whole tracks, never sees. What the head finds at each of the 160 positions is the file's business: a WOZ brings its own map, and the gap between two tracks is a position that holds nothing at all.

Some tracks are not bits at all. Version 2.1 of the format added the FLUX chunk, for disks where even a bit stream is more than the surface will honestly support: "it is now able to support flux transition timings with an accuracy of 125 nanoseconds". A flux track is a list of gaps — each byte is how many ticks of 125 nanoseconds passed since the last time the head saw anything, with $ff meaning carry on counting, so that "255, 255, 10 should be treated as 255 + 255

  • 10 = 520 ticks". The FLUX chunk itself is a second track map alongside TMAP, naming the positions whose track is stored that way, and the format says to prefer it where a careless file names a position in both.

Turning that back into bits is what a drive's data separator does, and the arithmetic is short: each gap is however many cells it comes nearest to, the transition falls in the last of them, and the ones before it held nothing. Thirty-two ticks is four microseconds, so the reference's own examples — "101 and 1001 bit sequences at approximately 64 and 96 ticks" — come out as one zero then a one, and two zeroes then a one.

No phase-locked loop is needed to chase the drive's speed, and it is worth saying why, because it looks like an omission. Every gap is measured from the transition before it rather than from the start of the track, so a drive that was running a shade fast puts each cell slightly out and none of it accumulates: the stream re-states where it is at every transition. It is converted once, when the disk goes in, and after that it is a track like any other.

WOZ disks are read only. Writing one back means the WRIT chunk and an account of where the write splice falls, and this only reads.

Auditing the machine itself

The corpus checks a processor and the WOZ set checks a drive. What sits between them — the language card's bank switching, the auxiliary memory soft switches, what reading a soft switch does as against writing it, and which ROM answers in the $Cxxx pages — has neither, and it is where an emulator's mistakes are both easiest to make and hardest to notice, because a machine with any of it wrong still boots.

Zellyn Hunter's a2audit is a bootable disk that checks exactly that, from inside the machine, and its aim is stated outright: "eventually, it should comprise a complete emulator test suite, enabling emulator writers to systematically identify and eliminate software-testable differences from real hardware." What makes it usable from a test rather than only by eye is that it reports each difference as a code — E000B and so on — documented one for one in the repository's v0/index.md.

A whole-machine test boots it on an enhanced IIe and reads the codes back off the screen, which has to be done as it goes: the suite calls HOME between groups of tests, so by the time it prints END everything it reported has scrolled away. The codes it finds are matched against a table, and that table fails in both directions, the same bargain the WOZ expectations make — a new code is a regression, and a listed code that stops appearing means the table is stale.

Everything it checks passes, and the table of known differences is empty. It was not always: E000B, the $Cxxx ROM test, stood in it, and it was a deviation this project had already written down in a comment before anything went looking for it — "every other address in here answers from the internal ROM whatever INTCXROM says, which is not what a machine with empty slots does but is what this has always done and what nothing so far has needed otherwise." a2audit was what needed it otherwise.

The $C100$CFFF range belongs to the slots, and three soft switches decide when the motherboard steals it back (Sather, 1985, p. 5-28):

INTCXROM SLOTC3ROM $C100$C2FF, $C400$CFFF $C300$C3FF
reset reset slot internal
reset set slot slot
set reset internal internal
set set internal internal

Where that says slot and the slot is empty, nothing drives the data bus and the processor reads the floating bus. This model answered from its own ROM instead, so a program looking for a card found one everywhere.

The third switch is the interesting one, because Apple never documented it. Sather named it INTC8ROM and describes it as "an unreadable soft switch, set by access to $C3XX with SLOTC3ROM reset, and reset by access to $CFFF or an MMU reset" — it is what lets the eighty-column firmware at $C300 reach the rest of itself at $C800 without setting INTCXROM and taking the whole range away from the slots to do it. This model had been folding it into "no card holds the $C800 window", which conflates two different states: a window nobody has claimed is not the machine's ROM, it is a floating bus.

Getting INTC8ROM's latch condition slightly wrong is worth recording, because a2audit caught that too. The condition is SLOTC3ROM alone — INTCXROM does not enter into it — and folding INTCXROM in leaves $C800 reading as ROM after the sequence STA $C007 / STA $C00B / LDA $C300 / STA $C006, which is exactly what the suite's data-driven table tries next once the first case is fixed.

One caution for anyone reading the same source: Table 5.4 on p. 5-22 lists INTC8ROM's set and reset conditions in the opposite columns to the sentence above. The sentence is what is implemented, because $CFFF releasing the window is the I/O STROBE' protocol that every card's firmware depends on, and the table would have it claim the window instead.

The video tests that follow the memory ones are not automatable and are not pretended to be: they put two modes on the screen and ask a person which looks right. The test skips them with the key the suite provides for it.

Testing the memory with no memory

misterblack1's Apple II Dead Test is a diagnostic ROM rather than a program: it replaces the F8 ROM at $F800 and runs a March-U memory test out of its own two kilobytes, using no RAM at all — not the zero page, not the stack. That is the point of it. Every other memory test needs working memory to run in, which is no use when the first bank is the broken thing, and this one beeps a bit number when it cannot even print.

For an emulator it reaches parts that a machine which boots never touches: what an address with no RAM behind it reads as, and whether the memory select blocks put the rows where the manual says. Two whole-machine tests run it, on an Apple II built in each of the nine configurations the 1978 manual gives select blocks for, and check that it reports the zero page and stack good and finds the right top of memory:

built with the ROM finds
4K $0FFF
8K $1FFF
12K $2FFF
16K $3FFF
20K $4FFF
24K $5FFF
32K $7FFF
36K $8FFF
48K $BFFF

It found one thing wrong on its first run, and it is a good illustration of how narrow the difference between two right-sounding answers can be. The ROM counts memory by writing $00 at the top of each 4K block and reading it back, on the stated assumption that "reads from empty locations return $FF regardless of what was written there". This model was answering with the video scanner's byte, so the ROM found 48K in a 4K machine.

Both are floating buses and they are not the same one. RAM reaches the processor through a latch, and for an address in RAM space the RAM SELECT' line goes low and that latch is gated onto the bus whatever is in it — with no row populated, "the RAM data output is floating when RAS' rises, because CAS' does not fall at any of the three rows of Apple RAM... the floating RAM output is interpreted as $FF by the latch" (Sather, 1983, p. 5-25). The video byte comes through at $C0xx instead, where RAM SELECT' stays high and it is the bus that floats. So an empty row reads $FF and a touched soft switch reads the screen, and this now says both.

The ROM loops for ever once it starts the full pass, which is what a dead test is for — you leave it running. The tests here stop at the banner, which is after the zero page, the stack, the page-error check and the RAM count and before the long pass overwrites the screen with its patterns.

Auditing the Mockingboard

a2audit's own readme points at Tom Charlesworth's mb-audit, which does the same job for the sound card: a program that detects what is in each slot and then works through the 6522s, the AY-3-8913s, the SC-01 and the SSI263s. It is deliberately deeper than an emulator strictly needs — "as it is intended as a full-correctness suite for emulators, then it goes deeper than what emulators currently need to emulate" — and it stops at the first failure, naming the component, the test and the sub-test.

It is assembled with ACME, which the devshell now carries, and run on a IIe with a Mockingboard in slot 4:

acme --report mb-audit.lst mb-audit.a      # writes `mb-audit`, which loads at $2000

It passes:

Slot #4 :Press CTRL+RESET (ESC to skip)
Reset tests passed

All tests passed (total=$000340)

Eight hundred and thirty-two tests, and getting there took nine fixes. Every one was a thing this project's own tests had agreed with, because they were written from the same misunderstanding — which is the whole argument for running somebody else's suite.

Four in the 6522's timers.

  • The counters did not run. They were stepped only while the timer was "running", which a one shot stopped being the moment it ran down. Real counters never stop: "after a time out, the T2 counters roll over to all 1's ($FFFF) and continues to decrement" (Western Design Center, 2010, p. 19), and it is the interrupt logic that a one shot disarms. Nothing detected the card at all until this was fixed, because the way to find a 6522 is to read a counter twice and check it fell by the cycles in between.
  • A loaded counter started a cycle early. "The counter will start counting down on the next PHI2 clock following the load sequence into high order T1 counter" (p. 17), so the fifteen cycles mb-audit counts out between the store and the read must show fourteen decrements. Each timer keeps its own mark now, and a catch-up will not drag a mark backwards — without that, the read of the interrupt line that follows every access on the same cycle handed the load's own cycle back as a tick and quietly undid it.
  • Timer 1 did not reload in one-shot mode. The datasheet describes the reload under the free run and then says "however" in a way that reads as though a one shot carries on decrementing instead. It does not: mb-audit loads $0280 in one-shot mode, waits for the underflow four times over and requires the high byte to read back as $02 each time. Only Timer 2, which has no latch to reload from, keeps counting through nought.
  • The reload was a cycle too eager. A counter spends one cycle at $FFFF before reloading, which is what makes a timer loaded with N repeat every N+2. T6522_A pins it down: load $0007 down to $0000 in turn, read the low byte six cycles later, and get $01, $00, $FF, $04, $02, $00, $01, $00. The $FF is that step showing through. It has to be remembered as a flag rather than read off the counter, because $FFFF is also an ordinary count and the two behave nothing alike.

Three in the AY-3-8913's bus.

  • A register read went to the wrong side of the port. The chip was putting its answer in the 6522's output register, and a program reading an AY points the data direction register at input first — an input pin reads the level on it, not what the output register holds. The chip drives the pins now.
  • The address latch took only four bits. The AY's address register takes the whole byte and only the low four name a register; the top four are compared against the chip's select pattern. Latch anything with a bit set up there and it stops answering, and a reset leaves it that way.
  • The bus never let go. The chip drives the data lines only while being read from a register it is pointed at; every other function leaves them in high impedance and the 6522 sees the pull-ups.

And two about reset, which is where the interesting one was.

  • Reset did not disable interrupts. The 8-bit core left the I flag clear. Reset is one of the hardware interrupts — the W65C816S datasheet lists RESETB at $00FFFC,D beside IRQB and NMIB — and "when an interrupt is executed, D=0 and I=1 in Status Register P" (Western Design Center, 2018, p. 30). The 16-bit core already had it right.

    This one is worth dwelling on, because it is exactly the sort of thing the corpus cannot see. Reset reaches the processor and the cards, but a card that was asking for attention is still asking — nothing about the line being pulled goes away. With the flag coming back clear, the first instruction the reset vector points at was interrupted before it finished and the machine went off to a handler instead of starting. It only shows when something is holding the line down across a reset, which is precisely the state mb-audit puts the machine in on purpose.

  • The reset line did not reach the card. A machine's reset went to the disk, the MIG and the memory card but not the Mockingboard, so a 6522 left with a timer running held the interrupt line down for ever. It does now — and it resets the way the RES pin does rather than by building the card again: "Reset clears all internal registers (except T1 and T2 counters and latches, and the SR)... T1 and T2, SR and the interrupt logic are disabled from operation" (Western Design Center, 2010, p. 36). mb-audit checks the exception by name, leaving $34 in T1's low latch and wanting it still there afterwards.

Two of mb-audit's stages cannot be judged by a program and are not pretended to be: it asks for a Ctrl-Reset, and it plays tones for a person to listen to. The first is a real reset of the emulated machine and the second is skipped with ESC, which the suite provides for exactly that.

The floating bus

An Apple II's video scanner reads a byte of RAM every cycle — all 65 of a scan line and all 262 lines of a frame, whether or not the byte is going to be displayed. That byte is the last thing to have been driven onto the data bus, so any address that nothing answers at reads it back: an empty slot's pages, the $C800 window with no card in it, a row with no RAM behind it, and every soft switch that is touched for its effect rather than read for an answer. On a IIe the status reads join in, reporting their answer in bit 7 and leaving the other seven to the bus.

This used to read as zero everywhere. It now reads what the scanner fetched, which is what makes the bus worth reading at all: it is how a program with no vertical-blanking switch to consult finds the beam, and it is why Sather warns that a program hunting for an auxiliary card must be "cognizant of the possible contents of display memory" (Sather, 1985, p. 5-38).

The address is Sather's, and every part of it is his. Table 5.1 (p. 5-8) says which scanner output drives which address line — A0A2 from H0H2, A3A6 from a four-bit sum, A7A9 from V0V2, and A10 upwards from the mode and the page — and the sum comes out of an adder in the IOU whose "equivalent adding circuit" is given on p. 5-9:

        1      1      0      1
              H5     H4     H3
   +   V4     V3     V4     V3
   ─────────────────────────────
   SUM-A6 SUM-A5 SUM-A4 SUM-A3

The constant is what makes the display start on a segment boundary: of the eight states of H5-H4-H3 only five are displayed, and 011 is where the first byte of a 40-byte section is addressed.

Two details of the counters matter and are easy to miss. The horizontal counter holds 65 states because it counts 0000000 and then 1000000 through 1111111, so H0H5 read zero for the first two cycles of every line and the byte there is fetched twice — which is why horizontal blanking covers 24 addresses in 25 cycles. And the vertical counter runs $FA$1FF rather than 0261, so the 192 displayed lines are $100$1BF and the 70 blanked ones are $1C0$1FF followed by $FA$FF.

Table 5.2 (p. 5-19) is the check on all of it, and a test asserts it whole:

HBL HBL
screen top last 16 of THIRD 40 and UNUSED 8 FIRST 40
screen middle last 24 of FIRST 40 SECOND 40
screen bottom last 24 of SECOND 40 THIRD 40
VBL last 24 of THIRD 40 UNUSED 8 and first 32 of FIRST 40

The last row is the one that wraps: horizontal scanning wraps at the 128-byte segment boundaries, so "the address scanned before address $400 is $47F" (p. 5-11), not $3F8.

In mixed mode the scanner switches to text addressing twice a frame. V4·V2 "identifies scan lines 160 through 191 and 224 through 261" (p. 5-19) — the first is the four text rows under the graphics, and the second is in the middle of the blanking, where it decides what is scanned and nobody can see it.

What is not modelled is the one-cycle lag Sather notes on that switch — "HIRES TIME switches low one video scanner clock after V4·V2 becomes true" — which moves the changeover by a cycle at the right-hand edge.

ShrinkIt archives

Most of what is on the Apple II archives is not a disk image at all. It is a .SHK or a .SDK — a ShrinkIt archive, from Andy Nicholas's 1989 archiver, in the format called NuFX — because that is what people used. A .SDK holds a whole disk; a .SHK usually holds files and sometimes holds a disk as well. Both are the same format, and everywhere a path is taken here takes either, unpacks it, and puts the disk in the drive:

zig65 --machine iigs --rom iigs01.bin --disk1 game.sdk

An archive that holds files rather than a disk is not an error worth a stack trace — it is the commonest thing a .SHK is — so it says so in words:

zig65: cannot read 'ProDOS 2.0.3.shk': that archive holds files rather than a disk

A disk that came out of an archive is write protected whatever --writable says. Putting it back would mean writing a ShrinkIt archive, which means compressing it, and this only reads; writing the disk over the top of the file it came out of would destroy the archive.

The compression is run-length encoding followed by LZW, in four kilobyte chunks — four kilobytes because a track of a 5.25-inch disk is four kilobytes, and this began as a program for compressing disks. That is also why every compressed stream starts with a byte holding a disk's volume number whether or not there is a disk anywhere near it. Two variants exist and both are read: LZW/1 starts its table again every chunk, and LZW/2 carries the table, the code width and the last code across chunks, starting again only when the table fills or a chunk turns out not to be worth compressing.

Two things about it are not in any description of the format and had to be read off real archives. Codes are packed low bit first — the other way round, the first nine bits of a stream come to a code nothing has defined, so that one announces itself immediately. And the code width goes up one code early, at 2^width - 1 rather than 2^width, which does not announce itself at all: the first chunk decodes perfectly either way and everything after it is wrong. It only looks early from the decoder's side. A decoder learns its table entry from the code before the one it is reading, so its table is always one behind the encoder's, and growing one code early is what keeps the two counting together — which is presumably why nobody writing the format down thought it worth a sentence.

There are two CRCs and they are not the same one, which is worth knowing before spending an evening on it. Both are CRC-16 with the $1021 polynomial. A record of version 3 puts a thread's CRC in the thread header, seeded $ffff, over the file's own bytes. LZW/1 keeps its own inside the stream, seeded zero, over the bytes padded out to a whole four kilobyte chunk, because that is what the compressor had in its buffer. A stream can have one, the other, both or neither, and src/shrinkit.zig checks whichever it is given.

None of this was guessed at twice. It was worked out against twenty-four real archives — nineteen LZW/2 and five LZW/1 — and every one of them decodes and matches its own CRC. src/diskfile.zig is the door all of it comes through: bare image, 2IMG or archive, the caller names a path and gets a disk.

A machine with the card fitted and nothing in the drive does what a real one does, which is not what anyone expects the first time: it sits there with the drive turning. The card's ROM waits for a byte off the disk and has no way of giving up — the wait is two instructions long, LDA $c08c,X and BPL back to it, with no counter and nowhere else to go — so a machine told to start from an empty drive waits for ever. The drive line says as much rather than leaving it to be guessed at.

alt+b resets out of it and lands at the BASIC prompt, which is what the reset key was for, and from there a disk put in with alt+1 starts with PR#6. That is the same sequence a real machine wanted.

Disks are write protected unless --writable says otherwise, and only then are changes saved back over the file, when the disk is taken out or the emulator is closed. That is every drive any of these machines has: the two on a Disk II card, the 3.5-inch drive in a IIc Plus's case, the intelligent one on a IIc's or a IIgs's port, and both of a IIgs's own. All but the first pair used to keep what was written and drop it on the way out, which was a quiet way to lose a morning's work.

The IIgs is the exception to --writable, which is a switch the eight-bit front end has and it does not: there a disk is writable unless the image itself says otherwise.

The writing lives in src/writeback.zig rather than in either front end, since both of them need it and neither can be built inside a test: a session is brought up with every field assigned by hand. What these take is somewhere to read and write, a path and the drive, which is what makes them checkable.

The new file is written beside the old one and moved into place, so an interrupted save cannot leave a disk that is neither, and it is read back and re-opened first so that the disk goes inside whatever wrapper it came in — a 2IMG keeps its header, because what is written out is the whole file and the disk is a slice of it.

What the drive is handed is not what is in the file. A floppy can only hold a bit pattern with the high bit set and no two zeroes in a row, so the bytes are re-encoded six bits at a time into the sixty-four patterns that qualify, laid out as a track of address and data fields with the gaps between them, exactly as a real disk is written. That is what makes a disk readable by the machine's own ROM and by DOS rather than by anything here: nothing in this understands files, catalogues or sectors on the machine's behalf.

Keys are handed over no faster than the machine will take them. A real Apple has no type-ahead — the keyboard is a single latch, and the firmware clears it on the way in to reading a line — so a key put there before the machine is ready is simply gone. Rather than lose the first character of every line typed at speed, the debugger waits until the machine is properly sitting in a read loop, asking for a key and coming away empty, before handing one over.

The processor is always in one of three states. Running runs the machine at a speed asked for in cycles a second, starting at the 1.0205MHz a IIe's own crystal gives, doubling and halving from there with alt++ and alt+-, and back to where it started with alt+0. Plus and equals both double, since they are the same key and reaching for shift to make a machine go faster is a nuisance. Step executes one per press of alt+space. Halted is reserved for a JAM opcode: the processor has genuinely stopped fetching and only a reset gets it back, and the status line names the opcode and the address it happened at.

A BRK does not halt, because by then the processor has pushed its state and vectored and is in perfectly good order; it stops somewhere that can be continued from. Not on an Apple III, though: there BRK is how a program calls SOS — the call number and the address of the parameter list follow it — so the III runs through them like any other instruction, or SOS would stop at its first call.

alt+b does not stop the machine. The reset key was the way out of trouble without reaching for the power switch, and a machine that was running goes on running from the reset vector rather than waiting to be told to run again. A halted one is the exception and comes back stopped, since a JAM is worth looking at from its first instruction rather than its thousandth, and halted is the one state a processor cannot be started out of.

Speed is counted in cycles rather than instructions, because an instruction is not a unit of time: a 6502 takes anywhere from two to seven cycles to do one, so a machine paced by instructions runs at a speed that depends on what it happens to be executing. Cycles are what the crystal counts, which makes a real machine's speed something that can be asked for exactly.

The status line shows both what was asked for and what is actually being managed, because the two part company at the top end. On the hardware this was written on the machine runs at its 1.02MHz for about four percent of one core, and flat out reaches something over 110MHz, or a hundred times the machine it is pretending to be, at about two thirds of a core. Asking for more than that simply reads back as less: the second figure is the honest one.

Time is turned into cycles once a frame and the cycles owed are then run in one go, rather than the emulator waking for each instruction. At a megahertz the latter is a million trips through the system call that sleeps, each to do a few nanoseconds of work, which is a good way to spend an entire processor on not emulating. Whatever a frame could not keep up with is given up rather than carried, since a backlog only makes the next frame later still.

A hard disk

A ProDOS block device in slot 7, which is the easy kind of disk: a Disk II hands the machine a stream of bits and leaves it to find the sectors, while a block device is asked for block 1234 and produces it. No encoding, no head to move, no timing.

zig build run -- --rom apple2e.rom --hard-disk volume.po

Unlike the other two cards this one needs no ROM, because its firmware is ours. The protocol is published and short enough to write: four bytes in the card's page say what it is, the last byte of that page says where to call it, and the driver copies what ProDOS left in the zero page into the card and tells it to go. The card moves the five hundred and twelve bytes itself, which is what a real one does with its own hardware and why a block device is so much faster than a floppy.

The card has two drives, ,S7,D1 and ,S7,D2, because that is how ProDOS counts and how every real controller is built. It is not a detail that can be waved away either. ProDOS 8's own startup, having found the volume it booted from, writes two entries into its device list for the booting slot — the unit it booted and that unit with bit 7 flipped — and then unstacks what it pushed on the assumption that it found both. A card declaring a single volume leaves that arithmetic one short, and the return that follows lands in the weeds: with one drive the card boots an Apple IIe by luck and a IIgs not at all, and with two it boots both. A drive with nothing in it is offline, which is what a controller with one disk attached says about the other.

PR#7 starts it. An Apple II will not do so on its own: the four bytes that say what the card is have $01 where a Disk II has $3c, and it is the $3c that the machine's own startup goes looking for. Real cards ship firmware both ways round for exactly this reason; this one says what it is. A IIgs is different — it scans its slots and boots the first thing that answers — so there the same card boots without being asked.

That fourth byte is $00, and what it means took some finding. It does not mean "an ordinary block device": it means a SmartPort, and that is not a reading of the manuals but of the machines. The IIgs's own 3.5-inch firmware in slot 5 has $00 there, and so does the IIc's, and so does every real card image to hand, including an Applied Engineering RamFactor — which is now a card in this machine, and see A megabyte in a slot — and each of them answers SmartPort calls at three bytes past its ProDOS entry point. ProDOS 8 believes them: from version 1.9 on it makes a SmartPort STATUS call as soon as it has finished its first ProDOS call to a device that claims to be one. This card had nothing there for a while, and a IIgs went into its monitor one instruction after ProDOS had finished printing its banner.

So it has a SmartPort interface now, which is the second way of asking the same card about the same disks. The entry point is the jump at $Cn0e, three bytes past the one ProDOS calls, and it is why both of them are jumps rather than the routines themselves. A caller puts the command and a pointer to its parameters inline after the JSR, so the routine has to step the return address over them on its way back; that stack work is the only part done in 6502, and it is thirty-one bytes. Everything else is the card's, as the block transfers already were: it reads those three bytes itself, does the work, puts the return address back three further on, and leaves the error where the firmware can pick it up.

It answers STATUS, READ BLOCK, WRITE BLOCK, FORMAT, CONTROL and INIT. The four calls that belong to a character device get "bad command", and so do the extended forms — the ones with bit 6 set and four-byte addresses — because this card does not claim to be an extended SmartPort and $CnFB says so.

The shape of the device information block came out of the IIgs's ROM rather than out of a book. At $ff58fd, just past an RTS, sits the constant part of what its own 3.5-inch drive answers with: 08, then DISK 3.5 padded to sixteen characters with spaces, then 01 c0 00 10. That is a length, a name, a device type of $01 for an Apple 3.5-inch disk, a subtype whose top two bits say the device is an extended SmartPort with removable media, and a version of 1.0. This card is neither extended nor removable, so its subtype is $00; it calls itself HARD DISK; and its type is the one thing here that is a choice rather than a fact, since $01 is the only code the machine will vouch for and $02 is the ProFile-shaped hard disk by the usual reckoning.

The volume is write protected unless --writable, as disks are, and is saved back over the file when the emulator closes if the machine wrote to it.

A megabyte in a slot

The other kind of disk that is not a disk. An Apple II has sixteen address lines and every one of them is already spoken for, so a card carrying more memory than the whole machine can address cannot show it to the processor; it has to be looked at through a window. Three registers hold a twenty-four bit address, a fourth is the byte at it, and the address steps on by itself each time that fourth register is touched. A program moves a block by setting the address once and reading one address five hundred and twelve times.

What comes out of that is a RAMdisk. --ramfactor-rom fits an Applied Engineering RamFactor in slot 5, --ramfactor says how much memory is on it, and ProDOS finds /RAM5:

zig65 --rom apple2e.rom --disk-rom 341-0027-a.p5 --disk1 prodos.dsk \
    --ramfactor-rom "ae ramfactor rom v1.4.bin" --ramfactor 1024K

None of the RAMdisk is here. The card's firmware is Applied Engineering's eight kilobytes and it holds all of it — the ProDOS driver, a SmartPort interface, DOS 3.3 and Pascal support, and a partition manager that PR#5 brings up — so what this had to get right was the hardware underneath, and the way to find out whether it had was to run their code on it. It runs. PR#5 puts up RAMFACTOR PARTITIONS ... SLOT = 5 on a IIe and on a II Plus; ProDOS links /RAM5 in and counts 2,048 blocks on a megabyte card, 512 on a 256K one; and a program saved to it loads back and runs. All four firmware revisions to hand behave the same.

The card is not Apple's, and the reason is that Applied Engineering wrote down what it does. Apple's own Apple II Memory Expansion Card came first and these are the same address and data registers — "RamFactor was designed to be totally compatible with virtually all software written for the Apple II Memory Expansion Card" — but the RamFactor's manual has a chapter for programmers and Apple's is hard to come by, and four of the RamFactor's own firmware ROMs were to hand to check the reading against. Software of the period calls a card at these addresses a Slinky whichever one it is.

Three of the four things worth knowing about those registers are surprising, and all three are in one paragraph of that chapter (Applied Engineering, n.d., Chapter 6).

The address registers read back, and the high one lies. "If the card has one Megabyte or less, reading the high address byte will always return a value in the range $F0-FF. The top nybble can be any value when you write it, but it will always be 'F' when you read it." A megabyte wants twenty address lines, so four of that byte's eight go nowhere, and lines nobody drives read as ones. That is not a curiosity to model for completeness: it is how the firmware tells a card of a megabyte or less from a larger one.

The counter ripples rather than adds. "Whenever the lower or middle address byte changes from a value with bit 7 = 1 to one with bit 7 = 0, the next higher byte is automatically incremented." Three counters in a chain, and the chain cannot tell a count from a write: storing $00 over a low byte holding $80 carries into the middle byte exactly as counting past $ff would, even though the address has gone down. Hence the manual's instruction to "always load the bytes in the order low-middle-high, and always load all three of them" — in that order every spurious carry lands in a byte that is about to be overwritten anyway.

The registers are asleep until the firmware page is touched. "After power up or Control-Reset, the registers on the card are all in a disabled state. They will be enabled by addressing any address in the firmware page $Cs00-CsFF." Nothing that goes about the card properly ever notices, since the firmware is reached through that page. What it protects against is a stray write to the slot's I/O quietly rewriting a megabyte.

The fourth thing is the one the manual does not settle, and the firmware does. $C08F + slot * 16 is called "Firmware Bank Select" and nothing more is said about it, but the ROM is eight kilobytes and the window at $c800 is two, so something has to choose. What the firmware does is store $FF to select the second bank and come back to the first with an ASL of the same address — which reads whatever is there and writes it shifted. An ASL leaves bit 0 clear whatever it read, and that only reliably selects a bank if bit 0 is the bank. So bit 0 is the bank, and the idiom is the evidence.

The whole ROM image falls out of one rule: the card decodes the twelve low address lines and that bank bit and nothing else. $Cs00 reads at bank * $1000 + s * $100 and $c800 at bank * $1000 + $800. So the first page of the image can never be read at all — it would be $c000, which is not a slot — and Applied Engineering put their copyright and the names of the two people who wrote it there. The seven pages after it are the seven slots' firmware pages, assembled seven times over because a JMP inside one has to name the slot it is in. The two banks differ only above $800, which is what makes it safe for the code that copies the partition manager out of the second bank to be running from its own slot page while it does so.

Slot 5, because the card's own manual puts it there or thereabouts — "in any expansion slot except slot 3", and "if you will be using Pascal 1.3, install the RamFactor in slots 4, 5, or 6" — and because slot 5 is the one slot this machine has nothing it must be. That makes the volume /RAM5, which is what the card was called on the machines that had one. A slot holds one card, so a machine has either this or the clock and not both.

Memory in a card comes in blocks of eight 256K chips, so a card holds 256K, 512K, 768K or a megabyte and --ramfactor takes one of those. The firmware works out which for itself, by writing a marker at the foot of each block on the way down and reading them back on the way up: a block that is not there fails to hold what was written, because past the last chip nothing drives the lines. All four sizes come back right, which is a better check of the address decoding than anything that could be written here.

A RAMdisk lasts as long as the machine is switched on, and that was true of the real card too — until you bought the RamCharger, which was a battery for it. --ramcharger PATH is that battery: the card comes up holding whatever PATH holds and writes it back when the emulator closes, so a file saved to /RAM5 in one run is still there in the next.

Sound

The speaker is one bit. Every access to $c030 flips the cone, and that is the whole of the hardware: no volume, no waveform, no timer. Everything a program plays it makes by choosing when to flip, so the only thing that carries any sound at all is the timing, in processor cycles. A machine can therefore be told to keep the cycle of every flip:

zig65.memory.AppleIIe(.{ .speaker_log = true })

and speaker.Renderer turns those into samples. It does not ask what the cone was doing at each sample; it adds up how long the cone spent each way round inside the sample and divides. That distinction is the whole of the fidelity. A sample at 44.1kHz is about twenty-three cycles, and a great deal of what these machines play is pulses shorter than that: looking once a sample misses them entirely and plays silence where there was a click, while adding up the time puts the right energy in the sample wherever inside it the pulse fell. A gentle high pass then takes out the constant part, since a cone cannot hold itself out and drifts back.

--audio PATH records what the speaker plays, as a WAV:

zig build run -- --rom apple2e.rom --audio session.wav

The machine is held at its own 1.0205MHz for as long as it is recording, and the speed keys are refused: sound played faster or slower is not the sound the machine made. Nothing stops the file growing — about five megabytes a minute, until the emulator is closed, which is when the sizes in the header are put right.

There is no playback yet. The debugger still rings the terminal bell when the speaker moves, which is all a terminal can do with it.

An Apple IIgs's sound

A IIgs makes its sound somewhere else entirely — an Ensoniq 5503 with thirty-two oscillators and sixty-four kilobytes of waveform storage of its own — and --machine iigs records it the same way:

zig65 --machine iigs --rom "Apple IIGS ROM 01.bin" --disk1 system.2mg --audio session.wav

One channel, because a IIgs has one speaker. The chip has sixteen output channels and a stereo card decoded three of the bits that name them, with even channels going to the right and odd to the left, but that was a card people bought rather than part of the machine and there is no card here.

The awkward part of recording this chip is that its own rate moves. It services one oscillator at a time at 894.886kHz, so how often any of them is looked at depends on how many are switched on: with all thirty-two running it goes round them 26,320 times a second, and with eight it is 89,489. A file has one rate and cannot move. So the recorder winds the oscillators forward to each sample's own moment and then asks what they came to, which reconciles the two in the direction that is always right — asking averages every pass since the last ask, so the fast case is a box average over exactly the right window rather than a value plucked out of the middle of one, and the slow case holds the last level, which is what the chip's converter does anyway.

That is also why the recorder does the winding rather than doing its asking afterwards. The machine catches the chip up on scan-line boundaries, and a recorder handed a chip already wound forward a whole line would get one sample's worth of sound per line and hold the level in between — a file saying 44.1kHz that was really at 15.7.

Because a sample's moment is worked out from the machine's own clock, a machine running slower than life still writes a file that plays at the right pitch and the right speed. It only takes longer to write it.

Printing

The parallel printer card is the simplest card the Apple II had, and slot 1 is where it went. There is no chip on it worth the name: eight data lines, a latch holding what was last put on them, and a strobe that goes out when the latch is written. Its manual gives the whole of the hardware in a sentence — "if data is stored at location $C080 + $N0, where N is the slot number, then the data will appear on Printer Board Data Lines DP0-DP7, and will remain until the next STORE instruction to that location is executed" — and the way to drive it from BASIC, which is POKE -16256 + N*16, DATA.

--parallel-out PATH fits the card and makes that file the printer:

zig65 --rom apple2e.rom --parallel-out session.txt
]PR#1
]PRINT "HELLO"
]PR#0

and session.txt holds

]PRINT "HELLO"
HELLO

]PR#0

which is what the printer would have printed, echoed commands and all, because PR#1 points the machine's output vector at the card and everything the machine prints goes there until PR#0 points it back.

The card's own 256 bytes of firmware are ours rather than Woz's, on the same footing as the clock card and the block device: what the card has to do is published, so nothing has to be supplied for it to work. It does the two things the manual says the card does. It strips the high bit, because an Apple II sets it on every character it prints and a printer wants seven-bit ASCII — the real card had a jumper and this is the setting anybody printing text used. And it adds a line feed after each carriage return, because "the Apple II adds a Carriage Return to the end of every line, and the Parallel Printer Card adds a Line Feed character to the end of each line". Neither happens to a byte poked straight at the latch: the hardware is eight bits wide and passes what it is given, which is what a program dumping the graphics screen depends on.

Nothing waits. A real card has an acknowledge line the printer raises when it has taken a byte, and this one always has: what is on the end of the cable is a file, and a file does not run out of paper.

In the library the card takes a std.Io.Writer and the caller owns it — opening, buffering, flushing and closing are all the caller's, and a card with nothing attached is a printer switched off rather than an error, which is exactly what the machine would see. src/parallel.zig is the whole of it.

var card: zig65.parallel.Card = .{ .fitted = true, .out = &writer };
machine.memory.printer_card = card;

A IIc has no slots and so cannot take one. Its printer port is a second serial port, at the addresses a card in slot 1 would have used.

The Corvus Omninet Transporter

The card that puts an Apple II on Corvus's network, and through it on a Corvus disk. It is not worked the way the disk cards are. A Transporter has a processor of its own and is told what to do in whole commands: the host lays a command vector down in its own memory and hands the card that vector's address, and the card reaches into host memory itself — it is a DMA device — does the work, and writes a return code where the vector said to (Corvus Systems, 1984, printed p. 30).

The whole host interface is one register, which is worth saying because the manual does not say it. Being written to be host-independent it describes a READY line and a STROBE and leaves where they live to each computer's own book. The card's own $Cn00 PROM answers that, and answers it by working the address out rather than by carrying it: it finds which slot it is in the way slot firmware always has — LDA #$60, STA $00, JSR $0000, then the return address off the stack — and then turns the page it is running from into the card's address arithmetically.

LDA $01         ; the page, $C7 in slot seven
ASL             ; four times, so $C7 becomes $70
ASL
ASL
ASL
ADC #$80        ; $F0 — which is $C0F0

Three bytes written there are a command vector's address, most significant first, with the READY line read back from the same place to know when the next one may go. That PROM is Corvus's code rather than something this can build, so it is handed in the way a machine's ROM is, and the card goes where the network software looks for it: "The network interface card should be in slot 7, as described in Chapter 1" (Corvus Systems, 1987). That is the block device's slot as well, and only one card fits in a connector, so fitting either takes the other out.

All seven commands are recognised and four are answered outright: Who Am I and Initialize give the node number, and Setup Receive and End Receive arm and disarm a socket, refusing any but the four that exist. The other two want somebody on the wire, and the honest answer there is not an error of this card's — a network with one node on it is exactly what "transmit failure, no acknowledgement after the maximum number of retries" looks like from the inside, so that is what Send Message and Echo return, with a node number that cannot exist refused before it is tried.

What is not here is the far end. Nothing answers on the network, because there is nothing on it: an Omnidrive would have to be modelled for any of this to reach a disk, and that has deliberately been left for later.

The serial card

A Super Serial Card in slot 2, which is a 6551 and two kilobytes of firmware. The firmware is a separate ROM again, so --serial-rom fits the card, and --serial-in and --serial-out are the two directions:

zig build run -- --rom apple2e.rom --serial-rom 341-0065.bin --serial-out sent.txt

PR#2 then sends everything the machine prints down the line, and IN#2 reads what is coming in as though it were typed. Bytes go in at the rate the card was set to rather than as fast as the machine will take them, since a card that hands over its input the instant it is asked is not a serial port at all; a byte written takes the time the rate says it takes, which is what software that drives a port by counting is waiting for.

Two things about the card are worth knowing. Its interrupt line is a jumper on the real thing, and it is normally out, so it is out here: the 6551 comes up with its receiver interrupt enabled and no firmware puts a handler anywhere, so a card wired in pulls the line the moment a byte arrives and takes the machine to pieces. setSerialInterrupts puts the jumper in for a caller that wants it. And nothing is sent to the machine until it first reads the status register, because a byte handed over before the firmware opens the port is thrown away by the programmed reset the firmware does on its way in, and a file would lose its first byte for no reason a person could see.

The cassette port

Two lines, each one bit wide. What goes out is the speaker over again: touching $c020 flips the output line, and a program writes to tape by flipping it in patterns, so the timing is the whole of what reaches the tape. It uses the same record and the same renderer as the speaker, because it is the same problem. What comes in is simpler still: the machine's input is a zero crossing detector, so reading $c060 says which side of zero the signal is on and nothing finer than that ever reaches the processor.

zig build run -- --rom apple2e.rom --cassette-out tape.wav
zig build run -- --rom apple2e.rom --cassette-in tape.wav

With no disk card fitted, SAVE and LOAD in Applesoft mean the cassette, and a tape written by one run loads back in another: the file is a real Apple tape, a ten second header of 770Hz and then the data. alt+t winds the tape back and plays it again, which is the nearest a file gets to pressing play on a recorder. Neither flag holds the machine to its own speed, since both are worked out from the machine's clock rather than the wall.

Both work at whatever speed the machine is being run at. A tape is paced by the machine's clock rather than by the wall, so a machine running sixty times over reads a tape sixty times as fast, exactly as the program reading it does. Loading has been checked at eight times, at sixty-four times, and with no pacing at all, where the whole thirty-two second tape goes by in a fraction of a second.

An Apple-1 has no cassette port of its own. Its port is on a card, the Apple Cassette Interface, and --aci-rom fits the card with its 256-byte PROM:

zig build run -- --machine i --rom wozmon.bin --memory 8K --aci-rom aci.rom \
    --cassette-in apple1-basic.wav

The card's program is entered from the Monitor with C100R, prompts *, and takes the Monitor's own address form: E000.EFFFR reads the tape into the second bank, and E000R afterwards brings up Apple's BASIC and its >. What the card does is settled from the schematic in Apple's leaflet (Apple Computer Company, n.d.): the PROM answers for $c000 to $c1ff, every access to the lower half of that clocks the flip-flop that drives the tape, and the input comparator is gated into the PROM's A0 line, so a read of $c081 returns one PROM byte while the signal is high and its neighbor while it is low. That is the whole of what the processor is told about the tape, and "All the ACI timing is done in software", so the model has no notion of tones or baud rates: the program measures the tape against the machine's clock, refresh stalls and all, and the tape is paced by that same clock. The BASIC tape loads byte for byte against an independent decode of the same WAV.

As a library

zig65 is a library as much as a program, and the two front ends in this repository — the eight-bit debugger in src/main.zig and the IIgs in src/gs.zig — are only its first callers. root.zig exports the cores as CPU(Clock, Memory), the memory models that make each machine, and the parts around them — disks, video, sound, the cards — with no terminal anywhere in them. Anything that can hold a machine and step it can embed one, and the sections below are the surfaces it does that through. Two uses show what it is for.

merle65, a Merlin-syntax cross-assembler for the 6502, 65C02 and 65816, embeds zig65 for two jobs. The first is as an oracle: an assembler is all details, and where the Merlin manuals leave one unsaid, merle65 runs the Apple II versions of the real Merlin inside an emulated machine and reads the answer back off the screen and out of memory rather than guessing — the emulator standing in for a shelf of Apple IIs nobody has to keep plugged in.

The second is the USR pseudo-opcode, which the emulator does not check but performs. USR hands its line off to a user-written 6502 routine that generates the output the line assembles to, so a cross-assembler running on no Apple II still has to run that routine somewhere; merle65 runs it under zig65, the user's program executing on an emulated machine and emitting exactly the bytes it would on a real one. Here the emulator is not consulted — it is the implementation.

zig65 leans on itself the same way, and the IIgs is the proof. A IIgs is an Apple IIe underneath: a chip called the Mega II answers its soft switches and owns the slow banks $e0 and $e1, and that Mega II is not a second implementation of a IIe written for the occasion. It is the very memory8/apple2e.zig the eight-bit machine runs, instantiated with a IIgs for a host — apple2e.AppleIIe(.{ .host = .given }). The compatibility a IIgs owner took for granted is the eight-bit library reused whole, which is why a fault mended in the IIe is mended in the IIgs in the same breath, and why the story of how it was found out is so often one machine's.

Reading the screen

Something embedding this rather than driving the debugger wants two things from the display, and both are on the machine model:

const mode = cpu.memory.mode();     // .text, .text_80, .low_resolution, ...
try cpu.memory.writeText(&writer);  // the characters on it

mode says what the display is doing, and mode.columns() gives forty, eighty, or nothing at all for a screen showing graphics. writeText writes what is on a text screen in the order somebody looking at it would read it, twenty-four rows run together with nothing whatever between them, so the caller slices it by the column count the mode gave. A screen showing graphics writes nothing.

For a screen showing graphics there is video.zig instead:

var line: video.Line = .{};
switch (video.scanLine(&cpu.memory, y, &line)) {
    .dots => {},              // line.dots and line.colours are filled in
    .text => |where| {},      // this row is text: where.row and where.line
}

One line at a time, because that is the unit the hardware makes and the unit a program switching modes halfway down the screen is working in. The line is filled in either way — text included — and the answer says which it is, so that a caller which can draw characters better than a bitmap can, a terminal for instance, knows when to. line.dots is the 560 dot positions the machine actually puts out, which is what a monochrome monitor shows; line.colours is one palette index per dot, video.palette turns those into RGB, and both are the same signal seen two ways. See Colour out of nothing for how the second comes out of the first.

Neither video.zig nor superhires.zig knows what a picture is, which is deliberate: a program embedding this to run a machine should not have to drag a graphics library along. picture.zig is the one file that does, and it is where the dependency on z2d stops:

try picture.write(io, gpa, .apple2, &cpu.memory, "screen.png", .{});

.apple2 or .super_hires says which of a IIgs's two displays to draw — ask the machine which is on the screen with superHiRes — and picture.paint fills a z2d.Surface for a caller that wants the pixels rather than a file. The lines are doubled by default because a dot on neither machine is square, and it repeats rather than interpolating, which is a decision rather than laziness: the dots are what the machine made, and smoothing them is a judgement about how the picture should look that belongs to whoever is looking at it. .monochrome = true writes what a black-and-white monitor shows instead, which is much easier to read text on.

The characters are the machine's, decoded: a text page byte is not ASCII, and neither is what a program stored. One character is not ASCII either way, since where ASCII has delete the machine's character generator has a solid block, and the firmware leaves that block where the cursor is; it comes back as $7f so that the cursor can still be found. Inverse and flashing are not represented — this is the text, not the way it is drawn. screen.decode is there for a caller that wants both.

examples/typing.zig is the whole of it working together, and runs against a real ROM:

zig build example -- apple2e.rom

It brings a machine up, waits for the BASIC prompt to appear, types a program in, waits for the answer, and prints the screen; then switches the machine to eighty columns and does it again. It is about a hundred lines, most of them the two helpers worth having: one that runs the machine until something it has printed says so, and one that types at it the way its keyboard works. That second one is the part worth reading before writing your own. The machine has one keyboard latch and no queue behind it, and the firmware clears the latch on its way in to reading a line, so keys written as fast as a program can think of them mostly vanish; each one has to wait until the machine is asking.

Making a disk

dos33 puts a DOS 3.3 disk together, and reads and writes the files on one. It works on the plain image a .dsk file holds rather than on the encoded surface a drive reads, so a disk can be built here and handed straight to disk2.Floppy.load for the machine to use, or opened from one that disk2.Floppy.save has written. Nothing in it allocates: the caller owns the image and every operation works in place.

var image: [zig65.dos33.image_size]u8 = undefined;
var disk = try zig65.dos33.Disk.format(&image, 254);

const prefix = zig65.dos33.binaryPrefix(0x0300, code.len);
try disk.writeParts("SHAPES", .binary, &.{ &prefix, code });

var files = disk.catalog();
while (files.next()) |file| {
    // "SHAPES B 004", the way CATALOG prints it
    std.debug.print("{s} {c} {d:0>3}\n", .{ file.name(), file.kind.letter(), file.sectors });
}

try disk.read("SHAPES", &writer);

format lays the disk out the way INIT does: a volume table of contents, a chain of fifteen empty catalog sectors, and every sector free but the one the catalog is on and the three DOS itself lives on. Those three are left as they are found rather than zeroed, so a caller who has copied an image of DOS into the first dos_tracks * bytes_per_track bytes keeps it and ends up with a disk that boots. Nothing here writes DOS for you, and a disk without it holds files perfectly well for a machine that booted from something else.

A disk on the file system is read in one piece, worked on in place, and written back, which is all openFile and saveFile do:

var disk = try zig65.dos33.Disk.openFile(.cwd(), io, "game.dsk", allocator);
defer allocator.free(disk.image);
try disk.write("NOTES", .text, notes);
if (disk.dirty) try disk.saveFile(.cwd(), io, "game.dsk");

The methods that write take a *Disk and the ones that only read take a *const Disk, so what a call will do to a disk is visible at the call. The exception is sectorAt, which hands back a writable sector whichever it is given: the image belongs to the caller and this only points into it.

dirty says whether anything has been written since the disk was opened or last saved, so a caller knows whether writing the image back is worth doing. format leaves it set, since a disk just laid out is not what any file holds, and saveFile clears it. Reaching in through sectorAt is not noticed: a caller working at that level sets it themselves. Nothing else in the module touches the file system, and a Disk is a slice and a flag, so one can be built entirely in memory and handed to Floppy.load without a file existing at all.

write puts a file on, over one of the same name if there is one, the way SAVE writes over a program; writeParts does it from several pieces laid end to end, which is what a file with a header in front of it wants. binaryPrefix and basicPrefix build the headers BSAVE and SAVE write. read writes a file's bytes to a *std.Io.Writer, delete takes one off and gives its sectors back, and setLocked is LOCK and UNLOCK.

The bytes handed over are the file's, and what they should be is the file type's business rather than this module's. A text file holds its characters the way the machine holds everything, with the high bit set, and ends a line with a $8d; DOS reads one back a character at a time and would make nothing of plain ASCII. A binary file starts with the four bytes binaryPrefix writes, and a BASIC program with the two from basicPrefix followed by the tokens, which are the machine's business either way.

What read gives back is whole sectors, because sectors are all DOS records: the catalog says how many a file has and nothing on the disk says how many bytes of the last one mean anything. For the types that carry it, the length is in the file's own first bytes, which is what those two prefixes are. A pair of zeroes in the middle of a file's track and sector list is a hole, left by a text file written at a record number nothing has reached yet, and reads back as a sector of zeroes.

All of this was settled against a real disk rather than against the book alone: the catalog of a DOS 3.3 disk reads back file for file, and a disk made here, with DOS itself copied into its first three tracks, boots on this emulator and answers CATALOG with what was put on it. DOS reads a text file written here with EXEC, and a program DOS saves onto such a disk reads back through read as the Applesoft it is.

Making a ProDOS volume

prodos does the same for the other filing system, and where DOS 3.3 knew only a 5.25 inch disk of 560 sectors, ProDOS works in 512 byte blocks and will address 65,535 of them, so the same code serves a floppy and a thirty-two megabyte hard disk. It works on blocks laid end to end, which is what a .po file, a .hdv file and blockdevice.Volume all hold; a 5.25 inch image in DOS order wants disk2.reorder first.

var volume = try zig65.prodos.Volume.format(image, "WORK");
volume.stamp = .from(2026, 9, 1, 12, 0);      // there is no clock in here to ask

try volume.makeDirectory("SOURCE");
try volume.write("SOURCE/MAIN", .{ .kind = .text }, text);
try volume.write("STARTUP", .{ .kind = .system, .aux = 0x2000 }, code);

var files = try volume.catalog("SOURCE");
while (files.next()) |file| {
    var kind: [3]u8 = undefined;
    // "MAIN            TXT       1", the way CAT prints it
    std.debug.print("{s:<16}{s}{d:>8}\n", .{ file.name(), file.kind.abbreviation(&kind), file.blocks_used });
}

It is the same shape as the DOS 3.3 one: format and open, openFile and saveFile, dirty, and write, writeParts, read, delete and setLocked, with find and catalog for looking. What ProDOS adds is directories, so everything takes a path rather than a name: SOURCE/MAIN, or /WORK/SOURCE/MAIN written out in full the way ProDOS writes one. makeDirectory makes one, and a directory has to be empty before it can go.

Two other things differ. ProDOS keeps a file's length to the byte, so read gives back the file rather than the blocks it sits in. And a file's blocks are found through index blocks, which write builds as the size demands: one block is a seedling, up to 256 a sapling with an index, and beyond that a tree whose key block indexes the indexes. A block pointer of zero anywhere in that is a hole and reads back as zeroes.

stamp is what goes on a file as it is written. There is no clock in the library to ask, so a caller that wants dates on its files says what they are, and one that does not gets what ProDOS shows as no date at all.

This too was settled against the real thing rather than the book alone. The module reads a ProDOS 2.4.2 master disk file for file, sapling index blocks and all. A volume made here, with the loader copied into its first two blocks and ProDOS's own two files written onto it by this module and nothing else, boots on this emulator to a BASIC prompt and catalogues itself:

]CAT
/ZIG65
 NAME           TYPE  BLOCKS  MODIFIED
*PRODOS          SYS      35   1-SEP-26
*BASIC.SYSTEM    SYS      21   1-SEP-26
 MADE.BY.ZIG65   DIR       1   1-SEP-26
BLOCKS FREE:  215     BLOCKS USED:   65

ProDOS opens a directory this module wrote and lists what is in it, its idea of how many blocks are free agrees with the bit map here, and a directory ProDOS creates on such a volume reads back through find and catalog with every field where this module puts its own.

A model is chosen with options: memory.AppleIIe(.{}) for the machine, or memory.Flat(.{ .bus_log = true }) to have it keep a record of what went past on the address bus. The record is what the corpus checks against hardware traces, and it costs about a quarter of the throughput, so nothing else asks for it.

How much memory a IIgs has

A IIgs shipped with 256K: 128K of fast RAM in banks $00 and $01, and the Mega II's 128K in $e0 and $e1. That was not enough for much — the machine's own desktop wanted more — so almost every one sold had a memory expansion card in it, filling banks upward from $02. --memory is that card, and it is not the one an eight-bit machine takes: a IIgs's expansion is memory the processor can address, while a RamFactor in a slot is memory it can only look at through a window. See A megabyte in a slot for that one.

zig65 --machine iigs --rom iigs01.bin --memory 1M --hard-disk system.2mg

The size is the machine's total, counting the Mega II's 128K, which is how a IIgs was always described. 256K is a machine with an empty expansion slot and is the default; 8M is as far as the address space goes, banks $00 to $7f, above which are the expansion ROM, the Mega II and the machine's own ROM.

There is no register here, and that is the whole point of the thing. A real card had none either: the firmware sizes memory at power-on by writing to each bank in turn and reading it back, so RAM that answers is RAM the machine finds and the first bank that does not is where it stops looking. Making the banks answer is the entire implementation. What proves it works is that the firmware touches exactly the banks that were fitted and no others — two, three, six, twenty, a hundred and twenty-six — which is the sizing routine walking up the memory and finding the top of it where it actually is.

The banks a card adds are plain memory, and that is worth saying because banks $00 and $01 are anything but: the language card, the soft switches, the auxiliary-bank rule and shadowing all live there. A card's banks are sixty-four kilobytes of RAM each with nothing in the way, including where the I/O space would be — $c030 in bank $02 is a byte of memory, while the same address in bank $00 is the speaker.

An Apple IIgs you can use

bootgs runs a ROM and tells you what it did, which is the right shape for finding out what a machine wants and the wrong shape for using one. --machine iigs is the other shape: a window with the machine's screen in it, a keyboard that reaches it, and nothing else. No debugger panes — a IIgs is a machine to use rather than one to step through, and the sixteen-bit core has no debugger around it yet.

zig65 --machine iigs --rom "Apple IIGS ROM 01 - 342-0077-B.bin" --disk1 work800.po

It boots, and you type at it. alt+1 and alt+2 change the disk in the 3.5-inch and 5.25-inch drives: each opens the same picker the debugger has, listing the config file's library, a blank disk, a typed path, and — when the drive has something in it — a way to eject it. The arrow keys move the highlight and return chooses. A blank made here is sized to the drive it goes in: an 800K ProDOS volume for the 3.5-inch drive, a 140K DOS 3.3 disk for the 5.25-inch one, scratch space made in memory that nothing on disk backs. Which drive a disk goes in is settled by how big it is, since eight hundred kilobytes only fits the 3.5-inch drive and a hundred and forty only fits the other — so naming a drive is a way of saying which one to empty.

A hard disk goes in slot 7, with --hard-disk, and holds any image up to thirty-two megabytes. It is the same ProDOS block device the 8-bit machines get — see A hard disk — and it is the only card this machine has anywhere to put, since everything else a IIgs would have had in a slot is built into it: 1 is the printer port, 2 the modem, 3 the eighty-column firmware, 4 the desktop bus, 5 the 3.5-inch drives and 6 the 5.25-inch. Seven is the one left over, and the firmware leaves the Slot register pointing there of its own accord — it writes $80 to $c02d while starting up — so nothing has to be changed in the Control Panel first. The machine scans its slots from seven down, so a hard disk boots in preference to a floppy. Give --hard-disk twice for the slot's two drives.

Every drive is written back to its file when the machine stops, and when a disk is taken out of one — the two floppies, the hard disk's two volumes, and a UniDisk 3.5 on the port. A disk the image itself says is write protected stays that way, which is the file saying something rather than this program assuming it; everything else the machine writes reaches the file it came out of. That was not always so: the floppies used to keep what was written in memory and drop it on the way out, which was a quiet way to lose whatever GS/OS had just saved.

The terminal's pointer is the machine's mouse. There is no version of a IIgs without one, so it is handed over without being asked for; what that costs is selecting text with the mouse, which most terminals still allow with shift held. The monitor pane is the mousing surface and the machine's own dots are the units, so dragging the pointer from one side of the pane to the other drags the machine's cursor from one side of its screen to the other. The two will drift apart, because the machine's cursor is its own: a mouse says how far it has moved and not where it is, and what the machine makes of that is up to whatever is running.

alt+q quits, alt+b presses the reset key, alt+g writes a picture of the screen, and alt+o and alt+s hold the two Apple keys down — a terminal reports presses and never releases, so those two toggle. Everything else goes to the machine. Which of the two displays is on the screen it takes from the machine — the New-Video register's top bit — so a program switching to Super Hi-Res switches the window with it.

The Apple keys are two things at once on this machine, and both have to be told: a IIgs keyboard's Command and Option keys are modifier bits in the desktop bus register at $c025 and the game connector's first two buttons at $c061 and $c062, which is where software written for an Apple II looks. The machine says so itself:

]PRINT PEEK(49249);" ";PEEK(49189)
0 8
]PRINT PEEK(49249);" ";PEEK(49189)
128 136

The second is after alt+o: $c061 has the button down, and $c025 has the Command bit and the key-held bit both up.

It runs at about 2.92 MHz against the 3.07 it asks for, which is 95% of the machine, and it says so on the way out. The other 5% is drawing: the emulator itself does 115 million cycles a second, thirty-seven times faster than it needs to.

It is not the debugger, and it is a separate program for a reason that is not laziness: src/main.zig is an 8-bit machine surrounded by registers, a disassembly and a memory view, and a 65816 is a different core with different registers and a twenty-four bit program counter. Every pane in it would have to be rebuilt. What the two genuinely share — turning a keystroke into what an Apple keyboard sent, and getting a screen into a terminal as well as the terminal allows — is one module, tools/terminal.zig, and the debugger was moved onto it rather than the code being copied.

The terminal is not the only thing the debugger was moved onto. Everything the 8-bit front end does that is not looking at the machine — pacing the clock and running instructions in arrears, handing typed keys to the keyboard as it becomes ready, changing the disks, moving bytes along the serial line, recording the speaker — is now a Session in src/session8.zig, with no front end in it and nothing from vaxis imported. The terminal debugger embeds one and adds the drawing and the keystrokes on top; a front end drawing into a window or a browser instead would embed the same Session, render from the machine's own screen surface (zig65.screen, zig65.video, zig65.superhires), translate its input into the model's latches and the Session's own actions, and pace itself by handing it a different std.Io. Building the machine still belongs to the front end that knows which Apple it is — the same reason the IIgs is a program of its own — so the Session is the driving core and not the whole of it.

The IIgs front end was split the same way, and for the same reason cpu16 and memory16 stand apart from the eight-bit core: src/gs.zig drives its machine through a Session of its own in src/session16.zig, a separate core because a IIgs is a different machine with a different keyboard bus, disk port, sound chip and a network on it. Neither front end shares the other's Session, and neither needs to; what they share is tools/terminal.zig and the library underneath.

If the IIgs front end ever grows a disassembly, or the 8-bit one grows a better one, it should come from merle65 rather than from a table written here. That is a Merlin-syntax cross-assembler for the 6502, 65C02 and 65816 which disassembles as well as assembles, it is Zig and MIT like this, and it already knows all three instruction sets. There was a second instruction table in this repository once, in src/decode.zig, and what became of it is the argument: it fell out of step with the opcode table the emulator builds, stopped compiling, and sat there unnoticed because nothing imported it.

A key is not the character it types

PR#3 typed at the debugger came out as PR33, which was reported from use and is a good bug twice over.

It only happens on a real keyboard. A terminal speaking the Kitty keyboard protocol reports which key was pressed and which modifiers were down, and leaves joining them up to whoever is reading — # on a US keyboard is shift and 3, so the key is 3 and the character comes alongside as text. Reading the codepoint by itself gets the key and throws the character away. Over a plain pipe the terminal has nothing to report but the character, which is why every test here had always passed.

And then it was reported again, because the first fix did not fix it. The key mapping had just been moved into tools/terminal.zig for the two front ends to share — except the script that was supposed to delete the debugger's copy never wrote the file, so the debugger kept its own, along with its own tests of it. Everything compiled, everything passed, and the shared version that had been fixed was the one nothing called. Which is an argument for checking what a change did rather than that it matched: the edit that removed the duplicate now prints the file's size before and after and asserts that no bare appleKey( call is left.

Control is the other way round and now says so: control-C folds the key, whatever the terminal says the chord typed.

Two settings: which monitor, and who draws the text

Neither front end sends a picture of a text screen by default, and that is deliberate: a terminal draws characters crisply and for nothing, where a picture of a character generator scaled into a pane costs four hundred and thirty kilobytes a frame.

But the terminal cannot draw MouseText at all — screen.decode has to map those thirty-two codes onto ASCII, and a program that uses them comes out as nonsense — and it cannot show what a real screen looked like. So the machine's own character generator is the default, and alt+x is the terminal's letters.

Which leaves the other question, and it is another question: which monitor you are looking at it on. That is alt+m, and it applies to everything drawn as pixels — Hi-Res, Lo-Res, Super Hi-Res and text alike — because it is one choice about one screen. Monochrome is the default.

Drawn in colour, eighty-column text is very nearly unreadable. That is not a fault in the rendering — it is what the machine does, and the technical reference says so outright:

if you use an ordinary color or black-and-white television set, 80-column text will be too blurry to read. For a clear 80-column display, you must use a high-resolution video monitor with a bandwidth of 14 MHz or greater.

A character stroke is one dot, a dot at eighty columns is one position of the 560, and one position is half a colour cycle. So the same line of text, first on a monochrome monitor and then on a colour one, off the same machine a second apart:

]PRINT "The quick brown fox jumps over the lazy dog 0123456789"
The quick brown fox jumps over the lazy dog 0123456789

The first is crisp and the second is a smear of red, green and blue. This is why people bought monochrome monitors, and it is why monochrome is what both front ends come up on. --monitor colour and --text terminal pick something else to start with, and alt+m and alt+x change either of them while the machine runs. line.dots is the monochrome monitor, and it has been in the renderer since it was written — it just had nothing asking for it.

Booting GS/OS

GS/OS boots, and most of what goes wrong on the way to it is configuration rather than emulation, so here is what a IIgs wants:

zig65 --machine iigs --rom "Apple IIGS ROM 01 - 342-0077-B.bin" --memory 1M \
    --disk1 "System 6 IIgs 6.0.4/System.Disk.po"
  • Memory first. A IIgs shipped with 256K and that is the default, and System 6 will not run in it: the machine stops on the ROM's failure screen with "RAM disk too large or not enough memory. Error=$0201", which is what a real 256K IIgs says to it. --memory 1M is the least that boots, and it counts the whole machine, the Mega II's 128K included; 8M is as far as the address space goes. The example config examples/iigs.ziggy is the place to write it down.
  • The disk goes where it fits. An 800K image — a .po, or a .2mg — can only go in the 3.5-inch drive, and a 140K one only in the 5.25, so --disk1 is just "the disk" and nothing has to say which drive. System 6 shipped on 800K disks. A 3.5-inch WOZ is the one image the machine cannot take: the 3.5-inch drive is modeled a byte at a time, so a flux-level recording of one, such as the System Disk 1.1 in the WOZ reference set, has to be converted to a block image first. A 5.25-inch WOZ is fine.
  • What boots first. The firmware scans from slot 7 down. A hard disk on --hard-disk is slot 7 and boots before either floppy; the 5.25-inch drive is slot 6 and an empty one is passed over; the 3.5-inch is slot 5. So a system disk in the 3.5-inch drive boots on its own, and a DOS 3.3 or ProDOS 8 disk in the 5.25-inch drive boots ahead of it — the Speed register's motor-on detectors slow the machine to 1 MHz for it, as on the real machine.
  • A hard disk is the way to install it. --hard-disk PATH takes any image up to thirty-two megabytes, and it is written back to its file when the machine stops, so an installation stays installed. Boot the System Disk, then put the Install disk in with alt+1 when the Installer asks for it, and swap back the same way. A floppy is written back too now — it never used to be, which was a quiet way to lose whatever GS/OS had just saved — unless the image itself says it is write protected, which GS/OS boots from happily and complains about only when asked to save.
  • The Control Panel forgets. Its settings live in the clock chip's battery RAM, and without --battery-ram PATH every start is a fresh chip with the firmware's defaults — which are what boot the command above, so this only matters once something in the Control Panel has been changed: a startup slot, slot 7 set to something other than the hard disk, the display or the speed.
  • "Check startup device!" means the scan found nothing to start from: the disk is not a startup disk (the Fonts and System Tools disks are not), or the drive it needed is empty. Control-Reset, which is alt+b, goes back through the reset vector to the scan rather than to BASIC, so a diskless IIgs never reaches a ] prompt on its own. For BASIC, boot a ProDOS 8 disk, or double-click BASIC.System in the Finder, which quits GS/OS for ProDOS 8.
  • Once it is up. The Finder takes the terminal's mouse. alt+o and alt+s are the Open Apple and Solid Apple keys, which is how the menu shortcuts are typed. alt+g writes a picture of the screen, alt+m and alt+x change how it is shown, and alt+q quits. It runs at the machine's own pace, so booting to the desktop takes what it took.
  • Without a screen. zig build bootgs -- ROM 60000000 System.Disk.po --banks 16 runs the same boot headless and reports what it did: how far the drive got, what is on the text screen, and where the processor is sitting, which is the shape to use when a disk will not boot and the question is why.

AppleTalk

A IIgs came with the network built in, and turning it on was a Control Panel setting: slot 7 to "Built-in AppleTalk", and slot 1 handed over to it. Both front ends can do that for you, or you can do it yourself with control-⌘-escape and keep the result:

zig65 --machine iigs --rom "Apple IIGS ROM 01 - 342-0077-B.bin" --appletalk --localtalk
zig65 --machine iigs --rom "Apple IIGS ROM 01 - 342-0077-B.bin" --battery-ram gs.bram --localtalk

--appletalk does what an owner did: lets the machine come up once and write its defaults into the parameter RAM, sets the two slots, and starts it again. --battery-ram is the battery: the parameter RAM is read from the file when the machine starts and written back when it stops, so whatever the Control Panel was set to is what it is set to next time. A file the firmware would not accept — the wrong size, or a checksum that does not agree — is ignored and the firmware writes its defaults, which is what it does over a flat battery.

--localtalk is the cable. It joins the multicast group LocalTalk over UDP uses, 239.192.76.84 on port 1954, which is the convention Mini vMac started and TashTalk brings real LocalTalk hardware onto, so two of these are on one network, and so is a Mini vMac, and so is a real Macintosh behind a TashTalk. Two on one host need the host to loop multicast back to itself, which a firewall may not let its network interface do; --localtalk-interface 127.0.0.1 puts them both on the loopback interface, where it always works. --localtalk-log PATH writes every frame that crosses, one a line with the cycle it crossed on. The status line has a row for it, lit when a frame has crossed in the last quarter of a second, saying which node number the firmware settled on and how much has gone each way — or that it is waiting for the firmware to turn AppleTalk on, if it has not.

The firmware only starts the network when something asks it to. Started from slot 7 — --startup-slot 7 to bootgs, or the Control Panel's Startup Slot — it brings the link up itself: tries a node number by enquiring for it, takes another if anyone answers, then looks for a server to boot from, and gives up when none answers. Under ProDOS 8 or GS/OS it is whatever program initialises AppleTalk that starts it. bootgs takes --appletalk, --startup-slot, --localtalk, --localtalk-interface and --localtalk-log too, and reports the frames sent and received, which is how all of this was checked: a small peer on the group answering the machine's enquiries, and the firmware moving to the next number.

ROM 01 and ROM 3 code the Control Panel differently — ROM 01's slot 1 has two choices and ROM 3's has three, AppleTalk itself the third, kept in a byte of the parameter RAM the older ROM never had — and each throws the whole RAM away over a value it does not know. --appletalk tries the newer coding first and settles for the older when the firmware objects.

The socket is Linux only: joining a multicast group is not something the standard library's I/O offers, so that one corner of the front ends talks to the kernel directly.

Where things sit

The debugger's window went through a rearrangement worth writing down, because the old one had a fault that is obvious once seen: the machine's screen was sized to whatever the machine was showing, so calling PR#3 made the pane grow from forty-two columns to eighty-two and the entire layout jumped sideways.

A monitor does not change size when a program switches to forty columns. The dots get wider, and that is the machine's business rather than the window's. So the screen is a fixed eighty-two by twenty-six, and everything else is arranged around it:

+--------------------------+-------------------+
| the machine's screen     | a page of memory  |
| 80 x 24, always          |                   |
|                          +-------------------+
|                          | registers, flags  |
+--------------------------+-------------------+
| what it is doing, disks  |
+--------------------------+

What the machine is doing sits directly under its screen either way. On a terminal too narrow for both columns the memory and registers move underneath that rather than above it, so a window too short for all of it loses the memory view rather than the line that says what the keys are.

Forty-column text is centred in the eighty-column monitor. A real machine draws each dot twice as wide instead, which is exactly what happens when the character generator is drawing — the picture is scaled to the pane — but not something a terminal can do with its own letters, and leaving them against the left edge looks like a fault rather than a screen.

What is and is not here

Every 8-bit processor is complete and, as far as the corpus can tell, exact. The machine around them is not.

  • Implemented. All 256 opcodes, decimal mode, the read-modify-write double write, the page-crossing penalties, the JMP indirect page-wrap bug, and the unstable stores whose written value and target address both depend on the high byte of the address before it was fixed up. ANE and LXA mix in a constant that on real hardware varies by part and temperature; 0xee is used, which is what reproduces the corpus, and it is commented as the guess it is.

  • The machine. Seven memory models: Flat, which is 64K of nothing but RAM and the only one the corpus can run against; AppleI, the 1976 machine, a terminal on the far side of a PIA rather than a video display, see below; AppleII, the 1977 machine, with Integer BASIC in its sockets and a board that took anything from 4K to 48K and could be one of the first, Revision 0, boards; AppleIIPlus, which is the same board with Applesoft, 16K, 32K or 48K, the ROM and a language card; AppleIIe, which adds 64K of auxiliary memory and the switches that page it in; AppleIIc, which is that same machine with everything soldered down; and AppleIII, the 1980 machine, which is not an Apple II at all — a 2 MHz 6502 with banked memory, a relocatable zero page and two 6522s where the II has soft switches, see below. Pick one with --machine flat|i|ii|ii+|iie|iic|iii, default iie, and --machine iigs for the sixteen-bit one.

    Either can be told where its processor is rather than working it out from where it sits. A model that is a machine's whole memory is the processor's memory field and walks back one field to reach it, which costs nothing and needs no pointer wired up; memory.AppleIIe(.{ .host = .given }) makes one that is handed a pointer instead, for a model that is a part of a larger machine. The Apple IIgs is what wanted it: its slow side is a Mega II, which is this machine, and it sits two fields down rather than one. The two things a model wants a processor for are the clock and the interrupt line, and nothing else, which is what makes the hook that small.

    The IIc is the IIe with no slots: the disk drive, the two serial ports, the mouse and the eighty column firmware are all built in and answer where the cards for them would have, so every address from $c100 to $cfff comes from the machine's own ROM and no switch changes that. There is no cassette, and the address its input used is the switch on the keyboard that says which width to come up in. Vertical blanking is latched rather than polled, and the bit means the opposite of what it means on a IIe: it goes up when the interval begins and stays up until it is read. It had a 65C02 in it from the first one sold, so --machine iic gets one without being asked. The drive in the case is always there and needs no ROM supplied, and --drives 2 says the socket on the back has one in it too.

    Two things about it are not a IIe's. What reaches the drives is an Integrated Woz Machine rather than a Disk II card — the same iwm.zig a IIgs uses — which is what lets the later firmware ask the port what it is and find no 3.5-inch drive on it. And where the original has 16K of ROM, every revision after it has 32K in two banks with $C028 picking between them, so the $c100 to $ffff a program sees is still always the machine's own and never a card's, but which of the two it is can change.

    Its two serial ports are here: the same 6551 the card had, twice, at the addresses cards in slots one and two would have put them, which is where the machine's own firmware looks. Nothing has to be fitted and no firmware supplied, because both are in the ROM.

    Its mouse works, and it works the way the machine's does rather than the way a card's does. A card counts for itself and hands over a position; the IIc has no counter, only four wires. Two of them put out a square wave as the mouse rolls and interrupt the machine on every edge of it, and two say which way that movement went, to be read within forty microseconds of being told. The firmware in the machine's own ROM does the counting and the clamping out of that stream of interrupts, so what is emulated is the four wires, the switches that enable the interrupts and say which line moved, and the button — which is the one thing on this machine that reads the other way round, the bit being up when it is not pressed. moveMouse and setMouseButton roll it, and the pointer the firmware keeps arrives where it was rolled to.

    Two things about those wires are worth knowing, because both were found by reading what the ROM does with them rather than by guessing. Asking which line moved does not clear it: the firmware asks once to find out whether the interrupt was the mouse's at all and again to find out which of the two lines it was, and clears them together afterwards, so a read that cleared would leave it seeing nothing the second time and concluding the interrupt was somebody else's. And the two direction lines do not agree about which way is which — the firmware inverts what it reads on the vertical one before counting it — so down the screen is that line low.

    The soft switches that move memory are implemented and tested; the ones that do not, like the annunciators, are recorded and otherwise ignored. The speaker toggles at $c030 as it should, including that a write toggles it twice too quickly to be heard, and a model can be asked to keep the cycle of every flip, which is what turning it into sound needs: memory.AppleIIe(.{ .speaker_log = true }), and see Sound above. The debugger has no way to make a tone, so it rings the terminal instead, at most five times a second, and can write a WAV. Vertical blanking is timed from the clock at 65 cycles a line and 262 lines a frame, and can be polled at $c019. A real IIe can only poll it: making it an interrupt is an Apple IIc feature that reaches a IIe through a mouse card, so it is off by default and switched at $c05a/$c05b where the IIc puts it.

  • An Apple-1. --machine i is the 1976 machine, and wants the Woz Monitor as its ROM: 256 bytes at $ff00, whose reset prints a backslash and waits for a line. Nothing after it is built like it. The display is not memory the processor can see but a terminal — a kilobyte of shift registers, forty by twenty-four, "leaving all of RAM for user programs" (Apple Computer Company, 1976) — fed one character at a time through a 6820 PIA at $d010 to $d013, whose four addresses the manual documents bit by bit: the keyboard's data with its high bit tied high and the key-ready flag that reading it clears, the display's data with the busy flag in bit 7 that the Monitor's ECHO waits on. The PIA is decoded the way the manual's "KBD/DSP Interface" drawing wires it — CS0 is A4, CS2 is the $Dxxx block, and the register is the low two bits — so it answers at every address in the block with A4 set, which matters because Apple's BASIC drives the display through $d0f2. The display takes a character at the next vertical retrace, which is sixty a second, and recognizes carriage return and nothing else among the control codes. Four of every sixty-five cycles are the refresh stalls the processor is held for, which is why the specification sheet's 1.023 MHz clock is an "effective" 0.960. RAM is 4K, as supplied, or --memory 8K for the second bank at $e000 where Apple's BASIC lived. The PIA is modeled as far as the board wires it, control registers included: the Monitor's first write to the display is #$7F to its direction register, which a model without the register select would print as a ?. Its two buttons are alt+b, RESET, and alt+c, CLEAR SCREEN, which is on the display and not the processor. The terminal is shown to the screen-drawing code as though it were a text page, cursor and all — the Apple-1's cursor is a flashing @, which is the Apple II's text code $40 exactly — since a terminal drawing it cannot tell the difference and should not have to. --aci-rom fits the Apple Cassette Interface, the one card the machine had, from its schematic; see the cassette port below, where a tape of Apple's BASIC loads and runs.

  • An Apple II. --machine ii is the 1977 machine, and wants its ROM: Integer BASIC and the old Monitor, 12K with the vectors at the top, which comes up at the Monitor's * and enters BASIC at E000G. Sather has it that "the only operational difference between the Apple II and the Apple II Plus is in firmware" (1983, Appendix G), and the board is the II Plus's; what the model adds is what the board could be. --memory populates its three rows the ways Apple did — 4K, 8K, 12K, 16K, 20K, 24K, 32K, 36K or 48K, the five standard sets of memory select blocks in the Reference Manual (Apple Computer, Inc., 1978, pp. 133134), with 12K-hires for the set that put two rows under the Hi-Res page — and a row with nothing in it is the floating bus, which is how Integer BASIC's cold start finds the top of memory: a 16K machine answers PRINT PEEK(76)+PEEK(77)*256 with 16384. --revision 0 makes it one of the first boards, before Revision 1 added "a COLOR BURST killer circuit to remove colors from text", "power-up RESET circuitry" and "the HIRES delayed video feature which gives the blue and orange colors" (Sather, 1983, Appendix G): text is green and violet on a colour monitor, Hi-Res has violet, green and white and bit 7 does nothing, and the machine comes up halted until reset is pressed, as its owner had to. A II Plus takes --memory 16K|32K|48K and is never Revision 0.

  • An enhanced IIe, and an original one. The IIe is the default machine, and which of the two it is comes from its ROM. The 1985 enhancement was four chips sold together: "the 65C02 microprocessor", "a new video ROM containing the same MouseText characters found in the Apple IIc", and "new Monitor ROMs (the CD and EF ROMs)" whose "identification byte at $FBC0 has been changed. In the original Apple IIe it was $EA (decimal 234), in the enhanced Apple IIe it is $E0 (decimal 224)" (Apple Computer, Inc., 1985b, p. xxix). So the machine reads that byte when the ROM is loaded and fits the rest of the kit to match: an enhanced ROM gets a 65C02 and the MouseText character generator, an original one a 6502 and the 1983 chip, and the title the firmware prints says which — Apple //e or Apple ][. The enhanced firmware uses the 65C02's instructions, so running it on a 6502, which --processor 6502 still allows, is announced. --video-rom original or enhanced is for an owner who changed one chip and not the other. Both boot in the whole-machine tests, and with the alternate character set on, Applesoft's FLASH prints MouseText on the one and flashing capitals on the other, which is the difference the chip swap made.

  • An Apple III. --machine iii is the 1980 machine, and wants its 4K system ROM, the "Sara" ROM. It is a 6502 at 2 MHz around 128K, 256K or 512K of RAM (--memory, default 128K), arranged the way the SOS Reference Manual's memory chapter sets out: a fixed S-bank at $0000-$1fff and $a000-$ffff, a 32K window at $2000-$9fff chosen by the bank register at $ffef, the zero page put on any page by the register at $ffd0 with the stack beside it, and the environment register at $ffdf that switches the ROM, the I/O space, the write protection over the top of the S-bank and the 1 MHz speed. Those registers are ports of two 6522s, which are here in full, with the vertical blanking on the E VIA's CB lines that the ROM waits for at power on and the interrupt line readable on its port A. Enhanced indirect addressing is here too: a pointer read off zero page picks up an X-byte from the extension page, and if its top bit is set the access goes to the bank pair the low bits name, laid end to end — which is how SOS reaches all of memory without touching the bank register, and how its loader put the interpreter where it goes. The built-in disk controller is the Disk II's state machine at $c0e0, kept at the 1 MHz it was designed for while the processor counts at two; the internal drive is on its first connector and --drives 2 puts an external one on the second. A bank number nothing is fitted behind decodes nothing and reads as the floating bus, and that is what the boot block on every SOS disk relies on: it sizes the machine by writing to bank 14 and to bank 6 and reading back, then counting down from 7 until a zero it stores is a zero it reads. A model that answered for every bank was taken for 512K, and SOS put the interpreter's segments on top of its own vectors. The screen is the III's forty or eighty columns of text, whose bytes are seven bits of ASCII with the top bit clear for inverse, drawn from a character generator that is RAM: empty at power on, loaded eight characters at a time through the screen holes of the two text pages when the download switch is on and the blanking ends, the way the console driver's LOADCHR lays them out. The ROM loads its font that way before it prints a word, and SOS loads its own, ruling lines and copyright sign included. The four graphics modes are the II's circuits under other names — 280 by 192, and 560 by 192 out of two 8K pages laid end to end in bank 0 — with the colour burst switched by VM0, since two of them are "black and white dots only" and two are in sixteen colours; and the colour text mode colours each character from the byte at the same place in the other text page. None of the graphics has been seen driven by software yet, only by tests. The keyboard is the encoder's: a letter key gives its uppercase code whether shift is down or not, and shift, control, alpha lock and the two Apple keys are reported separately at $c008, with a bit for the keys the console driver passes through untransformed — space, return, escape, the arrows; SOS reads a key on the interrupt the encoder's data-ready line raises on the E VIA's CA2, so keys are handed over as soon as the strobe is clear rather than when something polls, since nothing on a III need poll. The utilities take their menu keys. The clock chip is the MM58167 at $c070, whose five address lines are the low bits of the zero-page register, so SOS moves its zero page to name a register: milliseconds to months in BCD, the rollover status bit SOS checks after every read, the resets and the GO command, and the year in two of the RAM digits where SOS keeps it, since the chip has none. Its socket is empty unless --clock fills it, as Apple left it in most of the machines, and SOS finds that out from the floating bus; with a chip the utilities show the date and time and SOS stamps its files. Business BASIC boots and runs a program typed at it; the Confidence Program, Apple's diagnostics, comes up to its menu; /// Easy Pieces to its title; and Apple's Apple II Emulation disk turns the machine into a 48K II Plus that boots a DOS 3.3 disk swapped into the drive, catalogs it and runs an Applesoft program — which needed one more fact about the memory map: with the I/O space switched in, $c500 to $c7ff stays RAM, and that is where the emulation keeps the Disk II boot routine its PR#6 finds. All three of the machine's "three different audio generators" are here: the one-bit line at $c030, the fixed beep one access to $c040 rings for a tenth of a second which is SOS's bell, and the six-bit sound DAC on the E VIA's port B. All three are logged and come out in an --audio recording, the DAC mixed into the one speaker they share, though no III program in the catalogue drives the DAC, so it is checked by test rather than by ear; and the four slots, but for one card. The processor's speed is the hardware's: "the Screen timeshares the RAM on the opposite phase of the processor clock", so while the beam is drawing the processor gets every other memory cycle and runs at 1 MHz, and in each line's horizontal blanking, or with the video switched off, "the processor may run at a full 2MHz rate" — which averages to the 1.4 MHz SOS's manual promises with the display on, and which a test measures. The two VIAs are clocked at 1 MHz whatever the processor does, as the service manual says, and SOS's timings that count on them come out right. The joystick ports are there as SOS's JOYSTICK call reads them: the four switches at $c060, and the analog inputs behind a ramp the software times — select an input with the switch pairs at $c058, $c05a and $c05e, charge at $c05c, start at $c05d, and count against the D VIA's second timer until the flag at $c066 drops, the timer's high byte being the value — "a ramp type Analog to Digital converter" whose values "must be interpreted by the program", as the service manual puts it. Nothing is plugged into them from the front end yet, so they read as open. That card is the ProFile's: --hard-disk PATH puts Apple's five-megabyte hard disk on its interface card in slot 4, where every SOS.DRIVER file to hand puts .PROFILE. The card is two ports, a BUSY register and an addressable latch for CMD, the bus direction, the interrupt and the drive's reset, and the drive behind it answers the driver's handshake — response byte, $55 or $AA back, six command bytes down, four status bytes and the block up — in no time at all. Blocks move by the machine's pseudo-DMA: the driver holds the processor to 1 MHz on a 2 MHz memory system, points the zero-page register at the buffer's page and runs a page of SBC #1 / BEQ in the ROM at $f800, and on every fetch the card moves one byte to or from that page at the offset being fetched, the taken branch out included, which is why the driver saves bytes 0 and $fe of the page first. Business BASIC catalogs an empty volume on it and writes a file to it in the whole-machine tests; the image is 9728 blocks of 512, and a blank one is a ProFile the utilities' Format command lays a volume out on, as it does — .PROFILE typed over the .D2 it offers, a name, and a warning about formatting a large disk. --machine iii+ is a III Plus: the same model and the same ROM — the Plus's board differs by its interlaced video sync ROM, its keyboard, and a clock chip that Apple fitted and had working — so it is the III with the clock's socket filled and set from the host. The whole-machine tests boot it four times, see below.

  • Interrupts. The processor has both lines. The Mockingboard's timers, the mouse card and the serial card all pull the IRQ, and so does vertical blanking, and they share it properly: one letting go does not quieten another. An IRQ is level triggered and masked by the interrupt disable flag; an NMI is neither. Both are answered between instructions in seven cycles, pushing the flags with the break bit clear so a handler can tell them from a BRK. The IIe drives the IRQ line from vertical blanking, with a caveat in The interrupts.

  • A hard disk. A ProDOS block device with its own firmware, so it needs nothing but an image file, in slot 7 unless --slot says otherwise: status, read and write, and a boot block that PR#7 runs. Two drives, since that is how ProDOS counts and what its own startup expects of the slot it booted from. It answers SmartPort as well, which is the second way of asking the same card about the same disks and what ProDOS 8 from 1.9 on expects of anything with $00 in the fourth byte of its signature. Thirty-two megabytes is the most any one volume can be, since the block number ProDOS passes is sixteen bits. The same card goes in a IIgs, where it is the only card the machine has anywhere to put — see An Apple IIgs you can use.

  • A Mockingboard. Two AY-3-8910s, three square waves, a noise source and an envelope each, with a 6522 in front of each of them carrying the data on one port and the three lines that say what to do with it on the other. A program writing a register writes one port and then the other twice, which is what the sequences in every Mockingboard player are doing. The other half of a 6522 is its timers, and the first one is what a tune actually runs on: the player sets it to a frame's length and lets it interrupt, and the handler writes the next frame's worth of registers. --mockingboard fits it, and with --audio the recording becomes the two channels the card has with the speaker down the middle of them. Slot 4 is where it goes when nobody says, and --slot 2=mockingboard puts one anywhere else; a slot holds one card, so asking for this and --mouse in the same one is refused rather than one of them quietly winning. Like the mouse, this is checked against the published description of the parts and against the sound that comes out rather than against real software: a program written for it plays a chord whose six notes come out at the frequencies the chip's own arithmetic asks for, three on each side.

  • A mouse. An AppleMouse II card, in slot 4 by default, given its own firmware: the five bytes a program looks for to find one, and the table of eight routines it calls through. The position and the buttons come back where the card leaves them, in the screen holes, which are the bytes of the text page that no character is displayed from. It is the only card here that pulls the interrupt line of its own accord, and it does it for whatever it has been told to: when the mouse moves, when a button goes down, or once a frame at vertical blanking. --mouse fits it and lets the terminal's pointer drive it: where the pointer is over the machine's screen is where the mouse is, scaled to whatever the program has clamped it to. That is not what moving a real mouse feels like, since a terminal says where the pointer is rather than how far it moved, but it is what a terminal can say. Unlike the disk and filing system work, this one is checked against the published interface and the firmware running rather than against real mouse software.

  • A clock card. The machine has no clock of its own, so ProDOS stamps every file it writes with no date at all. --clock fits one, in slot 5 by default, given its own firmware: ProDOS finds the four bytes it looks for in the slot, calls the card, and reads the date and time as text out of the line buffer. The card is set from the host once and then keeps time by the machine's own cycles, so a machine run at a hundred megahertz sees the hours go by at that rate, and one that is paused sees them stop. --clock-offset shifts it, since the host's clock is read as UTC and the machine knows nothing of time zones. The month and the date ProDOS shows are the card's; the year is not, and cannot be, because the string a clock card sends has no year in it. ProDOS works the year out from the day of the week instead, and arrives at one of its own: told a Tuesday the first of September it answers 2020, and told a Thursday the first of September it answers 2022. That is ProDOS, not the card, and it is why ProDOS needed a new driver when its guesses ran out.

  • A memory expansion card. An Applied Engineering RamFactor, in slot 5 by default, up to a megabyte of it, reached through three address registers and one data register whose address steps on by itself. --ramfactor-rom fits it and --ramfactor says how much memory is on it; --ramcharger is the battery option, which keeps the contents in a file between runs. The RAMdisk, the ProDOS driver, the SmartPort interface and the partition manager are all in the card's own firmware, which is Applied Engineering's and is not here, so what is modelled is the hardware under it — including that the high address byte reads back with its top nybble set, that the three address bytes are counters in a chain rather than a number, and that the registers do not answer until the firmware page has been touched. PR#5 brings up the partition manager and ProDOS finds /RAM5. Slot 5 is where it and the clock card both go when nobody says, and a slot holds one card, so asking for both is refused; --slot puts either of them somewhere else. See A megabyte in a slot.

  • A serial card. A Super Serial Card, in slot 2 by default, given its own 2K firmware: the 6551's four registers, the rate it was set to, and the $c800 window that one card at a time borrows. PR#2 and IN#2 both work through the real firmware.

  • A parallel printer card. In slot 1 by default, with its own firmware: a latch at $c090 and the eight wires behind it, which is all the card ever was. PR#1 prints through it, --parallel-out makes a file the printer, and in the library it takes any std.Io.Writer. The high bit comes off and a line feed goes on after each carriage return, both because that is what the card did.

  • The cassette port. The output line at $c020 and the input at $c060, and --cassette-out and --cassette-in to write and play WAVs. A tape saved by one run loads back in another, at any speed the machine is run at. On an Apple-1 the port is the Cassette Interface card, which --aci-rom fits.

  • Game I/O. The three buttons and the four paddles. A paddle is a variable resistor and the machine cannot read one directly: it starts a timer at $c070 whose length the resistance sets, and counts round an eleven cycle loop until the timer runs down, so the position is time rather than a number. setPaddle and setButton put them where you want them, and the Monitor's own routine reads back exactly what was set, which is what the tests check by running that routine. On a IIe the first two buttons are the Open Apple and Solid Apple keys; the debugger toggles them with alt+o and alt+s, since a terminal cannot say that a key is being held down. The annunciators are not implemented.

  • Disks. A Disk II card, in slot 6 by default, with up to two drives, given the card's own 256 byte boot ROM. The drive is emulated as the bare thing it is: a motor, four magnets that walk the head a half track at a time — or a quarter when two of them are left on at once, which is where some protected disks hide their data — and a stream of bits going past. .dsk, .do and .po images are encoded into six and two exactly as a real disk is written, so the machine's own ROM and DOS read them the way they read a disk; a .nib is a recording of the nibbles rather than a description of the sectors and goes in as it stands; a .woz is a recording of the bits, which is what a copy-protected disk needs and what the drive reads natively; a .2mg says which order its sectors are in and is believed; and a ShrinkIt archive — .shk or .sdk, which is what most Apple II software is distributed as — is unpacked and the disk inside it used. Writing works and is saved back to the file when asked for — as nibbles for a .nib, which is the only file that can hold a track the machine laid out itself — except for a disk that came out of an archive, which is read only because putting one back would mean writing a ShrinkIt archive, and a .woz, which this only reads.

  • Text video. Forty columns, and on a IIe eighty, drawn out of both halves of memory the way the hardware reads them, with the alternate character set and both text pages. The switches that move the processor's window about are kept apart from the ones that decide what is displayed, so a program reading auxiliary memory does not change what is on screen.

  • Graphics video. Lo-Res, Hi-Res, both of their doubled forms, and text, in src/video.zig, a scan line at a time. What comes back is 560 dots and the colour those dots make, which is the same signal seen two ways. Text comes out of a character generator in src/font.zig, and whether it comes out fringed is the board's decision: white in text mode on every machine from the Revision 1 Apple II on, green and violet on a Revision 0 board and in the four text rows under mixed graphics everywhere. See Text, and why it is green at the edges.

  • An eighty column card. A Videx Videoterm, in slot 3 by default, which is what an Apple II Plus was given when forty columns would not do. It is not the machine's video with more columns: the card carries a 6845, two kilobytes of screen memory of its own, and a character generator in a socket, and it draws a picture the Apple's video knows nothing about. The processor sees five hundred and twelve bytes of that memory at a time through $cc00, and one of the card's sixteen addresses selects which quarter — the same access that picks a 6845 register, because the card decodes both out of the same four bits. Scrolling is the 6845's start address moving, not a copy. --videx-rom and --videx-charset fit it; both parts are Videx's and neither is here. PR#3 hands the screen over, and since the card had its own monitor and did not switch the Apple's off, alt+v says which of the two to look at.

    A IIe takes one too, and getting there is the interesting part: slot 3 on a IIe is where the machine's own eighty-column firmware lives, and SLOTC3ROM is what chooses between the two. A IIe comes up with it off, because it always has the extended eighty-column card in its auxiliary slot, so the machine's firmware answers at $c300 until a program asks for the card — POKE 49163,0 and then PR#3, which is what software written for a Videoterm does. INTCXROM overrides it either way. The card's sixteen device select addresses are its own whatever the ROM switches say, because "the bus and I/O signals are always available to a peripheral card in slot 3, even when the 80-column hardware and firmware are operating".

  • Any card in any slot, and two of a kind. Which slot a card goes in used to be a constant in the memory model, one per card, so a Mockingboard could only ever be in slot 4 and a machine could never have two of anything. It is data now: --slot 5=disk puts a controller in slot 5, --slot 2=mockingboard puts a sound card where a serial card usually goes, and a config file describes each card beside the slot it is in. Nothing has to be told where to look — a machine's reset counts down from slot 7 for a card whose page says it can boot, which is exactly why a card could be sold for seven slots.

    Left unsaid, every card goes where that kind has always gone: 1 the parallel printer card, 2 the serial card, 3 the Videoterm or an UltraTerm, 4 the mouse or a Mockingboard, 5 the clock or a RamFactor, 6 the Disk II, 7 the block device or a Transporter. Those were the slots the software of the day looked in, which is why they were the slots people used.

    Two cards in one slot is an error naming what is already there. It used to be the order of a chain of else ifs — a Mockingboard beat a mouse, a RamFactor beat a clock — and nobody was told.

    The apparatus is src/slots.zig and exists once. It used to exist twice, once per memory model, near-identically; that is how a Mockingboard came to be audible on a IIe and silent on a II Plus for as long as the card had been here. A IIc has no slots at all, and what it has at those addresses is four ports soldered to its board.

  • Five processors. --processor chooses: 6502, RP2A03, 65C02, R65C02 or W65C02. Each is exact against its own corpus, and the corpus has all five because the chips themselves differ. Left unsaid, --processor is the chip the machine was sold with, which for a IIe depends on its ROM; see the enhanced IIe below.

    Ricoh's RP2A03, the processor in a NES, is the odd one here: it is in no Apple II and never was. It is the NMOS part with the decimal correction left off the die, so the D flag is still a flag — SED sets it, PHP pushes it, PLP restores it — but ADC and SBC add in binary whatever it says, and it costs no cycle either way. Everything else about it, the undocumented opcodes and the phantom reads and the indirect jump bug included, is the 6502.

    The CMOS parts differ from each other as well as from the NMOS one. The plain 65C02 is what Synertek, NCR and GTE made. Rockwell's added sixteen instructions for setting or clearing one bit of a zero page byte and sixteen for branching on one, which are worth having because they touch no register and no flag: a byte two things are both looking at can be changed without disturbing what is being tested around it. Western Design's has those and two more that stop the processor, one until an interrupt and one until reset. On the parts without them, all thirty-four encodings do nothing.

    WAI and STP are the only instructions here with no corpus behind them, and for a good reason: an instruction that stops cannot be recorded a case at a time, and the corpus holds an empty file for each. They are tested by hand instead.

    What changed is not only the instructions it has and the NMOS part has not — the branch that always branches, the index registers on the stack, storing nothing, testing and setting bits, a pointer with no index, INC A — but everything the two do differently underneath. Decimal arithmetic gets the flags right and takes a cycle for it. The phantom read an indexed address costs happens at the last byte of the instruction rather than at an address nobody meant, which matters in a machine where reading an address can throw a switch. A read-modify-write reads twice rather than writing twice. An interrupt clears the decimal flag. Shifting an indexed address is a cycle shorter unless the index crosses a page. BIT against a constant leaves N and V alone. The undocumented encodings are instructions or else do nothing, and what nothing costs is anywhere between one cycle and four, which is recorded here from the corpus because no datasheet lists it. And the famous bug in an indirect jump across a page boundary is gone — but the way the chip fixes it is to make the same mistake first and then read the byte it should have, which is the cycle it costs.

  • The 65816 is a second core. It is not one of the processors above and cannot be: src/cpu16/ is its own core with its own 24-bit memory models, for the reasons given under The 65816. All 256 of its encodings are implemented and exact against its own recordings, along with the interrupt lines, which no recording can reach and which unit tests cover instead.

  • An Apple IIgs around it. src/memory16/apple2gs.zig, and enough of one to boot. The Mega II is a real Apple IIe — the same apple2e.zig the 8-bit machines use — with the fast side's 256K in banks $00 and $01 shadowing into its $e0 and $e1, the bank latch that decides which of its halves an address reaches, and the State register gathering the IIe's memory switches into one byte. The video controller draws Super Hi-Res out of $e1/2000 with a palette a scan line at a time, and interrupts on a line of the caller's choosing. The desktop bus is a microcontroller rather than a port, and carries the keyboard, the modifier keys, the two Apple keys and a mouse. The disk port is an IWM with a 3.5-inch drive and a 5.25 on the same cable, and the 800K drive reads, writes and formats. The Speed register's motor-on detectors drop the machine to 1.024 MHz from the access that turns a Disk II's motor on to the one that turns it off, which is what lets DOS 3.3 and ProDOS boot from the 5.25: the Hardware Reference says a Disk II works on a IIgs "only if the system speed is changed to 1.024 MHz before disk access is attempted". There is a clock chip with battery RAM, the interrupt ROM the Monitor's handler jumps into, and the block device above in slot 7 answering ProDOS and SmartPort both. The Ensoniq runs its thirty-two oscillators — walking their wavetables, halting and swapping and interrupting as the chip does — and --audio writes what they come to as a WAV. Memory goes up to the eight megabytes the address space holds, with --memory. Every IIgs ROM to hand boots it: ROM 00 and ROM 01, both ROM 3 dumps, the ROM 4 out of a Mark Twain, the 1986 beta, the Alpha ROM and a prototype.

  • The serial ports. Both channels of the SCC carry bytes now rather than only answering registers. A byte written to a data register goes out of that port and takes as long doing it as the rate the chip was told to use, which comes out of the time constant in WR12 and WR13 the way the manual says to put it in; a byte arriving from outside shows in RR0 and is taken away by reading. The modem port is channel B, and --serial-in and --serial-out are the two ends of its wire on a IIgs as they are for the Super Serial Card on the eight-bit machines, and --printer-out is the outward end of the printer port, which is channel A of the same chip. Only outward: the channel would carry bytes back perfectly well, but a printer is a thing you send to.

  • AppleTalk. The same chip in its other mode. With slot 7 set to the built-in AppleTalk in the Control Panel — or --appletalk, which sets it — the firmware puts the printer port into SDLC at 230,400 bits a second, FM0 encoded, clocked off the chip's own crystal with the receive clock recovered by its phase-locked loop, register for register the way Zilog's own LocalTalk application note does it. The model does what the manual says the chip does with a frame: the check sequence and closing flag go out on the underrun after the last byte, with the Tx Underrun/EOM bit down until they do and the transmit buffer reading full while the check goes; a frame arriving is a byte every eight bit times into a three-deep FIFO, its two check bytes included, with End of Frame on the second; address search drops frames for other nodes; the sender's trailing ones read as an abort; and the interrupts reach the processor, which is how the ROM's handler comes to find a frame's second byte waiting behind its first. --localtalk puts the machine on a network — LocalTalk over UDP, the multicast group Mini vMac and TashTalk use — and started from slot 7 the firmware acquires a node number, giving one up when a peer says it is taken, and goes looking for a server with NBP and receives what comes back. What is not here is what LocalTalk over UDP leaves out: the handshake before a directed frame is answered by the bridge itself, since a datagram cannot collide.

  • GS/OS boots. ProDOS 8 always did: Apple's System Disk 3.2 comes up on its menu and a ProDOS 8 program runs. GS/OS 6.0.4's system disk now boots to the Finder — the welcome screen with its progress bar, then the desktop with its menu bar, the disk's icon and the Trash — given a machine with a megabyte of expansion memory. On a 256K machine it stops on the ROM's system failure screen with its own words, "RAM disk too large or not enough memory. Error=$0201", which is what a real 256K IIgs says to System 6. In the front end the Finder takes the terminal's mouse: a double-click on the disk opens its window, a double-click on BASIC.System quits GS/OS for ProDOS 8, and what is typed there arrives. Five things stood between the loader running and the Finder appearing, none of them where the symptom pointed, and they are written up under What GS/OS wanted.

  • What a IIgs has not got. No modem: none of the six bits in RR0 that report something outside the chip is driven by anything, so carrier detect and clear to send read as nothing is there, because nothing is. And no live audio: the Ensoniq is summed and written to a file rather than played.

Building

Getting the code

The repository lives in two places that carry the same history. The Forgejo instance at https://git.ocjtech.us/jeff/zig65 is the web-visible one, and it is also on the Radicle network, where the repository's identifier is

rad:z4DFidSWsX9amrQTXd1ff1qwYfpL9

and rad clone rad:z4DFidSWsX9amrQTXd1ff1qwYfpL9 fetches it from any node that seeds it. Either clone is the whole project; the ROMs and disk images it is tested against are not in it and cannot be, for the reason given under The ROMs.

Zig 0.16 is required. The flake provides it and everything else. zig build builds the debugger optimised, since an emulator built for being stepped through runs about six times slower and that is the difference between keeping up with the machine it imitates and not; the tests keep Zig's default and the safety checks that come with it. Passing -Doptimize overrides both.

nix develop            # or install zig 0.16 yourself
zig build
step what it does
zig build build it
zig build run -- --rom PATH build and run it
zig build test unit tests
zig build corpus the whole SingleStepTests corpus, run in corpus/
zig build woz read the WOZ reference images, run in woz/
zig build functional Klaus Dormann's functional tests, run in functional/
zig build corpus16 the same for the 65816, against its own recordings
zig build bench16 measure the 65816's two dispatch strategies against each other
zig build bootgs -- ROM run a IIgs ROM against the machine model and report what it wanted
zig build gs -- --rom ROM the same as zig build run with --machine iigs in front of it
zig build corpus -Dprocessor=65C02 which chip's corpus to run: the only build option left, since the emulator holds them all
zig build bench instruction throughput, built ReleaseFast
zig build coverage kcov report over the unit tests
zig build example -- ROM run the worked embedding example
zig build check type check everything, emitting no binaries
zig build docs build the API documentation into zig-out/docs
zig build docs-serve build it and serve it, -Ddocs-port=N to choose the port
reuse lint check every file says who owns it and under what terms
nix build build the emulator with Nix, dependencies and all

With Nix

The flake also builds the emulator itself, so nix build produces a binary with the completions installed beside it and needs nothing but Nix:

nix build
./result/bin/zig65 --help

There are two dependencies. libvaxis is the terminal library the debugger draws itself with, and z2d is a 2D graphics library that turns a machine's screen into pixels and a PNG — used only by src/picture.zig, so that nothing which merely runs a machine has to link a graphics library.

The dependencies in build.zig.zon are fetched by Nix rather than by Zig, since the build sandbox has no network of its own. build.zig.zon.nix lists them, and is generated from build.zig.zon by zon2nix, which the development shell provides. Regenerate it whenever a dependency changes:

nix develop --command zon2nix --nix build.zig.zon.nix

Nothing needs editing afterwards: the generator writes no SPDX header, so REUSE.toml records the licence for it, as it does for the other generated file, flake.lock.

What runs on a push

.forgejo/workflows/test.yaml checks the formatting and the licence headers, runs the tests, and builds the debugger — which is where every machine and every processor gets type checked, because the memory model and the instruction table are both comptime parameters and main.zig instantiates the lot with an inline for. A configuration nobody builds is a configuration nobody compiles, and the nineteen pairings that are not the default would otherwise rot quietly. It reads every WOZ reference image as well and runs both of Klaus Dormann's functional tests, neither of which needs a ROM, and type checks the two corpus runners without fetching what they run on. It builds the Nix package too, which fetches its dependencies a different way and so goes wrong in its own ways. From main it also builds the documentation and publishes it.

The corpus is not run there. It is the strongest thing this project has to say about itself, but it is a gigabyte of small files fetched from somebody else's archive, and a test run that fails when an archive is down is a test run that teaches nothing. It is run by hand.

The API documentation

Zig writes it out of the doc comments, which in this project are most of the explanation of how an Apple II works, so it is worth reading:

zig build docs-serve            # then open http://127.0.0.1:8000/
zig build docs                  # or just build it, into zig-out/docs

It has to be served rather than opened. What Zig emits is not a page but a program: a WebAssembly viewer that fetches the source of everything it shows out of a tar file beside it, and a browser refuses to fetch anything from a file:// page. That is the same reason zig std runs a server rather than opening a file. tools/docs_server.zig is that server, and it serves one directory to one person on the loopback interface and does nothing else. It gives each connection a thread, which is not about throughput: a browser opens several connections at once and holds some of them open without sending anything, so a server that reads them one at a time waits on a speculative connection and never answers the real requests.

Shell completions

completions/ holds completions for fish and bash, and zig build installs them beside the binary, at share/fish/vendor_completions.d/zig65.fish and share/bash-completion/completions/zig65. Both shells already look under ~/.local/share, so:

zig build --prefix ~/.local

puts them somewhere they will be found. They complete the flags and what each one takes: only disk images for --disk1 and --disk2, matched whatever case the name is in, and a flag stops being offered once it is on the line, since none of them can be given twice. Neither needs anything else installed.

A test holds both files up against the usage text and fails if a flag is in one and not the other, in either direction, so they cannot quietly go stale.

Testing

zig build check type checks every source file and produces nothing: none of its artifacts is installed or run, so the build system passes -fno-emit-bin and the compiler stops after semantic analysis. It is the step to point a language server at.

The corpus runner is its own project, in corpus/, and takes the library from the checkout it sits in as a path dependency, so the two are always the same code. That is where the corpus itself is declared as well, which is the point of the arrangement: it is 880M compressed and 5.8G unpacked, and nothing that merely builds the library — or generates anything from the library's manifest — has to know it exists.

It runs the whole corpus in about eight seconds: the 256 files have nothing to do with each other, so they are read, parsed and run sixteen at a time. -Dssf narrows it to chosen opcodes and costs proportionally. Anything that disagrees with the hardware fails the build.

zig build corpus                       # from here, which runs it over there
zig build corpus -Dprocessor=65C02     # against a CMOS part instead
zig build corpus -Dprocessor=R65C02    # or Rockwell's, or W65C02 for WDC's
zig build corpus -Dprocessor=RP2A03    # or Ricoh's, against the NES recordings
zig build corpus16                     # the 65816, which is a core of its own
cd corpus && zig build run             # or from the project itself
cd corpus && zig build run -Dssf=69    # just opcode $69
./single_step_tests path/to/65x02/6502/v1

The corpus is still a lazy dependency of that project, so even there it is fetched only when the step is asked for. The binary takes the directory as an argument, so it can be pointed at a checkout by hand.

Two tests deserve singling out. src/system_tests.zig runs whole machines: each test powers one on with Apple's own ROM, waits for it to come up in BASIC, types a three-line program at the keyboard the way a person would — each key waits until the firmware is in a read loop asking for one, since there is one latch and no queue — runs it, and reads the three answers off the screen. An Apple II Plus and a IIe come up in Applesoft on their own; a IIc, whose drive is built in, waits for a disk until Control-Reset warm starts it into BASIC; an Apple II comes up in the old Monitor and is sent into Integer BASIC with E000G; and an Apple-1 loads BASIC from a tape through the Cassette Interface first. An Apple III is booted five ways. With nothing in the drive it goes to the RETRY its ROM prints after the diagnostics. From the SOS 1.3 System Utilities disk it comes up through the boot block, the kernel and the Pascal interpreter to the utilities' main menu, about forty seconds of machine time later, where a key opens the menu it names and another asks the clock chip for the date. From Apple Business BASIC, whose HELLO draws a banner and catalogs the disk before the ) prompt appears, it takes the same three-line program as every other machine and runs it. It runs Apple's own Apple II emulation, booting a DOS 3.3 disk swapped in and running the program in Applesoft as an emulated 48K II Plus. And with a ProFile image on its interface card in slot 4, Business BASIC catalogs the empty volume and writes a file to it, which reads and writes blocks through the card's pseudo-DMA. The ROMs are Apple's and are looked for in ~/dev/Apple ROMs, the disks in ~/dev/Apple Disks; a test whose file is not there is skipped, not failed, which is what lets CI run the file. On a machine that has them, a skip is worth noticing. The IIgs is the one that needs a disk: a ROM 01 with nothing to start from prints Check startup device! and keeps scanning, and Control-Reset there goes back to the scan rather than to BASIC, so it boots Apple's 1983 ProDOS examples disk from the 5.25-inch drive, whose startup program leaves the ] prompt. That boot is also what proves the Speed register's motor-on detectors: without them the machine reads a Disk II at 2.8 MHz and nothing on the disk loads.

src/instantiation.zig forces every declaration to be compiled, because Zig analyses a function only when something references it and instantiates a generic one only when something calls it with particular comptime arguments. Three separate bugs in this codebase lived in functions nothing called, and so were never compiled and never failed a build.

How it was found out

What follows is the long way round: the questions the machine asked that the manuals did not answer, and how each was settled. They are in the order they came up, which is roughly the order the machine was built in.

The 65816

There is a second core in src/cpu16/, for the 16-bit 65816, and it is deliberately not the same code. The two could have been one core generic over its width, and the five 8-bit processors would have paid for it: a runtime branch on the width flags in the operand path of the hot loop, and a memory interface addressed by something wider than sixty-four kilobytes. Today a u16 against a 65536-byte array means an out-of-range address is not a bug to be caught at run time but a program that will not compile, and widening that for the sake of a sixth processor would trade a guarantee the compiler gives for free against masks and assertions on every machine here. So the clock and the decimal arithmetic are shared, because neither knows what an address is, and everything shaped by the width of an address is written twice on purpose. Work on the new core cannot regress the 12.78 million cases the old one passes.

It is finished, and exact. All 256 encodings, all twenty-five addressing modes, both modes of the machine, and the interrupt sequence. Every one of the 5,120,000 recorded cases passes: every register, every cycle count, and every cycle of every bus trace.

The decoder is where this processor stops resembling the older one. A direct page that is not on a page boundary costs a cycle every time it is used. An index that carries out of its low byte costs one — and so does a sixteen-bit index whether it carries or not, because then there is no low half to add on its own. A write pays that cycle every time, since it must know where it is going before it goes there, while a read can start and put the high half right afterwards. Indexing can carry out of the data bank entirely, so an array may run off the end of one bank and go on into the next. All of that is checked cycle by cycle:

$ zig build corpus16
5120000 tests: 5120000 succeeded, 0 failed, 0 skipped, 0 not implemented
instructions: 256 of 256 encodings implemented, 5120000 cases run, 2560000 of them native
bus traces:   every cycle matches
cycle counts: all 5120000 cases match

Its corpus is a separate set, twenty thousand cases per opcode rather than ten because every encoding is recorded twice, once native and once emulating: 5,120,000 in all. The recordings are sharper than the 65x02 ones, too. A 65816 drives eight lines the 6502 has no pins for, and each cycle carries all of them — VDA and VPA saying whether the address is data, an instruction byte, both or nothing, and E, M and X saying what mode and widths the processor was in. A cycle that transferred nothing is written with a null value, so an emulator that logged it as a read of some byte is caught even though no register would ever show it. Three such disagreements turned up in the first run of the fourteen: XCE was putting the new emulation line on the bus a cycle early, and REP and SEP were both moving the address on and changing the widths before the cycle that is there to do it.

An opcode with no implementation is reported apart from one that is implemented and wrong — a distinction that mattered while the table was filling up and costs nothing now that it is full.

One thing the recordings cannot reach: the interrupt lines. An interrupt is a line going low rather than an instruction, so there is no encoding to record twenty thousand cases of. What they did settle is the sequence itself, through BRK and COP, and a hardware interrupt is that same sequence with two cycles of its own where the opcode and the signature byte would have been. The lines are implemented on that basis and covered by unit tests instead — including the one thing about them the corpus could never have shown, which is that while emulating, bit four of the pushed status is the 6502's break flag: set when an instruction asked and clear when a line did, the only way a handler can tell them apart when they share a vector.

How wide is this instruction?

The 65816 poses a problem the 6502 does not. On the older machine an opcode settles everything before the machine runs: which instruction, which mode, how many bytes, how many cycles. Here it does not, because the same encoding is two bytes and two cycles or three and three depending on M and X, and those change while the program runs.

There are two ways to live with that, and this core can be built either way. branch asks the flags inside the instruction. tables builds one dispatch table per combination of the two flags and keeps a pointer to the one in force, changing it only when a flag does, so that the width is comptime again and the whole instruction body specialises the way the 8-bit core's does.

zig build bench16 runs both in one process over a fixed stream of the implemented instructions, so no build or machine difference can get between them. Best of three, four million instructions, on an eight core Ryzen 5800X:

a width change every branch tables
instruction 143.5 M/s 52.4 M/s branch 2.74×
4 instructions 92.0 M/s 71.2 M/s branch 1.29×
32 instructions 88.4 M/s 86.7 M/s branch 1.02×
never 93.4 M/s 96.2 M/s tables 1.03×

branch is what the core is built with. The table strategy wins only where the widths never move at all, and then by three per cent, while it loses by nearly three to one where they move constantly — and real code sits at the wrong end of that, since a routine that reaches for REP #$30 and SEP #$20 around a piece of work changes width every few instructions rather than every few thousand.

The reason is not the branch predictor, which does fine. It is that four tables mean four specialisations of every instruction body, and when the widths move about, all four copies are live at once. The branch it saves is one well-predicted test against a flag already in a register; the cache pressure it buys is not nearly so cheap. That is the opposite of what I expected, which is the argument for measuring rather than reasoning. Both strategies are kept, and both pass the whole corpus, so the question can be asked again when there is more instruction set to ask it about.

Some of what it found is worth writing down, because none of it would have shown in a register and none of it is in a summary of the instruction set.

Decimal subtraction is not the 65C02's. The older CMOS part corrects the whole number at once — six off if the low nibble borrowed, sixty more if the byte did — and that is what cpu/add.zig does, correctly, for its own twelve and a half million cases. Do the same here and $b0 - $4d with a borrow comes out $5c; the hardware says $6c, which is what you get by taking each digit on its own and adding ten when it goes under. The two agree on every pair of digits that are really digits and part company on the six values a nibble can hold that are not. This is the reason the 16-bit core has an adder of its own rather than sharing the 8-bit one.

A read-modify-write is two different instructions depending on the mode. Natively, the cycle between the read and the write is an ordinary internal one with nothing driven. Emulating, the unmodified value goes back onto the bus first, which is what the NMOS 6502 did and what a program written for that machine may be relying on. The register widths are identical either way; it is the emulation line that decides. And when the accumulator is sixteen bits, the two halves are written back high first — the reverse of an ordinary store, which is two cycles and so plainly visible to anything watching.

The memory lock is a real signal. The 65816 holds MLB down across all three cycles of a read-modify-write so that nothing else on the bus can come between them. Every one of the thirty-four read-modify-write encodings failed on that one character until it was modelled.

The stack is two stacks, and which one you get depends on the instruction. While emulating, the pointer is held inside page one, so pulling from $01ff reads $000100 — that is the 6502's stack and a program written for that machine expects it. But the instructions this processor added were never promised any such thing, and step straight out of the page: PLD from $01ff reads $000200, and pushing two bytes from $0100 puts the second at $0000ff. So the confinement is per instruction rather than per access, and it is applied at the start of each one and when the register is read, never in between. The register still reads back inside page one afterwards, which is how both facts can be true at once.

A stopped processor lets go of the bus. Every other cycle has an address on it even when nothing is transferred, because the chip is still driving one. WAI and STP end with a cycle that has no address, no value, and not one of the eight signal characters set — the read line included, which nothing else does. It is the only cycle here where the processor is not on the bus at all.

A block move is not one instruction, and the recordings say so. MVN and MVP move a single byte and then back the program counter over their own three bytes, so a move of a whole bank is that instruction executed sixty-five thousand times rather than a loop the processor runs — which is exactly why an interrupt can be answered in the middle of one. No recording could hold that, so the corpus runs the machine for a hundred cycles or until the move finishes, whichever comes first, and a case that does not finish stops two cycles into the next fetch. The runner is told about that rather than left to fail on it: it keeps executing while the move has more to do and another whole execution fits inside what was recorded, and allows for the counter being two bytes on at the end. Only a block move is allowed that allowance — anywhere else a difference in cycle count is a wrong cycle count.

A branch across a page costs nothing natively. On a 6502 a taken branch that lands in another page spends an extra cycle putting the high half of the address right, and this chip still does — but only while emulating. Natively it fixes the high half for free, which is one of the quiet reasons the same code runs faster in native mode than in emulation. And when it does spend that cycle, it does not spend it where the 6502 did: the older part drove the half-built address that nobody meant, while this one simply holds the offset's own address again, as the cycle before it did.

A status register pulled off the stack does not take effect until the instruction is over. RTI pulls P first and then the return address, and through those later cycles the width lines are still driving the old widths. Apply the new ones where they are read and three quarters of the native cases disagree — which is exactly how often at least one of M and X changes. XCE and REP do the same thing with their own flags, so this is the family habit rather than a quirk of one instruction.

And the 6502's pointer wrap is gone. The older machine wrapped a zero page pointer inside the zero page, so a pointer at $ff took its high byte from $00, and the natural guess is that this one keeps that while emulating. It does not. The corpus said so three separate times, each on a case rare enough to turn up alone — once for a long pointer, once for a stack-relative one, once for (dp,X). What emulation mode does keep is the wrap in the index arithmetic, which is a different thing in a different place.

The core is a third of the story for a whole machine. bootgs booting System 6.0.4 for sixty million instructions on the same machine took 3.75 seconds before a profiling pass and 2.15 after it — about 28 million instructions a second, some twenty-five times a real IIgs — and what the pass found was not in the core at all but in three things done on every memory access that only needed doing when something changed. The serial chip's advance recomputed both channels' next event on every call, because its early return only fired when the clock had not moved, which it always has; now it trusts the next event it keeps, since everything that changes a channel's timing recomputes it on the way out, and it is two compares. updateVideo did two 64-bit divisions per access to learn things that only change on a scan-line boundary; it now returns at once while the clock is inside the line it last counted. And the decoder asked the ROM image for a bank's slice on every fetch from banks $fe and $ff, which is half of everything a IIgs executes, a division and a switch each time; the bases are a table now, filled once when the ROM is loaded. What is left in the profile is the core's dispatch, the operand fetch and the decoder's own switch, with bootgs's own bookkeeping beside them.

The disks are not where the time goes, on either side of the machine. While GS/OS reads its first megabyte and a half off the 3.5-inch drive, everything in src/iwm.zig and src/disk35.zig together is under three per cent of the run; a DOS 3.3 boot under the WOZ runner, which is the bit-level Disk II with the head over a real recording, spends under one per cent in src/disk2.zig and src/woz.zig. What that profile did find was that the WOZ and corpus subprojects had been running in Debug all along: standardOptimizeOption's preferred mode is not a default but a choice made only when --release is passed, and nothing passed it. Built as intended, the two boots of the DOS 3.3 master take five seconds rather than twenty-four, and the whole forty-five disk boot pass takes a minute and a half rather than most of an hour. The other thing it found was a IIe asking its Mockingboard whether it is interrupting on every memory access, six per cent of a boot for a slot with no card in it; the question is inlined now and the empty slot answers for free.

A machine to put it in

src/memory16/apple2gs.zig is the beginning of one, and it is deliberately the beginning: fast RAM in banks $00 and $01, the Mega II's slow RAM in $e0 and $e1, the ROM in whichever top banks the image turns out to hold, and the language card window that makes the reset vector reachable. Nothing else. Every soft switch it does not understand is counted rather than answered.

That is the point. There is no corpus for a machine, and the nearest thing to one is the ROM: it runs a self-test when the power comes on, and a model that is honest about what it has not got will be told, by the firmware, what to build and in what order.

$ zig build bootgs -- "Apple IIGS ROM 01 - 342-0077-B.bin"
reset to 00:fa62
after 2000000 instructions and 6812901 cycles, at ff:a5a8
emulation=false m=true x=true
time by bank: $00=1923 $e1=1 $ff=1998076

21 accesses to 5 soft switches this machine has not got, the first at $c039
  $c031  16
  $c039  2
  $c02d  1
  $c03c  1
  $c046  1

most visited addresses: $a5a5=644063 $a5a8=644063 $a5a0=232971 $a5a3=232971 ...

text screen (40 columns): all '@', so nothing has been written to it

Which says a good deal. The machine starts from the vector in the ROM, gets itself into native mode, and spends its time in the firmware rather than wandering into a bank of zeroes — so the memory map is close enough to be worth improving. And the list is the work, in the order the firmware wants it.

The first run of this had 905,512 unanswered accesses rather than 24,706, and 881,351 of them were $c019: the firmware was spinning on vertical blanking and getting nowhere. Reading the Hardware Reference and implementing what it sets out plainly — vertical blanking, the New-Video register at $c029, the Shadow register at $c035, the Speed register at $c036 and the State register at $c068 — took ninety-seven per cent of the remainder away and let the firmware get on to polling the keyboard, which is why $c000 and $c010 appear in the list now and did not before. Most of those registers are kept and read back rather than acted on, which is not the hardware but is what firmware checking that its configuration took will accept.

The Mega II is wired in for the switch page, which is the second reason the list is short: everything from $c000 to $c0ff that an Apple IIe really owns is answered by an Apple IIe, because memory8/apple2e.zig is one and is sitting inside this model. That is what took the list from twenty-seven kinds of unanswered switch to nine, and every one of the nine that is left is a IIgs subsystem — the keyboard microcontroller, the clock, the border colour, the sound and serial chips.

Only what a IIe genuinely owns goes across, and the list is of what to send rather than what to keep. The switch page is not divided neatly between old and new: a IIgs took a good many addresses a IIe was not using and gave them to hardware a IIe never had, so $c027 is the keyboard microcontroller here and nothing at all there. Handing that to a IIe would get an answer, and a made-up one — which is worse than getting none, because an unanswered switch is counted and shows up as work to do while a wrong answer is silent.

The Apple Desktop Bus is the third reason the list is short. An Apple IIgs does not read its own keyboard: a microcontroller does, over a four-wire bus it shares with the mouse, and the machine talks to it through four registers. Those registers are here and are exactly what the manual describes, down to which bits are read-only and the odd detail that bit 4 clears when the status register is read and not only the data one. What is not here is the microcontroller: the Hardware Reference sets out the registers and then says the commands the host sends "are located in ROM", which is a polite way of saying they are in a different book. So commands are accepted and acknowledged and nothing is invented about what any of them mean — a machine finds its keyboard, and does not find anything that needs a command answered.

The keyboard itself is deliberately not in the bus. On this machine the microcontroller feeds the same $c000 an Apple II has always had, the Mega II already owns that latch, and there should be one answer to "is a key waiting" rather than two that can disagree. So the bus reports the Mega II's.

And the manual answered the question this model was written around. The seam between a IIgs and the Mega II inside it is bit 0 of the New-Video register, the bank latch: set, the low bit of the bank address picks the Mega II's main or auxiliary 64K directly, so $e1xxxx is the auxiliary bank whatever the old soft switches say; clear, the Mega II ignores the bank bit and the switches decide, exactly as on a IIe. Which is a satisfying thing to find, because $c029 was also the first register the firmware asked for — the open question and the machine's first question turned out to be the same one.

Two more things worth writing down, since both were guesses before and are not now. $c019 is RDVBLBAR, and the bar is the whole of it: the bit is set while the beam is drawing and clear while it is not, the same way round as a IIe and the opposite of a IIc. And every bit of the Shadow register is an inhibit — one disables, zero enables — with bit 6 inhibiting the I/O and language-card space in banks $00 and $01 only, never in $e0 and $e1, where they are always there.

All four system ROMs behave the same way, and the two dumps of 341-0748 that have their halves exchanged run identically — same final address, same 1,585,244 cycles — which is iigsrom.identify proving itself on something other than its own tests.

The Mega II's 128K is its memory too, now: banks $e0 and $e1 are the IIe's two halves rather than an array of this model's own, which is what makes the video memory the video's. Which half an address reaches is the bank latch — set, the bank address says; clear, the Mega II ignores the bank bit and its own switches decide, so $e0 and $e1 are then the same memory. That last part is easy to get wrong and was: the first version of this had them as two separate banks always, which is a reasonable guess and not what the manual says.

The clock is the fourth. $c033 and $c034 are the data and control registers of the chip that keeps the date and the battery-backed settings — and the border colour, which shares the low nibble of the control register and has nothing to do with the clock at all. The handshake is documented exactly and is implemented: put the byte in, start the transfer, poll bit 7 until the chip says it is done. What the bytes mean is not documented here, the manual pointing at the Firmware Reference again, so the bytes are carried and counted and kept for whoever decodes them, and a program asking this machine the time finds out nothing — which is the truth, where a made-up answer would be a wrong date.

That took the list from 8,725 unanswered accesses to twenty-one, and what is left is five registers touched once or twice each: the disk interface, the serial chip, the slot ROM select, the sound chip, and an interrupt flag.

Which means the tally has done its job, and the next question needs a different instrument. The machine answers nearly everything it asks for and still ends up going round: 644,063 times through a four-instruction loop at $ffa5a0 that waits for the display to start and then for the blanking to start — a frame sync, and one that is working, since it gets to the end and is called again. So it is being used as a delay by something further out that is waiting for something else. The text screen is still every byte zero, so the firmware has not got as far as writing to it.

Following that thread found something else, and it is worth writing down because it is a mistake the manual invited. The second-hottest loop is at $ff8443: load a counter with 7,500, poll $c027 for bit 5, and on finding it read $c026. A machine that waits for bit 5 and then takes a byte out of the data register is a machine for which bit 5 means "there is a byte in the data register" — and this model had that flag at bit 2, because the figure for that register was read from the bottom up while every other figure in the book was read from the top down. The whole byte was mirrored. Every other register was right; that one was not, and no test of mine would ever have caught it, because the tests agreed with the implementation. The ROM did not.

So the firmware is waiting for the desktop bus to answer, and timing out after 7,500 tries, seven times over. What it is saying can now be read, because the Firmware Reference has the command set the Hardware Reference sent us away for. Byte for byte, the ROM says:

07 [00 32 00 24]   Sync, with four configuration bytes
73                 Disable Device SRQ, device 3
09 [51 00]         Read Microcontroller Memory      <- waits for a reply
08 [51 a5]         Write Microcontroller Memory
01                 Abort
0f                 Read Available Keyboard Layouts  <- waits for a reply

Knowing the lengths matters more than it looks: a command's data bytes go to the same register the command did, so a model that does not know how many to expect reads the next data byte as a command and loses the thread for good. Those lengths are implemented, and so are the two commands whose answers this model actually holds — Read Modes Byte and Read Configuration Bytes give back exactly what Set Modes and Set Configuration Bytes put there.

The two the firmware was waiting on turned out, on a closer reading, to be answerable without inventing anything — and answering them is what got the machine moving.

Read Microcontroller Memory asks for a byte of the microcontroller's own RAM, and the pair of commands around it is unmistakable: read location $51, write $a5 there. That is a memory check, and the microcontroller has RAM. What is in it at power-on is undocumented, so it starts as zeroes — but a byte written to it comes back, and that is the whole of what the check wants. Refusing to answer fails a test the hardware passes. The microcontroller's ROM, which the same command can also reach, is its program, and that is not modelled: ask for it and nothing is said.

Read Available Keyboard Layouts asks how many layouts the machine has. The manual gives the format exactly — a count, then that many identifiers — and what the count is is a configuration of the machine being modelled rather than a fact about the hardware, in the same way that which ROM is fitted is. This one is a US machine with one layout and one character set.

With those two answered the machine goes a great deal further: sixty-three bytes of conversation with the desktop bus instead of fourteen, and it starts executing in bank $00 and in the ROM's other bank rather than going round in the same four instructions.

Then it wanted the disk port, which on a IIgs is an IWM where a IIe had a card — sixteen addresses, of which eight are the stepper phases, two the motor, two the drive select, and four are a pair of flip-flops that decide which of the chip's registers the next access reaches. That is in src/iwm.zig, and the self-test's own loop is the specification: write to an odd address, read an even one, and the low five bits must match, because the mode register is write-only and the status register is how a program finds out what it wrote.

One thing there is worth repeating, because it took a while to see. The first version had the handshake register read as zero, reasoning that there is no drive attached and so nothing to be ready for. But that is a different claim, and a false one: the IWM is a chip on the board whether or not a drive is plugged into it, and a chip with nothing to do is ready. Reading zero models a chip that is permanently busy — not an empty disk port but a broken one — and the firmware waits for it forever. An hour of "why is it still stuck" came down to that distinction.

The same lesson came round again, and was not recognised in time. The desktop bus microcontroller has a command that asks which microcontroller it is, and this model refused to answer it: this is not any particular part, so a part number would be invented, and saying nothing looked like the more honest of the two. A ROM 01 never asks, so nothing showed. A ROM 3 asks, waits, and on getting nothing puts Fatal system error-> 0911 on the screen and stops — $09 being the desktop bus tool set. It is the handshake register again: a chip that is there answers, and refusing to models a broken part rather than an unknown one. With an answer, every IIgs ROM to hand boots — ROM 00 and ROM 01, both ROM 3s, the ROM 4 from a Mark Twain, the 1986 beta, the Alpha ROM and a prototype — and which answer makes no difference: nought, one, six and $ff were all tried and the machine cannot tell them apart, so only the answering matters.

Then the sound. An Apple IIgs makes it with an Ensoniq 5503 — thirty-two oscillators and sixty-four kilobytes of waveform storage of its own — and the processor cannot reach either directly: it goes through four registers, and those four are src/ensoniq.zig. Two details of that interface had to be exactly right and are both in the manual rather than guessed at. Bit 6 of the control register set means the RAM and clear means the chip, which is the opposite way round from what the bit's name suggests. And reading the data register lags by one — it hands back the byte the previous read fetched while fetching the one asked for now, so a program wanting ten bytes reads eleven times and throws the first away. Without that every byte comes back shifted by one, which a memory test reports as every location being wrong: a correct model of working RAM would look like broken RAM.

Behind those registers the oscillators run. Each is an address generator rather than a tone generator, which is the whole idea of the part: a 24-bit accumulator that a frequency is added to, some of whose bits are an address into the sound RAM, and the byte there is the signal. Which bits is Figure 5-7's business — the wavetable size decides how the sixteen address lines are shared between an oscillator's pointer and its accumulator, and the address bus resolution slides the accumulator's share up and down — and the figure is what addressOf is written from, corner by corner, in a test.

What happens at the end of a wavetable is the difference between the four modes, and it is also where two things had to be got right that a plain reading would miss. The first is when the interrupt happens. The Hardware Reference says an oscillator interrupts when it "reaches the end of its wavetable", not when it halts, and that distinction carries weight: Technical Note #23 describes the machine's own note synthesiser running a free-running oscillator over the first page of the sound RAM at zero volume purely as a clock — an oscillator that by definition never halts — so a model that interrupted only on halting would leave every envelope in the machine frozen. The second is what the interrupt register reads, and it was wrong here for a long time. Bit 7 is the interrupt line itself, and the line is active low, so the bit reads one while nothing has interrupted: Table 5-3 of the Hardware Reference gives it as "1: no oscillator has generated an interrupt ... this bit reflects the status of the IRQ line", and Apple's own specification for the chip, the Ensoniq DOC ERS, says it from the other side — "the OIR will force the IRQ line low indicating an interrupt, bit 7 will be set to a zero", with bits 0 and 6 "always read back as 1's". A model that read $00 while idle told the ROM's interrupt manager, which tests exactly that bit, that every interrupt in the machine was a sound interrupt; that is the whole of why GS/OS printed "Unclaimed Sound Interrupt" for ever, and it is told under What GS/OS wanted. The same document says how the interrupt is let go — "after the processor reads the OIR, the DOC will clear the appropriate interrupt request" — and what happens when a second oscillator interrupts before the first is read: the rest queue behind the register, first in, first out, and each read brings the next forward. The machine's own handler relies on the first of those: at $ffb8f6 it points the chip at the interrupt register, reads the data register twice — once for the lag and once for the value — tests bit 7, puts the pointer and the control register back and writes nothing to the chip.

The behaviour worth pointing at, though, is one that is not written down here at all. Technical Note #11 records that the DOC falls silent for no more than a single sample "whenever a swap occurs from a higher-numbered oscillator to a lower-numbered one", and that it does not do so swapping the other way. Nothing in src/ensoniq.zig arranges that. It falls out of scanning the oscillators in order, lowest first, as the chip does: an even oscillator woken by its odd mate has already been passed over this time round and cannot play until the next, and an odd one woken by its even mate has not been reached yet and plays immediately. The test for it asserts the anomaly rather than the tidy behaviour, because reproducing the anomaly is the evidence that the scan order is right.

What comes of all that goes to a file. gs --audio writes it as a WAV, which is the same answer the 8-bit side gives for the speaker and for the same reason: a terminal is not an audio device. The reconciling of the chip's moving rate with a file's fixed one is described under Sound.

That took the list to 128 accesses across seven switches, touched once or twice each — the disk interface register, the slot ROM select, the serial chip, an interrupt flag, and two video registers. What the machine wants now is not a register at all: it is a drive with a disk turning in it, and that is a larger piece of work than anything above.

The two clocks are now two clocks, which fixes the timing that was knowably wrong. A IIgs runs its processor at 2.8 MHz and its Mega II at 1.024, so anything reaching across to the slow side waits: the Hardware Reference puts a synchronised slow access at nearly three fast cycles, and three is what is used here. The Mega II's banks are slow, the soft switches are slow, and fetching from the slot ROM is slow — but the three registers inside the FPI itself are not, because the processor never leaves its own side of the machine to reach them, and reading the interrupt ROM is not either. The state register and the slot ROM select register live in both chips at once and so are written slowly and read quickly, which is the sort of detail that only a manual can tell you. The same three is used for the video's frame, so that the display and the processor agree with each other rather than one being exact and the other round.

Chasing that turned up a bug worth naming, because it had been quietly poisoning the very measurement this section is written around: $c100 to $cfff is slot ROM, and it was falling through to the soft-switch case where truncating the address to eight bits turned $c123 into switch $23. Reads of slot firmware were being counted as reads of soft switches that had never been touched.

The machine puts something on the screen

Shadowing was the last thing between the firmware and the display, and putting it in was the difference between a blank screen and Check startup device! in the middle of it — which is the right answer from a machine whose disk port is empty, and the first thing this model has ever drawn.

The reason is worth setting out, because it is the whole of why the register exists. The display buffers are in banks $e0 and $e1, on the Mega II; every program written before this machine existed puts its display in bank $00, on the fast side; and shadowing is the wire between them. A write to a shadowed address is made twice, once to fast RAM and once to the Mega II. A read is not — "read instructions access the high-speed shadowed bank" — and that asymmetry is the point, because it means old code keeps working at full speed except for the writes. The manual is equally plain about the cost: "only write instructions to the video locations require the system to operate at 1.024 MHz", so a shadowed write is three fast cycles and a read of the same byte is one.

Which areas are shadowed is the Shadow register, and its bits are all inhibits: text page 1, Hi-Res pages 1 and 2, the Super Hi-Res buffer, the auxiliary Hi-Res pages and text page 2, one bit each, set to disable. A reset clears the register, so a machine comes up shadowing everything. Two of them are not simply an area:

  • Bit 4 names banks rather than a buffer. It takes the Hi-Res pages away from the odd banks alone and leaves the even bank's alone, which the manual states in as many words.
  • Bit 3 is the Super Hi-Res buffer, a single 32K span from $2000 to $9fff in the odd bank — which means the two Hi-Res pages lie underneath its lower half. The two paths are separate and either one enabled carries the write, so inhibiting the Hi-Res pages does not stop a write at $3000 reaching the screen if Super Hi-Res shadowing is still on.

Which of the Mega II's two halves a shadowed write lands in is the same question as which half $e1xxxx reaches, and the Hardware Reference answers both in one sentence: "if shadowing is enabled or the software is addressing bank $E0 or $E1, an odd-numbered bank address will access the Mega II auxiliary memory automatically, without using the soft switches ... otherwise, the Mega II ignores the bank bit". So one function decides it for both, which is the point of having it. Note the two senses of auxiliary in play: the FPI decides whether to shadow from the bank number it can see, and the Mega II decides where it lands, and with the bank latch clear those two can disagree.

The same question comes back on the fast side, and this model had it wrong for a long while. Banks $00 and $01 are not just the first two banks of memory: "for standard Apple II programs, banks $00 and $01 take on the features of the main and auxiliary banks", and the Hardware Reference goes on to list text Page 1 and text Page 2 and both Hi-Res pages as being "of both banks". It has to work that way, because a program written for a IIe reaches the auxiliary sixty-four kilobytes by setting RAMRD or RAMWRT and then addressing bank nought, and there is nowhere else for that to land. So an even bank asks the same switches a IIe would — RAMRD, RAMWRT, ALTZP, and 80STORE with PAGE2 over the display pages — and an odd bank is the auxiliary outright, since native software is handed bank $01 by the Memory Manager and uses it as ordinary memory with the old switches in whatever state it found them.

Nothing noticed this for a long time, because everything that had been run either stayed in one bank or only wrote to the display, where shadowing was already picking the right half of the Mega II. What found it was ProDOS 8 counting its memory. Every version from 1.9 on turns on write-to-auxiliary, puts $ee at $0800, and reads it back to see whether there is another sixty-four kilobytes there — and with the switches ignored, that byte landed in bank $00 on top of the boot code ProDOS had just loaded, one byte of which was the $20 of a JSR $BF00. The symptom was a machine that printed its banner, said "Unable to load ATInit file" and hung, from a floppy and from a hard disk alike, while the same disk booted an Apple IIe perfectly. It took finding the one byte that differed between the disk and memory to see what had happened. With the switches honoured, ProDOS 8 1.9 and 2.4.2 boot a IIgs and run what is on the disk.

There is a second half to that story, and the ROM found it. The State register at $c068 gathers those same eight switches into one byte — ALTZP, PAGE2, RAMRD, RAMWRT, RDROM, LCBNK2, ROMBANK, INTCXROM — so that a program can save and restore the lot without touching eight addresses. This model kept it as a byte: written, remembered, read back, and moving nothing. That was harmless for exactly as long as nothing in banks $00 and $01 looked at the switches at all, which is to say until the paragraph above.

What found it was the firmware's own AUXMOVE. On a IIe that routine shuffles RAMRD and RAMWRT a byte at a time; on a IIgs it does not need to, because the processor can address both banks at once — at $ffc3c6 it is a single MVN between banks $00 and $01. But before it gets there, at $ffc39c, it reads the State register, clears RAMRD and RAMWRT with AND #$cf, writes it back, and puts it as it was afterwards. It has to: with the auxiliary bank read-enabled, a block move from bank $00 would take its source from the very bank it was meant to be filling. Against a register that only remembers, those two switches stayed where they were and AUXMOVE quietly copied a bank onto itself.

The Apple II System Utilities is what showed it. Its device drivers live in the auxiliary bank and its dispatcher switches the machine over before entering one; the driver's first act is to AUXMOVE its sixteen byte parameter block across, and it was arriving as zeroes. Reading a command of nought where $0d was meant, the driver returned error 1, and the screen said "An attempt to read or write data on a disk was unsuccessful" — for the hard disk, for the 3.5-inch drive it had just booted from, and by pathname as much as by slot and drive. Everything underneath was correct: the block device, ON_LINE, READ_BLOCK, SmartPort and AUXMOVE itself all answered properly when asked directly. It took following the utility down through its own dispatcher to the one instruction that mattered.

A smaller thing the same reading corrected: this model started its Speed register at $80, on the reasoning that the Firmware Reference says "system firmware, running in ROM, runs at the full 2.8 MHz". The Hardware Reference is exact where that was a guess — "the Speed register is cleared on reset or power up, except for bit 6, which on power up is set" — so a IIgs comes up at 1.024 MHz and is made fast, and making it so is one of the first things the firmware does. Starting it fast hid that instruction rather than obeying it. Bit 6 is the one bit a reset leaves alone, because it is how firmware tells a cold start from a warm one, and the firmware turning the speed up is why the correction costs almost nothing: 38.1 million cycles for the same eight million instructions, against 37.7 before, rather than the threefold difference a machine that stayed slow would show.

Down to one register

Having the Hardware Reference and the Firmware Reference both open as searchable text turned the remaining to-do list from seven addresses into one in a single sitting, because six of the seven were fully documented and the model had simply never been told. What was left, and where each was written down:

  • $c021, $c022, $c02b are the video controller's, and it has a file now — src/vgc.zig — as the clock and the disk port and the sound chip do. $c021 is monochrome or colour in bit 7 alone; $c022 is the text colour in the high nibble and the background in the low; $c02b names one of eight character sets, chooses NTSC or PAL, and says which of a keyboard's two languages is selected. The New-Video register moved in with them, which is where it belonged: it is the VGC's, and its bit 0 — the bank latch, the seam between the two halves of the machine — is the one bit in that chip that has nothing to do with video.
  • $c031 is the Disk Interface register and went to src/iwm.zig, because although it is a register of the machine rather than of the chip, it is the chip it configures: one of its two bits says whether the port is talking to a 3.5-inch drive or a 5.25, which decides whether the four phase switches step a head or spell a command. Which of the two bits that is, both manuals get wrong, and the ROM settled it — see Two manuals wrong, and a disk that boots anyway. It is also one of the few registers where reading back something other than what was written is correct — "0's are returned in the unused positions" — which is why the manual says to touch it only with a read-modify-write.
  • $c02d is the Slot register, and it has real consequences rather than being storage: it chooses, for each slot, between the device built into the machine and a card in the slot itself. Clear means built-in and the driver comes from the system ROM; set means the card answers at $Cn00. Nothing is fitted to any slot here, so setting a bit points the processor at a card that is not there and it gets what a real machine would give it, which is nothing. Slot 3 is missing from the register entirely — the IIe's own SETSLOTC3ROM still governs it, for the sake of the eighty-column card that used to live there — and bits 3 and 0 are reserved.
  • $c046 is the Interrupt Flag register, and every bit of it is gathered from elsewhere rather than stored: the mouse button now and as it was, the state of AN3, a quarter-second tick, vertical blanking, the Mega II's mouse, and whether the system IRQ line is asserted. The two mouse-button bits read as zero truthfully — there is no mouse, so its button is not down. AN3 comes off the Mega II, which is a real Apple IIe and tracks the annunciator like one; which way round the bit reads is settled in the comment, since the two manuals name $c05e and $c05f in opposite ways.

Every address the machine asks for is answered

The last of the seven was $c039, and it was the odd one out for a reason: it is the command register of a Zilog 8530, and an 8530 is not a register but a chip reached through one. Four addresses reach thirty-one registers, which is the whole difficulty. The Hardware Reference declines to explain the protocol and points at Zilog's manual, so src/scc.zig is written from that instead — section 2.3.4, which states it exactly:

  • Registers are reached in two steps. A write to a command register while the pointer is zero is a write to WR0, and its low three bits become the pointer; the next command access reaches the register named.
  • At the conclusion of that access the pointer resets to zero. Which is why WR0 and RR0 need no pointer written first and are reachable in one cycle.
  • There is one pointer for the whole chip: "the pointer bits may be written in either channel because only one set exists". So which channel is reached is decided by the address of the second access, and the first has no say.
  • Registers 8 to 15 need the Point High command written alongside the pointer bits, which is why a real command cannot be issued at the same time.
  • The data register is reached directly whatever the pointer says, and does not disturb it.

And the manual explains the machine's own behaviour in passing, which is the satisfying part: "if, for some reason, the state of the pointer bits is unknown they may be reset to 0 by performing a read cycle". The two reads of $c039 this firmware makes — the only accesses it makes to any of the four addresses — are a machine putting a chip into a known state before it starts. The first read gives back whatever the stale pointer was aimed at and is unusable; the second is the status. That is why it is two reads and not one, and the model answers both the same way a chip would.

Nothing is plugged into either port, so the status register says what a chip with nothing attached says: the transmit buffer is empty because it is, no character is waiting because none arrived, and no modem is raising a line because there is no modem. Each of those is sourced — "this bit is always in the set condition after a hardware or channel reset", "this bit is always 1 in Asynchronous mode" — rather than assumed, which is the same distinction the disk port's handshake turned on.

With that, the report says every address the machine asked for was answered. The to-do list this model has been steered by since it was written is empty, and what the machine wants next is not a register at all.

One consequence worth recording: the test that checks the counting machinery used to name a specific unanswered address, and implementing that address broke it — twice. It now finds an unanswered address rather than naming one, which is what it should always have done: the test is about the tally, not about any particular register. If a day comes when the whole page is answered it skips itself, and says so.

The $c02b lookup also connected two things that were written months apart. The Firmware Reference gives its bits 7 to 5 as a character-generator language — English, French, Danish, Spanish, Italian, German, Swedish — and the ADB microcontroller has a Read Available Keyboard Layouts command answering with a list of exactly those. The register and the command are two views of the same setting, and the model now names it in both places.

One language card, in two chips

The manual reading turned up a documented gap: bit 6 of the Shadow register is not about shadowing at all. It decides whether $c000 to $cfff in banks $00 and $01 is I/O or RAM, and with it whether those banks have a language card. Clear — the state a reset leaves — and the I/O space is there and the RAM behind it becomes a second $dxxx bank; set, and the bank is sixty-four kilobytes of contiguous RAM with neither. The model ignored the bit entirely and always gave banks $00 and $01 the ROM above $d000.

Implementing it broke the machine outright: it left the firmware executing zeroes in bank $00, three million instructions of nothing. Logging every language-card switch with the address that threw it showed the last two before the crash were $c083, and disassembling the ROM there gives:

$ff:1df7  LDA #$e1 / PHA / PLB     ; DBR = $e1
$ff:1dfb  LDA $c068                ; the State register
$ff:1dfe  BIT $c083                ; -> $e1c083
$ff:1e01  BIT $c083                ; twice, as the switch requires
$ff:1e04  STA $da4a                ; -> $e1da4a: the State register, saved
$ff:1e07  LDA $c036                ; the Speed register
$ff:1e0a  STA $da4b                ; -> $e1da4b, saved beside it

The first reading of this was that the firmware sets its data bank to $e1 so as to move the Mega II's language card and not the fast side's, and for some months the model had three cards — the Mega II's two and the fast side's own — moved by whichever bank the switches were touched in. That reading was wrong, and the two lines left off the disassembly say why: the data bank is $e1 for the two stores that follow, which save the State and Speed registers into the Mega II's card RAM before the firmware switches things around, and the switch is simply thrown from the bank the routine is already in. The hang that seemed to confirm the reading had another cause altogether — the interrupt vectors being pulled from language-card RAM, which the machine never does (see What GS/OS wanted) — and once that was fixed the ROM booted with one card exactly as it had with three.

What settled it was GS/OS, which is the kind of evidence a ROM cannot give: its loader throws $c08b from bank $00, block-moves its code into $e0/d000, and runs it there. With separate cards the ROM's interrupt manager, which writes the State register on every interrupt with the read-ROM bit set, moved the Mega II's card and left the fast side's where it was, so GS/OS read the ROM where it had just put its own code, and the ROM's JMP ($005e) at that address took it to $e0/0000. The Hardware Reference says as much when read again: the State register is one of "two registers ... that exist in both the FPI and the Mega II ICs", and is "written at 1.024 MHz" — a slow cycle, so that both chips take the write — "and read at 2.8 MHz in the FPI address space". Two copies of one thing, kept in step, not two things.

So there is one set of switches, mega2.card_switches, and the fast side's RAM behind the ROM answers to it as the Mega II's does. Bit 6 of the Shadow register still takes the card away from banks $00 and $01 altogether.

Since three machines here now have a language card, its switches are one type — src/languagecard.zig — rather than three copies of the same twelve lines. The Apple II Plus and the IIe moved onto it too. That protocol is worth having in one place: write-enabling takes two consecutive reads and a write disarms it, so that a read-modify-write cannot unprotect the card by accident, and bit 3 of the address chooses the bank the other way round from how the numbers read.

Which of the two four-kilobyte blocks is which is settled twice by the Hardware Reference — "the first of these blocks of RAM, block 1, occupies address space from $C000 to $CFFF", and "if bit 3 is 0, the first bank of RAM is used" — and it is observable rather than a matter of taste, because with the I/O space inhibited that block is reachable at $c000 directly. Note the numbering runs opposite in the two places: bit 3 clear is $c080 to $c087, which the soft switches call bank 2 and the memory map calls block 1.

What GS/OS wanted

Apple's System Disk 3.2 booted and GS/OS 6.0.4 did not, and it failed the same way from three different disks: the boot block read, the ProDOS 16 loader loaded and run, and then almost no disk work at all — thirty bytes of the thirty-eight thousand the whole attempt read — before the ROM played its failure tone and put up the "insert a disk" animation. Merlin 16+'s loader got far enough to say why it was unhappy, "Unable to load START.GS.OS file. Error=$002E", and $2E is the disk-switched code. Between that and the Finder lay five things. Every one was found by adding a print to the model and running the ROM once, and none by searching the ROM for byte patterns, which was tried first each time and found nothing.

Register 12 of the 3.5-inch drive reads one when the disk has been switched. The disk images were sound — every one of their 1,600 blocks went onto the surface and came back byte for byte — and the disk-switched latch was not stuck. What was wrong was the polarity of one status register, and the ROM says so itself. At $ff/5f3b it walks a three-entry table at $ff/5f7506 02 0c, write protect, disk in place and this one — reading each through the status routine at $ff/5fa4 and rotating each answer's bit 7 into $68, which after three rotations leaves register 12's bit at bit 5. $ff/5f5b then does AND #$20, and when that comes back set it strobes register 3 to clear the latch and loads #$2e. So a one there is the disk having been switched, and this model was answering one while nothing had been. Parker has that register the other way about, and he is the only writer who describes it at all — but he marks it as used by the IIgs ROM and documented in no publication available to him, and says only that he is "fairly certain" of it, while getting registers 2 and 6 either side of it right in the same table. Read the hedges. Corrected, the same attempt read a hundred and thirty-four kilobytes instead of thirty-eight, stepped the head off track zero and started the desktop bus.

One aside worth keeping, because it cost an hour: Parker prints the status routine as BIT Q6+1 / BIT Q7, and searching the ROM for those bytes finds nothing, because the second one is an LDA — the routine wants the value in the accumulator as well as in the sign flag. Printing the program counter at each read of the register found it in one run.

Bit 7 of the DOC's interrupt register is the interrupt line, and the line is active low. What stopped the boot next had a name, "Unclaimed Sound Interrupt", printed once a frame for ever, and the name was wrong. Nothing in the Ensoniq was set up to interrupt: not one oscillator had its interrupt bit on, and the chip's own line was down. What was wrong was what the interrupt register read while nothing had happened — $00. The ROM's interrupt manager at $ff/b8f6 points the chip at register $e0, reads it twice, and takes BMI past the sound check only when bit 7 is set; an idle register reading $00 made every interrupt in the machine a sound interrupt, handed it to the sound vector, whose default handler prints exactly that message, and jumped to the exit without ever reaching the vertical blanking check below. That is why the vertical blanking interrupt GS/OS enables through IntSource was never cleared and why the manager read $c046 twice in sixty million instructions: the path that asks who interrupted was never reached. The Hardware Reference's Table 5-3 has the bit right, and Apple's Ensoniq DOC ERS says it from the chip's side and adds the queue behind the register; the sound paragraphs under A machine to put it in have the detail.

Two lessons from that hunt are worth keeping. A visit count is not a call count: the ROM's failure tone at $ff/a0a4 looked like it was playing 3,552 times, and its entry ran once — the rest were turns of the loop inside it, which sent the investigation at a clear-to-end-of-line loop that was a symptom too. And a message is not a diagnosis: the message was the last link of a chain, and the answer was two links up, at the test the chain ran before it.

Interrupt vectors come from the ROM, whatever the language card is doing. With sound out of the way the machine went to $00/0000 and executed zeroes into a BRK loop. GS/OS's loader switches the language card to RAM with interrupts enabled and block-moves into banks $e0 and $e1, and a vertical blanking interrupt in the middle of the move pulled its vector from $00/ffee — language-card RAM nothing had written yet. The Firmware Reference's chapter 8 says what the machine does: "If I/O shadowing is on, the vectors contained in ROM are always used by the 65C816, regardless of the language-card settings. This allows you to run native-mode code with interrupts enabled in old applications." With shadowing off, the operating system "must copy the ROM vectors from $FFEE to $FFFF and the code from $C071 to $C07F into RAM at the same locations before enabling interrupts." The 65816 marks a vector pull with its VPB line, the memory model was already told the kind of every cycle, and decodeVector is where that line is now read.

One language card, not three. Then GS/OS called into the code it had just moved to $e0/de2f and found the ROM there: the ROM's interrupt manager writes the State register on every interrupt, and in this model that moved the Mega II's language card while the fast side's stayed where GS/OS had put it. The section above has the whole of it, including how the ROM routine that had argued for three cards was misread.

And then a megabyte. With all four the machine drew the welcome screen and stopped on the ROM's system failure screen with GS/OS's own words: "RAM disk too large or not enough memory. Error=$0201", the Memory Manager unable to allocate, on a machine with 256K. Given a 1 MB card it boots to the Finder.

The machine boots a disk

Check startup device! was the right answer to an empty disk port, and the next thing was to stop the port being empty. An IWM is a Disk II card's mechanism on one chip — the same four magnets, the same spindle motor, the same two drives, the same six-and-two encoded bytes going past the head — so the drives are the ones src/disk2.zig already models rather than a second set of them, and what the IWM adds is the three registers a card never had.

With a 5.25-inch image in the built-in drive the machine reads it, steps its head, boots, and comes up at an Applesoft prompt. Three bugs stood between those two sentences, and all three were found by running the ROM.

The disk turned three times too fast. cycles_per_nibble is measured in an Apple II's cycles, and a IIgs counts its own nearly three times as quickly for the same wall-clock time. So the boot PROM saw every third byte, never caught an address mark, and went round its read loop thirty-three thousand times before giving up — a perfectly good disk looking unreadable. The rate is now a field on the controller rather than a constant, and the IIgs sets it to match its clock.

$c011 and $c012 answered for the wrong card. These are the reads that ask which bank the language-card window is showing and whether it is showing RAM, and they belong to whichever card the bank belongs to, exactly as the switches that move them do. The firmware turns on this pairing: its routine for copying the Monitor into the fast side's card reads $c011 first so that it can put the bank back the way it found it, and handed the Mega II's answer instead it did the wrong thing and left DOS reading four kilobytes of zeroes where Applesoft should have been.

The expansion ROM was being treated as slot 7. $c100 to $c7ff is one page per slot, but $c800 to $cfff is the expansion ROM, shared between whichever card was last selected — and with no cards fitted it is the machine's own firmware. Truncating $cf00's page number to three bits gives slot 7, so with slot 7 pointed at a card that is not there the whole of $cf00 to $cfff read as nothing, and the firmware, which runs from up there, ran off into it.

bootgs grew two diagnostics along the way that earned their place. It records the last few addresses with their banks and collapses runs, because the profile deliberately throws the bank away and a machine sitting in a one-instruction loop otherwise fills the whole trace with where it stopped rather than how it got there — 00:9fc4 00:db67 00:c074 is what broke the second of those three open. And it reports what the fast side has mapped, since a machine that has run off the rails has usually done it by ending up with the wrong thing somewhere.

Eighty columns, and one word in a manual

The disk booted but the screen came out as forty-column text with every other column an unwritten byte. The counts said it plainly — nine hundred and seventy-four characters in the main half of the text page and not one in the auxiliary — and that is a whole screen of eighty-column text with half of it thrown away.

The cause was a word in the sentence this model's whole seam is built on:

if shadowing is enabled or the software is addressing bank $E0 or $E1, an odd-numbered bank address will access the Mega II auxiliary memory automatically, without using the soft switches ... otherwise, the Mega II ignores the bank bit

That says what an odd bank does and claims nothing at all about an even one. This model had read it as a rule about the bank's low bit either way, so with the latch set, bank $00 was the main half whatever the switches said — which makes eighty-column text impossible, because the firmware writes it by toggling PAGE2 between halves while writing to $0400 in bank $00. An odd bank is auxiliary outright; everything else asks the Mega II's switches, exactly as a IIe would.

With that, a IIgs boots a ProDOS disk and draws its software's title screen:

                             MERLIN-8  version 2.57

                                by Glen E. Bredon

                               Copyright (C)  1987
                          Roger Wagner Publishing, Inc.

The DOS 3.3 disk boots too, and prints NOT //C OR 128K //E before dropping to Applesoft — which is Merlin-8 v2.48 examining the machine identification bytes, finding $fbc0 = $e0, and correctly declining to run on a computer that did not exist when it was written. The emulator being faithful is what makes the software refuse.

bootgs reports the main-and-auxiliary split of the text page now, because those two counts were the whole diagnosis and there is no reason the next version of this problem should have to be found again from scratch.

The interrupts

That last sentence had been standing for a while, so: the machine raises interrupts now, and the processor takes them.

Everything this machine times is counted in frames, which is the tidy part. $c041 says which interrupts may ask for the line and $c046 says which have happened, and the two line up bit for bit — bit 4 the quarter-second tick, bit 3 vertical blanking, bit 2 the Mega II's mouse button, bit 1 its movement. Owning up is done at $c047 and $c048 rather than by putting a bit back.

The quarter-second interrupt is not a quarter of a second. The Firmware Reference names $c047 "clear the VBL/3.75Hz interrupt flags", and 3.75 Hz is exactly sixty divided by sixteen — so it is the frame counter with its low four bits masked off, and comes out at 0.267 seconds. Deriving it from the video rather than from a made-up cycle count is both simpler and what the hardware does, and it explains why one address clears it along with vertical blanking: they are the same counter.

The video controller has two of its own, at $c023, and that register is contrary in three separate ways worth writing down. Its status bits are read-only — "software can directly manipulate only the enable bits ... writing to the other bit positions has no effect". Clearing them happens at a different address, $c032. And there it takes a bit that is down: "writing a 1 into these positions or writing into the other bit positions has no effect". Everything else in this machine is cleared by touching an address; this one is cleared by writing a zero into a bit at a second address. The one-second interrupt fires from the same frame counter; the scan-line one is the odd one out and gets a section of its own below, because its source is not a register at all.

Two things the tests caught that running the ROM would not have. The line is a wire, not an event: it follows the enables and the flags continuously, so enabling an interrupt that has already happened raises it now, and owning up to one drops it before the instruction finishes. The first version updated it at the start of an access, which made every write to the enable register a whole access late. And the frame count is counted, not caught: watching for the edge is fine while the clock crawls forward a cycle or three at a time and wrong the moment anything moves it further, so sixty frames in one step counted as one. Shifting the clock by the blanking interval and dividing gives the count outright.

None of this changes how the machine runs today, and that is the honest result: this ROM's boot path never enables an interrupt, so the line stays down and the cycle counts are identical to before. What the machinery buys is everything that comes after boot — a mouse, a clock a program can read, anything that has to happen while something else is running.

Two smaller notes. $c090 to $c0ff is peripheral-card I/O, sixteen locations per slot, and a boot scan touches three of them — $c0b0, $c0d7, $c0fe, one access each. They stay on the unanswered list on purpose: an empty slot is a thing this model has not got rather than a register it has not built, and the difference is worth being able to see. And blanking() now brings the frame up to date before answering rather than keeping its own sum, because an access spends its cycles before the soft switch is answered and a $c019 read that sampled at the start would report the beam up to three cycles behind where it is when the byte reaches the bus.

Reading a chip's command set off the machine

Neither manual gives the clock chip's commands. Both describe the handshake and then decline: "to remain compatible with future Apple II products, use the firmware calls to read and write data to the RTC." So the firmware is the only description of this chip there is, and the way to read it is to watch it talk.

It talks more than anything else in the machine — three thousand and seventy-six exchanges before a third of a million instructions have run, in groups of three:

w3f w70 w13    w3f w74 w24    w3f w78 wb9    w3f w7c w8e

The second bytes step by four and take exactly thirty-two values from $00 to $7c; the first bytes take six consecutive values from $3a to $3f. That gives the format outright — five address bits in one byte and three in the other:

first  byte:  z 0 1 1 1 a7 a6 a5
second byte:  0 a4 a3 a2 a1 a0 0 0

Decoded that way, a hundred and seventy captured groups name addresses $56 through $ff, strictly ascending by one, with nothing left over. Getting a contiguous run out of an eight-bit address split across two bytes is not something a wrong guess does.

The first attempt read the first byte as 0011aaa1 instead, which fits the same sample and is wrong — and the model said so rather than hiding it, because commands it does not understand are counted the same way soft switches are. Nine hundred and ninety-six unrecognised commands out of three thousand is not a decode that works, and the count is what said so. With the shape corrected it is four, and those four are w37 w55 and w31 w00 at the very end, which are genuinely different commands — $37 and $31 are just below the $38 to $3f the parameter RAM answers to. They stay counted rather than guessed at.

The top bit was found rather than assumed too. The first seven hundred exchanges of a boot are wbe w70 r00 and wbf w00 r00 — the same commands with bit 7 set, each followed by a read transfer — and the thousand after them are the writes above. So the firmware reads the whole parameter RAM, finds nothing in it, and writes it back out with checksums, which is exactly the sequence the Firmware Reference describes for its TOREADBR vector. Even so, that bit is not what the model acts on: which way a byte goes is the control register's bit 6, and that much is documented. The command's top bit has agreed with it in every exchange observed, and there is no reason to model the same fact twice.

The parameter RAM keeps what it holds across a reset, which is the whole point of it: it is the one thing in this machine with a battery behind it, and a reset is not a flat battery.

The clock, out of the ROM rather than the trace

The parameter RAM came out of what the machine says. The clock could not, because nothing in a boot ever asks the time — three thousand exchanges and not one of them is a clock command. So the other oracle: what the machine is written to say.

The firmware's routine for reading the clock is at $fe/b5a0, and it does not list the commands. It counts them out:

        LDA #$FD
        CLC
loop    ADC #$04        ; $01, and $05, and $09, and $0d
        PHA
        ORA #$80        ; which for a read is $81, $85, $89, $8d
        SEP #$40        ; V set: this transfer is a read
        JSR send
        CPY #$00
        BNE second
        STA $03E1,X     ; first pass: keep it
        BRA next
second  CMP $03E1,X     ; second pass: and it had better agree
        BEQ next
        PLA
        BRA start       ; it did not, so start the whole thing again

Four commands four apart, with the top bit set for a read — and then the whole thing done twice and compared, starting over if the two passes differ. That last part is worth more than the command list, because it is only something you do when reading a number that is still counting: it says these four bytes are one thing rather than four, and that the thing is a clock. The write side, at $fe/b5e4, counts the same four commands out without the ORA #$80 and sends them from $e1/03e5.

So z000rr01, where rr picks one of the four bytes of a seconds counter and z says which way it goes. The count is seconds since the start of 1904, which is Apple's epoch everywhere; the chip keeps it behind the same battery the parameter RAM is behind, and this counts it in the machine's own cycles the way the 8-bit machines' clock card does — so a machine run at ten times its speed sees ten times as many hours go by, and a halted one sees none.

Two of the chip's commands are still not understood. A boot ends with $37 carrying $55 and $31 carrying $00, one each, and never mentions either again; they are neither the parameter RAM's range nor the clock's. They stay counted rather than guessed at, which is what the counter is for.

What it takes to see a date

bootgs can type at the machine now, which is the only way to make it do something it was not going to do by itself, and typing at it gives the answer to the obvious next question:

$ zig build bootgs -- ROM 20000000 disk.po screen.png 'NEW\r10 PRINT "HI"\rSAVE TEST\rCATALOG\r'
 NAME           TYPE  BLOCKS  MODIFIED
 PRODOS          SYS      30   2-SEP-26 12:00
 BASIC.SYSTEM    SYS      21   2-SEP-26 12:00
 TEST            BAS       1  <NO DATE>

The file is written, and it is stamped with nothing — and the clock chip was never asked. That is not the model: the PRODOS on that disk came off a Merlin-8 disk and announces itself as ProDOS 1.1.1, 18 September 1984, which is a year and a half older than the Apple IIgs. It knows how to find a clock card in a slot and nothing about a machine with one built in, so it never asks. Seeing a date on that line wants a ProDOS 8 from 1986 or later, which this project has not got a copy of.

Two manuals wrong, and a disk that boots anyway

An Apple 3.5-inch drive is a Disk II turned inside out. A Disk II is four magnets and a motor with all the cleverness on the card; here the card is gone — the IWM is the whole of it — and the cleverness has moved into the mechanism. It steps its own head one track at a time when told to, turns its own spindle when told to, knows whether there is a disk in it and whether somebody has pressed the eject button, and will answer any of fifteen questions about itself.

It answers them down one wire. The four lines that walked a Disk II's head stop being magnets: three of them plus a fourth bit borrowed from $c031 make a four-bit address, and the last one is a strobe. Name a register and read the IWM's status register, and bit 7 is that one question's answer; name it and pulse the strobe, and the drive does that one thing. So a program asking whether the disk is write protected does not read a write-protect line. It names register 6 and reads the sense line — the same wire that would have carried the answer to any of the other fourteen.

Nearly every answer is upside down: zero means yes. Zero on register 2 means there is a disk, zero on register 6 means it is write protected, zero on register 15 means a drive is connected.

The bit both manuals get wrong

The Hardware Reference says $c031 bit 7 is "read/write head select" and bit 6 is "disk drive select". The Firmware Reference agrees. Neil Parker's note on driving these drives says flatly that both are mistaken, and the machine's own ROM settles it in two places.

The first is what the firmware does before it touches a 3.5-inch drive at all:

LDA $C036 / AND #$FB / ORA #$80 / STA $C036     ; fast, and no motor-on slowdown
LDA #$40  / STA $C031                           ; and the 3.5-inch drives, please

$40 is bit 6. The second is its routine for naming one of the drive's registers, which is where bit 7 actually goes:

$fe/5fb8  BIT $C0E0    ; CA0 off, CA1 on, strobe off, CA2 off
          BIT $C0E3
          BIT $C0E6
          BIT $C0E4
          LSR A        ; bit 0 of the parameter -> CA2
          BCC +3
          BIT $C0E5
          LSR A        ; bit 1 -> SEL, which lives in $c031
          PHA
          LDA $C031
          AND #$7F
          BCC +2
          ORA #$80
          STA $C031
          PLA
          LSR A        ; bit 2 -> CA0
          ...

Bit 7 is set and cleared once per register selection, from one bit of a parameter, alongside three lines that are unambiguously an address. It is not a head select. It is the fourth bit of the address — and choosing which head reads is one of the sixteen things that address can name, which is presumably how a part of it came to be written down as the whole. That routine also fixes the bit order, which no table would have: the parameter's bits go to CA2, then SEL, then CA0, then CA1, so the register number is CA1 CA0 SEL CA2.

Eight hundred kilobytes

A disk turns at one speed on an Apple II, so its outer tracks — which are longer — hold exactly as much as its inner ones and most of the surface is wasted. A 3.5-inch drive varies the speed instead, five times across the disk, so that the surface goes past the head at the same rate wherever the head is. That buys twelve sectors on the outermost sixteen tracks and one fewer in each zone inward: 12, 11, 10, 9, 8, which over eighty tracks and two sides is 1,600 sectors of 512 bytes.

The encoding is the same six-and-two over the same sixty-four patterns, because it is the same constraint. What is not the same is what happens before it. Three bytes go to four — the low six bits of each, and a fourth byte holding the three pairs of top bits — and running through the whole sector are three checksums that are not checksums of the data but of each other's running totals. Each byte leaves as itself exclusive-ORed with one of them, a carry is chained from one addition to the next, and the third checksum rotates a bit round every three bytes. Apple patented it (US 4,564,941): the rotation is there so that an error in one byte cannot cancel an error in another.

Apple prints the algorithm twice in the SWIM Chip User's Reference, once for writing and once for reading, and the two do not agree. They are the same loop with the boundary drawn in different places — the write column ends each pass with the rotation and the read column begins each pass with it — so the write column performs one rotation more than the read column does, and the checksum it would leave behind is the read column's rotated once. Only one of them can be right, and it is not a coin toss: the firmware runs the read form, and an encoder that disagrees with the decoder is not an encoder. So the encoder here is written in the reading form, which makes it the exact inverse of the thing that has to accept it. That also settles the last group, which has two bytes in it rather than three because 524 does not divide by three.

Reading it in three steps

None of the above is worth anything until a real ROM reads a real disk, and the way to find out was to make the disk say more each time.

Eight hundred kilobytes of zeroes first. The firmware switched the port to the 3.5-inch drives, strobed fourteen commands into the drive, started the spindle, read 2,753 bytes off the surface and put Check startup device! on the screen — which is the right answer, because there is no volume on a disk of zeroes.

Then the same disk with a genuine ProDOS boot block copied into block 0 and nothing else. 16,372 bytes read, and the machine stopped being the firmware: it was running at $00/0896 in emulation mode, which is ProDOS's own loader, looking for a PRODOS that was not there. Block 0 had come off the surface, through the address field, through the nibblising and the three checksums, and into memory intact, and the processor was executing it.

Then a whole volume — formatted by src/prodos.zig, with PRODOS and BASIC.SYSTEM copied off a 5.25-inch disk:

            PRODOS BASIC 1.5
        COPYRIGHT APPLE  1983-92

]

245,221 bytes read off the surface, twenty-five commands strobed into the drive, and a prompt. One detail in the report is worth the look: by the time it gets there the port says it is talking to the 5.25-inch drives again, because the firmware puts $c031 back the way it found it when it has finished — which is exactly what the note on driving these drives says a well-behaved routine should do.

Writing works the same way round: a byte going down under the head goes back into the nibbles the track is made of, so a program that saves a file to a 3.5-inch disk finds it there afterwards. Booting BASIC.SYSTEM off one and saving a program to it does exactly that — the catalogue shows the file and the free block count goes down by one.

Formatting one works too, and getting there found two things wrong, which is the usual return on making a machine do something it has not done before. Writing a sector means putting bytes back where the track already says they go; formatting means laying the track out from nothing — its address fields, its data fields, and the right number of sectors for whichever of the five speed zones it is in. Nothing else asks that of a drive, so nothing else had found either fault.

The first was the handshake register, which is the same lesson as the one above and I did not recognise it. It answered $80 always, on the reasoning that a chip with nothing to do is ready — right for the first byte and wrong for every one after it. The firmware's track-writing loop is six instructions and only one of them waits:

ff5ac7  LDA $0f5c,y      ; the byte
ff5aca  BIT $c0ec        ; the handshake
ff5acd  BPL $5aca        ; wait until it will take another
ff5acf  STA $c0ed        ; and put it down

Told yes every time it never waited, and wrote a byte every twenty-five cycles onto a disk that turns one every forty-eight. The register counts from the last byte now: an idle chip is ready, and one that has just taken a byte is not ready again until that byte has had as long as it takes to pass the head.

The second was the sector pitch. This model spaced sectors 770 encoded bytes apart, worked out from the drive's nominal figures — 489.6 kilobits a second, about 398 revolutions a minute for a twelve-sector track, 9,230 bytes to share between twelve. That is the right arithmetic on numbers that are only ever quoted as "about", and it is twenty-five bytes a sector short of what the machine actually does. Watching a IIgs format a disk and looking at where it put the address marks gives 748, 1543, 2338, 3133 — 795 apart, 9,540 to the track. With the shorter slot the twelfth sector wrapped onto the first, and the verify that follows found eleven address marks where it had written twelve.

With both put right, a SmartPort FORMAT call comes back $00: 240 commands strobed into the drive, 1,431,680 bytes written to the surface and 1,377,295 read back off it, the head left at track 79. And the two halves of this program agree about the result — disk35.zig's own decoder reads all 1,600 blocks back out of what the firmware laid down, every byte zero, which is what a freshly formatted disk holds. The Apple II System Utilities says "Formatting ... Done!"

Colour out of nothing

There are no colours in an Apple II. There is a shift register clocked at 14.318 MHz, and what comes out of it is a stream of dots that are on or off. Colour happens in the monitor, because that dot rate is four times the NTSC colour subcarrier, so any pattern that repeats every four dots looks to a television like a colour — and which colour depends on which four-dot pattern and on where in the four the beam happens to be.

So src/video.zig has one thing in the middle of it: a line of 560 dots. Every mode writes into that line, and then one rule colours all of it. The Hardware Reference states the rule for Double Hi-Res and it is the same rule for everything:

Color is determined by any 4 adjacent dots along a line. Think of a 4-dot-wide window moving across the screen: at any given time, the color displayed will correspond to the 4-bit value ... that corresponds to the window's position ... divide the display column number by 4, and use the remainder.

Writing it that way round rather than as four separate mode renderers turns out to pay for itself three times over.

Lo-Res needs no table of colours. The obvious way to draw a Lo-Res block is to look its nibble up in a table of sixteen and paint it on. The hardware does not do that: it puts the nibble out as a four-dot pattern, over and over, and the monitor makes a colour of it. Do the same and the nibble comes back out of colourise unchanged — the sixteen colours are not a table, they are the sixteen four-dot patterns. Two things then fall out for free that a table would have had to be told about. A Lo-Res screen on a monochrome monitor is the vertical stripes it really is. And because a block is fourteen dots rather than sixteen — three and a half colour cycles — the pattern does not restart in step with the block, so neighbouring colours disturb each other at the join, which is a thing real Lo-Res does.

Double Lo-Res needs no correction. Every account of this mode says the auxiliary half's colours come out rotated and have to be rotated back. They do not: that rotation is an artefact of the table, and it is the table's job to undo it. Put the nibble out as the pattern and the phase looks after itself, because seven dots is not a whole number of colour cycles and the next block starts wherever the last one left off.

Hi-Res's half-dot delay is just a shift. Bit 7 of a Hi-Res byte is not a dot. It delays the other seven by one position of the 560 — half a dot at Hi-Res's own resolution — which slides them into the other half of the colour cycle. Violet becomes blue and green becomes orange, and neither the renderer nor the colouring rule has to know that those are the names.

Two things a test found

The direction of that rotation is not in the manual, or rather it is and the scan is too badly mangled to read it. It was fixed instead from the four things about Hi-Res everybody knows: $55 is violet, $2a is green, and with the high bit set they are blue and orange. One rotation direction gives all four and the other gives none of them.

Writing that test found something else. Filling every byte of a line with $55 does not give a violet line, and the test failed saying so. Seven dots to a byte is an odd number, so the parity of the dots flips at every byte boundary: a line of one colour takes two byte values alternating, $55 $2a $55 $2a for violet and $2a $55 $2a $55 for green. That is why the manual's tables of Hi-Res colours have four columns of byte values in them rather than one, which until then had looked like scanning damage.

Chapter 7 of the technical reference says it outright, in the middle of a paragraph about something else, and it is worth quoting because it is the hardware confessing that the software has to clean up after it:

each bit pattern corresponds to three and a half cycles of the color signal, so the phase relationship between the data bits and the color signal changes by a half cycle for each successive byte of data. Here, however, the bit patterns produced by the hardware are the same for adjacent bytes; the color compensation is performed by the high-resolution software, which uses different color masks for data being displayed in even and odd columns.

Text, and why it is green at the edges

Text goes through the same line of dots. A character is seven dots either way: in forty columns each is drawn twice as wide, which is fourteen positions, and in eighty it is seven and the two halves of memory alternate. Same chip, same bitmap, different dot clock — the //e has no separate eighty-column font.

Which means text would get coloured by the same rule as everything else, and come out fringed — green and violet down the edges of the letters, a character stroke being one dot wide, a dot two of the 560 positions, and two positions a colour — and on the very first Apple IIs it did. Then Apple stopped it. Sather is exact: "In the oldest Apple IIs (Revision 0), the COLOR BURST was always present on the VIDEO signal. This resulted in distracting green and violet text characters. A Color Burst Killer was added in Revision 1 to eliminate the COLOR BURST in TEXT MODE" (Sather, 1983, p. 8-5). A colour monitor colours nothing without a burst to lock to, so text-mode text is white, and it stayed that way: on the IIe "the COLOR BURST does not occur if the TEXT soft switch is set" (Sather, 1985, p. 8-4), through the gate the IIe Technical Reference calls CLRGAT'. It is the TEXT switch and not the row that decides: "in MIXED mode, the four lines of text at the bottom of the screen are still green and violet because the COLOR BURST is present." So colourBurst is a question every machine answers — a Revision 0 board always yes, every later Apple II, II Plus, IIe and IIc no in text mode, and a IIgs yes until its Monochrome/Color register says otherwise — and video.scanLine colours the line or leaves it black and white accordingly.

This README said the opposite for a long time, citing the IIe Technical Reference's account of the IOU generating the burst from the horizontal counter on every line. It does generate it on every line; CLRGAT' is what stops it reaching the video output, and that manual's chapter 7 never says so in words. Sather does, twice, and the Apple II itself was the check: --machine ii --monitor colour shows white text, and with --revision 0 the same text is green and violet. line.dots is unchanged by all this, since a monochrome monitor never saw the burst either way, and it is what picture.write with .monochrome = true hands back.

Double Hi-Res, checked against a machine

The doubled modes went in with tests and no software behind them, which is a gap worth closing rather than leaving written down. Turning the mode on the way a program does — graphics, not mixed, Hi-Res, 80COL, 80STORE so that PAGE2 steers the processor's window rather than the display, and annunciator 3 — and then filling the auxiliary half with $7f and the main half with $00:

mode double_high_resolution, an3-double=true, col80=true, store80=true
line 0: aux $7f $7f, main $00 $00
first 32 dots: #######.......#######.......####

Seven dots on and seven off, with the auxiliary byte first. That is the whole of Double Hi-Res in one line: the mode switches select it, 80STORE puts the POKEs in the right half, and the renderer reads the halves in the right order.

Looking at it in the tools

alt+g in the debugger writes a picture of the screen — the whole 560 by 192 of it, not what the terminal is showing — to screen.png, or to wherever --screenshot PATH says. It is the way to see what the machine is actually drawing, since a terminal cell cannot.

bootgs takes a fourth argument that does the same for an Apple IIgs, and that one is worth a look, because the picture comes off the Mega II: a IIgs draws its old-style screen with a real Apple IIe inside it, which is what this model has in it, so the same video.scanLine that draws a IIe's screen draws a IIgs's without being told anything new. Booting eight hundred kilobytes off the 3.5-inch drive and asking for a picture gives a ProDOS prompt in green and violet, which is the first thing this project has ever shown of that machine rather than described.

The one part of this that is data

Everything else here is behaviour, worked out from what the hardware does. A character generator cannot be: an 'A' is an 'A' because somebody at Apple drew one. So src/fonts/ holds the shapes, transcribed from three chips, and it is the only place in this project where a ROM's contents are built in rather than asked for on the command line. That is a deliberate exception and worth being plain about — 2 KB of letter shapes is not firmware, and without them nothing can draw a screen at all. They carry no copyright notice of this project's and are marked CC0, the way generated files here are.

src/font.zig picks between them and does what the video logic does on the way out, which is where the interesting part is:

  • An Apple II and a II Plus have 341-0036: sixty-four shapes and no more. Inverse and flashing are not in the chip at all — they are an XOR gate on the way out of the shift register, which is why one bitmap serves all four quarters of the byte range.
  • A IIe has 342-0133-A, or 342-0265-A if it is an enhanced one, and both hold 256 entries with the inverse shapes stored already inverted. That table is the alternate character set. With the primary set — which is what a machine comes up in — the video strips bit 6 from $40-$7f before the byte reaches the ROM, so those codes land on the inverse shapes at $00-$3f, and then the flash clock decides whether to turn them back over. Which is why flashing exists in the primary set and not in the alternate one: a rule that reads as arbitrary in the manual until you see where the bit went.
  • The two IIe chips differ at $40-$5f and nowhere else. The original spends those thirty-two entries on a second copy of the inverse uppercase — the copy the primary set reaches by clearing bit 6, and so one that nothing ever displays from directly. The enhanced part spends them on MouseText instead, which is why MouseText cost Apple nothing but a chip swap.

The flash clock itself is counted from the machine's own cycles rather than from the wall, which is what the hardware does — it comes off the video counter — and is consistent with everything else here: the clock card, vertical blanking and a disk turning are all paced by the processor. So a machine run fast flashes fast, and a halted one holds still.

In the debugger

The debugger draws it in the same pane the text screen gets, and how well depends on the terminal.

A terminal that speaks the Kitty graphics protocol — Ghostty, kitty, WezTerm and others — gets the pixels themselves: the whole 560 by 192, scaled into the pane by the terminal, which is the machine's screen rather than an impression of it. A mixed screen keeps its four rows of text as text, because a terminal draws characters better than a picture of a character generator does, so the image is clipped to the graphics above them.

A terminal that does not gets the upper half block. A cell is about twice as tall as it is wide, so one carries two stacked dots as its foreground and background colours: forty cells by twenty-four rows is forty by forty-eight dots, which is exactly a Lo-Res screen, and eighty columns is exactly a Double Lo-Res one. Those come out dot for dot. Hi-Res does not and cannot — two hundred and eighty dots do not go into forty cells — so what is drawn is every fourteenth dot position, one Hi-Res byte's worth: enough to see that a program is drawing and roughly what, not enough to read.

The picture costs something to send, and it took two goes to get that down to something reasonable.

It is sent only when it has changed, worked out by hashing the memory the renderer is about to read — eight kilobytes against a hundred and seven thousand dots — and no more than twenty times a second even then.

That is not as much of a saving as it sounds, because a machine sitting at a BASIC prompt with nobody touching it changes its screen about eight times a second: four for the flash clock, and four more because the firmware rewrites the cursor character. At 430,080 bytes a frame that is three and a half megabytes a second for a blinking cursor.

So it goes compressed. o=z in the Kitty protocol means the pixels are a zlib stream, and a screen of mostly-black text is exactly what deflate is good at. Twenty seconds at that idle prompt went from 67,901,053 bytes to 340,057 — the same hundred and fifty-seven pictures, two kilobytes each instead of four hundred and thirty. A screen of Hi-Res being drawn compresses less well and still comes down eighty times, to five kilobytes a frame.

Looking at it

Nothing here draws anything, so the check is to make the machine draw and then look. A IIe with a ROM and no disk comes up in Applesoft, so:

HGR:FOR C=0 TO 7:HCOLOR=C:FOR X=C*35 TO C*35+34:HPLOT X,0 TO X,159:NEXT:NEXT

comes out as eight bands in the order a real machine gives them — black, green, violet, white, black, orange, blue, white — with the thin coloured fringes at the joins that a real machine has too. GR with the sixteen Lo-Res colours comes out in the order the manual names them, from a renderer with no table of colours in it anywhere. And a screen of HCOLOR=3 diagonals comes out as the green-and-violet moiré that Apple II line drawing has always looked like.

An interrupt whose source is a picture

Super Hi-Res is the Video Graphics Controller's display and it is nothing like the one the machine inherited. There is no dot stream and no colour subcarrier: there are pixels, with colour numbers in them, and the numbers mean whatever a palette in memory says they mean. Which is why it is src/superhires.zig and not more of src/video.zig — different chip, different grid, different idea of what a colour is.

Thirty-two kilobytes of bank $e1 hold all of it: two hundred lines of a hundred and sixty bytes at $2000, then two hundred scan-line control bytes at $9d00, then sixteen palettes of sixteen twelve-bit colours at $9e00. The control byte is the interesting one, and it is why this is drawn a line at a time. Every line decides for itself which palette it uses, whether it is 320 pixels or 640, whether Color Fill is on — and whether the beam reaching it raises an interrupt.

That last one is worth stopping on. Every other interrupt in this machine comes from a register: something is enabled, something happens, a bit goes up. This one comes from a byte in the middle of the picture. A program changes what interrupts and when by drawing, and the model has to walk the control bytes as the beam crosses them — counted rather than caught, the same way the frame counter is, and clamped to one frame back because walking further is walking over lines already looked at.

Two modes that are not what they look like

Color Fill turns colour $0 from a colour into "the same as the pixel to my left". A run of solid colour costs one byte instead of six, which is what it is for; the price is fifteen colours a line rather than sixteen, and the manual notes that the first pixel of a line must not be $0 because there is nothing to its left and "an undetermined color results".

640 mode is four pixels to a byte and two bits each, which sounds like four colours and is not. The palette is divided into four groups of four, and which group a pixel uses depends on where in the byte it is: the first pixel takes the third group, the second the fourth, the third the first, the fourth the second. So all sixteen colours are still available along a line even though any one pixel can only be four of them. The rotation looks arbitrary until you notice what it is for — two adjacent 640 pixels are half a dot each, and half a dot is what makes colour on a composite monitor, so the groups are arranged to put the pairs that dither well next to each other.

What checked it

No real Super Hi-Res software has been run against this, because there is none here to run: the 800K disk this project can boot is ProDOS 8 and forty columns of text. What there is instead is the format tested against the manual clause by clause, and a picture written through the machine — into bank $e1 of the real model, with the real bank latch and the real memory map — that uses all four of the things above at once: a rainbow in 320, the same colours as fill runs, 640 showing its four groups side by side, and fifty bands each with a palette of its own. All four come out.

Writing that test caught the seam again, in the place it always catches people. Turning Super Hi-Res on means writing bit 7 of $c029 — and writing just bit 7 clears bit 0, which is the bank latch, and without the latch a write to bank $e1 goes through the old soft switches and lands in the main half of the Mega II instead. Nothing reaches the buffer and the screen stays black. Real software sets both, and the machine's own firmware comes up with the latch already set, which is why the picture worked before the test did.

Two bugs a second front end found

Writing the IIgs's front end — a second way of drawing a machine's screen into a terminal, and at the time a second program as well — turned up two things in the debugger's own display that had been wrong all along and were invisible while there was only one of them to look at.

A text screen was being sent as a picture and drawn as text over the top of it. The picture cost a megabyte a frame and then nothing could be seen of it.

A still screen was never still. The change test hashed the display page, and neither page is full: the text page shows 960 bytes of its kilobyte and the Hi-Res page 7,680 of its eight. The gaps are the screen holes, which peripheral firmware uses as scratch and writes constantly — so the hash changed several times a second on a screen nobody was touching, and the picture went down the wire again every time. Hashing the displayed bytes instead, by walking the rows the way the renderer does, fixes it.

Between them, eighteen seconds of a IIgs sitting at a ProDOS prompt went from 103,894,474 bytes of terminal traffic to 7,923. The machine on the screen is identical; it was all a picture of a text screen nobody could see, being redrawn because a screen hole moved.

The network, and an interrupt that was never the sound chip's

The README said it in one sentence for a long time: no AppleTalk, which is the same chip in a different mode. Making that untrue turned out to be three separate pieces of work, and only one of them was the chip.

What the firmware says to it. Nothing in the ROM programs the SCC for the network on an ordinary boot, because nothing asks it to; the link layer is started by whatever program initialises AppleTalk, or by the firmware itself when the Startup Slot is 7. Turning that on meant finding the Control Panel, which is the clock chip's parameter RAM, which is checksummed — and no manual gives the checksum. The firmware does: its routine at $ff/b61d in ROM 01 rotates two hundred and fifty-one overlapping words into a running sum and stores the sum and the sum with every other bit flipped. Reading it took two tries, because the loop's ROL takes in the carry the CPX before it left, which inside the loop is always clear, and a first reading that gave the processor's ADC its carry back got a different answer from the machine's — a difference that was briefly suspected of being a fault in the 65816 core until the corpus was run again and passed every case. The slot bytes were found the same way, off the defaults table the ROM writes, and ROM 3 turned out to keep slot 1's third choice in a byte ROM 01 never had.

With slot 7 set and the machine started from it, the firmware programmed the chip with a thirty-byte table — SDLC, eight bits, FM0 with the CRC preset to ones, the transmitter off the baud rate generator and the receiver off the DPLL, a time constant of six, the DPLL into FM mode and then search mode — which is Zilog's LocalTalk application note register for register, and which also says what the chip's crystal is, since six gives 230,400 only from 3.6864 MHz. And then it enabled the quarter-second interrupt and died.

The interrupt. It died on the failure screen with interrupts disabled, having gone into the ROM's dispatcher and come out through the Sound Tool Set's handler as an unclaimed sound interrupt. The dispatcher reads the Ensoniq's Oscillator Interrupt register and goes to the sound handler when its top bit is clear. The Hardware Reference's prose says an oscillator "sets" that bit; its table says a one is "no oscillator has generated an interrupt" and a zero is "one of the 32 DOC oscillators has generated an interrupt; this bit reflects the status of the IRQ line" — and the line is active low. This model had the prose, so an idle sound chip read as an interrupting one, and the first enabled interrupt of any kind was routed to a tool set that was not running. Nothing had noticed because nothing on the boot path had ever enabled one. The fix is one bit, and it is the difference between a machine whose interrupts work and one whose do not.

The chip. After that the firmware sent six hundred and forty enquiries for node 1, thirty-two broadcast requests to send and thirty-one name lookups for a server, and gave up, which is what a IIgs alone on a cable does. Every one of them went through a model that had the underrun bit permanently set, so the firmware never waited for the check sequence before dropping the driver; the log of every register access — bootgs writes one now — is what the transmitter, the receiver and the interrupts were built against. The proof was a peer on the multicast group claiming node 1: the firmware asked for it six times, was told six times, and took node 2, then received the peer's answers to its lookups through the interrupt, the FIFO and the End of Frame bit, the way the ROM's own handler expects them.

Where the time goes

Wizardry takes twenty-three seconds to put its title on the screen, and the question was whether any of that was the emulator's. None of it is, and finding that out found a drive turning four per cent slow.

The front end, paced to 1 MHz, used five per cent of one core for the whole boot; headless and unpaced, eighty machine seconds took 3.3 seconds of wall time in a Debug build. So the host is not where the time goes, and the machine was booted headless with the drive's track, motor and the video mode logged every quarter second, and every read of the data register counted:

when what the machine is doing
0 to 1.75 s the boot PROM homes the head: eighty steps with a twenty millisecond wait each, before a byte is read
1.75 to 19.75 s the loader reads, the motor on for seventeen of the eighteen seconds, the head moved 322 times
19.75 to 23.2 s computing, drive off
23.2 s high resolution on, and the title

While the head is on a track the loader takes five to six thousand nibbles a quarter second, which is most of what goes past: several sectors a revolution, never the one-a-revolution trap a bad interleave falls into. The head goes back to track 0 four times in the middle of the load, which looked like error recovery until Apple Pascal 1.1 was booted the same way: its disk driver homes the head after every motor restart, and Wizardry is a Pascal program running its own copy of that runtime. The rest is the game's files being where they are, on tracks 21 to 23 and 29 to 33, with the head shuttling between them.

The one thing that was the emulator's: a disk laid out from a sector image was given 6,656 nibbles a revolution, the nibble file's figure, which at four cycles a bit is 208 milliseconds — 288 rpm against the drive's 300. Every read-bound load ran four per cent long. A revolution is now the WOZ specification's 51,200 bits, 6,400 nibbles, and a nibble file's 6,656-byte track is read with a bit cell one thirty-second shorter so that it turns at speed too; the WOZ reference pass came out the same forty-two boots of forty-five afterwards as before.

Layout

src/root.zig the library: what a program embedding this is handed
src/cpu8.zig the processor, generic over a clock
src/cpu8/opcodes.zig builds the dispatch table at comptime
src/cpu8/opcodes/*.zig one file per mnemonic, 88 of them
src/cpu8/addressing_modes.zig where an operand lives, and the cycles spent getting there
src/cpu8/{stack,registers,flags,program_counter}.zig the parts
src/cpu8/add.zig binary and decimal addition and subtraction
src/cpu16.zig the 65816, a separate core: see The 65816 above
src/cpu16/*.zig its parts, its addressing modes, and its table
src/clock.zig the cycle count every other part reads the time from
src/instantiation.zig forces every pairing of machine and processor to compile
src/memory8.zig which memory models an 8-bit processor can be given
src/memory8/*.zig flat RAM, an Apple II+, and a IIe or IIc
src/memory16.zig the same for a 65816
src/memory16/*.zig what it sees, addressed twenty-four bits wide
src/languagecard.zig the language card's switches, which more than one machine has
src/disk2.zig the Disk II: the card, its drives, and six and two
src/iwm.zig the IWM: an Apple IIgs's disk port, and which drives it reaches
src/disk35.zig the Apple 3.5-inch drive: its fifteen registers, and 800K of zoned six and two
src/woz.zig WOZ: a disk as the bits that go past the head
src/twomg.zig the 2IMG wrapper, which is a disk image that says what it is
src/shrinkit.zig ShrinkIt archives: the NuFX format and both of its compressors
src/diskfile.zig a disk out of whatever file was named: a sector image, a .2mg, a .nib, a .woz or a ShrinkIt archive
src/dos33.zig DOS 3.3 on the disk: making an image, and the files on it
src/prodos.zig ProDOS on the volume: directories, index blocks, and the bit map
src/screen.zig reading the text screen: the modes and the character set
src/video.zig drawing the graphics screen: dots, and the colour four of them make
src/font.zig the character generator: which chip, which entry, and the flash clock
src/fonts/*.zig the shapes themselves, transcribed from the chips
src/superhires.zig the IIgs's own graphics: pixels, palettes and a control byte a line
src/vgc.zig the Video Graphics Controller's registers, and the bank latch hiding among them
src/picture.zig turning either display into pixels and a file, over z2d
src/speaker.zig when the cone moved, and turning that into sound
src/cassette.zig the cassette port, whose output is the speaker over again
src/mockingboard.zig a Mockingboard: two of each chip, and turning what was written into sound
src/ay3.zig the AY-3-8910: three squares, a noise and an envelope
src/via.zig the 6522 in front of each of them, and the timer a tune runs on
src/ensoniq.zig the Ensoniq 5503: thirty-two oscillators and the sixty-four kilobytes they walk
src/wav.zig either of those written to a file, because a terminal has no speaker
src/slots.zig the seven expansion slots: what a card is, where it is, and what its page and its sixteen switches answer
src/parallel.zig a parallel printer card: a latch, eight wires, and the firmware in front of them
src/serial.zig the 6551 in a Super Serial Card
src/blockdevice.zig a ProDOS block device, and the firmware it answers with
src/clockcard.zig a clock card, and the firmware ProDOS asks the time through
src/ramfactor.zig an Applied Engineering RamFactor: a megabyte behind four addresses, and the banked ROM in front of it
src/mouse.zig an AppleMouse II card, and the firmware a program calls it through
src/videx.zig a Videx Videoterm: a 6845, two kilobytes seen a quarter at a time, and its own character generator
src/ultraterm.zig a Videx UltraTerm: the same again with four kilobytes, nine formats, two clocks and attributes
src/adb.zig the Apple Desktop Bus: a microcontroller where a IIgs's keyboard port would be
src/rtc.zig the IIgs's clock chip, its battery RAM, and the border it shares an address with
src/scc.zig the Zilog 8530: thirty-one registers reached through four addresses, two serial ports, and in SDLC mode the network
src/localtalk.zig the far end of the LocalTalk cable: the bridge to LocalTalk over UDP, without the socket
src/iigsrom.zig how a IIgs ROM image is laid out, and which of them this one is
src/main.zig the command line, the machine table, and the eight-bit debugger
src/bench.zig how fast the processor runs, in instructions a second
src/gs.zig an Apple IIgs with a keyboard and a screen and nothing else, reached with --machine iigs
tools/terminal.zig the screen and keyboard bits both front ends share
src/ltoudp.zig the socket under the bridge, shared by gs and bootgs
tools/config.zig a file that says how to set a machine up, so nobody has to remember
.ziggy-schema what an editor checks such a file against while it is being written
tools/bootgs.zig a IIgs ROM run with no screen, saying which addresses it asked for
tools/bench16.zig which way the 65816 should dispatch, measured rather than argued about
tools/docs_server.zig serves the API documentation, since it cannot be opened as a file
examples/typing.zig driving a machine from another program
corpus/ the SingleStepTests corpus runner, its own project
woz/ the WOZ reference images, its own project for the same reason

Each part finds its way back to the processor with @fieldParentPtr rather than holding a pointer, so a CPU can be built in place with no wiring.

Licence

MIT, and every file says so. The project follows the REUSE specification: each file carries an SPDX-FileCopyrightText and an SPDX-License-Identifier, the licence text lives in LICENSES/, and reuse lint checks that none of it has been forgotten. flake.lock is JSON with nowhere to put a comment, so its notice sits beside it in flake.lock.license.

reuse lint

The dev shell provides the tool.

Reference list

The material this was written from, in APA form: author, date, title, source. Two of these are cited by the instruction implementations by section and page — MOS Technology's manual is the primary source and explains why the processor does what it does, and Carr covers each instruction in one place, which is often the faster lookup — and most of the rest are cited by the file whose behaviour they settled. Where a source was consulted rather than cited, its entry says so, and a date the document does not itself state is given as n.d. rather than guessed at. Where the same author and date name more than one of them, the entries carry the letters the source comments cite them by — Apple published a great deal and dated little of it.

  • 2IMG (2MG) disk image format. (ca. 1995). https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt

    Written by the Apple II community rather than by Apple, and the only image format in the family that says what it is: a sixty-four byte header giving the sector order, the write-protect tab, the DOS volume number and where in the file the disk starts. It is one page long and unambiguous, and the fields nobody had thought through are marked as such rather than reserved for a future that never arrived. src/twomg.zig is built against it.

  • Apple Computer, Inc. (n.d.-b). Apple IIc programmer's guide to the 3.5 ROM (Part No. 030-1196-A). https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Computers/Apple%20II/Apple%20IIc/Manuals/Apple%20IIc%20Programmer%27s%20Guide%20to%20the%203.5%20ROM.pdf

    Apple's supplement for the 32K ROM IIc — the machine whose version byte reads $00, and the one that gained the UniDisk 3.5. Chapter 3 is the Protocol Converter, which "always begins at address $C500" and "to ProDOS and Pascal 1.3 ... appears to be a block device" (p. 20): every call it answers, with parameters and error codes. Appendix A is four fifths of the book and is Apple's own listing of that firmware with comments and labels intact, including the eighteen pages of PC.PACKET that src/cbus.zig is built from — the device raises REQ, the host answers with /BSY, and a packet of sync, command mark, ids, contents and checksum crosses at thirty-two cycles a byte with timing the listing itself calls critical. It prints no date; the part number is its only marking. Being a scan, and one whose hex columns are the worst case for OCR, every constant taken from it was checked against the bytes of the ROM at the address the listing gives.

  • Apple Computer, Inc. (n.d.-c). Apple III SOS device driver writer's guide. https://www.apple3.org/Documents/Manuals/Apple%20III%20SOS%20Device%20Driver%20Writer's%20Guide.PDF

    Appendix D's table of the I/O page and the environment as a driver sees them, which is where the remaining switches — the A/D converter, the smooth-scroll and character-download registers, the slot interrupt status bytes — have their addresses confirmed.

  • Apple Computer, Inc. (n.d.-d). ProDOS 8 technical reference manual. https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/os/prodos/Apple%20ProDOS%208%20Technical%20Reference%20Manual.pdf

    Chapter 6 is what src/clockcard.zig and src/blockdevice.zig are built from: 6.1 is the clock and calendar routine a card supplies — its signature, its two entry points and the string it writes — and 6.3 is the disk driver protocol, down to the ROM code conventions of 6.3.1 and the $CnFE status byte that says how many volumes a card has and which of status, read and write it will do. The copy linked is an online transcription rather than a scan; it states no publication date of its own, and it carries a list of the printed manual's errata at the end.

  • Apple Computer Company. (n.d.). Apple-1 cassette interface. http://www.bitsavers.org/pdf/apple/apple_I/Apple-1_Cassette_Interface.pdf

    The leaflet for the ACI, the one card Apple made for the Apple-1: the R to C jumper that puts it in the $Cxxx block, the program at C100 and its * prompt and ssss.eeeeR form, the tape format as Apple described it — a 1 kHz cycle a one, a 2 kHz cycle a zero, a ten second header — and the schematic, which is the only description of the card's logic and is what the model of it in src/memory8/apple1.zig is read from: the chip-select decode, the output flip-flop and the comparator gated into the PROM's A0. Undated; the Internet Archive's copy is catalogued as 1977.

  • Apple Computer Company. (1976). Apple-1 operation manual. https://archive.org/download/Apple-1_Operation_Manual_1976_Apple/Apple-1_Operation_Manual_1976_Apple.pdf

    Twelve pages, and the whole of what Apple wrote about its first machine: the specification sheet with its 1.023 MHz clock and 0.960 MHz effective rate, the hardware notes with the PIA's four addresses described bit by bit and the read-key and write-display routines, the "KBD/DSP Interface" drawing that gives the PIA's chip selects, the refresh stalls, and the Woz Monitor as a hex listing with Wozniak's comments — the primary source for that program, against which wozmon.bin was checked line by line. src/memory8/apple1.zig is built from it.

  • Apple Computer, Inc. (1977). Apple II parallel printer interface card installation and operating manual. https://archive.org/details/Apple_II_Parallel_Printer_Interface_Card_Installation_and_Operating_Manual

    Twenty-odd pages for a card that is a latch and eight wires, and the one sentence that matters is the whole of the hardware: data stored at $C080 + $N0 appears on the printer's data lines and stays there until the next store. The rest is the firmware Woz fitted into 256 bytes — line widths, tabs, an escape character for talking to the card — and the two behaviours src/parallel.zig reproduces: the high bit comes off, and the card adds the line feed the Apple does not send.

  • Apple Computer, Inc. (1978). Apple II reference manual (Part No. 030-0004-00). https://apple2history.org/dl/Apple_II_Redbook.pdf

    Universally the Redbook. The original machine rather than the IIe, which makes it the match for the Apple II+ ROM this was being developed against. Section D has the memory map, the $C0xx addresses and the schematics; section C has an annotated listing of the Monitor, which is what the debugger is stepping through when an Apple ROM is loaded, and a 6502 opcode table on page 100. Pages 133 and 134 have the rules for populating the three rows of RAM with 4K and 16K chips and the five standard sets of memory select blocks, which is where --memory on an Apple II gets its sizes. src/memory8/apple2plus.zig and src/disk2.zig cite it.

  • Apple Computer, Inc. (1980). Apple III boot ROM listing (Rev. 1) [Assembly listing; D. T. Craig, Apple III Computer Information Doc. 085]. https://www.apple3.org/Documents/SourceCode/DTCA3DOC-085_apple_3_boot_rom_listing.pdf

    The Sara ROM's own source: the disk core routines, the power-on diagnostics and the boot. It is what says the ROM waits for retrace on the E VIA's CB2 flag — VWAIT BIT CB2INT ; WAIT FOR RETRACE — and names the registers by Apple's equates, and reading it is how the Apple III model was brought up: each stall in the diagnostics was a line in here.

  • Apple Computer, Inc. (1981a). Apple III owner's guide. https://www.apple3.org/Documents/Manuals/Apple%20III%20Owner's%20Guide.PDF

    What the machine tells its owner: RETRY and DIAGNOSTIC in the corner of the screen and what to do about them, the keyboard, the drive, and in appendix E the specifications — "2 MHz peak" for the clock — that the model's constants are taken from.

  • Apple Computer, Inc. (1981b). Apple III standard device drivers manual. https://www.apple3.org/Documents/Manuals/Apple%20III%20Standard%20Device%20Drivers%20Manual.PDF

    The console as a program sees it: chapter 3 lists the video generator's four graphics modes — 280 dots "black and white dots only", 280 "in any of 16 possible colors, with limitations", 560 black and white, 140 in sixteen colours — which is what says two of the modes carry a colour signal and two do not, and the escape sequences and control codes the console driver answers.

  • Apple Computer, Inc. (1982a). Apple III SOS reference manual: Vol. 1. How SOS works. https://www.apple3.org/Documents/Manuals/Apple%20III%20SOS%20Reference%20Manual%20Volume%201%20-%20How%20SOS%20Works.PDF

    Chapter 2 is the one place Apple wrote the memory system down for a programmer: the S-bank and the switchable bank, the bank register, zero page and stack relocation, and enhanced indirect addressing with its X-bytes and bank pairs — "the X-byte can range from $80 (banks 0 and 1) to $8m (bank m), where m is the number of the highest bank", and the singular $8F that pairs the S-bank with bank 0. The Apple III model's banking is built from that chapter and cites it by page.

  • Apple Computer, Inc. (1982b). Apple III SOS 1.3 source code listing (P. R. Santa-Maria, Assembler, 2006). https://www.apple3.org/Documents/SourceCode/Apple3_SOS_1.3.pdf

    SOS itself, assembled. Its equates are the register map — Z.REG, E.REG, B.REG, and every switch in the I/O page — and its loader is what settled how the machine's memory is sized: SYSBANK is the bank register as the boot block left it, and MEMSIZE follows from that. The disk boot was traced against this listing line by line.

  • Apple Computer, Inc. (1982c). Apple III level 2 service reference manual. https://archive.org/download/Apple_III_Service_Reference_Manual-OCR-1982/Apple_III_Service_Reference_Manual-OCR-1982.pdf

    Apple's own account of the hardware, for the technicians who fixed it: the memory system and its address decode, the two VIAs line by line, the clocks, the video logic, the I/O slots and their timing, the keyboard and its code table, the Apple II emulation mode, the schematics. It is what settled the processor's speed — the screen takes the other phase of the clock while it draws, and the processor has the whole 2 MHz only while the screen is off — that the VIAs run on the 1 MHz clock, that $c500 to $c7ff is RAM whatever the I/O switch says, that the bell at $c040 is a 0.1 second 1 kHz tone, that the bank register's bit 6 is the Solid Apple key, and that the keyboard hands over uppercase codes with shift reported beside them.

  • Apple Computer, Inc. (1983a). Apple III console driver 1.31 source code listing (D. T. Craig, Ed., 1998). https://www.apple3.org/Documents/SourceCode/apple3_SRC_Console_Driver.pdf

    SOS's console driver, which is the program that loads the character set into the video generator: its LOADCHR is the layout of the download cells in the screen holes — which page holds the code and which the image, and where each row goes — and TCOLOR is the foreground-and-background byte the colour text mode reads from the other text page. The Apple III's character download is that routine read backwards.

  • Apple Computer, Inc. (1983b). Apple III ProFile hard disk driver 1.30 source code listing (D. T. Craig, Ed., 1998). https://www.apple3.org/Documents/SourceCode/apple3_SRC_Profile_Driver.pdf

    The whole of the ProFile as an Apple III sees it: the card's four addresses and what each bit of its BUSY register means, the control codes read at $CN00 that work the addressable latch, the CMD/BSY handshake with its response bytes and $55/$AA answers, the six command bytes and four status bytes, and the pseudo-DMA — the driver's own comments on which bytes of the buffer page the false address of the exit branch touches are what gave away how the ROM's page of SBC #1 / BEQ moves a block. src/profile.zig is built from it and cites it by page.

  • Apple Computer, Inc. (1984a). Apple IIc technical reference manual. https://archive.org/download/Apple_IIc_Technical_Reference_Manual/Apple_IIc_Technical_Reference_Manual.pdf

    Not the machine this emulates, but the one that documents the piece of it that a IIe does not have. Vertical blanking on a IIe is something a program polls at $c019; being able to route it to the interrupt line instead is a IIc feature, switched at $c05a and $c05b, and those are the addresses the IIe model uses for the enable it keeps switched off by default. The same manual covers the mouse card firmware, which is the other way that interrupt reached a IIe.

  • Apple Computer, Inc. (1984b). SmartPort. In Apple IIc technical reference manual.

    The call numbers, the shape of each parameter list, the status codes and the error codes are all published in this chapter and in the technical note that followed it, and this card's SmartPort side follows them. What is not in a book, and came out of the IIgs's own ROM instead, is the layout of the device information block: at $ff58fd its 3.5-inch drive's constant part reads 08 "DISK 3.5 " 01 c0 00 10, which settles the order of the fields, that $01 is the type of an Apple 3.5-inch disk, and that the subtype's top two bits are the extended and removable flags. src/blockdevice.zig cites both.

  • Apple Computer, Inc. (1984c). ProFile level II service manual (Part No. 072-0116, update 1). https://www.apple3.org/Documents/Manuals/Profile_Level_2_Manual.pdf

    The drive's own manual, for fixing it: troubleshooting, the service procedures with the format and debugger programs, and appendices on the hard disk assembly, its format and its firmware, with the schematics. What the ProFile model takes from it is what the driver's source leaves implicit — that an Apple III's blocks are 512 bytes where a Lisa's are 532, that reading block $ffffff returns the drive's status, and the bit-by-bit meaning of the four status bytes.

  • Apple Computer, Inc. (1985a). Apple II Super Serial Card user's manual (Part No. 030-1258-C). https://archive.org/details/super-serial-card-user-manual

    The card the ACIA in src/serial.zig sits on. Most of it is for the person using the card rather than for a program — the two blocks of switches, the cable, and the command characters a terminal program sends to set the baud rate, the data format and the parity — but appendix C is the firmware, and it is the one that matters here: the Pascal 1.1 protocol, the bytes that identify the card as a serial device, and the entry points BASIC and Pascal call it through. The scan carries no text layer.

  • Apple Computer, Inc. (1985b). Apple IIe technical reference manual. https://archive.org/details/Apple_IIe_Technical_Reference_Manual

    The machine this emulates, and chapters 2, 4 and 7 are what src/memory8/apple2e.zig is built from: chapter 4 is the memory map and the bank switching, chapter 2 the built-in I/O and the soft switches that drive it, and chapter 7 the MMU and IOU that implement both. src/screen.zig and src/video.zig cite chapter 2 as well, and it is where the sentence that settles the default monitor comes from: on an ordinary colour set "80-column text will be too blurry to read", which is why the eighty-column modes come up in monochrome. Appendix A is the one that bears on the processor: it sets out where the 65C02 departs from the 6502, including the JMP indirect page wrap that this emulator reproduces and a BIT immediate that behaves differently, which is one of the differences the split opcode tables account for. The plain scan carries no text layer, so search the _text.pdf instead; there is also a PDF at applelogic.org and the plain scan itself.

  • Apple Computer, Inc. (1985c). The disk interface. In Inside Macintosh (Vol. 3, pp. III-33III-38). Addison-Wesley. https://vintageapple.org/inside_o/pdf/Inside_Macintosh_Volume_III_1985.pdf

    Chapter 2 of Volume III is the Macintosh described part by part, and its disk interface section is Apple's own statement of the same fifteen registers Parker's article describes: which combination of CA0, CA1, CA2 and SEL addresses each one, what each answers, and which four of them can be written by holding LSTRB high. It is a Macintosh book and the drive is the same Sony mechanism, which makes it the source behind that article — and it agrees with it on all fifteen, so src/disk35.zig's table is checked against two independent statements of it rather than one. It is also where the tachometer's rate comes from: "the TACH line produces 60 pulses for each rotation of the drive motor", which this model had at fifty until the page was read. Two rules stated there are easy to miss and both matter: the IWM sits on the odd side of the data bus, so only odd-addressed byte accesses reach it, and Q6 must be turned back off before the Disk Driver next runs.

    This was cited as undated from a three-page extract until the whole volume was read; the volume carries Apple's 1985 copyright, and the section runs from III-33 to III-38 rather than the III-34 to III-36 the extract holds. The extract is still the quickest way to the register tables themselves.

  • Apple Computer, Inc. (1985d). The AppleTalk Manager. In Inside Macintosh (Vol. 2, pp. II-261II-346). Addison-Wesley. https://vintageapple.org/inside_o/pdf/Inside_Macintosh_Volume_II_1985.pdf

    The network from the software's side: what a frame is to the link layer — a destination, a source, "an eight-bit quantity, contained in the frame's header, that identifies" the protocol above, and the data — and the layers above it. It describes the Manager rather than the wire, so the handshake and the timings src/localtalk.zig needs come from Zilog's application note instead; this is cited for the shape of the frame. The values of the four control types are in neither and are taken from the firmware.

  • Apple Computer, Inc. (1985e). Apple IIc technical reference manual. Addison-Wesley. https://archive.org/download/a2ctrm/Apple2cTechRef01.pdf

    Apple's own reference for the machine, and the one that covers not just the original IIc but the revision after it: a UniDisk 3.5 flag down the margin marks every paragraph that applies only to the 32K-ROM machine. That is what makes it, and not the 1984 manual above, the source for what the later ROMs do. Appendix F is the family differences: F.2 says that where the original reserves $C020 through $C02F, "the Apple IIc with 32K of ROM uses $C028 to switch in or out the extra 16K of ROM", and F.10 restates it as toggling "between its two 16K banks of memory" — which is the whole of the bank switch memory8/apple2e.zig implements. Section 11.5.5 is the disk controller unit, "also known as the Integrated Woz Machine", which is why a IIc's disk port is an IWM here and not a Disk II card.

  • Apple Computer, Inc. (19851990). Apple II technical notes: Mouse (Nos. 17). Developer Technical Support. https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Digitizers/Apple%20Mouse%20Interface%20Card/Documentation/Apple%20II%20Mouse%20Technical%20Notes.pdf

    Seven short notes about the AppleMouse II card, revised between 1985 and 1990, and src/mouse.zig takes the shape of the card from them and from the IIe technical reference. #5 is where the identification bytes come from: "the AppleMouse II card is identified by a value of $20 at $Cn0C ('X-Y Pointing device, type zero') and a value of $D6 at $CnFB", and it says that the $CnFB byte is not part of the Pascal 1.1 protocol, which is why identifiers looks for five bytes rather than four. #3 is the mode byte, and is careful about what turning the mouse off does and does not do; #1 is the interrupt environment a program has to keep around a mouse call; #4 is a firmware bug affecting ServeMouse.

  • Apple Computer, Inc. (1986). Ensoniq DOC ERS (Rev. 1.0) [Engineering specification, marked confidential]. https://www.brutaldeluxe.fr/documentation/cortland/v4_13_EnsoniqDOC.pdf

    Apple's own specification for the 5503, eleven typed pages dated June 25, 1986, and the one document that gives the Oscillator Interrupt register exactly: bit 7 is the state of the IRQ line and reads zero when an interrupt is pending, bits 0 and 6 always read as ones, a read clears the request, and interrupts from more than one oscillator queue behind the register first in, first out. The interrupt register in src/ensoniq.zig is built from it. It also describes, under the control register's interrupt-enable bit, an "interrupt table" that remembers an oscillator finishing while its interrupt is disabled and delivers it if the bit is set later, which is not modelled.

  • Apple Computer, Inc. (1987a). Apple IIgs firmware reference. Addison-Wesley. https://archive.org/details/Apple_IIgs_Firmware_Reference_HiRes

    The book the hardware reference keeps pointing at, and the one that has what it does not: chapter 9 is the Apple Desktop Bus microcontroller and lists every command the host can send it, which is what the ADB registers here are built against. Chapters 6 and 7 are the disk port and the SmartPort, and pages 157 and 158 are the two pages that make an intelligent drive work on any machine and not just the one it was written for: the phase-line combinations that enable and reset the bus, how the host hands out unit numbers, and how the last drive on the chain says that it is the last. Figure 6-1 on page 110 gives the order the drives go in. Appendix E is a soft switch list, and appendix H is the clearest statement anywhere of what shadowing is for. Beware which copy: the one commonly linked from the Cortland archive is the 1986 beta draft with chapters 2, 3, 5, 8 and 9 missing — including the one chapter that matters here — and a 1987 scan may have no text layer at all. The HiRes scan at the Internet Archive is complete and searchable, and its text is the copy to grep.

  • Apple Computer, Inc. (1987b). Apple IIc memory expansion card reference manual (Part No. A2G0047). https://archive.org/download/apple-2c-memory-expansion-card-reference-manual/Apple2cMemoryExpansionCardReferenceManual.pdf

    Forty pages on the card that goes in the connector inside a IIc, and the only place that says plainly what the four registers at $c0c0 are: a twenty-bit address counter in three bytes, low first, and a data register that steps the counter every time it is touched (p. 7). That last sentence is what makes the card work at all, and what the RamFactor's manual says of its own card in almost the same words — the two are the same idea, one in a slot with its firmware aboard and one soldered to the machine with its driver in ROM.

  • Apple Computer, Inc. (19871990). Apple IIgs technical notes (Nos. 1125). Developer Technical Support. https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Computers/Apple%20II/Apple%20IIGS/Documentation/Apple%20IIGS%20Technical%20Notes%2011-25.pdf

    Short notes issued between editions of the two references above, and three of these fifteen bear directly on the sound chip. #11, "Ensoniq DOC Swap-Mode Anomaly", records that the output falls to the zero-crossing level for no longer than one sample period "whenever a swap occurs from a higher-numbered oscillator to a lower-numbered one", and not the other way about — which src/ensoniq.zig reproduces without being told to, by scanning its oscillators in the order the chip scans them. #23, "Toolbox Use of DOC RAM", explains that the Note Synthesizer runs a free-running oscillator over the first page of the sound RAM at zero volume purely as a clock, which is what settles that an oscillator interrupts at the end of its wavetable rather than when it halts. And #19, "Multichannel Output with the Apple IIgs Note Synthesizer", says what the top four bits of an Oscillator Control register are for: a stereo card decodes three of them, even channels to the right and odd to the left. The other twelve are toolbox and firmware matters, of which #25 is a correction list for the firmware reference above and #13 a bug in ROM 1.0's modem firmware.

  • Apple Computer, Inc. (1988). SWIM chip user's reference. https://bitsavers.org/pdf/apple/disk/sony/SWIM_Chip_Users_Ref_198801.pdf

    The chip that replaced the IWM, and the only Apple document that sets out the 3.5-inch track format: the five speed zones and their sector counts, the address and data fields byte by byte, and the three-checksum nibblising algorithm — which it prints twice, for writing and for reading, in two forms that do not agree. src/disk35.zig cites it, and follows the reading one.

  • Apple Computer, Inc. (1989b). Apple IIc technical reference manual (2nd ed.). Addison-Wesley. https://archive.org/download/AppleIIcTechnicalReference2ndEd/Apple%20IIc%20Technical%20Reference%202nd%20ed.pdf

    The revised edition, and the only one that describes every IIc Apple shipped: the first edition (1985e) stops at the UniDisk 3.5 machine and knows nothing of the memory expansion card or the IIc Plus. Chapter 11 is the hardware, and it is the source for the one chip a IIc Plus has that no other IIc does — the multidrive interface glue, "two ICs: a custom integrated circuit (the Mc7 chip) and a 2 KB static RAM" (p. 327), whose pins Table 11-15 names and whose RAM Table 11-24 addresses with A0 to A4 from the system and RAMA5 to RAMA10 from the chip itself. src/mig.zig is built on those two tables; what they do not say — which address works which latch, and how the buffer's page moves — is read out of the ROM 5 firmware instead, and says so where it is.

  • Apple Computer, Inc. (1989a). Apple IIgs hardware reference: For Apple IIgs and 1 MB Apple IIgs (2nd ed.). Addison-Wesley. https://archive.org/details/Apple_IIgs_Hardware_Reference_HiRes

    The machine the 16-bit core goes into, and the reference for everything built around it: the Mega II that is a IIe on one chip and gives the slow side its 1 MHz, the shadowing of banks $00 and $01 into $e0 and $e1, the VGC and its Super Hi-Res modes with a palette per scanline, the Ensoniq 5503 with its thirty-two oscillators, and ADB as a microcontroller rather than a port. src/memory16/apple2gs.zig is built against it throughout, and chapter 2 in particular: the bank latch, the two clock domains and the whole of shadowing come from it. Chapters 2 and 3 say the same things from opposite ends — chapter 2 from the chips outward and chapter 3 from the address map inward — and the pair is often what settles a question neither one does alone. Read the _djvu.txt: the tables survive OCR well enough to be trusted, and it is searchable in a way the PDF is not.

  • AppleWin. (n.d.). AppleWin [Computer software]. GitHub. https://github.com/AppleWin/AppleWin

    Consulted rather than cited: its WOZ read path is where the rule that a held byte is not taken away while the shifter is empty was confirmed, after Sather's chapter established that the hold exists and how long it is. Reading a second implementation is the cheapest way to find out whether a rule derived from prose is the rule the hardware actually follows.

  • Applied Engineering. (n.d.). RamFactor user's manual (Version 1.5). https://garrettsworkshop.com/files/GR8RAM/ae_ramfactor_manual.pdf

    What src/ramfactor.zig is built from, and the reason this project models Applied Engineering's memory expansion card rather than Apple's own: chapter 6 is written for programmers and has the hardware in it. "RamFactor Hardware" is two pages and settles almost everything — the five registers and where the slot puts them, that the three address bytes can be read as well as written, that the top nybble of the high one reads back as $F on a card of a megabyte or less, that touching the data register steps the address, and that the step is a ripple carry that a write trips as readily as a count does. The sections around it are what the card looks like from outside: the identification bytes at $Cs00-$Cs07, $CsFA and $CsFB, the screen holes the firmware leaves the card and partition sizes in, the layout of the partition table in the card's own first two hundred and fifty-six bytes, and the SmartPort — which it calls the Protocol Converter, that being what Apple called it first. The manual carries no date; the firmware says 1986. It does not say which bit of the bank select register is the bank, and the firmware does: see A megabyte in a slot.

  • Carr, J. J. (1984). 6502 user's manual. Reston Publishing. https://archive.org/details/6502um

    The source of the (Carr, 1984, p. N) citations, which all point into chapter 16, its instruction-by-instruction reference on pages 245 to 277. Note that chapter has no entry for TAY, and the comment on that instruction says so rather than pointing at a page that does not discuss it. The PDF is the copy to search.

  • Dormann, K. (2020). 6502_65C02_functional_tests [Computer software]. GitHub. https://github.com/Klaus2m5/6502_65C02_functional_tests

    Four test programs that exercise the processor from the inside and then stop — at a known address if everything agreed, at the point of disagreement otherwise. Two cover every documented opcode and addressing mode of the 6502 and the 65C02; one is Bruce Clark's decimal mode test, carried here with configuration options added; and one checks IRQ, NMI and BRK through a feedback register the host has to provide. Where the SingleStepTests corpus checks one instruction at a time, these check what happens between them, and all four pass: see What the corpus cannot see. The .lst listings are what the success addresses are read out of and are the place to look when a run settles somewhere else — note that the interrupt test's is $06F5, the jmp * of the success macro, and not the jmp start three bytes past it, which sits under the same banner and cannot be reached. Pinned to commit 7954e2d.

  • Fort, M., Pratt, P. C., & Mitchelmore, R. (2012). LTOVRUDP.h [Source code]. In Mini vMac. GitHub. https://github.com/minivmac/minivmac/blob/master/src/LTOVRUDP.h

    Where LocalTalk over UDP comes from: Mini vMac's own transport for it, and the statement of the convention every other implementation follows. A UDP socket bound to any address with the address reusable, joined to the multicast group 239.192.76.84 on port 1954; four bytes of sender in front of every frame, so that a node can tell its own frames from everyone else's when the group hands them back; and the reason it cannot just look at the node number, which the file spells out — during address acquisition another node legitimately sends from what this one thinks is its own. src/ltoudp.zig opens its socket the same way.

  • General Instrument. (n.d.). AY-3-8910/AY-3-8912/AY-3-8913 programmable sound generator (pp. 5-185-23). https://map.grauw.nl/resources/sound/generalinstrument_ay-3-8910.pdf

    The chip that makes the noise on a Mockingboard, as six pages out of a General Instrument data book, and the arithmetic src/ay3.zig is built on is stated there in a sentence: a tone generator works "by first counting down the input clock by 16, then by further counting down the result by the programmed 12-bit Tone Period value". The noise generator does the same with a five-bit period and the envelope counts the clock down by 256 instead, which is why the periods in a tune are the numbers they are. Registers R0 to R15 are set out here too, down to the four bits of R15 — hold, alternate, attack and continue — that pick the envelope shape. This copy has no date and no text layer; the one at alldatasheet.com is the same pages behind a challenge page.

  • lampmerchant. (2021). ltoudp.py [Source code]. In TashTalk. GitHub. https://github.com/lampmerchant/tashtalk/blob/main/tashtalkd/daemon/protocol/ltoudp.py

    The daemon that puts a real LocalTalk cable on the same multicast group, and so the clearest statement of what crosses and what does not: a frame goes out as the sender's tag and the frame without its last two bytes, which are the check sequence, and the daemon does "not retransmit RTS/CTS frame[s]", nor an enquiry for a node it is already answering for. The bridge in src/localtalk.zig keeps the handshake off the network for the same reason and answers the machine's requests to send itself.

  • National Semiconductor. (1995). MM58167B microprocessor real time clock [Data sheet]. https://archive.org/download/MM58167/MM58167.pdf

    The Apple III's clock chip. Table III is the register map, Table I the BCD digits of each counter and which bits of them are unused, Figure 1 the interrupt register's bits, and the text the resets, the GO command and the rollover status bit that SOS reads after every counter to know whether the chip was rippling while it looked. src/mm58167.zig is built from it and cites it by page.

  • MAME Development Team. (n.d.). apple3.cpp, apple3_m.cpp, apple3_v.cpp [Source code]. In MAME. GitHub. https://github.com/mamedev/mame/tree/master/src/mame/apple

    Where the manuals stop, the schematic starts, and MAME's Apple III driver is the reading of it that has been checked against the machine's software for twenty years. What was taken from it, and is marked as its in the code: which VIA each line is wired to — the blanking on the E VIA's CB1 and CB2, the keyboard on its CA2, the interrupt status on its PA7 — what the VIAs read at reset, the rule for which zero-page reads latch an X-byte (Z ^ $0C00) and where the stack goes (Z ^ 1), the meaning of the four video mode bits, and that the sixteen addresses under the VIAs are RAM when the VIAs are hidden. Where MAME and the manuals differ — the bank a number above the highest fitted selects — the manuals and the boot block won.

  • Charlesworth, T. (2026). mb-audit [Computer software]. GitHub. https://github.com/tomcw/mb-audit

    A program that audits a Mockingboard from inside the machine: it finds what is in each slot and then works through the 6522s, the AY-3-8913s, the SC-01 and the SSI263s, stopping at the first failure and naming the component, the test and the sub-test. Deliberately deeper than an emulator needs, and run against real cards as well as emulated ones, which makes it the better authority where it and a datasheet disagree — see Auditing the Mockingboard for the three 6522 faults it found here. Assembled with ACME.

  • Ollie, J. C. (2026). aigrette65 [Computer software]. https://git.ocjtech.us/jeff/aigrette65

    An assembler for the 6502 and 65SC02 in the dialect of Frank Kingswood's as65, reimplemented from the outside with the original as an oracle. It is what assembles the two of Dormann's tests that ship as source only, whose author will not switch away from as65: see Assembling the two Dormann does not ship.

  • Black, A., & KI3V. (2026). appleII_deadtest (Version 1.0.2) [Computer software]. GitHub. https://github.com/misterblack1/appleII_deadtest

    A diagnostic ROM that replaces the F8 ROM and runs a March-U memory test out of its own two kilobytes, using no RAM at all — not the zero page and not the stack — so that it can report on a machine whose first bank of memory is bad. It is what settled what an Apple II reads at an address with no RAM behind it, and two whole-machine tests here run it across all nine memory configurations: see Testing the memory with no memory.

  • Hunter, Z. (2025). a2audit [Computer software]. GitHub. https://github.com/zellyn/a2audit

    A bootable disk that audits an Apple II from the inside: the language card's bank switching, the auxiliary memory soft switches, what reading a soft switch does as against writing it, and which ROM answers in the $Cxxx pages. Where the SingleStepTests corpus checks a processor and the WOZ set checks a drive, this checks the machine between them, and it reports each difference as a code documented one for one in the repository's v0/index.md — which is what makes it readable by a test rather than only by eye. Booted by a whole-machine test here: see Auditing the machine itself.

  • Morris, J. K. (2018). WOZ disk image reference 2.1. Applesauce. https://applesaucefdc.com/woz/reference2/

    The format that writes a disk down as its bits, and the reference is unusually good: it gives the chunks exactly, and then goes further and tells an emulator author what to do with them — the four-bit window that puts back the noise a cleaned image had taken out of it, the rule for scaling the head's position when it moves to a track of a different length, and what an empty quarter track should read as. src/woz.zig and the drive in src/disk2.zig are written from it, and reference 1.0 covers the older images the same reader accepts.

  • Morris, J. K. (2021). WOZ test images (Version 1.4) [Data set]. Applesauce. http://evolutioninteractive.com/applesauce/woz_images.zip

    Forty-five 5.25-inch images "intended to assist Apple ][ emulator developers in testing their implementations of floppy drives and drive controller cards", with a readme that puts them in order of difficulty: start with the System Masters, then the ordinary copy protections, then the cross-track checks. It is the closest thing a drive has to the processor's corpus, and it is what this was built against.

  • MOS Technology, Inc. (1976). MCS6500 family programming manual (Publication 6500-50A). https://archive.org/details/6500-50a_mcs6500pgmmanjan76

    The source of the (MOS Technology, 1976, Section N.N, p. N) citations in the opcode implementations. Chapters are organised by what the processor is doing rather than alphabetically, so LDA is under the data bus and LSR under shifts and rotates. The PDF is searchable, and there is another scan of the same publication, useful when one copy's OCR drops a heading.

  • Parker, N. (1994, February). Controlling the 3.5 drive hardware on the Apple IIGS. https://archive.org/details/IWM-Floppy_Disk_IO_Controller-Controlling_the_3.5_Drive_Hardware_on_the_Apple_II

    The only description anywhere of the fifteen registers inside an Apple 3.5-inch drive: what each one answers, what each control function does, and which of them the IIgs ROM uses that no Apple publication mentions. It is also the source that says the hardware reference and the firmware reference are both wrong about which bit of $c031 selects the drive, which the ROM confirms. src/disk35.zig and src/iwm.zig cite it.

    It is wrong about one thing, and it is the one thing it says it might be. Register 12, the disk-switched bit, is given as "0 = user ejected disk by pressing the eject button, 1 = disk not ejected"; the article marks that register as used by the IIgs ROM and documented in no publication available to its author, and says only that he is "fairly certain" of it. The ROM has it the other way up — see src/disk35.zig for the routine at $ff/5f3b that settles it — while agreeing with him on the registers either side of it in the same table. A hedged sentence in a good article, and the hedge was the right one.

  • Sather, J. (1983). Understanding the Apple II. Quality Software. https://archive.org/download/understanding_the_apple_ii/understanding_the_apple_ii.pdf

    The Apple II and II Plus gate by gate, and the one account of what changed between the boards. Appendix G lists the revisions — what Revision 1 added, that a II Plus differs from a II in firmware alone, that 4K RAM went away at Revision 4 — and chapter 8 has the video: the colour burst killer that made text white from Revision 1 on (p. 8-5), the D7 delay that gave Hi-Res its blue and orange (p. 8-19), and the Revision 0 board that had neither. src/memory8/apple2plus.zig and src/video.zig are built against it.

  • Sather, J. (1985). Understanding the Apple IIe. Quality Software. https://archive.org/details/Understanding_the_Apple_IIe

    Chapter 9 is the Disk II controller card, gate by gate: what each of the sixteen soft switches does inside the card rather than to the drive, how the sequencer ROM and its two flip flops turn bits into bytes, and what the parts on the card are for. It is where the NE556 that keeps the drive turning for a second after $c088,x is written down, and it says both what that second is for and the one thing that cuts it short. src/disk2.zig cites it, and so does the sequencer in woz/src/sequencer.zig. The text is the copy to grep.

  • SingleStepTests. (n.d.). 65816 [Data set]. GitHub. https://github.com/SingleStepTests/65816

    The 16-bit core's own corpus, a separate repository because it is a separate shape: twenty thousand cases per opcode rather than ten, and eight signal lines recorded per cycle rather than a direction. Fetched as a Zig dependency rather than committed.

  • SingleStepTests. (n.d.). 65x02 [Data set]. GitHub. https://github.com/SingleStepTests/65x02

    The test corpus for the 8-bit processors, ten thousand recorded cases for every opcode of every one of the five. Also fetched rather than committed.

  • Synertek. (n.d.). SY6551 asynchronous communication interface adapter (pp. 3-1773-184). http://dunfield.classiccmp.org/r/6551.pdf

    The ACIA behind the Super Serial Card, as eight pages out of a Synertek data book: the copy linked carries no date and no text layer, so it has to be read rather than grepped. src/serial.zig takes two things from it. The control register's low four bits are the baud rate table — fifteen rates from 50 to 19,200, derived from a 1.8432 MHz crystal, and a sixteenth encoding meaning an external clock, which the card modelled here does not provide. And figure 8 is the status register format, which is where the awkward part is: carrier detect and data set ready are reported the wrong way up, a nought meaning the other end is there, and the parity, framing and overrun bits raise no interrupt of their own.

  • Videx. (1983). UltraTerm installation and operation manual. https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/80%20Column%20Cards/Videx%20UltraTerm/Manuals/Videx%20UltraTerm%20-%20Installation%20and%20Operation%20Manual.pdf

    The later card's manual, and src/ultraterm.zig is written from section 8.d.3 of it. That section gives the sixteen device select addresses a meaning for each direction, which the Videoterm's did not need — "it is the reading of a particular address that will set a specific operating mode" — and sets out the mode control port bit by bit, including the two dot clocks, which is where 17.430 MHz is confirmed as the Videoterm's own crystal. It also describes the attribute register: "the high nibble sets the attributes for characters with the high bit set. The lower nibble sets the attributes for characters with the high bit clear". Appendix F prints the firmware, and its equates are what settle where the screen memory appears: DISP0 EQU $CC00, and HPBANK EQU %10000000 for the bit that banks it away again. The scan has a text layer.

  • Videx. (1982, January). Videoterm installation and operation manual (001053 VT-MAN-000). http://www.apple-iigs.info/doc/fichiers/videoterm.pdf

    The card's own manual, and src/videx.zig is written from chapter 5. It gives the device select bit by bit — "the very lowest bit, bit 0, controls whether a register is being accessed (0) or the contents of the register is being accessed (1) ... the next two bits, bits 2 and 3, determine which page will be selected" — and is explicit that one access does all of it at once, which is why selecting a 6845 register also moves the window. Pages 5-12 are the memory map: a kilobyte of firmware at $C800, five hundred and twelve bytes of screen memory at $CC00, and the paging that reaches the rest of the two kilobytes. Chapter 5 also walks the 6845's eighteen registers, and the manual carries a full assembly listing of the firmware. The scan has a text layer and is searchable.

  • Using the ProFile with the Apple /// [Instruction sheet]. (n.d.). https://www.apple3.org/Documents/Manuals/Using%20the%20Original%20ProFile%20with%20the%20Apple3.pdf

    Four pages that say where the ProFile's interface card goes — slot 4 — and how the utilities format the drive, which is the procedure the model was checked against.

  • Western Design Center, Inc. (2010). W65C22 (W65C22N and W65C22S) versatile interface adapter (VIA) datasheet. https://www.westerndesigncenter.com/wdc/documentation/w65c22.pdf

    The 6522 as its current maker documents it, register for register the chip the Mockingboard and the Apple III carry. Table 2-5 is the peripheral control register — which edge on each control line raises its flag, and the "independent" input modes whose flags a port access does not clear — and Tables 2-2 and 2-3 say what a port reads on an input line, which is what let the Apple III's ROM find the values it was looking for.

  • Western Design Center, Inc. (2018). W65C816S 8/16bit microprocessor [Data sheet]. https://archive.org/details/w65c816s_bm

    The 16-bit processor's own data sheet, and section 3.5 is the list of addressing modes src/cpu16/addressing_modes.zig is checked against. WDC counts twenty-four of them against the W65C02S's thirteen, and this enum has twenty-five, because 3.5.5 folds the long indirect jump in with the short one: "with the Jump Long (JML) instruction, the Program Bank Register is loaded with the third byte of the pointer". Here (Absolute) and [Absolute] are separate, since the operand syntax differs and only one of them touches the program bank.

  • Worth, D., & Lechner, P. (1981). Beneath Apple DOS. Quality Software. https://archive.org/details/beneath-apple-dos

    Where the shape of a disk comes from: chapter 3 lays out a track field by field, gives the six and two encoding and the sector skew tables, and explains why a .dsk file has to be put through all of that before a drive can read it. src/disk2.zig and src/dos33.zig cite it.

  • Worth, D., & Lechner, P. (1984). Beneath Apple ProDOS. Quality Software. http://www.apple-iigs.info/doc/fichiers/beneathprodos.pdf

    The same pair on the newer filing system, and chapter 4 is where src/prodos.zig takes the layout from: the volume directory, the seedling, sapling and tree file structures a file grows through, the storage types and the subdirectories. All of it was checked against a real volume as well. The scan linked carries no text layer, so it has to be read rather than grepped.

  • ZiLOG, Inc. (2001). SCC/ESCC user's manual (UM010901-0601). https://www.alldatasheet.com/datasheet-pdf/view/96934/ZILOG/Z8530/+7W845UUGIxhbbCvYB+/datasheet.pdf

    An Apple IIgs's two serial ports are a Z8530, and four addresses reach thirty-one registers, which is the whole difficulty. The IIgs hardware reference declines to explain the chip and points at this manual instead, so this is where src/scc.zig comes from: section 2.3.4, "Z85X30 Register Access", states the two-step protocol exactly — a write to the command register while the pointer is zero sets the pointer, and the next access reaches the register it names — and chapter 5 is every write and read register in turn. Between them they settle the aliasing the file models: the part has nine read registers and sixteen places to look for them, and which six of those places repeat another register is Zilog's map rather than a convenience.

Bibliography

Material worth having that nothing in the source cites, in the same form as the reference list above. Each is here because it is the companion volume to something in that list, or the place to look next for a part of the machine this does not model.

  • Apple Computer, Inc. (n.d.-a). Apple Disk II technical procedures. https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Peripherals/Disk%20Drives/Apple%20Disk%20II/Manuals/Apple%20Disk%20II%20Technical%20Procedures.pdf

    The service manual for the drive itself rather than for the card: how the mechanism is put together, what each adjustment does, and the alignment procedures. Nothing in the source cites it — the drive is modelled from the card's side, where Sather and the WOZ reference say what the card sees — but it is where the parts behind that behaviour are described: the four magnets, the speed, the head, and the ones this does not model at all.

  • Apple Computer, Inc. (1985a-svc). Apple IIc flat panel monitor technical procedures. https://archive.org/download/a-list-list-of-appleii-manuals/AppleService%20Technical%20Procedures%20-%20Apple%20IIc%20Flat%20Panel%20Monitor%20KBS.pdf

    The service manual for the IIc's optional liquid crystal display: what its modules are, how they fail, how the case comes apart, and the part numbers. It describes the panel as hardware and says nothing about what software sees of it, so it settles questions about the product and none about the machine. Here because the flat panel is a part of the IIc this does not model.

  • Apple Computer, Inc. (19851986). Cortland documentation set. https://www.brutaldeluxe.fr/documentation/cortland/

    Thirty-odd documents published under the IIgs's codename, before it had its name. Three bear on this: the Cortland Hardware Reference, which is the earlier draft of the Apple IIgs hardware reference; Cortland I/O Timing, which is where the 2.8 MHz processor and the 1.024 MHz Mega II are set against each other; and the Vegas ROM Diagnostics, which describes the power-on self-test this project spends its time watching — though the scan of that one is typescript and the OCR of it is close to unreadable.

  • MOS Technology, Inc. (1976). MCS6500 family hardware manual (Publication 6500-10A). https://archive.org/details/MCS6500_Family_Hardware_Manual

    Nothing in the source cites it: it is about pin assignments, bus timing and the support chips rather than the instruction set, and mentions NOP once in passing. Listed because it is the companion volume to the programming manual and is where the timing behind the cycle counts is set out. The PDF is searchable; the scan at 6502.org is the better image but carries no text layer, so it cannot be.

  • Videx. (1983). UltraTerm quick reference guide. https://archive.org/download/Videx-UltraTerm_Quick_Reference_Guide/Videx-UltraTerm_Quick_Reference_Guide.pdf

    Two sides of one card, and the companion to the UltraTerm manual in the reference list — the same year, the same publisher, and dated by its own "Copyright 1983 Videx Inc." rather than by association with the manual. It lists what a user or a program sends the card: the input commands and the screen editing on one side, the rest of the output commands and the operating system commands as typed from BASIC on the other, with the attribute escape set out as a lead-in followed by two digits, the first choosing the standard attribute set and the second the alternate. Nothing in the source cites it because the manual says all of it at greater length and is the better thing to quote; it is here because it is the form the card's owner actually had beside the machine, and because a one-page summary is a quick way to check that a command list is complete.

  • Sidhu, G. S., Andrews, R. F., & Oppenheimer, A. B. (1990). Inside AppleTalk (2nd ed.). Addison-Wesley. https://vintageapple.org/macbooks/pdf/Inside_AppleTalk_Second_Edition_1990.pdf

    The protocol specification proper, from the wire up: LLAP's frame, its four control types, the dynamic node assignment, the inter-frame and inter-dialog gaps, and every layer above. Nothing in the source cites it because Zilog's application note carried what the chip's model needed and the firmware carried the rest; it is where to look for anything the bridge in src/localtalk.zig does not yet do, which is most of the protocol.