A dependency-free Swift package for an independently implemented Apollo Guidance Computer Block II emulator. New code is released under 0BSD. The basic and extended instruction groups are implemented, including the double-word operations and multiply/divide. It supports program interrupts and CPU-driven timer/counter scheduling. A DSKY value model and terminal renderer are implemented. Original Luminary 099 can select P00 with quiescent inputs; spacecraft peripheral models remain partial.
Requires Swift 6.0 or later. Built and tested locally on Apple Silicon with Apple Swift 6.4. The library uses the Swift standard library only; the demo uses Foundation for timing/output. Other platforms have not yet been tested.
swift test
swift test -c release
swift run -c release agc-demo
swift run -c release agc-demo --benchmark 100000000
swift run -c release agc-demo --alarm-demo 1201
swift run -c release agc-demo --alarm-demo 1202
swift run -c release agc-demo --restart-demo
swift run -c release agc-demo --luminary-startup-demo
swift run -c release agc-demo --luminary-p00-demo
swift run -c release agc-demo --luminary-descent-preflightThe default demo increments an erasable-memory counter ten times using actual AGC instruction encodings. The benchmark runs the same five-instruction loop with masking so that it stays valid across counter overflow. It is a narrow core throughput measurement, not a representative Apollo workload.
The descent qualification diagnostic enters P63 through the original guest and verifies its expected IMU-off rejection (00210). A CPU-time input timeline provides deterministic device events for subsequent spacecraft modeling. Powered descent is not yet established.
swift run --package-path terminal -c release agc-terminalThe separate SwiftTUI frontend boots Luminary into P00
and provides live primary keys, held PROCEED, pause, and quit. It targets
macOS, Linux, and Windows using SwiftTUI's portable APIs; only macOS has been
verified locally. It requires Swift 6.4. The root OpenAGC package remains
UI-independent, dependency-free, and usable by other applications.
For repeatable input without SwiftTUI:
swift run -c release agc-demo --dsky-keys V05N09EAll Swift tests use Swift Testing (@Test, #expect, and #require).
An external flight simulator can implement AGCDevice
to supply sensors and receive guest output strobes at CPU instruction boundaries.
Spacecraft models stay in the embedding package, and time stays CPU-driven.
The package exports the OpenAGC library and agc-demo executable, plus the
optional AGCExperiments library with public-domain original AGC fixtures.
import OpenAGC
let cpu = CPU()
// Physical fixed bank 2 appears at logical address 04000 (octal).
// CA 0100; AD 0101; TS 0102; TCF 04003.
try cpu.loadFixed([0o30100, 0o60101, 0o54102, 0o14003], bank: 2)
try cpu.storeWord(OnesComplement.encode(17), at: 0o100)
try cpu.storeWord(OnesComplement.encode(-5), at: 0o101)
try cpu.run(instructions: 3)
let sum = try cpu.peek(0o102)
assert(OnesComplement.decode(sum) == 12)
assert(cpu.registers.memoryCycles == 6)step() executes one fetched instruction and returns its memory-cycle count
(MCTs). run(instructions:) executes an exact instruction budget. Both stop
and throw on an invalid operand memory access. Prefixes such as EXTEND and INDEX
are counted as instructions and retain their state across calls.
peek is a passive debugger read: it does not re-edit the editing registers.
storeWord writes a native 15-bit word (including machine instructions) through
the logical memory map. deposit is a lower-level, raw 16-bit CPU-bus write:
it can inject overflow into A/Q and performs overflow correction for 15-bit
destinations. Use storeWord for ordinary program/data loading. Writes to
fixed memory are ignored; only loadFixed changes rope contents. Loading
validates the entire array before changing memory; input words must be 15-bit
and contain no parity bits. RopeImage loads binary rope formats (see below);
no assembler is bundled. A short bank load preserves words outside the specified range.
registers returns a value snapshot. A/Q retain their raw 16 bits, L is a
native 15-bit word, Z is 12-bit, and EB/FB retain their hardware bit positions.
reset clears CPU state, channels, and erasable memory while preserving rope.
It starts execution at 04000 by default with interrupt permission disabled;
this is a deterministic software initialization, not a hardware restart sequence.
A CPU owns its storage and must be used on one executor/thread at a time.
It is intentionally not Sendable. Create separate CPU instances for
independent runs.
- One's-complement addition with end-around carry, both signed zeroes, 16-bit overflow retention and correction, and integer conversion helpers.
- 2,048 erasable words, 36 fixed banks of 1,024 words, EB/FB/BB aliases, fixed-fixed banks, and channel-7 superbank selection. Uninstalled fixed banks produce an error instead of indexing outside storage.
- Memory-mapped A/L/Q/Z and the zero register, bank-zero aliases, and CYR/SR/CYL/EDOP editing-register transformations.
- Basic instructions: TC, CCS, TCF, DAS, LXCH, INCR, ADS, CA, CS, INDEX, RESUME, DXCH, TS, XCH, AD, MASK. RELINT/INHINT track the interrupt-permission flag; EXTEND selects the extracode decoding of the following instruction.
- Extended instructions: READ, WRITE, RAND, WAND, ROR, WOR, RXOR, EDRUPT, DV, BZF, MSU, QXCH, AUG, DIM, DCA, DCS, INDEX, SU, BZMF, MP. MP and DV use independently derived control-pulse arithmetic, including signed zero, overflowed inputs, zero divisors, and internal register aliases.
- MCT accounting, including conditional branches, counter service, and RUPT. An internal integer clock schedules timers from executed cycles. There is no host-clock pacing or external time-advance API.
- 512 channels with L/Q aliases, scaler channels, TIME6 gating, and latched DSKY relay output. Host-side channel access and primary DSKY key input.
TC Q needs care: TC first latches the old Q contents, then replaces Q with the
return address. The next step() executes the latched instruction at address
2. A conventional RETURN therefore includes two fetched TC instructions,
two MCTs, and leaves Q equal to 3. This also permits instruction execution
from A/L and from erasable memory. A debugger deposit invalidates the fetch
latch.
All basic and extended opcode groups now dispatch. Arithmetic uses explicit bit widths and wrapping operations, so guest divide-by-zero and arithmetic overflow do not trap the host. Invalid switched-bank operand accesses can occur after PC advance; execution stops with that state retained.
Double-word instructions use encoded low-word addresses, low-word-first
execution, and the documented register overlap behavior. RESUME restores
ZRUPT and replays BRUPT without advancing the already-fetched PC twice. It
clears interruptInProgress, independently of the RELINT/INHINT mask.
EDRUPT saves Z and the instruction-register contents in ZRUPT/BRUPT and
forces checkout interrupt entry at address zero when no external priority
request is present. A pending request selects its priority vector even during
EDRUPT checkout; the external GSE monitor interface is not modeled.
The instruction implementation notes describe
nominal timing, edge cases, and validation boundaries.
Opt-in night-watchman, TC-trap, and RUPT-lock monitors can trigger automatic GOJAM and latch channel-77 fault bits. There is no parity circuitry, standby, or spacecraft peripheral model beyond the initial DSKY interface. Manual GOJAM restart entry is implemented (see below). Serial and additional rate-output counters remain unimplemented. Interrupt-save addresses are used by EDRUPT/RESUME and can also be accessed by software. Channels are mostly passive storage, with L/Q aliases, fixed-bank selection, read-only scaler channels, and TIME6 gating/shutoff. Other device-specific masks and side effects remain unimplemented. These omissions mean instruction-level tests passing does not establish flight-software compatibility or a cycle-accurate hardware model.
The next milestones are peripheral models, broader original self-check coverage, and expanding the spacecraft peripheral models beyond the quiescent P00 profile.
The core uses ordinary Swift switch dispatch. CPU storage is allocated once,
with masked/range-checked addressing before pointer access. A batched run
borrows CPU state once and performs no allocations or per-instruction callbacks.
This avoids array copy-on-write and closure context overhead in the execution
loop. Normal optimized Swift builds are used; -Ounchecked is not required.
The implementation and hand-written unit programs are original code, based on the hardware specification in these primary documents:
- Hugh Blair-Smith, AGC4 Memo #9: Block II Instructions, revised June 1967, especially memory/register diagrams and basic/extracode definitions.
- Bernard Savage and Alice Drake, E-2052: AGC4 Basic Training Manual, Volume I, January 1967, for number representation and instruction examples.
The 0BSD license covers the newly written package and benchmark code. The optional terminal package has third-party notices
On an Apple M1 MacBook Air with Apple Swift 6.4, five release-build trials of 100 million instructions each gave a median 130.347 million instructions/s (range 129.854–131.091 million/s). Each trial warmed up separately and ended
The 145 tests pass in release configuration and in a debug build with Address Sanitizer. They include exhaustive sign-extension/correction and integer round-trips, both zero representations, overflow, instruction sequencing, banking, editing registers, channel operations, bounds validation, double-word register overlaps, EDRUPT/RESUME sequencing, and MP/DV integer-oracle checks. The arithmetic tests also sweep every raw 16-bit A pattern against seven representative operands, covering 458,752 input pairs for each of MP and DV. Optimized assembly inspection confirms the normal execution loop contains no ARC retain/release or dynamic exclusivity calls. Error paths can allocate a Swift error object.
RopeImage validates a complete 73,728-byte Block II image before
CPU.loadRope replaces fixed memory. The default format is big-endian,
left-aligned 15-bit data, parity ignored, with file banks 2,3,0,1,4,...,35.
Byte order, bit alignment, bank order, and optional odd-parity validation are
explicit options; the loader does not guess the format. See the
image format specification.
swift run -c release agc-demo --rope image.bin --instructions 1000 --check-parity --trace
swift run -c release agc-demo --help--start, --fb, and --eb take octal values (FB 00–37, EB 0–7).
The instruction budget is decimal and required for image execution.
Use --little-endian, --right-aligned, or --physical-banks to select
other encodings. FB selects the ordinary bank register; this CLI does not
initialize the superbank channel. The library supports that channel.
nextInstruction() previews the effective fetch without execution;
stepTraced() and run(instructions:tracing:) produce value-only records
with raw/effective instruction words, INDEX, registers, banks, and MCTs.
They handle latched TC Q and RESUME fetches. The normal batch loop has no
trace callback or tracing-enabled branch. A throwing observer stops after
its instruction has committed.
Tests also execute a contiguous sequence of the original public-domain Aurora arithmetic self-checks: 606 instructions / 1,022 MCTs, reaching RUPTCHK without entering ERRORS. This exposed and corrected DCA L overflow handling. The fixture notes document provenance, reproduction, and coverage. The sparse fixture is not a complete flight image; full timer/peripheral conformance remains future work.
cpu.requestInterrupt(.time3)
try cpu.run(instructions: 100)Requests are latched and coalesce until accepted. The ten Block II sources are serviced in priority order (TIME6 first, hand-controller last), through vectors 4004–4050 octal. The core automatically delivers an eligible request before executing the next instruction. It preserves other pending requests, prevents nesting, and defers delivery for INHINT, A overflow/underflow, EXTEND/extracodes, and immediately before RELINT/INHINT/EXTEND.
RUPT takes three MCTs without retiring a guest instruction. It saves advanced Z in ZRUPT and the effective INDEX-modified instruction in BRUPT. RESUME replays that saved word; A/L/Q/banks are saved by guest software. Requests may also interrupt a latched TC Q instruction or a RESUME replay.
servicePendingInterrupt() explicitly performs only the entry sequence and
returns an InterruptDelivery, or nil if inhibited. step() includes entry
cycles in its return value. A traced instruction has an optional interrupt
record: memoryCycles measures the instruction itself, while
totalMemoryCycles includes entry. nextInstruction() remains a passive
preview of the program instruction that an interrupt could postpone.
All request APIs run on the CPU's single executor; they do not create host threads or wall-clock events. Tests cover all vectors, priority, coalescing, inhibition, replay, EDRUPT priority, and normal/traced equivalence. An original Aurora RELINT fragment also runs with a host-injected TIME3 request and a minimal RESUME handler. A separate test now runs the complete RUPTCHK with the original WAITLIST and interrupt infrastructure; see below.
The interrupt rules follow AGC4 Memo #9, printed p. 21.
CPU() now advances timers from executed MCTs, including RUPT entry and
stolen counter cycles. No external clock or time-advance API is involved.
TIME1–TIME5, TIME1 carry into TIME2, TIME6 channel-13 gating/countdown,
read-only scaler channels, and timer-triggered interrupts are implemented.
Device inputs can latch CDU/optics/PIPA/rotation counter updates; those
requests consume CPU cycles when serviced.
cpu.timing exposes integer clock pulses, scaler state, counter MCTs, and
TIME6 output counts. Traces include counterMemoryCycles separately;
totalMemoryCycles includes counters, interrupt entry, and the instruction.
The original Aurora TIME4 wait now finishes on CPU-generated timer events.
Use CPU(timersEnabled: false) for isolated instruction tests. The synthetic
demo/benchmark does this to preserve its original workload; --rope uses the
default running timers. Exact periods, phase choices, boundary granularity,
API conventions, validation, and remaining limitations are documented in
docs/timers-and-counters.md.
The expanded public-domain Aurora fixture now runs the complete RUPTCHK sequence with original WAITLIST, the T3 vector/handler, task callback, TASKOVER, register restoration, and RESUME. It passes in 645 instructions / 1,067 MCTs, including four counter cycles and one interrupt entry. Controlled setup executes the original empty-list initialization first; this is not a complete Aurora boot. No guest-code patches, host scheduler, external clock ticks, or injected interrupts are used in this test.
The test checks overflow/INHINT deferral and the exact postponed instruction, rejects unpopulated fixed fetches and data operands, and compares ordinary batch execution with traced state. WAITLIST is a delayed-task scheduler: other guest code can run while TIME3 counts toward the next task. The core executes polling and overflow loops normally, without idle fast-forwarding.
The fixture extractor also now handles two-word task addresses correctly. Its four offline Python regression tests cover those records and image integrity. Fixture notes document source provenance, initial conditions, reproduction, and coverage limits.
cpu.dsky is a value snapshot with program/verb/noun, three signed registers,
annunciators, and raw relay words. print(cpu.dsky.terminalDisplay) renders it;
--dsky prints the final panel for a rope run. cpu.pressDSKYKey(.verb)
latches a real key code and KEYRUPT1 request. The renderer has no host clock.
Display words are captured when written, so multiplexed channel-10 updates
survive subsequent writes. Snapshot allocation stays outside execution.
--alarm-demo 1201 exhausts the original Aurora Executive's five VAC areas;
--alarm-demo 1202 exhausts its seven core sets. The original code sets
FAILREG and the PROG lamp buffer. Diagnostic continuation runs original
PINBALL conversion/output code, which sends actual guest channel writes:
PROG VERB NOUN
R1 01202
R2
R3
LAMPS: PROG
This reproduces the resource-exhaustion alarms and guest-driven display.
Aurora's abort stop is resumed under diagnostic control; the experiment is
not a complete Apollo 11 radar-overload/restart replay. The optional
AGCExperiments target bundles the public-domain guest words separately
from the 0BSD emulator core.
Five additional original self-checks now pass: I/O aliases, 65,532 multiplies, 20,490 divides, signed-count/overflow sweeps, and erasable-memory/editing checks across all eight banks—over 3.5 million guest instructions. DSKY and alarm notes document the interface, experiment setup, provenance, reproducibility, and remaining work.
cpu.restart() now performs the two-MCT GOJAM sequence: preserve memory,
A/L/banks and elapsed time, save Z in Q, reset instruction/interrupt control,
clear specified output registers, and enter at 4000. DSKY relay rows survive;
the RESTART lamp lights and can be acknowledged by pressDSKYKey(.reset).
This differs from the cold debugger reset() operation.
agc-demo --restart-demo executes the original Aurora GO/GOPROG entry twice,
with GOJAM in between, and verifies its software restart counter advances
while an erasable marker survives. It stops before STARTSUB and is not a
complete flight-software boot. CPU(nightWatchmanEnabled: true) enables the
scaler-phase night watchman fed by guest accesses to erasable 0067.
CPU(hardwareAlarmsEnabled: true) additionally detects TC traps and RUPT lock.
All timing comes from CPU execution. Channel 77 latches faults until a channel
write clears it. Detection follows scaler strobes; instruction-boundary
execution still limits timing precision.
The scaler channels have also been corrected to their documented 3200 Hz rate; TIME6 remains 1600 Hz. Timer phases and existing conformance/alarm results are unchanged. Restart notes describe preservation rules, timing, sources, and remaining limits.
agc-demo --luminary-p00-demo runs the bundled public-domain Luminary 099 rope
with an explicit quiescent LM input profile and enters V37E00E through the
original keyboard/Executive/Pinball code. The terminal shows PROG 00, with
no self-check errors or hardware restarts. All time comes from CPU execution.
The IMU is off; cold-start phase-table alarm 1107 remains in guest history.
agc-demo --luminary-startup-demo retains the zeroed-input diagnostic baseline,
which reports input-related alarms and does not reach P00. Native 15-bit device
snapshots use CPU.setDiscreteInput(_:at:); output writes cannot change input
discretes. All 36 rope bank sums validate. Startup notes
record the input profile, keyboard stages, diagnostics, and remaining limits.