Skip to content
openorbitPublic

About

Permissively licensed Apollo Guidance Computer Emulator

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

OpenAGC

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-preflight

The 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.

Optional interactive DSKY

swift run --package-path terminal -c release agc-terminal

The 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 V05N09E

All Swift tests use Swift Testing (@Test, #expect, and #require).

Using the library

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.

Implemented

  • 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.

Remaining work and accuracy boundaries

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.

Implementation and provenance

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:

The 0BSD license covers the newly written package and benchmark code. The optional terminal package has third-party notices

Initial core measurement

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.

Rope loading and tracing

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.

Program interrupts

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-driven timers and counters

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.

Original WAITLIST and complete RUPTCHK

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.

DSKY and original Executive overload alarms

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.

GOJAM restart entry

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.

Luminary startup and P00 entry

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.

About

Permissively licensed Apollo Guidance Computer Emulator

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages