Technical Reference · Revision 1.0
SEQ/OS
A bare-metal step sequencer for the IBM 5160. It owns the machine from the boot sector up: no DOS, no operating system underneath, and a socketed 8087 the boot gate requires. Assembly holds the hardware; a resident C kernel holds the policy. At 120 BPM, MIDI clock leaves COM1 every 20.8 ms.
- Target
- IBM 5160 (PC/XT)
- CPU
- AMD D8088 @ 4.772727 MHz
- Memory
- 640 KiB conventional, required
- Display
- IBM MDA → 5151
- Timing floor
- 838.0965 ns
- Boot image
zout/seq720.img· 720 KiB- C kernel
KERNEL_C_ADDRtokernel_c_end- Network
- NE2000 @ 0x300, polled
- Song store
- 512 B pages, IDs not pointers
- Patterns
- 960 PPQN · 64 steps · 256-slot Gbar
- Arrangement
- tracks × placements · 16 cursors
- UI state
- semantic IDs · 768 B far session
- Persistence
- contiguous
SEQSTORE.S86extent - Build bounds
src/link/kernel.ld+zout/kernel.map
Chapter 0Using this manual
SEQ/OS is tested in source, in emulators, and on its IBM 5160. This manual marks which kind of evidence supports each claim.
Every operational claim carries one of three marks. They are not severity levels; they identify the source of the claim.
xt8088.py or another explicit model, not measured on the machine.The distinction matters most for timing. tools/xt8088.py
executes the real image and charges instruction, prefetch, refresh, and
measured memory-region costs. Those figures stay Modelled. Separate hardware probes have measured RAM,
MDA, EMS, network, MIDI loopback, and foreground activity on the 5160.
Those figures are Confirmed. Agreement between
the two improves confidence; it does not turn a model result into a hardware
measurement.
Colour carries one more rule:
Anything on this black ground is what the 5151 actually displays, or output captured from the running image. It is never decoration. Everything else in this manual is ink on paper.
Some chapters carry labs. Each lab advances one bounded state machine and reports the operation, changed state, applicable limit, and result. A lab is a teaching aid, not evidence. It reproduces the surrounding contract, so the reference text remains complete when the lab is closed.
Recurring terms — PIT, IRQ0, UART, serializer, and the rest — carry quiet glossary links: the first appearance in a section tints under the pointer, a click opens a two-line definition, and the popup links to the full entry in Appendix B. A return button in the corner retraces the jump, so looking a term up costs nothing but the click.
Chapter 1The hardware platform
An ex-laboratory IBM 5160 with an AMD D8088, an Intel C8087-3, an IBM 5151 display, five serial connectors, and a PicoMEM-backed memory and network path.
1.1The machine
The 5160 is the IBM PC/XT: an 8088 at 4.77 MHz on an 8-bit ISA bus, eight expansion slots, no built-in video. What makes this unit useful is the set of cards a previous owner installed.
| Item | Value | Status | Evidence |
|---|---|---|---|
| Model | IBM 5160, Made in USA | Confirmed | Rear ID label |
| Acquisition POST | 256 KB OK, then keyboard error 301 | Confirmed | First power-on photo |
| Current RAM | 256 KiB planar plus PicoMEM conventional extension to the required 640 KiB; 4 MiB EMS | Confirmed | POST, HWPROBE, and EMSBENCH |
| CPU | AMD D8088, date code 8422GMA | Confirmed | Component-side photo, marking legible |
| CPU clock | 4.772727 MHz | Inferred | Stock 5160 via 8284A; not measured |
| 8087 FPU | Intel C8087-3; required at boot and used for foreground math | Confirmed | Chip marking, hardware probe, and boot gate |
| Keyboard | XT-protocol board fitted; DIN port functional | Confirmed | Clean POST and live Cassette BASIC session, 2026-07-25 |
| Monitor | Green phosphor, IBM Personal Computer Display | Confirmed | Front badge, green POST |
| Monitor model | IBM 5151, P/N 5151001 | Confirmed | Monitor rear label |
| Phosphor | P39 long-persistence | Inferred | 5151 specification |
Acquisition evidence is not the current configuration
The preserved 301 photograph records the machine as
acquired. An XT-protocol keyboard was fitted later. Current images also
depend on the installed PicoMEM conventional-memory extension and on the
working 8087. The boot path checks the 640 KiB memory floor and the
coprocessor before it enters the sequencer.
1.2The clock chain
Every timing property of this project comes from one crystal. The 5160 has a single oscillator at 14.31818 MHz, chosen because it is four times the NTSC colour‑burst frequency. That mattered for CGA. Here it just means the divisions are awkward numbers.
The timer chip has three channels. On an XT all three are already in use, so SEQ/OS has to decide which ones to take.
| Ch | Wired to | Stock use | SEQ/OS use |
|---|---|---|---|
| 0 | IRQ0 | BIOS 18.2 Hz tick | Taken. One-shot event scheduler |
| 1 | DMA0 | DRAM refresh | Never touched. Stop it and RAM forgets |
| 2 | Speaker | Tone generation | Taken. Free-running 838 ns reference |
Channel 1 drives DRAM refresh. Reprogramming it corrupts memory, so it is
left alone. Channel 2 is gated by a bit in port 0x61: set the gate
bit and leave the speaker‑data bit clear, and it free-runs silently. That
gives a continuously counting reference clock with no audible output, which is
what lets SEQ/OS measure its own jitter without external hardware.
1.3The card cage
All eight positions are visible from the rear. Seven hold cards; slot 2 is empty. Read left to right:
An in-machine census found three responding 8250s: 3F8h with
IRQ4, 2F8h without an observed IRQ, and non-standard
0278h without an observed IRQ. The supported image declares
only 3F8h as a musical UART. It does not turn a discovered port
into an output automatically.
The installed PicoMEM 2 supplies the conventional-memory extension, 4 MiB of EMS, microSD boot storage, direct upper-memory PSRAM, and the NE2000-over-Wi-Fi route used for netboot, monitoring, and network MIDI. The original MFM controller and ST-412 remain installed; PicoMEM appears as a second fixed disk under its BIOS. Confirmed
1.4The address space
The 8088 has 20 address lines, so it can see 1 MB. It reaches that with
segment × 16 + offset addressing, which means a
physical address like 0xB0000 is written B000:0000.
SEQ/OS claims the bottom 640 KB of that megabyte — the full
conventional-RAM range through 0x9FFFF — and stage 2
refuses to boot a machine reporting less. On the bench 5160 the motherboard
supplies the first 256 KB; the PicoMEM card (§1.3) serves the
remainder.
0x9FFFF. A restore-safe
hardware probe also qualified 96 KiB of PicoMEM PSRAM in adapter space.
Fixed-far banks 12 and 13 occupy the complete 32 KiB windows at
C0000–C7FFF and D8000–DFFFF.
ConfirmedWhy the video buffer matters so much
The screen is 4000 bytes at B000:0000. A word store writes
one character and its attribute. That direct interface is simple, but it is
not free: physical MDA reads measured 374–386 KiB/s and writes
421 KiB/s. The renderer therefore composes in ordinary memory, compares
against a PSRAM shadow, and writes bounded changed fields or rows in
foreground. Deadline code never touches MDA memory.
1.5The I/O space
Separate from memory, the 8088 has a 64 KiB I/O space reached by
IN and OUT. Assembly owns the resident hardware
boundary. The table includes active driver ranges and physically discovered
ranges that remain disabled in the supported image.
| Port | Chip | Purpose in SEQ/OS |
|---|---|---|
| 0x20 | 8259 PIC | Command — end-of-interrupt written at the close of every IRQ0 |
| 0x21 | 8259 PIC | Mask — set to 0xFC, leaving only IRQ0 and IRQ1 live |
| 0x40 | 8253 PIT | Channel 0 count — the one-shot scheduler reload |
| 0x42 | 8253 PIT | Channel 2 count — free-running reference |
| 0x43 | 8253 PIT | Mode/command register, and the channel-2 latch |
| 0x60 | Keyboard | Scan-code data, read by the IRQ1 handler |
| 0x61 | Port B | Bit 0 gates PIT channel 2 on (bit 1 left clear so it stays silent); bit 7 pulsed high then low to acknowledge each scan code |
| 0x268–0x26B | PicoMEM EMS | Four 16 KiB EMS page registers; foreground only |
| 0x278 | 8250 UART | Physically detected third UART; not declared as a musical port |
| 0x2F8 | 8250 UART | COM2 — the debug monitor link and crash dump, 9600 8N1 |
| 0x2A0–0x2A3 | PicoMEM control | Qualified control range; the modeled command-88 storage adapter is not a resident physical backend |
| 0x300–0x31F | NE2000 | Polled Ethernet, netboot, monitor, inference, and network-MIDI transport |
| 0x3BC | MDA parallel | Present on the adapter; physical gate/trigger output remains disabled pending protected fixture tests |
| 0x3F8 | 8250 UART | COM1 — the one declared physical musical output |
1.6The serial divisor contract
MIDI is just serial data at 31250 baud. An 8250 UART divides its input clock by 16, then by a programmable divisor. The stock IBM serial card runs on a 1.8432 MHz crystal, and that number does not cooperate:
1843200 / 16 = 115200 baud, the maximum
115200 / 31250 = 3.6864 ← not an integer. no divisor works.
The divisor register holds whole numbers only, so a UART receiving the stock 1.8432 MHz clock cannot produce 31250 baud exactly. A 2.000 MHz UART clock makes the required divisor an integer:
2000000 / 16 = 125000 baud, the new maximum
125000 / 31250 = 4 ← exact. divisor latch = 4.
The driver records that hardware contract as
MIDI_DIV equ 4. COM2 remains outside the musical table and uses
DBG_DIV equ 12, which gives 9600 baud from the ordinary UART
clock. The source and emulator prove scheduling and serialization. The
current hardware census proves the bases and one IRQ; the individual serial
cards' clock and routing qualification is still open. The physically
qualified musical path today is timestamped network MIDI through PicoMEM and
the MOTU M2.
Why the table fails closed
A MIDI byte occupies a 31.25-kbaud wire for 320 µs. Adding an unqualified UART changes the wire-rate and interrupt budget, not just an address constant. The model permits three musical ports, but a physical destination outside the one-entry table is rejected. No runtime probe silently expands it.
1.7The display is a design constraint
The IBM 5151 is an 80 by 25 character display. The installed unit is confirmed as IBM part 5151001. Its long-persistence phosphor is consistent with P39. SEQ/OS treats characters and attributes as the physical display unit; it does not assume a bitmap or a Hercules graphics extension Confirmed.
| Rows | Owner | Contract |
|---|---|---|
| 1 | Shell | View title and transport summary |
| 2–22 | Active view | Semantic projection supplied through the view registry |
| 23 | Shell | Status, warning, or transaction result |
| 24 | Overlay | Prompt and editor composition when active |
| 25 | Shell | Contextual command strip |
The song model contains no row, column, glyph, or colour. A versioned view registry maps stable semantic IDs to projections. The renderer composes one bounded row and the assembly leaf emits only its requested span. A hardware cursor supplies immediate focus feedback while larger damage is retired in later foreground turns. Chapter 12 describes that boundary.
Reading MDA memory to avoid a write is slower than performing the write.
The physical 5160 copied planar RAM to MDA at 276 KiB/s; an equal-row
compare scanned at 244 KiB/s and an all-changed compare fell to 67 KiB/s.
SEQ/OS therefore compares against a 4,000-byte ordinary-RAM shadow at
92480h, with a 250-byte validity bitmap. The shadow removes
unnecessary MDA traffic without paying the MDA read penalty
Confirmed.
Shift+F12 enters the explicit 8087 tunnel saver. It renders eight rings and 128 projected points, uses the tube's persistence for its trail, and reacts to the existing passage diagnostics. Bare F12 still opens the view switcher. The next key dismisses the saver without also executing that key's ordinary command. Physical acceptance on the 5160 observed 16 passage dispatches, peak energy 253, and zero transport overruns Confirmed.
Chapter 2The boot sequence
There is no operating system on this machine. The BIOS loads 512 bytes and jumps to them. Everything after that is code in this repository.
2.1Power to first instruction
The path from the power switch to SEQ/OS goes through the BIOS and then leaves it behind almost immediately:
INT 10h, no
BIOS service is used again. Confirmed2.2Stage 1: the 512-byte problem
The BIOS reads exactly one sector — 512 bytes — checks that its last two
bytes are 0xAA55, and jumps to it. That is the entire contract.
512 bytes is not enough for a sequencer. The boot sector establishes a safe
low-memory state, records the boot unit, checks the 640 KiB conventional-RAM
contract, discovers disk geometry, and fetches the 64-sector stage-2
loader.
stage1:
cli ; no interrupts while the segments are inconsistent
cld ; string ops count upward — assume nothing about BIOS state
xor ax,ax
mov ds,ax
mov es,ax
mov ss,ax ; every segment to 0: flat 64 KB view of low memory
mov sp,0x7C00 ; stack grows DOWN from the boot code, away from it
mov [boot_drive],dl ; preserve the BIOS boot unit
int 0x12 ; capture POST's conventional-memory report
Four details in this entry matter, because each one encodes an assumption that would be a bug if it were wrong.
Zeroing every segment register
The BIOS does not promise the state of DS, ES or
SS. Setting all of them to 0 means that from here on, a 16-bit
offset is a physical address in the first 64 KB — no segment
arithmetic anywhere in the program. The far jump at the end of stage 1
(jmp 0x0000:stage2) exists for the same reason: it forces
CS to 0 as well, so even the code segment is normalised.
Putting the stack immediately below the code
SP = 0x7C00 places the stack top exactly at the first byte of
the boot sector. Since the stack grows downward, pushes move away from
the code into free memory below. Had the stack been placed above, it would have
grown down straight through the program that is executing.
Saving DL
The BIOS passes the drive it booted from in DL. Nothing else is
guaranteed. Stage 1 needs it for every INT 13h call, and the value
is stored before any code can clobber the register.
Checking the memory supplier, not its brand
The supported contract is 640 KiB of conventional RAM. If
INT 12h reports less, stage 1 tests one word at each 16 KiB block
boundary from 256 through 640 KiB with restore-safe
AA55h/55AAh cycles. That accommodates a stale BIOS
report without accepting an untested extension. Stage 2 prints the reported
size and first failing segment and permits Ctrl+Alt+Delete if the gate fails.
2.3Walking the disk geometry
BIOS disk sectors are addressed by cylinder, head, sector — where the arm is, which side of the disk, and which sector around the track. The standard 720 KB boot image has 9 sectors per track, 2 heads, and 80 cylinders; PicoMEM HDD images use 63 sectors per track and 16 heads. Sector numbers start at 1, not 0.
Stage 1 first asks INT 13h AH=08h for the boot unit's actual
geometry, then converts each absolute image LBA to CHS. It reads
one sector per INT 13h call. The
BIOS can read several at once, but a multi-sector read that runs off the end of
a track fails on some controllers. Reading singly is slower and always correct
— a good trade when the code runs once at boot.
mov ah,0x08 ; BIOS geometry for the unit in DL
int 0x13
and cl,0x3F ; sectors per track
mov [disk_spt],cl
inc dh ; maximum head -> head count
mov [disk_heads],dh
mov ax,[disk_lba]
xor bx,bx
mov bl,[disk_spt]
div bx ; quotient = track, remainder = sector
xor dx,dx
xor bx,bx
mov bl,[disk_heads]
div bx ; quotient = cylinder, remainder = head
The 9-by-2 floppy result is the walk below. On a PicoMEM HDD the same LBAs map through its wider 63-by-16 geometry without changing the payload.
STAGE2_SECTORS), starting at cylinder 0, head 0, sector 2 —
sector 1 being the boot sector itself. The read spans eight track segments
and crosses into a fourth cylinder.
The figure shows the first 30 reads; the same CHS walk continues for 34
more. The raw image size is emitted by each build rather than copied here.Why the image is padded to 737,280 bytes
The public boot artifact is a standard 720 KiB disk image. The fixed size gives firmware and emulators a complete 80×2×9 geometry while the generated raw payload can contain the loader, split resident extents, thirteen fixed-far banks, and the boot song:
FLOPPY_BYTES = 737280 ; 80 cylinders × 2 heads × 9 sectors × 512 bytes
Stage 1 reads exactly 64 sectors. Stage 2 then follows
image-layout.inc to fetch every other extent. The build rejects
a loader that exceeds its 64-sector envelope and a complete payload that
exceeds the padded image.
2.4Stage 2: taking the machine
Stage 2 re-establishes the flat segment state, validates an optional
netboot handoff, and selects one extent reader: BIOS INT 13h for
disk boot or the resident S86N service for netboot. It loads and checksums the
lower text, relocated assembly, thirteen fixed-far banks, and boot song. Only
then does it set MDA mode 7. That INT 10h is the last BIOS
service. The loader jumps to the relocated runtime at 0000:3A00,
where the system takes the hardware:
call install_unexpected_vectors ; 14 stubs: divide error, NMI,
; breakpoint, overflow, IRQ2-7...
; Own both timing and keyboard interrupts. The foreground C kernel
; consumes the bounded scan-code queue filled by IRQ1.
mov word [0x08*4],irq0 ; IVT entry 8 ← the scheduler
mov word [0x08*4+2],0
mov word [0x09*4],irq1 ; IVT entry 9 ← raw keyboard
mov word [0x09*4+2],0
mov al,0xFC ; 1111_1100 — only IRQ0 and IRQ1 live
out PIC_MASK,al
Writing four bytes to address 0x0020 redirects every timer
interrupt from the BIOS's 18.2 Hz tick routine to irq0 in this
program. The BIOS clock stops advancing, which is fine — nothing else is
running that reads it. Four bytes at 0x0024 do the same for the
keyboard.
The PIC mask is the other half. 0xFC is 11111100
in binary — a 1 masks an interrupt off, a 0 lets it through — so only IRQ0 and
IRQ1 survive. Everything else the machine could interrupt about is silenced.
Reading the keyboard without the BIOS
The system services IRQ1 itself rather than leaving the BIOS handler
installed — INT 16h is dead here, along with the rest of
the firmware. The split between assembly and C follows the same rule as
everything else:
assembly does the timed hardware transaction, C does the thinking.
in al,0x60 ; take the scan code
mov dl,al
in al,PORT_B ; acknowledge: pulse bit 7 high, then low
mov ah,al
or al,0x80
out PORT_B,al
mov al,ah
out PORT_B,al
; ...then push one byte into a 16-entry ring and EOI. That is all.
The handler makes no decisions. It captures one byte, acknowledges the
hardware, and drops it in a 16-entry ring buffer. Every question that requires
judgement — is this a make or a break code, is 0xE0 a prefix or a
key, is Shift held, what does this scan code mean — is answered in
src/platform/keyboard.c, from the main loop, where taking a few microseconds
costs nothing.
That decoder is a state machine, not a lookup: it tracks the
0xE0/0xE1 prefixes, the six-byte Pause sequence, and
nine modifier flags, and emits a keyboard_event carrying key,
modifiers, ASCII, and press/release. The same events feed the workspace
registry, forms, overlays, specialist editors, monitor, and saver dismissal.
The ring separates interrupt and foreground ownership
IRQ1 can fire while foreground code is rendering, parsing a packet, or advancing a storage transaction. The producer writes a slot before it advances the head; the foreground consumer owns the tail. A full ring drops the incoming byte and increments the overflow diagnostic rather than overwriting unread state.
2.5Memory after boot
0x3A00; the
loader-lifetime range is cleared at handoff and owned by generated near
state. The exact C address and linked end are read from the assembly
constant and zout/kernel.map, not duplicated in this
manual.2.6The running data flow
Once STI executes, two things run at once: an interrupt handler
with a hard deadline, and a main loop with none. Keeping them separate is the
core of the design.
This split is the answer to a specific hazard. If drawing happened inside the interrupt handler, every screen update would delay the next MIDI byte. If MIDI happened in the main loop, a slow redraw would delay the beat. Instead the handler does the minimum that must happen on time, sets a flag, and leaves.
A boolean cannot report how many boundaries elapsed while foreground was delayed. The boundary mirror is therefore counted, and the token queues retain their own high-water, underflow, and overrun diagnostics. A missed admission becomes a visible fault; it is not converted into a silent timing change.
Two kinds of timing evidence
The arithmetic and assembled instruction paths run under
xt8088.py, which charges the four-byte prefetch queue, refresh,
and measured regional memory tiers. Physical scopes separately measure RAM,
MDA, EMS, foreground service, Wi-Fi delivery, host dispatch, and MIDI
loopback. The model remains an instruction-granularity estimate. Hardware
captures remain authoritative when the two disagree. Chapter 15 shows the
optimization cases where that distinction changed the decision.
2.7When it crashes
On a machine with no operating system, a wild jump does not produce an error message. It produces a hang, or a screen of garbage, or nothing at all — and on a floppy-based edit cycle, “nothing at all” is close to undebuggable. So the system claims the interrupt vectors it does not expect, and turns a crash into a report.
install_unexpected_vectors points fourteen vectors — divide
error, NMI, breakpoint, overflow, and IRQ2 through IRQ7 — at stubs that each
record their own vector number and fall into one common recorder. That recorder
captures the interrupted machine state off the stack and does three things with
it:
tests/test_monitor.py
includes test_unexpected_vector_crash_record, which triggers a
vector and checks the record.The three delivery routes cover separate failure modes. A display-path
failure may leave COM2 usable. A disconnected serial cable leaves the screen
as the available route. If both routes fail, the record remains in low memory,
where a warm restart can recover it with peek.
Note the ordering: render, transmit, then halt. The HLT
is deliberate and final — no attempt to continue, because continuing after an
unexpected interrupt on a machine with no memory protection means corrupting
state that the crash report was supposed to preserve.
Chapter 3Toolchain and workflow
Nothing is built on the target machine. Source stays in the
working tree; the preferred wrapper mirrors it to llmonster,
runs the native IA-16 and emulator gates there, and returns target
artifacts. The same image then runs in models, DOSBox-X, or the 5160.
3.1Two tracks, one image
NASM owns the boot and resident assembly. IA-16 GCC owns the foreground policy. Generated manifests connect them: the near linker map, far-bank registry, resident-state offsets, and image-layout table are derived from the same linked artifacts that enter the disk image. The final NASM pass packs a 64-sector loader plus addressable extents rather than one monolithic assembly-and-C blob.
image-layout.inc gives stage 2 the exact LBA,
destination, byte count, and checksum for each extent. The clean raw payload
is 353,792 bytes; seq720.img pads it to 737,280 bytes.
ConfirmedTwo NASM flags carry most of the weight on the assembly side.
-f bin: no object format at all
NASM's bin output writes exactly the assembled bytes — no
ELF header, no relocations, no linker step. That is essential, because the BIOS
loads sector 1 into memory and jumps to the first byte. Any header would be
executed as code.
CPU 8086: making the assembler the guard rail
The 8088 and 8086 share an instruction set (the 8088 differs only in having
an 8-bit external bus). Declaring CPU 8086 means any instruction
introduced later — a 186's PUSHA, a 386's 32-bit register —
fails to assemble rather than producing a binary that crashes
on the real machine. The risk it catches is habit: reaching for an instruction
that exists on every processor since 1982. There is no debugger on the target,
so catching it at build time is worth the constraint.
3.2The C path
Assembly owns the machine boundary — boot, interrupt entry and exit, PIT/PIC/UART registers, the deadline scheduler. Ordinary logic (pattern operations, UI state, file formats, protocol handling) does not need to be written by hand, and 16-bit C is a perfectly good language for it.
Host GCC and Clang are not valid IA-16 substitutes
Neither Clang nor host GCC can produce 8088 code, and GCC's
-m16 flag is not a substitute: it emits 386-era
instructions wrapped in a 16-bit mode prefix and uses the wrong data model.
The result assembles and links and then fails on an 8088. The only real
option is ia16-elf-gcc, a GCC fork with a genuine 16-bit x86
backend.
tools/ia16.sh is the compiler entry point. On the native
amd64 build host, SEQ86_IA16_NATIVE=1 selects the installed
tools. Elsewhere it uses the pinned container. The wrapper limits concurrent
compiler processes, kills a stalled tool after 30 seconds, removes failed
outputs, and rejects non-ELF object files before a later link can consume
them.
The linker script enforces the resident contract
C code cannot simply be told “live at KERNEL_C_ADDR.” That
is the linker's job. The symbolic form of the contract is shown below; the
numeric values live only in src/link/kernel.ld and
src/asm/seq86.asm.
. = RESIDENT_START; ; same boundary as KERNEL_C_ADDR
.entry : { KEEP(*(.entry)) } ; the ABI header, first, always
.text : { *(.text .text.*) }
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) }
.bss : { *(.bss .bss.*) *(COMMON) }
kernel_c_end = .;
ASSERT(SIZEOF(.entry) == 16, "kernel entry header ABI changed")
ASSERT(SIZEOF(.bss) == 0, "kernel C must not rely on a zeroed BSS")
ASSERT(kernel_c_end <= RESIDENT_CEILING,
"kernel C consumed its reserve")
Each assertion converts a resident ABI or capacity error into a build failure.
- Header exactly 16 bytes. The assembly reaches into the C
image at fixed offsets —
KERNEL_C_INIT equ KERNEL_C_ADDR+6and friends. Add a field to the header and every one of those offsets silently points at the wrong function. The assert catches it at link time. - No BSS. Ordinary C assumes zero-initialised statics,
because a normal runtime clears BSS before
main. Here nothing does — the image is loaded off a floppy and jumped to. Rather than write a clearing loop and hope everyone remembers it, the build refuses to link if a single uninitialised static exists. - Resident ceiling. The kernel must remain within segment
zero while retaining the reserve required by later plans. The exact linked
end is reported by
zout/kernel.map; the assertion makes a budget overrun a build failure.
Two build gates on the compiled output
@test -z "$$($(IA16) ia16-elf-nm -u $<)"
@! grep -Eq '[[:space:]](div|idiv|mul|imul)[[:space:]]' zout/kernel.dis
The first rejects any undefined symbol. Freestanding code has no libc, no
heap and no startup runtime, but the compiler will still happily emit a call to
memcpy for a struct assignment or a division helper for a
/ on a long. On bare metal those become jumps into
nothing, so an empty undefined list is checked rather than assumed.
The second gate turns Chapter 2's timing rule into a mechanical check:
no
MUL or DIV while the transport runs — is enforced
mechanically, by grepping the disassembly. A single
DIV r/m16 costs up to 38 µs of jitter, more than the
entire interrupt handler. Rather than trusting everyone to remember that, the
build greps for it and fails. That is why sequencer_render_tempo converts BPM
to digits by repeated subtraction, and why step_column computes
step × 5 as step + (step << 2):
a written rule became a build failure, and the code changed shape to match.
Both gates passed on the clean bfb60f2 image
Confirmed: no unresolved runtime symbols and no
resident C DIV, IDIV, MUL, or
IMUL.
The ELF container name does not identify the instruction set
ia16-elf-objdump reports the container format as
elf32-i386. That is normal for this backend and does
not mean the code is 32-bit. Disassembly must use
with -Mintel,i8086, or objdump decodes the operands at the wrong
width and the listing is confident nonsense.
Lab 3.1 Apply the text-placement rules Advance the placement ledger and report the near owner, far-bank use, timing rule, and final capacity.
The 8088
forms a 20-bit physical address from two 16-bit registers:
physical = segment × 16 + offset. The kernel
pins its data segment at zero, so an ordinary C pointer is just a 16-bit
offset — and everything such a pointer can reach lies in physical
00000–0FFFF. That first 64 KB is the
resident image: interrupt vectors, state, stack, the assembly
hardware layer, and the C kernel, all competing for one address space
that must retain measured reserve.
Executing from the far
bank changes only CS; DS stays zero, so far
code still reads and writes near state through ordinary pointers. That
permits code to leave the 64 KB segment while ordinary data pointers
remain near. The instrument applies the manifest rules to representative
placements and reports the resulting capacity. The bars are schematic.
3.3The assembly–C boundary
Assembly and C are compiled separately and share no linker symbol table. Assembly enters C through a fixed 16-byte header. C requests assembly-owned operations through one versioned 56-byte table. Generated state offsets and compile-time assertions close the data-layout boundary.
S86K,
header version 5, and the table advertises assembly/C ABI 26.
ConfirmedHow the layout hazard is closed
The red box above is a real risk: irq0 increments
step_pending by writing to a hard-coded assembly label, while C
reads state->step_pending by struct offset. Insert one field in
the C struct and they refer to different bytes — with no compiler error, no
linker error, and a symptom that looks like a timing bug.
The fix is a compile-time assertion built out of an illegal array size:
/* IRQ0 and the C kernel share this prefix; fail the build if it drifts. */
typedef char kernel_state_step_pending_at_19[
__builtin_offsetof(kernel_state, step_pending) == 19 ? 1 : -1];
If the offset is right the type is char[1]. If it is wrong the
type becomes char[-1] and compilation stops. The current header
pins the complete shared prefix and every later assembly-visible owner,
including queues, output, arrangement, automation, monitor, FPU, and network
profile state. gen_resident_state.py emits the matching assembly
labels from the ownership manifest.
The linker script, disassembly checks, generated ownership map, and negative-array assertions convert boundary rules into build inputs. A layout, instruction, or ownership mismatch therefore fails before an image is produced.
Foreground C also has a configurable set of code segments. Stage 2 loads
and checksums thirteen S86F banks; generated near stubs
expand a caller's return to IP:CS and enter medium-model C
while DS and every ordinary data pointer remain in segment zero.
Monitor writes and resident copy-out/fill helpers reject every normalized
range overlapping a configured bank. All thirteen banks now contain linked
code; bank 13 owns the 8087 saver. The shared surface also
carries the structural deadline queue, the variable-pattern summary and
bounded compiler state, the structural inference cursor, and the live
structural editor — the machinery of Chapter 6
(assembly/C ABI 26).
Far text is the default
The banks are not an overflow annex; they are where foreground code belongs. The reasoning is about friction, not cost. An empty far call/return costs 29 CPU clocks over a near call Modelled — cheap against any real body — but near bytes are the scarcest resource in the machine: one 64 KB segment holds state, stack, resident assembly, the C kernel, and the headroom every unfinished subsystem will need. A build in which near placement is effortless and far placement takes ceremony fills the near image first, whatever the documentation says. So the ceremony points the other way.
Foreground-only C — editors, validators, query and projection
bodies, service bodies, protocol parsing — compiles into the fixed
far text unless the placement manifest,
config/far-text.json, records a justification for keeping it
near: an IRQ-path caller, a measured leaf that fails its timing gate across
a far boundary, or membership in the assembly entry surface. An unjustified
near placement fails the build the same way an unknown state owner already
does. Silence is not a disposition.
Nothing at this boundary is maintained by hand.
From the manifest, tools/gen_far_text.py
emits each S86F header, the bank linker scripts, the stage-2
ABI constants, and the segment-zero call stubs — a far entry offset
exists in exactly one place. Each migrated function stays in its home
translation unit, which is compiled twice: the near pass excludes far bodies
behind a preprocessor guard, and the medium-model far pass supplies them to
the bank link. If manifest and source disagree, the near link fails with a
duplicate or undefined symbol rather than shipping two copies of the
function. The stub a near caller reaches is four instructions:
song_pattern_editor_accept:
pop ax ; the caller's near return address
push cs ; rebuild it as a far IP:CS return in place
push ax
jmp 0x2800:0x127A ; enter the bank; RETF comes home directly
Code inside the bank obeys three call rules, all machine-checked. Near
calls out of a bank go only through fixed retf gates
pinned in the relocated assembly runtime — validate, copy-in,
copy-out —
reached as direct far calls, because an 8088 indirect far call
through memory blocks interrupt entry for 53 clocks plus the effective
address, enough on its own to fail the 20-count jitter gate; the bank build
rejects the instruction form outright. Small shared scalar leaves are
cloned into the bank rather than called, because duplicating a byte-reader
is cheaper than a gate. And the bank holds code only: string tables and
other rodata fail the bank link, so near wrappers own such
tables and pass them in as ordinary near data.
| Quantity | Value | Mark |
|---|---|---|
| Fixed-far artifact occupancy | 304,341 / 425,984 bytes | Confirmed |
| Near text free now | 13,445 bytes: 10,100 upper + 3,345 lower | Confirmed |
| Forecast margin | 28,351 fixed-far; 1,693 near | Confirmed |
| Empty far call/return overhead vs near | 29 CPU clocks | Modelled |
Banks 1–9 use the planar timing tier. Banks 10–13 use the
PicoMEM PSRAM tier; banks 12 and 13 sit in the qualified upper windows at
0xC0000 and 0xD8000. The cycle model and patched
DOSBox-X charge instruction fetch according to those physical regions.
The eligibility checker supplies the placement proof. It traces every
resident function from interrupt, deadline, crash, and network timing roots;
an unresolved indirect call is conservatively TIMED. The near allowance
stands at zero, with zero unwaived FAR-ELIGIBLE near bytes. Explicit waivers
cover fixed dispatch stubs and gates, measured MDA primitives, the fixed C
entry table, and named resident-DS helpers. Each waiver carries its reason in
near-eligibility.json.
The forecast has one fixed-far capacity number: the thirteen banks that
the boot image loads. Together they hold 425,984 bytes. The clean
bfb60f2 linked image uses 304,341 bytes; planned platform work
and contingency need
93,292, leaving 28,351 bytes
Confirmed. Conventional
far_reserve and isolated upper-memory fragments are not
configured banks and are excluded. Near text is checked separately and has
1,693 bytes of forecast margin.
Lab 3.2 Cross a fixed-far boundary Advance one editor call through its generated stub, bank body, direct near gate, and two-word return.
The 8088 executes
from CS:IP and computes
physical = CS × 16 + IP. A near call
stays inside one segment and pushes one word — the return offset. A
far call crosses segments and pushes two — offset and
segment — and only RETF pops both. The generated stub's
entire job is to convert the first shape into the second, in place, in
four instructions.
The instrument uses
song_pattern_editor_accept() called from near foreground
code. In the clean bfb60f2 image, the generated stub target
is 2800:127A and the generic copy-in gate is
0000:5728. Caller-side addresses are illustrative. The stack
display reports the return shape at each operation.
3.4Live modules
A live module is a small binary loaded into the fixed
2000:0100 arena. The raw monitor path supports developer probes
and recovery. The managed path authenticates one module identity, registers
bounded providers, and controls activation, quiescence, and replacement.
These use a second linker script, src/link/module.ld, with a
different contract:
| Resident kernel | Live module | |
|---|---|---|
| Script | kernel.ld | module.ld |
| Load address | near image + S86F at 2800:0000 | 2000:0100 |
| Header magic | S86K | S86M; S86C retained as a loader test |
| Entered by | Assembly, through the header | Raw monitor RUN or managed lifecycle |
| Returns | Never unloaded | RETF |
| Result | Versioned call result | Module-defined bounded output; probes conventionally use SEG:0120 |
| BSS allowed | No | No |
| Capacity | Near text plus thirteen protected S86F banks | 32 KiB arena minus the 0100h load offset |
Probe modules use a fixed result block at SEG:0120, so the
host can load, run, and read a known address. Managed modules use the
20-byte S86M header and current ABI 2.10. The loader checks image and workspace
bounds, entry offset, capabilities, and XOR16. The manager additionally
computes a sliced BLAKE2s-128 content hash and requires explicit trusted local
confirmation before initialization.
The manager retains one content identity and at most eight provider descriptors in no more than 256 resident bytes. Provider calls are single-flight and bounded by declared input, output, and work limits. Replacement waits for a safe queue horizon, provider yield, affected-output closure, unload, validation, and a stopped global restart. A provider ID or label alone cannot bind to different code because resolution also compares the content hash.
A separate S86O ABI owns 30000–37FFF only while
transport is stopped. Its checksum, entry, and workspace are validated
before a foreground far call. Its header is erased on return so playback can
reclaim the bank.
The same rules that apply to the kernel apply here — no BSS, no undefined
symbols — plus the runtime contract from the monitor: enter with
CS = DS = SEG, interrupts enabled, all general
registers volatile; do not replace IRQ0, do not touch PIT channel 1, do not
leave interrupts off.
3.5The development loop
The running image carries one monitor parser behind serial, emulator TCP,
and NE2000 UDP transports. tools/seqctl can connect directly or
use the persistent control-plane service on llmonster. Physical
mutation and capture commands require the calling worktree to hold the
machine lease.
load transfers a binary in bounded chunks and can invoke it as
a far subroutine. module-reload invalidates the old header before
upload, then validates and runs the replacement. module-manage
adds content identity, explicit confirmation, provider registration, and
activation. All paths retain the same low-level call contract:
- enter with
CS = DS = SEG, interrupts enabled, on an ordinary OS stack; ESand all general registers are volatile;- return with
RETF; - must not replace IRQ0, must not touch PIT channel 1, must not leave interrupts off.
3.6The monitor protocol
Frames are a sync byte, a header, a payload, and an XOR check. There is no framing library on either end — the parser has to fit in a few hundred bytes of 8088 assembly.
docs/development.md
and the passing tests in tests/test_monitor.py, which execute
these packets against the assembled image.tools/remote-make.sh -j x86 # build -> zout/seq720.img
tools/remote-make.sh -j test-monitor # execute monitor tests against the image
make run-monitor # boot it; COM2 becomes TCP :8687
# emulator monitor:
tools/seqctl --tcp 127.0.0.1:8687 ping
tools/seqctl --tcp 127.0.0.1:8687 state
# physical workflow; lease is exclusive across worktrees:
tools/seqctl owner
tools/seqctl claim manual-update
tools/seqctl netboot deploy
tools/seqctl --udp 192.168.68.86:8686 state
tools/seqctl release
3.7The verification gates
Verification has four layers: build refusal, host execution, assembled image execution, and preserved physical evidence. Each layer answers a different question.
Build refusal
These checks run as dependencies of the image. A failure prevents the artifact from being published.
| Gate | Catches |
|---|---|
CPU 8086 (NASM) | Any instruction the 8088 does not have |
nm -u must be empty | An implicit runtime helper with no freestanding implementation |
| Disassembly scan for multiply/divide | An uninterruptible arithmetic instruction in resident or fixed-far C |
ASSERT(SIZEOF(.entry) == 16) | ABI header growth silently shifting every fixed offset |
ASSERT(SIZEOF(.bss) == 0) | A static that assumes zero-init nobody performs |
ASSERT(kernel_c_end <= resident ceiling) | The C kernel consuming its required reserve |
Generated state + offsetof assertions | C layout drifting from assembly-visible absolute owners |
| Far-bank relocation, rodata, and indirect-call scans | A bank that is not immutable code or that crosses an unbounded gate |
| x87 ownership scan | An ESC instruction outside an approved foreground owner |
%error on sector overflow | Stage 2 exceeding the 64 sectors stage 1 actually reads |
| Memory, stack, and forecast scopes | An owner overlap, capacity overrun, or insufficient stack margin |
Execution checks
Host suites exercise model, storage, transaction, query, module, rendering,
and protocol code with deterministic fixtures and corruption sweeps.
partest.py distributes emulator cases over available cores.
DOSBox-X runs the shipping image at fixed 4.77 MHz for boot, UI, x87,
monitor, network, and screenshot gates. xt8088.py executes the
same machine code with explicit instruction, prefetch, refresh, and memory
charges.
| Gate | Method | What it can prove | What it cannot |
|---|---|---|---|
model_scheduler.py |
Independent arithmetic plus deliberate mutants | Wrap, fractional carry, ideal-deadline reload, and unsigned comparisons | Instruction or bus timing |
xt8088.py scopes |
Execute assembled images with the calibrated 8088 model | Instruction-path bounds, queue pressure, drift, and modeled jitter | Electrical behavior and exact BIU/EU overlap |
| Patched DOSBox-X | Real-time shipping-image scenarios with regional memory charges | Integrated boot, UI, monitor, network, x87, and visible latency | Cycle-exact interrupt entry |
| 5160 catalog and captures | Leased netboot sessions, scenario catalog, modules, profilers, loopbacks, and exact framebuffer captures | Actual RAM/MDA/EMS cost, foreground load, Wi-Fi delivery, and device output | Unexercised hardware configurations |
tools/ui_hardware_test.py runs the same named UI catalog used
by DOSBox-X on the physical IBM 5160. It warm-reboots every scenario into
the deployed image and saves the boot-ready frame, named checkpoints, and
final 4,000-byte MDA framebuffer. Automation cases select N02 through the
shipping Destination modal and assert MIDI decoded by the gateway on TCP
port 8688. The runner injects Set-1 bytes as a second IRQ1 producer, so the
physical keyboard must remain untouched during the run.
tools/seqctl owner
tools/seqctl claim <agent-name>
python3 tools/ui_hardware_test.py
tools/seqctl release
The scheduler mutant for signed comparison records a concrete failure: below 91 BPM, a valid period exceeds 32,767 counts. A signed comparison reads it as negative. The production path uses the unsigned relation. Chapter 15 covers the separate prefetch and loop experiments, including cases that passed a local model and failed on the physical machine.
forecastscope adds linked occupancy, planned work, and
contingency, then checks that sum against the configured banks. Near text
has a separate capacity check. Conventional reserve and unallocated
upper-memory fragments are excluded.
Lab 3.3 Charge the configured-bank forecast Add linked occupancy, planned work, and contingency to the thirteen-bank ceiling, then run the separate near-text check.
The clean bfb60f2 image and planned
fixed-far work must fit the thirteen banks named in
resident-memory.json. Plan 10 is charged in the linked
image; the 93,292-byte future demand includes 45,292 bytes of platform
work plus 48,000 bytes of contingency. Nothing else is counted as
available capacity.
Chapter 4The network stack
The XT gets an Ethernet card so a program on another machine can propose edits to a pattern. Nothing about that is allowed to delay a MIDI byte. This chapter is about what had to be given up to make that true.
4.1Why the machine needs a socket
There is already a link to the outside: the monitor of §3.6, reachable over UDP port 8686, carrying ping, peek, poke, load and run. That is a debugger's channel. Every exchange is one request and one reply, each fitting in a single datagram, and it exists so a person can reach into a running machine.
The plan in plans/inference-integration.md needs something
different. A model running on another machine proposes sequencer edits;
requests carry state snapshots and replies carry typed operations, and neither
reliably fits in one datagram. That needs a stream, and a stream needs
sequencing, retransmission and flow control.
So SEQ/OS opens exactly one outbound TCP connection, to one configured gateway, on port 8688. It does not speak HTTP, JSON or TLS Confirmed — those live in the gateway on the modern machine, which is also where documentation and build symbols are kept so they never consume XT memory.
The two links are kept separate on purpose. If the TCP client wedges, the UDP monitor still answers, because it shares no state and no code path with it. A debugger that fails whenever the thing it debugs fails is not a debugger.
| Monitor | Inference client | |
|---|---|---|
| Transport | UDP, port 8686 | TCP, gateway port 8688 |
| Direction | Inbound request, outbound reply | Outbound connection only |
| Implemented in | Assembly (ne2000.inc) | C (net_tcp.c) |
| Privilege | Raw memory: peek, poke, load, run | Typed operations only, no memory access |
| State kept | None between datagrams | 560 bytes, one connection |
4.2Polled NE2000 service
The card sits at I/O 0x300. The ordinary way to service a NIC
is to let it raise an interrupt when a frame lands. SEQ/OS writes zero to the
interrupt mask register during initialisation and never enables one
Confirmed:
mov dx,NE_IMR
xor al,al ; deliberately polled
out dx,al
Two reasons, and the second is the one that decides it. The XT bus cannot share IRQ lines between cards (§1.3), and the serial cards have first claim on the lines that are free. More importantly, an interrupting NIC is a machine that stops sequencing under load: every arriving frame would preempt the deadline path, and the arrival rate is set by whoever else is on the network.
That argument is not new here. The same arithmetic already ruled out taking a byte interrupt from the UART — three MIDI ports at 31250 baud would need about 9,400 interrupts per second and 20–30% of the CPU in entry and exit overhead alone. A NIC under flood is the larger version of the same problem, so it gets the same answer: poll it from the foreground, where there is no deadline to miss.
net_poll therefore runs in the main loop, between
monitor_poll and the C kernel, in the band of
Fig 2.4 marked as having no deadline. The cost is latency:
a frame waits until the foreground reaches it. §4.8
measures that wait.
4.3One frame per pass
Polling only helps if each poll is bounded. net_poll reads the
card's current page pointer, compares it against its own, and returns
immediately when they agree. When they differ it reads the four-byte ring
header, checks the frame length against a 600-byte ceiling, copies at most one
frame, and hands that one frame to C.
The line that makes the bound hold is at the end of the routine Confirmed:
.done: cmp byte [net_did_rx],0
jne .restore
call net_tcp_poll
If a frame was processed this pass, the timer poll is skipped entirely. One pass does one thing: it handles a received frame, or it advances the connection's timers, never both. Under saturation, receive work crowds out retransmission — which is the right way round, since a link delivering frames is not one whose retransmit timer is urgent.
net_poll. A pass handles
one received frame or one timer transition, never both. C is called with the
frame buffer and returns a single word saying what to do with it; every port
write stays in assembly.The header check is not decoration. If the next-page pointer read out of the ring falls outside the ring's own bounds, the code resets the ring rather than following it. A corrupt pointer followed is a walk through NIC memory at DMA speed, and the frame that produced it arrived from the network.
4.4Where the stack is cut
Assembly owns the card; C owns the protocol. The seam is one call, through
the resident kernel header at KERNEL_C_NET
(§3.3), passing the connection state, the current
tick, the frame buffer and its length.
What makes the seam cheap is the return convention. C answers with a single
16-bit word, and the top bit means “transmit this” while the low
fifteen carry the length. C never touches an I/O port. It composes a reply into
memory, returns a length, and assembly clocks it out to the card. Every
OUT instruction stays on the side of the boundary where the build
enforces the timing rules — NASM's CPU 8086 directive and the
grep for div and mul of
Table 3.3.
The reply is built in the buffer the request arrived in. When C returns a
transmit length, assembly sends from net_rx directly — there is no
copy into a separate transmit buffer, because a 600-byte frame that arrives
becomes the 54-byte acknowledgement that answers it. The idiom shows up in the
C as passing the frame pointer where a payload pointer is expected, with a
length of zero: never read, but a valid pointer.
4.5A fixed-state TCP client
There is no heap and no allocator. There is one connection, and it is 560 bytes of storage reserved in assembly.
That number is asserted in three places, which is what it takes when two
toolchains have to agree on a size neither can see the other declare
Confirmed: TCP_CONNECTION_SIZE equ 560
reserves the bytes in seq86.asm; a negative-array typedef in
net_tcp.h fails the C compile if sizeof disagrees;
and a Makefile rule reads both files with sed and refuses to build
if they differ. This is the same guard as NTEMPI, added after the
tempo-table mismatch of §3.7 showed what an
un-gated duplicated constant costs.
| Given up | Consequence | Why it is acceptable here |
|---|---|---|
| Reassembly queue | An out-of-order segment is dropped and the expected sequence re-acknowledged | One connection over a LAN to one gateway; reordering is rare and recovery is a retransmit |
| Multiple send flights | tcp_tx_write refuses while a flight is unacknowledged |
Request/response traffic; the caller has nothing to send until the reply lands |
| Window scaling, SACK, timestamps | 256-byte windows in both directions | The gateway is one hop away and the payloads are small typed operations |
| Listening sockets | Outbound connections only | Nothing on the network has any business opening a connection into the sequencer |
The receive ring shows the constraint most plainly. The buffer is 256 bytes
and its head and tail indices are u8. There is no wrap check
anywhere in the enqueue path:
connection->rx[connection->rx_tail++] = *data++;
The index wraps because the byte wraps. A 200-byte buffer would need a
modulo or a compare-and-reset on every byte received, and
DIV r/m16 is 144–184 clocks on the 8088 and cannot be
interrupted — one of them in a per-byte loop costs more jitter than the whole
interrupt handler. The buffer size is 256 because that is the number that makes
the arithmetic disappear.
Overflow is handled before that loop is reached rather than inside it: a
separate 16-bit count tracks occupancy, and a segment larger than the remaining
window is dropped and re-acknowledged without ever entering
queue_data.
Closing a connection increments the local port and folds it back to 49186 when it would leave the ephemeral range. A reconnection therefore uses a different port than the one before it, so a delayed segment from the previous connection cannot be accepted by the new one.
4.6Checksums without MUL
The Internet checksum is one's-complement addition, so no multiply is needed to compute it. The part that needs care is the end-around carry, because C cannot read the carry flag. It is recovered by comparison instead:
static u16 sum_word(u16 sum, u16 word)
{
u16 old = sum;
sum += word;
if (sum < old)
++sum;
return sum;
}
An unsigned sum smaller than either input is a sum that carried. IA-16 GCC
compiles this to an add and a conditional increment, with no call to a runtime
helper — which matters, because the build requires nm -u to come
back empty and a helper that the compiler emitted on its own would not link
(Table 3.3).
Verification avoids arithmetic the same way. Rather than clearing the
checksum field, recomputing, and comparing, the code sums the header with its
checksum still in place and tests the result against 0xFFFF. A
correct header including its own checksum sums to all ones. The field is never
saved, cleared or restored.
The TCP pseudo-header gets the same treatment: its fields are folded into the running sum one at a time instead of being assembled in a scratch buffer. Twelve bytes that are never written anywhere.
4.7Time measured in MIDI clocks
TCP needs a clock to decide when a segment was lost. This machine has no wall clock. PIT channel 0 is the event scheduler, channel 1 refreshes DRAM and must never be touched, and channel 2 is the measurement reference (§1.2). There is no spare timer.
So the network borrows the one that is already running. A counter is
incremented inside irq0, on the path taken by every scheduler
event, whether the transport is playing or stopped
Confirmed:
.advance_time:
%ifdef NET
inc word [net_ticks]
%endif
The retransmission timeout is 120 of these ticks. Not 120 milliseconds — 120 scheduler events. And the interval between scheduler events is the MIDI clock period, which is set by the tempo.
| Tempo | Scheduler period | RTO (120 ticks) | Connection dropped after 5 retries |
|---|---|---|---|
| 50 BPM (slowest) | 50.0001 ms | 6.00 s | 30.0 s |
| 120 BPM (default) | 20.8334 ms | 2.50 s | 12.5 s |
| 300 BPM (fastest) | 8.3333 ms | 1.00 s | 5.0 s |
Changing the tempo retunes the TCP stack. Periods are read from the generated table of §3.1, so these are exact Confirmed; what they mean on a real network is not Inferred. Six seconds at the bottom of the range is slow but harmless. One second at the top is short enough that a gateway which pauses to run a model could be retransmitted at while it is still working, and five such timeouts close the connection after five seconds.
The mechanism costs one INC on a path that was already
executing, and it needs no second timer, no calibration, and no
interrupt. Whether an interval that moves with the tempo is the right clock
for a network protocol is a separate question, and an open one
(Appendix A).
4.8What the gate proves
make network-scope runs the real network-enabled boot image on
the cycle-counting 8088 and an NE2000 model, with the MIDI clock running, in
six conditions: no card present, a card connected and idle, a flood of
malformed 600-byte IPv4 frames, and a flood of valid full-size TCP
segments, a stream of maximum-size inference frames, and an accepted pattern
proposal with preview rendering.
SEQ/OS resident network timing scope (cycle-counting 8088 model) 240 MIDI clocks per case; model accuracy is approximately +/-15% workload jitter worst IRQ loop max slope ppm frames/s over absent 15.58 pc 10.00 pc 31146 pc +0.000 0.0 0 PASS idle 14.64 pc 9.00 pc 29363 pc -0.181 0.0 0 PASS invalid-ip-flood 13.29 pc 9.00 pc 29777 pc -0.123 104.5 0 PASS valid-tcp-flood 16.20 pc 10.00 pc 28766 pc -0.255 38.7 0 PASS inference-frame-stream 17.12 pc 10.00 pc 28802 pc -0.046 18.3 0 PASS pattern-proposal 13.11 pc 10.00 pc 37285 pc -0.122 0.4 0 PASS gate: jitter <= absent + 20 PIT counts; foreground loop <= 40000 counts; slope delta <= 1 ppm; no overruns units: 1 PIT count = 0.838 us; foreground gate = 33.52 ms resident C image: 10380 bytes of 51968-byte ceiling guest time modeled: 16,895,398 instructions; CPU 4.7727 MHz, PIT 1.1932 MHz inference setup maximum: 193,799 instructions / 500,000 OVERALL: PASS
The image size and reserve come from the built artifact and
src/link/kernel.ld; the scope does not carry a second
hard-coded copy of that bound.
The column that carries the claim is the last one. Zero scheduler overruns in every condition, including a link saturated with valid TCP segments: no deadline was missed Modelled.
Jitter identifies the longest uninterruptible instruction in flight when IRQ0 arrives; it does not rise in proportion to average load. All six mixes remain within a 3.83-count spread even though foreground work and frame rates differ substantially. The absent case is not an empty loop: it still runs the pattern compiler and runtime of Chapter 6.
Foreground-loop latency is the limiting measurement
The longest interval in this capture is the accepted pattern proposal: 37,285 PIT counts, or 31.25 ms, including resident validation and preview rendering. That is 150% of one 120-BPM MIDI-clock period. Maximum-size inference frames, parsed in 32-byte slices, reach 28,802 counts or 24.14 ms; the no-card case reaches 31,146 counts or 26.10 ms. A poll that consumes a network frame defers model and view work, so parsing does not stack on a complete projection pass Modelled.
The 40,000-count gate is a 33.52 ms work-unit bound, not a promise
that every poll completes within one MIDI-clock period. It spans 1.6
periods at 120 BPM and 4.0 at 300 BPM. MIDI clock still leaves from IRQ0,
and step_pending is a counter rather than a flag, so delayed
foreground steps are recovered rather than dropped
(§2.6). Deadline timing, foreground latency, and
queue reserve are therefore separate gates.
The remaining columns are regression thresholds rather than claims about
hardware. Fitted period moves by less than one part per million between
conditions, so the presence of network traffic does not bend the tempo. Worst
interrupt entry latency stays at or below 10 counts. The malformed flood is
processed at 104.5 frames per modelled second, the valid TCP flood at 38.7,
and the sliced inference stream at 18.3 —
the spread between them being the cost of checksumming and acknowledging a
frame that survives validation instead of discarding it at the IPv4 header.
All three rates reflect that frames are handled one per foreground pass and
every pass also carries the pattern engine; docs/timing.md
records the dated measurement matrix.
What this gate cannot prove
The six workloads above remain Modelled. The 8088 model charges instruction fetch, the 8-bit bus, DRAM refresh, regional memory rates, and the four-byte prefetch queue. Absolute cycle figures retain the documented ±15% interpretation. The installed PicoMEM NE2000, netboot route, and timestamped UDP MIDI path have since run on the 5160 Confirmed; that physical evidence does not turn the separate TCP inference-flood workloads into hardware measurements.
The protocol logic is checked separately and on the host, where it can be
run against adversarial input: make test-network exercises
net_tcp.c directly, and make test-network-image
drives the assembled boot image through connect, transfer and teardown. Both
pass on the current tree Confirmed.
Chapter 5The song as a database
The sequencer keeps the song in a storage engine, not in program variables: 512-byte pages, B‑tree indexes, copy-on-write commits, and power-cut recovery on an 8088 with 640 KiB of RAM.
5.1A song larger than memory
An eight-minute recorded performance is about half a million timed records — roughly 9.4 MB encoded. The machine has 640 KiB, and the resident C kernel can address only the first 64 KB of it with ordinary near pointers. So the design rule is absolute: no resident pointer ever points at song data. Canonical objects live in a page graph on a storage backend and are reached through 32-bit IDs. RAM holds a cache of recently used pages, the current transaction's dirty state, and the small bounded projection the interrupt handler actually plays from.
This splits the sequencer into four kinds of state, and the boundaries between them are the architecture:
| State | Lives in | Reached by | May contain |
|---|---|---|---|
| Canonical song model | Storage pages | IDs, via queries | Musical meaning only — no MDA coordinates, glyphs, or cursor positions |
| Edit transactions | Resident, bounded | Typed commands | Mutations against named object revisions |
| Playback projections | Resident, bounded | IRQ0, directly | Pre-resolved data safe for the deadline path |
| View projections | Far view pages | Read-only queries | Summaries shaped for one editor or inspector |
Neither the MDA renderer nor a hot-loaded module can make a private
structure canonical by writing it directly. The renderer reads through
song_query_pattern and song_query_transport; it never
receives a cache handle or a canonical page pointer. That rule is what lets the model grow past RAM without any consumer noticing.
5.2The 512-byte page
Every canonical object is encoded into 512-byte pages: a 20-byte header, then 492 bytes of payload. 512 because it is the sector size of everything this machine will ever store to — a floppy, an XT-IDE, a PicoMEM — so one page is one storage transaction, never a partial one.
| Offset | Field | Notes |
|---|---|---|
| 0 | magic | S86P — offset reserved; the encoder does not write it yet |
| 4 | type | superblock / internal / leaf / summary / detail |
| 6 | flags | |
| 8 | generation | 32-bit, wrap-safe comparison |
| 12 | payload length | |
| 14 | entry count | |
| 16 | checksum | 32-bit, add/shift only |
Page 1 is the superblock. Its payload carries the ASCII signature
S86S and two independently checksummed 20-byte
root slots. Each slot names a generation, a root page, feature bits, and a
song length, and closes with its own checksum. Boot reads both, discards any
slot that fails its checksum, and follows the newer surviving generation
— the comparison is wrap-safe, so the scheme does not die at
232. Why two slots becomes clear in §5.5: the pair is what
makes commit survivable.
The checksum obeys the same law as everything else in this project: it is computed incrementally, at most 64 bytes per foreground work unit, using an add-and-shift state seeded from two constants. A full page costs eight quiet visits to the checksummer, never one long one. Capacity is bounded by design rather than discovered by crash: 65,536 pages, 32 MiB of song, B‑tree depth at most 4.
5.3Indexes without MUL
Four different indexes hang off the root — resources, arrangement,
pattern detail, control points — and they differ only in key width.
Every key and record is fixed-width, which buys the crucial property: finding
entry n inside a page is a shift and an add, and binary search within
a page needs no multiply at all. One shared search primitive,
song_fixed_search, serves all four shapes; one page-local editor,
song_btree, performs insert, delete, split, and merge for all of
them. Splits are deterministic two-page splits; parent propagation and page
allocation belong to transaction policy, not to the codec.
The layout is not asserted by hand — a tool derives it and a gate refuses the build if the derivation and the C constants disagree:
SEQ/OS canonical song format v1 page: 512 bytes = 20 header + 492 payload indexes kind key internal/leaf fanout int/leaf depth comparisons resource 6 10/14 49/35 3 7 arrangement 12 16/20 30/24 3 6 pattern detail 12 16/20 30/24 3 6 control 8 12/16 41/30 3 7 required eight-minute fixture timed records: 500,000 encoded: 19,208 pages / 9,834,496 bytes / 9.38 MiB max depth: 3 page reads
Read the last line the way the deadline path has to: reaching any record in
a 9.38 MB song costs at most three page fetches, and each fetch is
bounded work through the cache below. Depth 3 measured against a designed
ceiling of 4 — the eight-minute song does not even use the tree's last
level. Confirmed by make
test-song-layout on the current tree.
Lab 5.1 Descend and split a B+tree page Run a three-level lookup or insert into a full leaf under the fixed page geometry.
The expensive unit is not a comparison, it is a page fetch — a sector staged through the bounce buffer in sixteen polled slices. A binary tree over half a million records would be about nineteen levels deep: nineteen page reads per lookup. A B‑tree with thirty children per internal page reaches the same records in three, because depth shrinks with the logarithm base fanout. The tree is shallow precisely because each page is as wide as 492 bytes allow.
Inside a page, entry
n lives at 20 + n × entry_size. With a
16-byte internal entry that multiply is n << 4; with a
20-byte leaf entry it is (n << 4) + (n << 2)
— shifts and adds, never MUL, the same law Chapter 3
greps the disassembly for. Fixed width is what makes binary search
inside a page legal on this CPU. Keys below are shown as small integers;
on the machine a key is 12 opaque bytes compared bytewise.
5.4The page cache
Between the resident kernel and the page store sits a cache of 128 far
slots — 64 KB at 0x10000–0x1FFFF, one page per
slot, with an 8-byte metadata entry per slot in a separate table at
0x3C000. Replacement is the classic clock: a hand sweeps the
slots, gives every referenced page a second chance, and evicts the
lowest-priority unreferenced, unpinned victim. Priorities are fixed —
view pages evict first, transaction pages next, playback pages last —
so a burst of editor scrolling can never push out the page the music is
standing on. If two full sweeps find nothing evictable, the cache says so
(ALL_PINNED) instead of stalling.
On a miss, a cold fill runs a
sixteen-state machine, and the page's checksum is verified — in 64-byte
slices, like everything else — before the slot's tag is
published. No consumer can ever observe a half-loaded or corrupt page under a
valid tag; a bad sector surfaces as CORRUPT, not as a wrong
note. Lookup itself is iterative: the cursor pins one page, follows one
child, releases, and returns to the foreground loop — a tree descent is
three bounded visits, not one long recursion.
Behind the cache, the page store speaks a polled contract shaped exactly
like the NE2000 driver in Chapter 4: one far read or one far write
per foreground pass, staged through a 64-byte bounce buffer. Two backends
implement it — the eight-page canonical RAM store at physical
3F000 and a host-side sparse-file store used by the test
gates. A real disk backend slots in behind the same contract without
touching a single consumer.
5.5Committing without stopping the music
Edits arrive as typed transactions: at most 8 operations, at most 240 encoded bytes, each naming the object revision it believes it is editing. A stale revision is rejected, not merged. Accepted transactions do not modify pages in place — nothing ever modifies a live page in place. Instead the commit machine builds the next generation alongside the current one.
The order carries the safety argument. Detail page, then pattern page, then resource root are copied into the alternate set — read, patched, re-checksummed, written, every step one bounded work unit of at most one 64-byte far copy or one page-store phase per foreground poll. Only when all three children are durable does the machine write the superblock: the new root into one slot, the old root preserved in the other, then a flush. Power can fail at any instant and boot still finds either the complete old song or the complete new one, because a torn superblock write corrupts at most one slot's checksum and the other slot still validates.
The claim is checked by enumeration.
test_song_commit re-runs the commit and kills it after poll 0,
after poll 1, after poll 2, every single cut point through the entire
sequence, and after each cut asserts that superblock selection on the
resulting image yields a fully consistent generation, never a hybrid.
Confirmed on the current tree.
The publication model also defines undo. Undo is not a rollback: the previous generation is sitting intact in the just-vacated page set, so undo is simply another forward commit whose payload is the old content — the same commit protocol at generation N+2, whose content happens to sound like generation N. Revisions also stay distinct: the song generation advances on every accept and every undo, but a pattern's own revision advances only when its content actually changed, so a transport edit does not spuriously invalidate a pattern edit in flight.
5.6What IRQ0 actually plays
None of the machinery above is allowed anywhere near the interrupt handler. IRQ0 remains pure assembly and consumes pre-resolved residents only: the structural deadline queue and effective-step banks of Chapter 6, plus the 16-step velocity array kept as the pattern-v1 compatibility mirror. The rule is absolute: IRQ0 never walks a B‑tree, never touches the cache, never waits on the store, never traverses a pattern record.
The projections are filled by cooperative machines that run in the
foreground loop's spare passes: resolve the pattern through the lookup
cursor, validate its pages through the cache, decode — each pass
one bounded step. kernel_poll dispatches at most one unit of
model work per pass: the commit machine if a commit is pending, else the
pattern compiler and runtime, else a plain cache/store poll. The pattern the
listener hears switches only at a publication boundary — when the
commit's final flush completes, the kernel republishes queries onto the new
generation, invalidates the cache (an alternating page ID must not resurrect
stale bytes), and recompiles the playback projection. An edit becomes audible
once, whole, at that boundary; a commit in progress adds no delay to the
music. Different mutation classes activate at different boundaries
— §6.4 gives the full activation table.
5.7The INT 60h module gate
Chapter 3's live modules reach the song across a real service boundary.
A loaded S86M module reaches the song exclusively through
INT 60h: opcode in AX, a caller buffer in
DS:SI, capacity in CX. The current ABI is 2.10
and defines operations 0 through 77. The original song and transaction
calls remain at the front. Later ranges add structural patterns, events,
harmony, instruments and destinations, arrangements, automation, semantic
views, and the preview/accept/reject/undo lifecycle. A minor-version check
prevents a module from calling a range its header did not declare. The gate
accepts buffers only inside the
module window 0x20000–0x27FFF, copies them through bounded
resident scratch, and copies back values only. After validating the caller
range it re-enables interrupts — the real-mode INT entry
cleared them — so the bounded copies and the C service itself remain
preemptible by the scheduler. No kernel function pointer,
cache handle, page pointer, display address, or IRQ callback ever crosses.
Authority is the deliberate restriction: a module's SUBMIT_TRANSACTION is
accepted only as a preview. It rides the same
propose‑then‑confirm path as an inference proposal from Chapter 4
— a human at the local controls promotes it or it dies. The demo module
proves the contract from the hostile side, deliberately submitting a
stale-revision transaction first and asserting the rejection, then a valid
one and asserting it parks as a preview. On the 8088 model, the
worst-case legal envelope — 8 operations, 91 encoded bytes —
previews and accepts inside the 40,000-count foreground work-unit bound
Modelled, so even a maximal module transaction
cannot stutter the playhead.
One honesty note: on an 8088 this is a trust boundary, not a hardware one. There is no protected mode; a malicious module can still write anywhere. The gate's value is that a correct module cannot accidentally depend on kernel internals, so the kernel stays free to move them.
5.8What the gates prove
make test-song-model runs twelve host binaries plus the
layout derivation — one gate per subsystem. All pass on the current
tree Confirmed. The ones that carry the load:
| Gate | What it proves |
|---|---|
test-song-layout | The byte-level format: a tool re-derives every offset, fanout, and depth from first principles and refuses the build on disagreement with the C constants |
test_song_commit | Power-loss safety after every commit work unit (§5.5), and that undo is a correct forward commit |
test_long_song | A 19,208-page (9.38 MiB) song on the sparse backend: page 19,208 is fetched directly — page IDs are not secretly 16-bit-near — and a watched detail page is read from the store exactly once, so the cache is not quietly re-fetching |
test_song_page_cache | Clock eviction, pin overflow, and that a corrupt page is reported, never published |
test-c-module | The assembled loader, 20-byte S86M header, ABI and capability checks, bounded copies, stale rejection, and representative service calls through INT 60h |
model-scope | On the cycle-counting 8088 model: the maximal 8-op transaction stays under the 40,000-count work-unit bound Modelled |
The long-song gate closes the loop this chapter opened. The fixture is the eight-minute performance from §5.3 — half a million timed records, fifteen times the machine's total RAM — and the same code path that boots the demo song walks it: three page reads to any record, through a 128-slot cache, on a store that moves 64 bytes per pass. The song outgrew the machine, and nothing that consumes it had to be told.
Chapter 6Variable patterns
A pattern owns its length, its step order, its timing grid, and its place in a global bar — there is no fixed 16-column grid. The interrupt handler that plays it stays small anyway: it consumes absolute deadlines computed ahead of the beat.
6.1Variable pattern geometry
The pattern-v1 model — still present as the playback compatibility
mirror — is a 16-byte velocity array indexed by position.
Deleting a step would renumber every step after it, so any edit protocol
built on positions races its own history. The variable model gives each
pattern and each step a stable 32-bit ID and revision. A pattern record is
294 bytes: 1–64 ordered step IDs, a Last Step named by ID, a timing
grid of 1–65,535 pulses per step at 960 PPQN, and a Gbar — a
global bar of 1–256 structural playback slots. A step record is 36
bytes and today interprets two fields: a SKIP flag and the
pattern-v1 compatibility velocity. Reordering moves IDs, never contents, and
a step keeps its identity when its neighbours are deleted.
Confirmed — the record sizes are static
asserts in src/song/pattern/transaction.h.
The foreground validator refuses a candidate whose Last Step names no owned step, whose step content or ownership is invalid, whose grid or Gbar time is zero, whose operations are duplicated or stale, or which leaves no effective step at all. What survives validation defines playback: the non-skipped prefix of the step list through Last Step is the local cycle, and it repeats until the Gbar's slot count is consumed. A partial final pass stops exactly at the Gbar boundary.
song_pattern_cycle_next on the host, not drawn from
the specification Confirmed.The Gbar is what makes pattern length a local property. Without it, a five-step pattern against a sixteen-slot neighbour would free-run for the 80 slots their least common multiple needs. Cutting the partial pass at the boundary is the design decision that prevents it: the Gbar is an absolute re-synchronisation point, so patterns of different lengths can never drift apart for longer than one global bar — polymeter that always comes home.
6.2Compiling ahead of playback
Chapter 2's scheduler rule — compute the reload from the ideal
deadline, never from “now” — applies here unchanged.
Structural playback is a second stream of deadlines on a different grid. Structural time
is defined at 960 PPQN, so the build generates PIT counts per pulse for
each of the 21 tempos as unsigned 16.16 fixed point
(tools/gen_tempo_table.py →
src/song/tempo_table_generated.h). At 120 BPM one pulse is
621.4488 counts, held as 0x026D.72E4; a 240-pulse step — a
sixteenth — is accumulated by shift-and-add DDA with the fractional
word carried, so the grid cannot drift, and no multiply appears anywhere in
the path. Converting one step interval costs 872 counts of portable C,
inside the 2,000-count leaf budget Modelled.
The product of compilation is absolute: the 32-bit PIT count at which the next structural boundary falls. That number exists because the MIDI accumulator's high word grew a 16-bit epoch — the low word still wraps in lockstep with the 16-bit counter exactly as §2.6 relies on, and the epoch extends the ideal deadline into an integer a queue token can carry.
The compiler is a foreground state machine bounded like everything else
in the kernel: one step record per pass. Each pass reads one 36-byte record
from the committed far bank into the near step buffer, evaluates Skip and
Last Step, and appends to an effective-step projection in one of two far
banks at 3B000/3B100; the active bank flips at
publication, so a half-built projection is never live. A projection is
admitted only if its tempo and grid can sustain the queue horizon of
§6.3, and publication records the source song and pattern revisions
separately from the committed revision — the fact §6.4's
compatibility bridge depends on. One effective-step scan, far transfers
included, costs 1,499 counts Modelled.
Near memory holds none of this in bulk. A full pattern state —
record plus 64 step records — is 2,602 bytes, and the writable near
arena has no room for standing copies of it. The step sets live in the
fixed far scratch map: dirty publication pages at
38000, committed step records at 39000, structural
previews at 3A000, the two effective banks at 3B000
and 3B100. Resident near state keeps the 224-byte runtime and
the 782-byte editor — cursors, one staged step, and the UI value
mirrors. The measured far-scratch high water is 4,768 bytes
Modelled.
6.3A queue of deadlines
Compiled boundaries cross into interrupt context through a
single-producer, single-consumer ring: foreground C publishes tokens with
deadline_push, and IRQ0 inspects and removes at most one per
entry. Each token is 8 bytes, fixed by the assembly/C ABI:
| Offset | Field | Notes |
|---|---|---|
| 0 | deadline, low word | absolute 32-bit PIT count (§6.2) |
| 2 | deadline, high word | |
| 4 | Gbar slot | 16-bit, 0–255 today |
| 6 | effective-step index | into the published bank |
| 7 | boundary flags | bit 0 GBAR_START, bit 1 LOCAL_START |
The sizing carries the concurrency argument. The usable horizon is 32
tokens, but the backing ring is 64 slots — 512 bytes — so a
replacement token can be staged in a slot the consumer cannot be reading,
then published by a single word store that rewrites head and tail together.
There is no CLI anywhere in the path: head belongs to IRQ0,
tail to the foreground, both are monotonic bytes, and their unsigned
difference admits all 32 entries without a sentinel slot. The separate count
byte is diagnostic only. Publication costs 160 counts of assembly
Modelled.
IRQ0's side stays minimal. If the head token is due it copies the Gbar
slot, effective index, and flags to the boundary mirrors, increments the
boundary serial, pops the head, and bumps the saturating
step_pending — the same counter Chapter 2 described, fed
from a new source. It then reloads ch0 with the earlier of the ideal MIDI
deadline and the next token. Deadlines within 16 counts of each other are
coalesced into one interrupt, because independently rounded pulse and MIDI
accumulators can otherwise schedule a back-to-back interrupt the machine
cannot take; 16 counts sits inside the 20-count jitter gate.
An empty queue while playing is recorded, not repaired. The saturating
deadline_underflows diagnostic increments and no structural
event is synthesised, while MIDI clock continues from its own ideal deadline
— a starved pattern engine must not bend the tempo. Start and restart
publish two tokens, the immediate boundary and its neighbour, before
exposing playing=1, which gives IRQ0 the horizon it needs at
300 BPM without ever traversing a pattern record in interrupt
context.
6.4Active-pattern publication
Structural edits ride the same envelope law as Chapter 5: at most 8
operations, at most 240 encoded bytes, each naming the revision it believes
it is editing. The operations are create and delete pattern, insert, delete,
and move step, set Last Step, set grid duration, set Gbar length, set or
clear Skip, and set velocity. One editor state machine serves every caller.
It loads the committed record, streams the step set through one near buffer,
validates, applies, and stages the complete candidate — record and
steps — in the preview bank at 3A000. Accepting publishes
incrementally, in bounded passes: step records to the committed bank, then
the model, then recompilation.
When an accepted edit becomes audible depends on what it changed:
| Change | Activates at |
|---|---|
| Anything, while stopped | Immediately |
| Velocity | The next structural boundary |
| Skip, Last Step | The next Gbar |
| Grid duration, Gbar length | Restart |
The table is the musical contract. A velocity tweak lands on the next step without disturbing the cycle. A change to which steps exist waits for the bar to come around, so the listener hears a new bar rather than a torn one. A change to time itself waits for a restart, because re-deriving every in-flight deadline mid-bar would move the playhead under the listener.
The pattern-v1 world is still there — Chapter 5's transport, tempo,
and 16-step velocity paths did not change — so a bridge keeps the two
models coherent. It copies the first 16 compatibility velocities into the
legacy projection and permits only a newer revision to replace an older one,
so a concurrent transport or tempo publication cannot roll structural state
back. The regression that proves it runs against the assembled image in
make test-monitor Confirmed.
6.5One transaction boundary
Three interfaces reach the editor, and all of them enter the same validation and revision machinery. There is no privileged path, and every door serializes IDs and copied values only — no far address crosses any of them.
The monitor gained four commands: 19h
returns the variable-pattern summary, 1Ah pages the ordered
step records at most four per frame, 1Bh reports queue and
compiler diagnostics — pending count, high water, underflows —
and 1Ch submits a local structural transaction.
Modules got the six ABI 2.1 opcodes of §5.7, including registration as a transform provider. A module transaction still carries MODULE authority and parks as a preview; a human at the local controls promotes it. The reference transform is deliberately awkward: it spans four separate RUN invocations — legacy query, structural summary, step page, submit — yielding between each, and restores its image checksum after every phase because its inspectable result block lives inside the validated binary. It exists to prove the cooperative rule from the hostile side, the way Chapter 5's demo module proved stale rejection.
Inference gained a second contract,
seq86.pattern-structure/v1. The positional 16-velocity contract
of Chapter 4 cannot express “make it a five-step cycle”, so the
resident streams a structural snapshot — ordered step IDs with flags
and velocities, grid and Gbar values, the selected step, remaining capacity,
and the active playback revision — across multiple 240-byte frames; a
64-step pattern does not fit in one, and the upload cursor is resident
state. Prompts mentioning length, Skip, Last Step, cycle, Gbar,
polymeter, or duration route to the structural contract; compatible prompts
keep the positional one. The gateway normalises the model's reply into the
same typed operations the monitor and the module gate submit.
6.6What the pattern gate proves
make pattern-scope runs the assembled NE2000 boot image on
the cycle-counting model — the modelling caveats of
§4.8 apply here too — and constructs only the
initial canonical fixture. Compilation, publication, activation, deadline
production, inference preview, module execution, and MIDI clock output all
run the production resident paths.
SEQ/OS pattern-time scope (cycle-counting 8088 model) 480 steady MIDI clocks per structural case workload compile/step jitter drift loop max queue under 64-effective 5765.4 pc 11.33 pc -0.068 14637 pc 31/32 0 PASS 63-skipped 5332.0 pc 15.18 pc -0.024 12897 pc 31/32 0 PASS 5-in-256 4482.0 pc 13.56 pc +0.080 13648 pc 31/32 0 PASS tempo-rebuild 5765.4 pc 13.21 pc +0.076 18697 pc 1/32 0 PASS network workload jitter delta slope delta loop max under absent +0.00 pc +0.000 32809 pc 0 PASS structural-proposal -1.26 pc -0.727 32113 pc 0 PASS module-transform-flood -3.65 pc -0.373 39091 pc 0 PASS portable/assembly leaves: pulse_to_pit=872 pc, effective_scan=1335 pc, queue_publication=158 pc resident text=63688 bytes; far scratch high water=4768 bytes gates: jitter <= 20 pc, drift <= 1 ppm, foreground <= 40000 pc, leaves <= 2000 pc, zero underflows/overruns OVERALL: PASS
This dated capture records the feature-landing baseline. Its resident-text and scratch figures are historical; Chapter 3 carries current capacity. In this capture, steady structural workloads retain 31 of 32 tokens; the deliberate tempo-rebuild sequence atomically restarts at one, then refills, and underflows stay zero. The one-token-per-pass refill keeps pace even through five consecutive tempo publications and restarts. The 64-step compile performs 64 direct far-step reads and zero page reads; there is no cache to hit or miss in the active scratch projection. And the three named leaves — pulse-to-PIT conversion at 872 counts, an effective-step scan with its far transfers at 1,335, queue publication at 158 — all sit under the 2,000-count leaf budget Modelled.
The limiting measurement in the capture is the same one identified in §4.8: the paged ABI 2.3 event-provider transform under a maximum valid TCP flood reaches a 39,091-count foreground pass. The dense same-time event workload reaches 38,562 counts, 96% of the 40,000-count gate. That is the closest any modelled workload comes to the bound, and it strengthens the appendix's case that the gate should be stated in scheduler periods rather than absolute counts. The gate also reports resident text — 63,688 bytes at this capture — and far-scratch high water on every run, so a later commit cannot silently trade memory for timing or the reverse.
Host-side, make test-pattern-time runs five sanitised
binaries — cycle model, transactions, compiler, runtime, editor
— and the structural regressions in make test-monitor
drive the same behaviour through the assembled image: non-16 patterns, short
Gbars, running activation, tempo recompilation, forced queue underflow and
recovery. Together they define the current regression surface.
Chapter 7Events, anchors, and recording
A step says where an event belongs. An anchor says when it actually happens. Keeping those facts separate lets SEQ/OS move a trigger, preserve a recorded performance, and still hand IRQ0 a tiny bounded token.
7.1Anchor timing
The selected detail page holds at most 32 anchors and 64 payload events. Every anchor keeps stable pattern and nominal-step IDs plus signed Q1.15 displacement from −1 through +1 step. A trigger owns an ordered payload list, so one nudge moves the coordinated action atomically. An independent anchor owns one raw MIDI, named-control, or SysEx-reference payload. Actual time changes; nominal ownership does not.
Committed and preview records live at 3D000 and
3E000. They never enter the near pointer domain. Inference and
S86M pages expose copied IDs, revisions, flags, and values; the pattern-v1
view remains an exact projection of trigger velocity.
7.2Three-step compilation
The fixed-far compiler examines previous, current, and following nominal
steps, suppresses occurrences outside the placement, and insertion-sorts at
most 32 actual times in the 384-byte scratch window. Stable ties use actual
time, nominal occurrence, anchor order, then payload order. Complete
32-byte semantic tokens enter the 64-slot ring at 30200; each
token already contains the deadline and small payload IRQ0 needs.
An event deadline may wake IRQ0 between MIDI clocks. The handler dispatches every coalesced due token, advances no musical clock unless its ideal deadline is also due, processes any structural boundary, then arms the earliest remaining deadline. Canonical records and C stay out of the interrupt path.
7.3Editing ahead of playback
A stopped accept publishes immediately. During playback the preview bank stays immutable while the compiler locates each touched anchor's next local occurrence from both its old and new displacement. It discards only the affected future windows, rebuilds the earliest target from preview, and publishes when that replacement is ready. If an early scheduling window has closed, that occurrence keeps its old token and the following local pass is used. Velocity, trigger, payload, and microtiming edits therefore follow the step rather than waiting for a Gbar; transport never stops and the old canonical bank is never partially overwritten.
7.4Recording without a live UART
Physical MIDI receive is deliberately disabled until an installed UART, crystal, and IRQ line can be measured. The hardware-neutral pipeline is complete: timestamp injection, running status, realtime interleaving, bounded SysEx chunks, take-complete/incomplete state, nearest-step correlation, and recording transaction generation. Synthetic batches retain every byte, timestamp-derived displacement, order, and recorded-origin flag through the resident far store, inference snapshot, and module query.
Recorded payloads may be timed by their anchor but may not be silently thinned: inference schemas and normalisation exclude destructive operations, and both resident inference and fixed-far module submission reject recorded payload rewrite or deletion.
The ABI 2.3 provider is part of the real compiler path, not a host-only oracle. Each foreground invocation receives at most seven copied 32-byte tokens, submits copied output, and yields. The compiler stages a whole transformed window in fixed-far scratch and publishes it only after every page validates. A critical window, missing yield, malformed output, checksum failure, unload, or monitor-scratch conflict leaves the canonical queue untouched; recorded tokens must remain byte-exact. IRQ0 never invokes module code.
7.5What the event gate proves
make event-scope drives the assembled 8088 image for 240 MIDI
clocks with 32 anchors in a three-step window, 32 simultaneous payloads, and
alternating displacement endpoints. It separately keeps inference and
module-transform traffic active, while the receive model injects the
physical 3,125-byte/s MIDI rate.
workload tokens jitter drift loop max irq windows under three-step-32 32 10.97 pc -0.215 34088 pc 9 pc 7/8 0 PASS same-time-32 32 12.17 pc -0.308 38562 pc 10 pc 7/8 0 PASS alternating-endpoints 32 10.86 pc -0.351 33713 pc 11 pc 6/8 0 PASS receive: 9374/9374 bytes, ISR=97 pc, capture=104/128, loss=0 provider flood: jitter delta=-3.65 pc, slope delta=-0.373 ppm, loop max=39091 pc, under=0 resident text=63688 bytes; resident state=5544 bytes; event far scratch=3520 bytes gates: jitter <= 20 pc, drift <= 1 ppm, foreground/work unit <= 40000 pc OVERALL: PASS
This is a dated feature-landing capture; its resident sizes are not the current memory forecast. In this run, the densest case stays at 12.17 PIT counts of jitter, the longest foreground pass at 39,091 counts, and every event, structural, and scheduler underflow/overrun counter remains zero. The receive model retains all 9,374 bytes with a 104-of-128 high-water mark. Its shared maximum-gap schedule represents coincident network, keyboard, inference, and provider work rather than adding four independent stalls. Those are model results, not UART enablement; hardware capture remains an explicit later gate Modelled.
Chapter 8Logical notes, conditions, and harmony
The event compiler answers a musical question before a future instrument driver answers an electrical one: what note exists, for how long, and why did it sound?
8.1Pitch without a device
A pitch collection owns an ordered set of stable pitch-definition IDs. Each definition is a signed Q16.16 semitone displacement from the collection reference. A note stores those two IDs, velocity, an unsigned Q16.16 gate in 960-PPQN pulses, and an optional expression-set ID. It stores no MIDI channel, note number, frequency, CV voltage, or display row. Equal temperament is one collection among many; unequal, descending, microtonal, and arbitrarily labelled collections use the same record shape.
The input normaliser pairs running-status note-on/off messages, treats velocity-zero note-on as note-off, handles overlapping equal pitches, and turns each completed pair into one nonzero gate. An unmatched half remains an explicit diagnostic. Microtiming moves the onset while preserving that gate duration, and placement end closes any gate whose natural deadline would escape the placement.
8.2Conditions, chords, and ties
A ratio condition stores M:N and either its HEAD or TAIL
complement. The compiler advances a cached phase once per placement Gbar,
so repeated occurrences in a shorter local cycle share one answer. No
division runs in the resident path. A suppressed trigger still consumes
structural time but contributes no note or attached payload.
The foreground order is fixed: choose the occurrence, evaluate its condition, expand its bounded chord rule, resolve microtiming, then compare the result with the next participating trigger. A forward tie extends only equal transformed logical notes when both triggers fire. Suppression, changed pitch, absence, or placement exit ends the ordinary gate instead. Declarative chord rules produce at most eight ordered voices using either collection degrees or Q16.16 semitone intervals; inversion and register shifts happen before any future device voice is assigned.
8.3The logical hot queue
Foreground compiles 32-byte START, EXTEND, and STOP tokens into a
128-entry queue at physical 30E40. Each token carries stable
logical and pitch IDs, resolved Q16.16 pitch, condition trace, future voice
token, velocity, and chord ordinal. The 24-entry active-gate table is
admitted before publication. A 25th simultaneous gate or an expansion that
cannot fit the hot window is rejected before playback.
The assembly-visible runtime prefix is pinned at
0000:7E00. IRQ0 owns the queue head and dispatch diagnostics;
foreground owns the tail and publishes it last. IRQ0 does not evaluate a
condition, walk a pitch collection, expand a chord, call C, or enter a
module. It only consumes deadline-ordered logical actions. Stop and
placement end walk the fixed 24-gate table and leave no sounding logical
lifetime behind.
Semantic compilation is resumable. One foreground turn scans at most four non-note tokens, while a begun anchor completes its bounded eight-voice expansion atomically. The cursor and in-progress-window bit preserve chord diagnostics across turns; Gbar phase advances only after the complete semantic window reaches its horizon. Provider validation and this compiler run on separate loop turns, keeping their worst cases from adding into one foreground gap.
Lab 8.1 Compile logical notes Run a condition, tie chain, or eight-voice chord through the semantic and admission rules.
The compiler's output is not a
sounding note — it is a 32-byte START, EXTEND, or STOP token in the
128-entry hot queue at physical 30E40. IRQ0 consumes those
tokens by deadline and does nothing else: no condition, no collection
walk, no chord expansion, no C. Everything musical is decided in the
foreground, before publication, which is why every ceiling here is
checked before a token is published, never during playback.
For each occurrence the foreground
runs the same sequence: choose the occurrence, evaluate its condition,
expand its bounded chord rule, resolve microtiming, then compare the
result with the next participating trigger. Each scenario below walks
one leg of that order with the real limits: M:N ≤ 32,
eight chord voices, 24 active gates, 128 hot tokens.
8.4Three mutation boundaries
Resident pitch, note, condition, tie, and chord operations use stable IDs,
expected revisions, preview state, and explicit activation boundaries.
Referential validation rejects unsafe deletes and malformed gates, ratios,
or chord expansions. The seq86.notes/v1 and
seq86.pitch-collections/v1 inference contracts expose the same
musical identities as typed proposals; their schemas intentionally contain
no device-routing fields and treat labels as untrusted data.
ABI 2.5 introduced the harmony range retained by current ABI 2.10. Operations 19–24 page copied pitch, condition, note, logical-gate, and chord records; operation 25 submits a bounded typed preview; operation 31 pages the logical runtime and copied hot tokens. The reference chord module receives one copied note plus collection context and may return at most eight stable definition references. Invalid, stale, unloaded, or non-yielding provider output falls back to the unchanged input note with a diagnostic; no partial chord is published.
8.5What the notes gate proves
make notes-scope runs the assembled NE2000 image through the
queue and gate ceilings, alternating condition suppression, the longest
admitted tie chain, every START/EXTEND/STOP stop prefix, and forced closure
with all 16 addressable COM1 channels active. It separately keeps the real
module transformer and network flood active.
workload tokens active queue irq loop max forced repeated-eight-note-chords 128 8/24 128/128 12 pc 35792 pc 0 PASS 16-channel-saturation 32 16/24 32/128 9 pc 52447 pc 0 PASS alternating-condition 8 1/24 8/128 9 pc 19521 pc 0 PASS maximum-tie-chain 25 1/24 25/128 8 pc 18424 pc 0 PASS stop-with-16-active 16 16/24 16/128 10 pc 14009 pc 16 PASS module/network load: jitter delta +0.24 pc, drift -0.189 ppm, foreground 36807 pc, provider runs=2 PASS admission oracle: 128 hot tokens, 24 active gates, eight chord voices; 25th gate rejected by host differential OVERALL: PASS
This dated capture records the original notes-gate baseline. In the capture, every workload drains to zero active gates with no logical or scheduler underflow and no scheduler overrun. The longest in-gate foreground interval is 35,792 PIT counts against the shared 40,000-count gate; the 16-channel-saturation case deliberately exceeds the admission ceiling, so it proves serializer high-water detection instead of the foreground wall gate (§9.5 derives why twelve simultaneous starts is the true attack limit). Worst IRQ entry is 12 counts against the 20-count gate, added jitter stays below 20 counts, and fitted drift stays below 1 ppm. These remain cycle-model results until the same image is measured on the installed 8088 hardware Modelled.
make output-scope extends that assembled proof across the
physical-output boundary. It drains a 72-byte admitted COM1 burst, three
independent 96-byte emulated UART queues, 24 MPE-style expression attacks,
and a simultaneous 24-voice panic while requiring the inference and live
module-planning regressions. Steady-state modeled lateness is 154 PIT counts
on COM1 and at most 317 counts on each optional port, below one 382-count
MIDI byte time; every trace is exact and drop-free. COM3/COM4 and parallel
gate service remain emulator evidence, not supported physical hardware.
Chapter 9Time, shared
Everything this manual has described so far — patterns, events, logical notes, the projection pipeline — ends at the same narrow place: one 8088, one interrupt line, one serial wire, and a 16-bit counter ticking every 838 nanoseconds. This chapter describes how one processor divides its time between deadline work and foreground compilation. It also states the arithmetic bounds at which the design stops working.
9.1One-shot PIT scheduling
SEQ/OS keeps two of the 8253's three channels for itself. Channel 2 free-runs as the reference clock: a 16-bit down-counter that anything may latch and read, but nothing may reset. Channel 0 is a single alarm clock: a one-shot, re-armed at the close of every IRQ0, always set to the nearest deadline the system knows about. There is no periodic tick. The BIOS's 18.2 Hz heartbeat is gone, and with it the usual tax of tick-based systems — work quantised to tick edges and a constant drizzle of empty interrupts. An event here lands where it is due, at counter resolution, and the machine is interrupted only when something is actually due Confirmed.
The cost of the one-shot model is discipline. If the alarm is ever set wrong — too late, or against a stale notion of “now” — nothing corrects it until the wrong wake fires. The scheduler therefore treats arming as the most carefully guarded arithmetic in the image: every reload derives from a fresh channel-2 latch, compares in 32 bits, and classifies what it finds before a single count is written to the timer.
| Quantity | Value | Note |
|---|---|---|
| One PIT count | 838.0965 ns | 1,193,182 counts per second |
| One CPU cycle | ¼ count | 4.772727 MHz — exactly 4 cycles per count |
| One MIDI byte | 382 counts | 10 bits at 31,250 baud; 320 µs |
| One MIDI clock period | 2,982,955 / BPM counts | 24 PPQN; 24,858 at 120 BPM |
| One 16-bit wrap | 65,536 counts | 54.93 ms — maximum interval for single-wrap extension |
| The foreground gate | 40,000 counts | 33.52 ms per kernel poll, wall-clock |
IRQ0 is the only worker at the deadline, and each of its wakes performs a bounded unit: at most one F8 clock byte, one queued UART byte, one released packet, one consumed boundary. Nothing under the interrupt loops by queue length. When more work is due than one wake may do, the handler does one unit and re-arms the alarm a fixed 128 counts out — a declared backlog that drains at a bounded, measurable rate rather than an unbounded loop with interrupts masked.
9.2Five deadline ledgers
The alarm is armed to the minimum over five independent ledgers, each with a single foreground producer and IRQ0 as the only consumer:
| Ledger | Depth | Carries |
|---|---|---|
| MIDI clock ideal | — | The next F8 instant, advanced by an exact fixed-point period so the grid never drifts |
| Boundary deadlines | 32 | Step and Gbar structure, pushed by the compiler ahead of playback |
| Semantic events | 64 | Compiled occurrence windows (§7.2) |
| Logical hot tokens | 128 | START / EXTEND / STOP at 32 bytes each (§8.3) |
| UART ready | 1 + 96 B | The earliest instant the wire can accept another byte, plus the serializer ring it drains |
The direction of every arrow is fixed: the foreground publishes tail-last, the interrupt consumes head-first, and no path under the deadline ever waits on the foreground. When the foreground pushes a deadline that undercuts the armed alarm — a boundary landing inside the current horizon — the push itself retargets the one-shot under a brief interrupt mask, because the scheduler otherwise re-derives its targets only inside IRQ0 Confirmed.
One ledger is different in kind. The serializer is a 96-byte ring with a 75% admission ceiling at 72 bytes, and it is the only elastic element between compiled music and the wire: bursts land in it whole, and the wire drains it one paced byte at a time. Every ceiling in §9.5 is ultimately a statement about this ring or the wire behind it.
Logical compilation may advance while transport is stopped. The output projection does not consume that stopped lookahead into a built-in serializer. A committed start first lets projection catch the logical tail; the kernel publishes live transport only after both tails match. This start barrier is specified with automation in §13.2.
9.3Extending 16-bit time
The reference counter is sixteen bits wide, so “now” repeats every
65,536 counts — every 54.93 ms. Deadlines, however, live across
seconds of song. The scheduler therefore carries time as an
extended value: the hardware counter below, an epoch word above,
and a single rule joining them — one authority
(sched_now) updated at every latch, advancing its epoch
exactly when a fresh sample reads below the previous one. That inference
is airtight only if consecutive latches always land less than one full
wrap apart. The system's guarantee comes from the clock ideal: playing or
stopped, a wake fires at least once per MIDI clock period, so the latch
interval is bounded by the tempo itself.
Follow that bound to its end and it becomes a number a musician can
read on the front panel. One clock period is 2,982,955 / BPM
counts; it equals one full wrap at 45.52 BPM. Below that
tempo the alarm cannot even be set — the one-shot's reload is the
same sixteen bits — and the epoch inference loses its ground truth.
The enforced floor is SONG_TEMPO_DECIBPM_MIN = 460:
46.0 BPM, a clock period of 64,847 counts, clearing the wall by 689 counts
— about one percent Confirmed. The
slowest tempo this instrument will ever play is not a taste decision.
It is the shadow of a 16-bit register cast across the tempo knob.
9.4Sharing one CPU
Nothing here is concurrent. Every count the interrupt spends is a count the compiler does not get, and the emulator's cycle model prices each wake precisely Modelled: a fast UART byte wake costs about 650 counts end to end; a full wake that releases a packet, scans the ledgers, services the wire, and re-arms spans roughly 1,900; a clock wake sits near 1,100. During a chord's byte stream — one ~650-count wake per ~650-count byte cadence — the machine spends roughly 85% of wall time in the handler, and the 40,000-count poll gate is measured in exactly that stretched wall time. This is why foreground work ships in slices sized in far-window transfers (§3), and why one poll never carries more than one projection.
One consequence follows directly from those spans. A
handler that runs 1,900 counts cannot arm an alarm for a deadline that
falls 400 counts after its own entry — by the time it reaches the
timer, the moment has passed. No single-interrupt design escapes this;
the scheduler must classify the result explicitly. A target found up to
DEADLINE_CROSS_WINDOW (2,048 counts, chosen to clear the
1,900-count worst span) behind a fresh latch is classified as
crossed inside the handler: it gets a bounded follow-up wake,
the work happens a hundred-odd counts late, and no failure is recorded
— because none occurred. Only a target beyond the window, with no
declared backlog, increments sched_overruns; and the timing
gates require that counter to finish at zero. The ledger stays honest in
both directions: real losses count, physics does not.
Lab 9.1 Service one-shot deadlines Advance one interval containing MIDI clock, an eight-note attack, serializer backlog, and a due event.
At 120 BPM, MIDI clock is due every 24,858 counts and a 16th-note step every 149,148. The compiler has already done its work in the foreground: eight START tokens sit in the hot queue, due together on the next clock instant, their packets projected. Everything below is the interrupt's view: five ledgers, one alarm, and a strip of wall time showing who owns the CPU.
9.5Arithmetic limits
Every rate in the system reduces to bytes on one wire and counts on one timer, so the ceilings can be written in closed form. A note's lifetime costs nine wire bytes — a three-byte pitch-bend and a three-byte note-on at START, a three-byte note-off at STOP. The wire carries 3,125 bytes per second; the clock takes 0.4 × BPM of them; admission promises never to book more than 75% of what remains, with bursts held to the serializer's 72-byte ceiling. From those five numbers, everything below follows.
| Ceiling | Value | Governed by |
|---|---|---|
| Wire throughput | 3,125 B/s | 31,250 baud, 10 bits per byte — physics |
| Sustained notes, 120 BPM | 256 /s | 0.75 × (3,125 − 48) / 9 |
| Sustained notes, 300 BPM | 250 /s | clock share grows to 120 B/s |
| Scheduler release path | ≈290 /s | one packet per ~2,030-count wake chain — does not bind at any legal tempo Modelled |
| Simultaneous attack | 12 voices | 72-byte burst ceiling / 6 bytes per START |
| Burst drain | 23.1 ms | 72 bytes × 382 counts at wire pace |
| Tempo floor | 46.0 BPM | one clock period must fit a 16-bit wrap; the arithmetic limit is 45.52 |
| Tempo ceiling | 999.0 BPM | the clock handler consumes about 37% of the CPU |
| In-burst F8 displacement | ≤ 3.6 ms | handler spans; phase-true — the grid itself never moves Modelled |
| Foreground stall tolerance | 23–39 ms | a full admission fill keeps sounding this long unattended |
Three derivations follow.
The attack limit is twelve. A START is six bytes, and a single instant may admit at most the 72-byte burst ceiling: twelve simultaneous note-ons, exactly. The eight-note chord the compiler supports (§8.2) fits with room for its own releases; the 16-voice saturation workload exists precisely because 96 bytes must be refused at compile time — rejected or routed to another destination, never thinned on the wire mid-performance.
The wire always binds before the scheduler. The release path could push roughly 290 note lifecycles per second; the wire, after clock and admission, allows 227–256 across the whole legal tempo range. The gap is deliberate: the scheduler is provisioned so it does not become the limiting resource. At saturation, the 31,250-baud MIDI wire reaches its limit before the scheduler does.
The floor and the ceiling are both hardware limits. Below 45.52 BPM the clock period exceeds one 16-bit wrap and neither the alarm nor extended time can be maintained — §9.3's wall, enforced at 46.0. Above, there is no cliff, only a slope: at the 999.0 BPM ceiling the clock period is 2,986 counts against a ~1,100-count clock handler, so a third of the machine is spent saying tick. The asymptote — the tempo at which clocking consumes everything — sits near 2,700 BPM. The enforced ceiling keeps the instrument well below that slope.
Lab 9.2 Apply the MIDI wire budget Charge clock and note bytes against one second of wire capacity at representative loads and tempo limits.
The bar below is one second of the COM1 wire: 3,125 bytes. The clock takes its share first, notes book against 75% of the remainder, and everything past the admission line is reserved slack the compiler refuses to promise. Each step spends the budget a different way and reads the verdict off the same arithmetic the admission oracle runs.
9.6Network events arrive early
COM1 and the network have different timing contracts. COM1 consumes bytes at the PIT deadline. A network destination receives an absolute timestamp before that deadline and the gateway performs the final dispatch. The installed configuration declares one physical destination, P01, and two network destinations, N02 and N03. Each network destination selects remote port 1 through 16 and a lookahead from 20 to 250 ms. The default is 60 ms. All network lanes in one song use the same lookahead so a routing change cannot introduce an unreported phase offset.
The foreground output service serializes timestamped event frames into a
96-byte staging lane and drains at most 48 bytes per turn. The frame type is
30h. It carries session and sequence identity, the destination,
the gateway deadline, and complete MIDI bytes. Seven events can share one
UDP datagram: the measured wire size falls from 574 bytes as individual
datagrams to 154 bytes as a batch, a 73.17% reduction
Confirmed.
| Measurement | Result | Gate |
|---|---|---|
| Qualified path budget | 24.148329 ms | ≤25 ms reserve |
| Default lookahead margin | 35.851671 ms | 60 ms minus path budget |
| Guest launch work | ≤98.1 µs | bounded foreground path |
| Gateway dispatch | 0.256 ms | host timestamp queue |
| MOTU M2 loopback | 0.763 ms | physical MIDI observation |
| Maximum late event | 2.7365 ms | ≤5 ms |
| Two-port spread | 0.0089 ms | ≤1 ms |
| Ten-second run | 480 events | zero loss and zero reorder |
The fitted network residual is 23.129 ms. That value describes Wi-Fi and host delivery, not MIDI phase. The gateway holds an early event until its timestamp, so variable packet arrival consumes lookahead rather than moving the note. Physical COM1 remains the transport clock and transport-command output. A global network clock is not part of the current resident contract.
9.7What the timing gates prove
The claims in this chapter are gated, not asserted. make notes-scope (captured in §8.5) runs the
assembled image through every workload above and holds it to: worst IRQ
entry latency at most 20 counts — the measured worst is 12, inside
a chord's byte stream; sched_overruns and both underflow
counters at exactly zero; every foreground poll under 40,000 wall counts
for in-admission workloads; clock jitter growth under module and network
flood below 20 counts; and fitted clock drift below 1 ppm. The
deliberately over-admission saturation case is held instead to proving
the refusal: serializer high-water at or past the 72-byte ceiling with
every token still dispatched and terminated.
The COM1 scope is cycle-model evidence Modelled.
The derivations in §9.5 are arithmetic on the declared UART contract.
make network-midi-bench checks batching and deadline arithmetic;
the dated physical capture supplies the N02/N03 path measurements in
§9.6 Confirmed.
Chapter 10The inference engine
The prompt key submits a plain-language request. A validated proposal appears in a preview overlay and applies only after an explicit keystroke. The model is the least trusted component in the system. This chapter is about why that ordering — capability outside, authority inside — is what makes the feature safe to have at all.
10.1Proposal authority
The monitor of §3.6 can write any byte in the machine. The inference channel, running over the same card, can write none. Table 4.1 drew that line at the transport; this chapter follows it up the stack. A model's reply enters SEQ/OS only as a typed proposal against a named state surface, is validated by the kernel against the revision it claims to be based on, becomes ghost state on the MDA, and is applied when a person presses ENTER. Reject discards it. Nothing in the path grants the gateway authority to mutate state Confirmed.
Three machines carry the work, and each keeps a different thing:
XT resident kernel authority — snapshots, validation,
| ghost preview, accept / reject / undo
| framed protocol over the one TCP connection (§4.5)
|
LAN gateway (tools/seqgw) policy — contract registry: prompts,
| schemas, normalizers, compute sandbox
| OpenAI-compatible HTTP, grammar-constrained JSON
|
model server (llmonster) capability — one local model on one
GPU, swapped as models improve
The split is load-bearing in both directions. The kernel layer exists for trust, not capability, so it does not weaken as models improve; a frontier model plugged into the same gateway would gain no authority. And the gateway layer is host-side policy — its prompts, schemas, and operation vocabularies can change daily without touching the firmware the XT boots, which matters on a machine whose deployment path is writing a boot image.
Inference is local by decision, not necessity. The model server is a machine on the same LAN; the sequencer works without internet, the stack is owned end to end, and no request is billed. A frontier API through the same adapter would raise the capability ceiling and costs the architecture nothing — the contract layer is model-agnostic — but it is declined on those grounds.
10.2State contracts
A state contract is a versioned view of one resident state surface that a model may propose against. Each contract fixes four things: a bounded snapshot encoding the resident builds through queries, the revision the snapshot was taken at, the operations a proposal may carry, and the preview semantics the kernel enforces when one arrives. The pattern contract's snapshot is the simplest of them:
offset size field
0 4 'S86Q' — the contract key
4 4 nonzero pattern object ID
8 4 nonzero pattern revision
12 4 committed song generation
16 1 selected step (0..15)
17 1 step count (16)
18 16 velocities (0..127)
34 N untrusted UTF-8 prompt (0..206 bytes)
The revision inside the snapshot is the contract's spine. Every proposal must name the revision it reasoned from, and the kernel refuses one whose base has moved — a stale proposal cannot clobber an edit made while the model was thinking. Revisions and generations never move backward; undo publishes the bounded before-image as a newer generation (§5.5).
Each contract carries two vocabularies, deliberately unequal. The wire
vocabulary the XT validates is a handful of declarative opcodes —
for patterns, replace and set-step. The model-facing vocabulary the
gateway offers is richer: nine verbs for the same surface. The gateway's
normalizer reduces every semantic operation to wire opcodes before
anything is sent, so firmware complexity does not grow with model
capability. A universal replace_* fallback stays in every
vocabulary so a request nobody enumerated remains expressible —
capability grows by adding contracts, not by enumerating operations for
every conceivable intent.
| Contract | Key | State it fronts | Model-facing verbs |
|---|---|---|---|
seq86.pattern/v1 | S86Q |
the front pattern's sixteen step velocities | euclid · rotate · clear · fill · scale · accent · set_step · compute · replace_velocities |
seq86.pattern-structure/v1 | S86S |
ordered steps, durations, Gbar placement, cycle | insert / delete / move step · set_last · set_duration · set_gbar · set_skip · set_velocity · set_cycle · rotate · delete_pattern |
seq86.transport/v1 | S86T |
playing flag, tempo over the 21-entry table | set_tempo (BPM, snapped) · set_playing |
seq86.events/v1 | S86E |
anchors, typed event payloads, Q1.15 microtiming | nudge · set_microtiming · quantize · insert / update raw event · create / update trigger · delete_event |
seq86.notes/v1 | S86N |
logical notes, conditions, ties, chords | set_gate · set_note · set / clear condition · set / clear tie · chord create / assign / update |
seq86.pitch-collections/v1 | S86P |
named pitch collections and definitions | create / update / delete collection · create / update / delete definition |
seq86.instrument/v1 | S86I |
instruments, destinations, controls, ports | use_control · rename_instrument · set_expression · set_voice_strategy · assign / edit destination · offer_provider |
seq86.arrangement/v1 | S86A |
tracks, placements, the song timeline (§11) | add / delete track · add / move / resize / duplicate / delete placement · reorder_track · set track / placement mute · set_track_kind · set_track_instrument |
seq86.control-sequence/v1 | S86C |
recorded points, generated segments, trigger controls, and bandwidth (§13) | nudge / set recorded point · set generated endpoints and HOLD/LINEAR policy · attach trigger control · create a bounded control-only placement |
On the XT, the prompt composer selects the contract from the screen it was opened on and from the prompt itself, then builds that contract's snapshot; structural asks route to the structure contract, event-level asks to the events contract, and everything else keeps the positional pattern route Confirmed. Destructive authority is thinned before it exists: schemas and normalizers for the recorded-range contracts exclude destructive operations outright (§7.4).
Adding a contract is a fixed recipe rather than a design event. Host side: a snapshot codec, a system prompt stating authority and vocabulary, a generation schema, a normalizer, a registry entry, and an inferscope task suite with objective validators. Resident side: a state service with revision, preview, and accept semantics, plus a snapshot builder. The pattern service is the template the other eight followed.
10.3The gateway
The gateway (tools/seqgw) is deliberately small and
deliberately bounded. It keeps at most eight sessions, one 4,096-byte
request and one 4,096-byte response per active session, and an ordered
event log per request for resumption. It is a development endpoint, not
durable storage Confirmed. Its protocol side
is the session layer of §4.5's framed messages:
HELLO and resume, request upload, streamed events, terminal states.
Dispatch is four bytes. The first four bytes of an assembled request select the contract handler from the registry, and the handler's decoder checks every snapshot field before any model work — a malformed snapshot ends the request as an ERROR event without spending a token. Everything after that point is the handler's policy: render the messages, build the schema, call the model, normalize the reply.
The model call itself is one HTTP POST to an OpenAI-compatible chat-completions server, temperature 0, with the response read under a hard size cap. Two details in that call were paid for in benchmark failures. Thinking is pinned per request — never inherited from server defaults, because a template that thinks by default opens a think block a JSON grammar can never close, and the entire constrained output lands in the reasoning channel with the content empty. And the response format carries the full per-request schema, which the server compiles to a grammar that holds the model's output token by token Confirmed.
Serving lives on llmonster — one RTX 5090, 32 GB, one model resident at a time. The deployed default is a dense 27B quantized model with thinking off: a perfect score on the reference suite at about 3.7 s mean (§10.6). Thinking on is the escalation path for hard or ambiguous requests — slower but strictly more capable. Model or network failure at this layer becomes an ERROR event and nothing more; §4 already established that no network state can stop the sequencer or enter IRQ context.
10.4Grammar-constrained decoding
The schema the gateway sends is built fresh for each request, and its identity fields — contract name, base revision, transaction ID — are constants, not types. The model is grammatically incapable of proposing against a stale revision or answering the wrong request. Around that fixed prefix the schema is a union of two arms: a proposal carrying an explanation and exactly one operation, or an error carrying a refusal string.
The rules that shape this schema were measured, not guessed; each came out of a benchmark failure Confirmed:
- Union arms share their leading keys. Under a grammar, the first emitted key selects the arm. When the proposal arm opened with a key the model did not habitually type, the grammar railroaded it into the error arm and it refused 13 of 14 tasks. Both arms now open with the same analysis and identity prefix, so the choice happens at the operation-or-error key.
- The error arm must exist. The original schema required an operation, so refusing an out-of-contract request was syntactically impossible — asked for a tempo change under the pattern contract, the model invented pattern edits instead of declining. A refusal the grammar can express is a refusal the model will make.
- Semantic operations discount precision. Models
select typed operations reliably but miscount hand-built sixteen-value
arrays: wrong euclidean gaps, density drift, explanations contradicting
their own patterns. Exact placement math executes host-side in the
normalizer;
replace_*remains for whatever was not anticipated. computeis the algorithmic escape hatch. One pure expression over the current state, evaluated in an AST-whitelisted sandbox — no attribute access, no foreign names, no string constants, no power operator, bounded ranges and node count — and validated like any other result. Models write correct formulas far more reliably than correct sixteen-value literals; ramps, fades, and mirrors stop depending on token-by-token arithmetic.- Explanations state intent, never step numbers. The ghost preview shows the exact result; step lists in prose were wrong about a fifth of the time even when the patterns were right.
- No domain glossaries in prompts. Coached prompts fixed some tasks and broke others — a wash that also overfits the prompt to anticipated requests. Concept knowledge belongs in the model, not the prompt.
- Schema and normalizer agree mechanically. A stale schema once forced the model to generate shapes the normalizer rejected; a regression test now pins the served schema to the normalizer's requirements.
10.5One bounded repair attempt
A request can fail at five places between the keystroke and the ghost. The design rule is that each failure is handled by the layer holding the information needed to fix it, every path ends in exactly one terminal event — DONE, ERROR, or CANCELLED — and nothing retries without new information.
| Rung | Failure class | Response |
|---|---|---|
| Grammar | malformed shape, stale identity | Cannot be emitted. Prevention, not retry |
| Normalizer | valid shape, wrong meaning — a rejected expression, an out-of-range parameter | One repair round-trip; a second rejection ends the request as ERROR |
| Model call | HTTP failure, oversize reply, timeout at the model server | ERROR event. Never re-asked |
| Transport | TCP disconnect anywhere in flight | Session, active request, and last event sequence survive on the XT; the next HELLO resumes and the gateway replays stored events past the acknowledged sequence |
| Resident | a gateway silent too long; a proposal that lies about state | CANCEL after 1,200 scheduler ticks; kernel validation refuses the proposal before preview exists |
The repair rung deserves its mechanism in the open. When the normalizer rejects a reply — the sandbox refuses an expression, a parameter fails a bound the schema could not express — the gateway extends the conversation with the model's own JSON and a single sentence naming the rejection, then calls the model once more under the same grammar Confirmed. The gateway holds the conversation, so it is the only layer that can repair rather than merely report. The ladder has exactly one rung because the XT is waiting with one event slot and a tempo-coupled timeout; an unbounded repair loop would spend that budget hoping. A reply that chose the error arm is never repaired — a refusal is an answer, not a defect.
Resumption is the transport's version of the same idea. The XT's
532-byte inference service keeps the session, the active request, and the
last accepted event sequence across a disconnect; HELLO carries all three,
and the gateway replays only what was missed, with duplicates dropped by
sequence number. An idle reconnect keeps its session. An active request
the gateway no longer recognizes completes locally as an error event, so
the XT never hangs on a gateway that lost its memory
Confirmed. The 1,200-tick timeout backstops
the whole ladder, and because net_ticks counts scheduler
events, its wall-clock length varies with tempo — an open question
Table A.1 already carries.
What is deliberately absent: the gateway never re-asks the model the same question hoping for a different answer — at temperature 0 the answer would be the same — and the XT never silently re-sends a request. Retry without new information is a loop, not a strategy.
Lab 10.1 Validate one inference proposal Trace a pattern request through snapshot, gateway validation, one repair, resident preview, and explicit accept.
The pattern screen fronts pattern 1 at revision 7 — a four-hit backbeat with a pickup on the last step — and the transport is playing at 120 BPM. Every number below is replayed from the production code path: the snapshot bytes, the frame sizes, the rejection string, and the ghost velocities are outputs of the same codec, sandbox, and normalizer the gateway runs.
10.6What inferscope proves
Adapter claims are gated the way timing claims are.
tools/inferscope.py drives the production path —
registry prompts, schemas, normalizers — over per-contract task
suites and scores the final wire proposal with objective validators:
spacing balance, density, exact figures, velocity envelopes, refusal
behavior, and injection containment. Two strategies exist,
production with thinking and production_fast
without; every adapter or model change gets a run, and records stream to
JSONL so a partial run survives a crash.
The reference results at temperature 0: the legacy adapter scored 12/17 on the binary-era suite; the operation vocabulary raised that to 15–17/17 across models without thinking and 17/17 with it; and the deployed default — the dense 27B, thinking off — scores 17/17 at about 3.7 s mean Confirmed. The velocity and transport suite recorded the following day is the regression baseline for the contract era. Thinking mode runs up to about 25 s on the dense model — inside the gateway's 30 s model timeout, but close enough to the resident's tempo-coupled 1,200-tick budget at high tempi that escalation policy has to mind the tempo.
Two disciplines keep the numbers honest. Validators must be tight enough not to reward a stock answer — a loose “syncopate” check is gameable by one memorized pattern — which matters doubly if they are ever reused as rewards for tuning. And injection resistance is structural rather than promised: identity fields are schema constants, the kernel validates every proposal, and nothing applies without a keystroke. The one surface where model text reaches the XT is the explanation string, and it is length-capped and display-sanitized, never executed Confirmed.
What the suite does not prove is unchanged from every other chapter: all resident-side behavior is cycle-model evidence Modelled until the real machine runs it, and the physical-hardware gate of Appendix A remains the entry ticket for calling any of this supported.
Chapter 11The arrangement
The arrangement places patterns on as many as sixteen tracks along a shared timeline. Each placement names a pattern, start instant, and repeat count. It uses the same 512-byte pages, typed transactions, and bounded work units as the single-pattern path while keeping IRQ0 on the pre-resolved queue.
11.1Tracks and placements
The model is three record types, and they follow Chapter 5's rule that canonical data is IDs, never pointers. A track is an ordered lane with a kind (note, automation, or hybrid), mute and solo flags, an instrument binding, and an order index that doubles as its merge priority. A placement puts one pattern on one track: a 32-bit start pulse on the 960 PPQN grid, a pass count from 1 to 65,535, a mute state, and a revision policy — the pin-or-follow choice §11.5 is about. The arrangement summary record ties the set together and carries the generation counters everything else validates against.
song_track_record 32 bytes id · revision · kind · flags · label
instrument · order_index · placement
count · first/last pulse bounds
song_placement_record 24 bytes id · revision · track · pattern
start_pulse (u32, 960 PPQN)
passes (1..65535) · state · rev_policy
A placement's end is computed, not stored as truth: pattern step length × Gbar slots × passes, evaluated by checked shift-and-add because the 8088's MUL is banned from every hot path (§5.3). An end that would overflow the 32-bit pulse line is refused at the transaction boundary rather than wrapped — the same posture as every other validator in the system. A comparison helper classifies any two placements as disjoint, touching, overlapping, or straddling a boundary; overlap on one track is rejected before it can exist Confirmed.
The resident committed image is deliberately small: four tracks and eight placements live in a 376-byte near shadow, with cached placement ends so nothing downstream recomputes them. That window is an IA-16 near-segment implementation bound, not a hardware claim — the 640 KiB machine has room for far more, and Plan 11's paged design retires the shadow entirely in favour of copy-on-write pages, a 64-track directory, and a 4,096-placement range index. Edits arrive as typed transaction batches of at most eight operations, applied atomically: a rejected batch leaves the committed shadow byte-for-byte untouched Confirmed.
| Bound | Value | Where it binds |
|---|---|---|
| Resident tracks / placements | 4 / 8 | 376-byte near committed shadow |
| Active playback cursors | 16 | 64 bytes each, far table at 0x37000 |
| Near hot controls | 120 B | per-track deadlines + merge state, ≤128 ceiling |
| Restart-point cache | 9 points | near, generation-stamped |
| Transaction batch | ≤8 ops | atomic accept or untouched reject |
| Merge comparisons per select | ≤16 | one per active track |
| Page-cache actions per compile poll | 1 | shared paged detail reader |
| Deadline queue | 48 slots | 16 reserved resident · 40 at full merge |
| Jitter budget | 20 counts | same gate every subsystem answers to |
11.2The restart-point index
Pressing play mid-song is only honest if every voice that should already be sounding actually sounds. Rather than solve the general problem — reconstructing arbitrary mid-pattern state — the arrangement names the instants where the problem is trivial: a restart point is pulse 0 or any boundary no placement straddles. A host-side oracle computes the full set; the resident keeps a nine-entry cache rebuilt in one bounded pass from the committed shadow's cached placement ends, stamped with the generation it was computed at. A stale cache refuses every pulse except 0, which is valid unconditionally — stop and rewind can never be argued with Confirmed.
The cache is also the authority boundary. Only the transport's local start request consults it, and it revalidates every requested pulse no matter who asked: no module opcode and no inference proposal carries the vocabulary to claim a start instant, so a model that lies about state (§10.1) still cannot start playback inside a placement.
11.3Sixteen cursors, one merge
Each active track owns a placement cursor: 64 bytes in a far
table at 0x37000, inside the playback bank — sixteen
cursors, 1,024 bytes. A cursor carries the placement and pattern it is
executing, its pinned revisions, pass and Gbar counters, its next compiled
deadline, and a status word (idle, compiling, ready, waiting, resolving,
exhausted, tearing down). Two absences are the design. Conditions keep no
per-cursor state — the condition phase is recomputed from the pass
counter on demand, so there is nothing to corrupt across a restart. And a
placement boundary always constructs a fresh cursor, so pass counters,
condition phase, and voice state cannot leak from one placement into the
next even when the same pattern repeats back-to-back
Confirmed.
Filling a cursor's next deadline is foreground compile work under Chapter 6's rules: one page-cache action per poll, through one shared paged detail reader — never a tree descent and a content fetch in the same work unit. The boot pattern keeps a resident fast path that bypasses paging entirely. Choosing among sixteen ready heads is the merge: at most sixteen comparisons select the earliest deadline, with ties broken STOP-before-START and then by track order index, so a simultaneous note-off and note-on always release the voice first and the result never depends on scan order. Per-track head deadlines and validity masks live in a 120-byte near hot-control block; IRQ0 itself is untouched — it still consumes pre-resolved tokens from the same queue as §5.6, and never walks a cursor.
11.4Starting, waiting, tearing down
A bounded execution coordinator owns the lifecycle. Before a cursor may start, the pattern's header metadata is resolved through the paged reader — a cursor never begins against an unverified pattern. When a placement ends, a successor scheduler finds the earliest active placement at or after the boundary with the same primitive the restart path uses; a gap becomes a WAITING cursor that advances at most 65,535 pulses per work unit, publishing nothing. On a restart into the middle of the song, every cursor's fractional deadline is rebased in one pass so all sixteen tracks agree about where “now” is.
Teardown has a separate bounded path. A placement that ends with voices sounding must stop them — at the right instant, without an unbounded sweep. Every gate ID is namespaced at birth: the high nibble is the owning cursor slot, the low twelve bits a bounded local token, so ownership is recoverable from the shared 24-slot voice table with no per-cursor voice list. Teardown inspects exactly one voice slot per foreground poll; each owned open voice becomes a STOP head at the placement end, which the merge sorts ahead of any START at the same instant, and the stops drain through the queue before the physical release path runs — the ordering bug the audit caught and fixed Confirmed.
Automatic song end follows the same publication rule as an explicit
Stop. After every cursor is exhausted and the logical and physical output
horizons drain, the coordinator clears both playing flags, restarts boundary
state, queues MIDI Stop FCh, and invalidates the UI. The
invalidation is required because no keyboard event occurs to damage the
Arrange playhead or transport glyphs. The assembled UI scenario waits for a
finite placement to stop and checks both cells
Confirmed.
Lab 11.1 Merge a placement boundary Run bounded teardown, STOP-before-START ordering, and successor initialization at one shared pulse.
Two tracks are active. Track 1's placement A (a bass pattern, one open voice) ends at pulse 3,840 — exactly where placement B begins. Track 2 sustains a pad across the boundary and is not involved. The numbers below follow the documented bounds: one voice-slot inspection per poll, sixteen comparisons per merge select, one page action per compile poll.
11.5Pinned generations
Editing a pattern that an arrangement is playing raises a question the
single-pattern chapters never faced: which revision should a placement
sound? The placement record answers it per placement.
FOLLOW_LATEST tracks the newest committed revision;
PIN_AT_START freezes the revision that was current when the
cursor started, and carries the pin in the far cursor itself, so a restart
naturally restores it with no extra near state.
The storage underneath is the canonical paged detail tree: a B+tree keyed by pattern and step order, with a reserved sentinel row holding the 12-byte pattern header ahead of 12-byte step records. The kernel keeps two roots — the active generation and a retained one — and the compile path takes both, so a pinned cursor keeps resolving its frozen revision after a newer one is published. The assembled regression pins one cursor and follows another across a live edit: the pin keeps sounding velocity 100 while the follower observes 77, and overwriting a revision still owned by a live pin is refused Confirmed.
The honest limit: for patterns other than the selected one, only structural content pages in today — timing, skip flags, velocities. Full note, event, and control-chain semantics for arbitrary retained revisions is Plan 11's work, alongside the paged track directory and the far restart pages.
11.6Modules and proposals
The arrangement joins both external surfaces the same way every other subsystem did. For live modules (§3.4, §5.7), six direct S86M opcodes — 43 through 48 — page out the summary, tracks, placements, restart points, and active cursors in bounded 12-byte-headed pages (a cursor crosses the wire as a 36-byte record), and submit typed batches that are accepted only while playback is stopped. Accept authority is forced to MODULE on the resident side regardless of what the wire claims, and the op vocabulary simply contains no cursor write and no restart claim — the capability argument of §11.2 again. A shipped reference module builds a bounded song form by duplicating the first placement three times, end to end, through the same checked arithmetic as every other caller Confirmed.
For inference, seq86.arrangement/v1 is the eighth row of
Table 10.1 and followed the fixed recipe of
§10.2: a snapshot of the track and placement
window, twelve model-facing verbs the normalizer reduces to wire opcodes,
overlap and overflow validated host-side before anything is sent, and the
same ghost-preview, keystroke-accept path as every other contract. A
proposal can rearrange the song; it cannot start it.
11.7What the arrangement gates prove
The reserve arithmetic is written down before it is measured. Each
active track may hold two structural tokens in flight, so the resident
window books 4 × 2 + 8 = 16 queue
slots and a full sixteen-track merge books
16 × 2 + 8 = 40 — both inside
the 48-slot queue with the boot path's own margin intact.
arrangementscope then drives the assembled image and checks
the ledger: merge comparisons peak at 16 of 16, queue depth at 16 of the
40-slot full-merge budget, jitter at exactly the 20-count gate, and zero
overruns Modelled.
The cost side is charged in the open. The arrangement's far text is distributed through the generated fixed-far banks; the near ledger owns the restart cache and shared detail reader under the same named-owner rules as §3.2. The four-track/eight-placement committed window remains an implementation bound. Natural completion now waits for the deadline, logical, physical, UART, and network staging queues to drain, emits MIDI Stop, and republishes the Arrange transport row. The last two fixes in the current branch align editor overlays and redraw that automatic stop state. Timing remains model evidence unless a table explicitly names a physical capture.
Chapter 12The interface as a projection
The interface reads semantic state and emits MDA cells. It does not add display fields to the song model. That separation lets one keyboard, one monitor protocol, and optional view providers use the same transaction rules.
12.1No screens in the song
Canonical records contain stable IDs, revisions, values, and relationships. They contain no screen name, row, column, glyph, viewport, or cursor. A view asks the query layer for a bounded semantic projection. The renderer then assigns that projection to rows 2 through 22 inside the stable frame from §1.7.
This is an ownership rule. The song service owns truth and revision checks. The view registry owns which projection and actions a surface supports. The renderer owns cells. Removing a view cannot orphan canonical state, and adding one cannot create a new mutation path.
12.2Registry and session
The view registry is versioned. Each entry declares a semantic kind, projection provider, action set, editor authority, inference contract, and damage policy. Built-in views and a managed view provider enter through the same lookup. An unknown kind or incompatible schema returns to the last resident view.
Ephemeral interaction state lives in a 768-byte arena at
90000h. The complete header is a transaction/control record.
Ordinary rendering uses a smaller capsule containing the current view,
interaction mode, viewport, cursor, modal, status, and dirty depth. That
avoids copying and decoding fields a row composer cannot use. Near kernel
state retains only a 24-byte invalidation, motion, and service handoff
Confirmed.
| Address | Bytes | Owner |
|---|---|---|
| 90000h | 768 | Versioned session arena |
| 90300h | 384 | Mutually exclusive projection workspace |
| 92480h | 4,000 | Ordinary-RAM MDA cell shadow |
| 93420h | 250 | Per-cell shadow validity |
| 936B0h | 292 | 8087 saver state |
12.3Rendering one bounded row
Projection publication and visible damage are separate foreground quanta. A provider first publishes retained semantic state and returns. Generic damage then composes at most two authoritative rows in an interactive turn. A background full refresh emits one row per turn. Event, deadline, and output reserves are rechecked before every UI quantum.
The composer writes a 160-byte row in the session arena. The assembly leaf copies the requested span to MDA and to the ordinary-RAM shadow. A later write validates only those cells. Full redraw invalidates its row before the write. Crash output bypasses the shadow because it never returns to the foreground renderer.
A DOSBox-X interaction capture measured the visible response separately from full-refresh completion. Pattern-to-Arrange navigation became visible in 77.96 ms in the Plan 7 capture and 89.63 ms with the Phase 7.5 monitor surface; the remaining full refresh completed in 242.89 and 240.21 ms. Form feedback became visible in 97.99 ms. These are emulator workflow measurements, not 5160 latency claims Confirmed.
A form edit may invalidate its value row and one dependent row. The damage gate therefore permits 60 MDA cells, not 30. The bound names the value dependency; it does not assume that one field affects only one row Confirmed.
Lab 12.1 Retire bounded view damage Advance one navigation request from a Set-1 key event to semantic publication and row emission.
The instrument separates authority from display work. The canonical generation remains unchanged throughout this navigation request.
12.4Workspaces and editors
The 384-byte workspace is resumable scratch, not a screen shadow. Only one projection owns it at a time. A provider copies values into the workspace, yields, and resumes from explicit cursor state. The renderer receives no cache handle or canonical pointer.
A specialist action may retain its completed projection page for the
following semantic-context turn. Resident code clears provider
SOURCE_READY while that page is retained. A module response to
an older operation-70 page then receives BUSY from operation
71 instead of replacing shared scratch beneath the new context. Source-ready
returns after context publication Confirmed.
Every specialist work bit — action, transaction, patch, refresh, context, and dirty work — is independently sufficient to enter the renderer. Retiring the generic service bit cannot strand an accepted action. When seven dirty rectangles overflow, the redraw resumes after the completed context stage instead of rebuilding the page and context. The recorded editor-scope maximum fell from 51,890 to 34,541 PIT counts against the 40,000-count foreground ceiling Modelled.
Insert, Delete, and field adjustment enter the common typed editor dispatcher. Keyboard input, monitor commands, inference proposals, and module actions therefore reach the same validator. An edit creates a candidate or preview. Accept, reject, and one-level undo are the publication controls. An editable cell never mutates committed state directly.
Editor overlays declare columns per view. Tracks places the kind at column 7 and the instrument in columns 10 through 23; Arrange uses columns 5 and 7 through 12. The value dock clears columns 1 through 78 before it composes a new value. This prevents old capacity text from surviving beneath a shorter value and makes preview alignment part of the UI scenario Confirmed.
Specialist projections preserve their selected semantic identity across refreshes. When a commit removes that ID, the projection chooses the surviving visible ID with the smallest unsigned semantic-ID distance, then sub-object distance. It records the repair for both the warning row and the inference snapshot. A stale selection cannot silently name a different object.
12.5The 8087 tunnel saver
The saver resides in fixed-far bank 13. Two of its eight rings advance on each 48 Hz tick. The foreground performs waited 8087 projection; IRQ paths only update the coherent passage diagnostics the saver reads. The state is 292 bytes: 128 current cells, eight ring records, P39 pacing, and passage-reactive seed and energy.
The saver is explicit. When inactive it installs no timeout, hook, or
animation poll and mutates no saver state. A regression runs 60,000 modeled
CPU steps before activation and after dismissal and requires the full state
to remain byte-identical. The optional s86m-saver module
renders one fixed snapshot for hot-reload visual work; it is not the
resident animation.
12.6What the UI gates prove
make test-ui checks the codecs, registry, controller,
semantic providers, editor dispatcher, and row spans. make
test-ui-system boots the assembled image and drives complete keyboard
scenarios. make ui-scope charges projection and damage under
the cycle-counting model. make saver-scope runs the longer
resident-inertness and 8087 geometry gates.
The same scenario catalog can run sequentially on the physical machine
with tools/ui_hardware_test.py. Each scenario starts from a
warm reboot and leaves exact MDA checkpoints plus a result record. Automation
is observed as decoded network MIDI rather than inferred from screen state.
BUG-009 and BUG-010 were found on the 5160; BUG-011 was found by the
cycle-counting editor scope. The bug record keeps those evidence domains
separate Confirmed.
The ordinary UI suite also enters and dismisses the saver. Physical acceptance on the IBM 5160 verified passage reaction, display restoration, transport control after dismissal, and zero transport overruns Confirmed.
Chapter 13Control and automation
Automation stores either recorded points or generated segments. Playback converts both forms into the same bounded control stream, resolves conflicts deterministically, and admits output before it reaches a device.
13.1Recorded and generated data
A recorded point retains its stable point, sequence, anchor, and touched anchor revision. Its signed Q16 pulse time and value are exact observations. Duplicate times remain separate point IDs. Editing one point does not merge, thin, or retime another.
A generated segment names both endpoint IDs, times, and values. It also
names the transfer function, sampling cap, target quantization, fallback,
and endpoint policy. HOLD emits no intermediate samples and
stores a zero cap. LINEAR requires a positive cap no greater
than 96 updates per second per MIDI port. Recorded and generated records
stay distinct in storage and in the inference contract.
| Form | Record | Invariant |
|---|---|---|
| Sequence | 40 B | Stable target, track priority, form, interpolation, and activation |
| Recorded point | 36 B | Exact retained value and signed Q16 pulse time |
| Generated segment | 56 B | Explicit endpoints, HOLD/LINEAR policy, cap, and quantization |
| Trigger context | 28 B | Stable trigger and attached control-event revisions |
13.2Exact generated endpoints
Transport start is an output-publication barrier
Stopped foreground work may compile semantic events and logical tokens. A registered output provider may inspect the oldest stopped token, but its answer remains retained. Built-in physical and network serializers do not consume ordinary lookahead until a start is committed. During that pending handoff, the output projection catches the logical queue tail; only then does the kernel publish live transport. Initialization and panic flushing remain available while stopped. This keeps generated values in the run that owns them instead of emitting them during an edit.
The LINEAR compiler uses an integer digital differential analyzer. It divides the total value change into an integral step and a retained remainder. Each sample advances the error term and distributes that remainder without floating point. The first and last output values equal the declared endpoints exactly. Restarting the segment from its canonical origin reproduces the same sequence.
The 96 Hz cap bounds generated work. Admission charges clock traffic and already-booked note/control output first, then permits automation only inside 75% of the remaining wire capacity. A full budget removes generated edits from the inference schema, and the resident validator rejects a hand-built bypass.
13.3Conflict resolution
Every candidate receives one total ordering key: actual time, track priority, anchor order, then payload order. Lower numeric track priority wins an exclusive conflict. Continuous sources hand off at the value computed for the handoff time. Raw messages keep their order and are not suppressed merely because their timestamps match.
The resolver emits the winner and records diagnostics for each loser. The diagnostic names the target, source IDs, conflict class, and winning priority. A conflict therefore cannot disappear as an undocumented output omission.
Lab 13.1 Resolve an exclusive control conflict Order three same-target candidates and publish one value with loser diagnostics.
All three candidates target the same exclusive parameter. Priority 1 precedes priority 3. The later anchor and payload fields settle only equal-priority ties.
13.4The automation transaction
Keyboard editors, monitor commands, inference, and modules encode the same typed control transaction. The dispatcher checks authority, target identity and revision, record bounds, generated bandwidth, and safe activation. It then builds one candidate state. Accept swaps the existing 1,030-byte committed and candidate states. Reject clears the candidate. Undo performs the same bounded swap in the other direction.
Current module ABI 2.10 operations 74 through 77 expose that exact preview, accept, reject, and undo lifecycle. The ABI allocates no third automation state. Committed playback and its hot queue do not observe the candidate before accept.
13.5Specialist providers
Automation ABI operations 54 through 67 page copied targets, sequences, recorded points, generated segments, conflict diagnostics, and bandwidth state. A provider receives stable IDs, revisions, and copied records. It returns a bounded proposal and yields. Resident C performs complete validation before candidate publication.
The control specialist view publishes a semantic page from those same
records. Its editor actions enter the common dispatcher, and its inference
snapshot begins with S86C. A model cannot reinterpret a drawn
lane: the request states whether each object is recorded or generated and
carries the exact bandwidth left for generated work.
13.6What the automation gates prove
make test-automation covers duplicate-time retention,
independent point edits, exact DDA endpoints and remainder distribution,
restart from origin, trigger coupling, priority and handoff, equal-limit
bandwidth refusal, stale target refusal, typed placement creation, and
attempts to thin or convert recordings. The inference contract runs under
both test-inference and test-automation.
The assembled UI scenarios exercise recorded and generated editors,
preview, accept, reject, undo, and specialist projection. Target builds
keep the private automation runtime inside 90480h–9247Fh
and retain the 96-update/s per-port limit
Confirmed.
On the physical backend, those scenarios select N02 through the ordinary Destination modal and inspect decoded network-MIDI bytes. The generated segment case requires its endpoint value during the live run. That path exposed the stopped-lookahead publication defect described in §13.2.
Chapter 14Persistence and extensions
SEQ/OS stores immutable 512-byte pages inside one contiguous file on a standard FAT16 image. Catalog roots publish last. Extensions store provider identity and bounded parameters, never executable addresses.
14.1A store inside FAT16
The interchange disk is a standard 32 MiB FAT16 image.
tools/seqstore.py prepare creates one contiguous,
sector-aligned SEQSTORE.S86 file, 24 MiB by default. It
initializes the catalog and allocation pages and records a nonzero media
identity plus the exact extent page count.
The XT does not allocate clusters and does not edit the BPB, root directory, or FAT. Its discovery machine accepts only 512-byte sectors, scans at most two root entries or 32 FAT links per poll, requires the exact 8.3 name, and exposes the first LBA only after the complete chain is proved contiguous and agrees with the file size. Logical page IDs remain relative to that qualified extent.
Host and resident responsibilities
A host may copy, back up, and inspect the FAT16 image with ordinary tools. It must prepare or defragment the extent while SEQ/OS is not using it. The resident then repeats discovery. A previous first LBA is never treated as durable identity.
14.2Catalog and transfer
Catalog entries name media identity, song identity, generation, root pages, allocation state, and descriptive metadata. Lookup copies and validates at most one 64-byte page slice per poll. Allocation scans 64 bits per poll. Reclamation marks the immutable roots it can reach and frees only pages outside the live and retained sets.
Import, export, and migration are streaming state machines. They validate page envelopes, checksums, generation, and link records before publication. Migration rewrites page references for the destination extent and rebuilds each checksum. A bad page, link, destination, media identity, or cancellation terminates without publishing a partial catalog entry.
14.3Crash-safe publication
A transaction builds a new immutable graph beside the current one. It writes content pages first, then their index pages, then the resource or catalog root. The inactive superblock root slot is written last. The committed query and IRQ projection continue to use the old generation until that final flush completes.
Each foreground poll performs at most one 64-byte far copy or advances one page-store operation. After the root changes, the publisher invalidates cached pages so an alternating page ID cannot retain bytes from the old generation. Undo publishes the before-image as a newer generation; it does not move the generation counter backward.
Lab 14.1 Publish the inactive root Write one replacement graph child-first and select the recoverable generation after each durable boundary.
The active superblock continues to name generation N while generation N+1 is incomplete. A power cut at any intermediate step therefore selects N.
14.4Modules by content identity
The managed-module registry occupies 212 near bytes. It admits one S86M image and at most eight provider descriptors. The manager records a BLAKE2s-128 image identity, build ID, ABI and capabilities, timing budget, test evidence, fallback, activation class, and bounded call diagnostics.
The loader still validates the 20-byte S86M header, full-image XOR16, current ABI 2.10, entry and workspace bounds, and capability mask. Content identity adds lifecycle authority: a remote request may name a previously approved image, but a new content hash requires explicit local confirmation. Unload quiesces providers, closes their outputs, rejects incomplete previews, and restores the resident fallback before releasing the image.
14.5Macros, reduction, and seek
Extension records use a separate S86X envelope inside
canonical pages. Macro records are 84 bytes, harmonic contexts 96,
reduction request or preview records 112, and checkpoint-directory records
52. Provider-driven records begin with the same 56-byte identity: module
hash, stable provider ID, kind, major version, declared fallback, and at
most 32 parameter bytes. Reserved bytes must be zero. No segment, entry
offset, or module-private state is persisted.
A macro stage admits at most 16 records only when their declared work and expansion fit the caller's limits. It runs them in stage, priority, and object-ID order. A reduction builds a separate immutable candidate root, rechecks the source revision and protected-event count, and retains the old root for undo only after accept.
Checkpoint lookup reads at most eight directory records per poll and accepts only the current song generation. Greatest position at or before the target wins; greatest checkpoint ID breaks a tie. The seek plan restores nothing until the body checksum, condition and control hashes, and all bounded state counts match. Missing or invalid checkpoint data falls back to deterministic reconstruction from position zero.
14.6The physical boundary
The current boot image still uses an eight-page RAM page-store backend and
the 128-slot canonical read cache. FAT16 discovery, catalog, transfer,
allocation, reclamation, extension execution, and the modeled PicoMEM
command-88 adapter all compile for IA-16. The command-88 client is not in
RESIDENT_OBJECTS and is not connected to the page-store
poller.
Physical persistence is therefore unsupported. Closing that boundary requires qualification of the installed PicoMEM firmware handshake, connection of the bounded single-sector adapter, and the complete assembled storage scenarios on the 5160. The host FAT16 image and its tools are usable now; that does not imply that the resident writes the SD card.
14.7What the storage gates prove
make test-storage-extensions covers FAT16 discovery,
contiguous-chain refusal, catalog pages, lookup, allocation, reclamation,
transfer, migration, import/export framing, managed-module lifecycle,
extension codecs and execution plans, reduction transactions, and
checkpoint seek. The power-loss harness interrupts publication after every
64-byte work unit and accepts only the complete old or complete new
generation.
A clean bfb60f2 run passed all storage-extension groups,
including 18 extension tests and 22,261 checks
Confirmed. It proves the software and artifact
contracts. It does not close the physical backend boundary in
§14.6.
Chapter 15Performance on the 8088
The 8088 executes from a four-byte prefetch queue fed by an eight-bit bus. Hot-loop performance depends on encoded bytes, bus use, and taken branches as well as published instruction clocks. SEQ/OS applies that model, then keeps a change only when the relevant end-to-end gate passes.
The method follows Michael Abrash's measurement-first optimization work: establish a repeatable timer, account for the processor's fetch behavior, change one mechanism at a time, and test the complete program before keeping a local win. Zen of Assembly Language supplied the immediate inspiration; the later Graphics Programming Black Book preserves the same method. The routines below are SEQ/OS implementations, not copied listings.
15.1Four bytes ahead
The installed AMD D8088 has the stock four-byte queue Confirmed. Its bus interface fetches one code byte in a four-clock bus transfer whenever the bus is free and the queue has room. The execution unit consumes those bytes independently. Data reads, writes, I/O, and DRAM refresh can prevent a refill. When the execution unit needs a byte from an empty queue, it waits.
A taken branch, call, return, interrupt, or other control transfer replaces IP and discards queued bytes from the old path. The target begins with an empty queue. A compact straight-line loop may therefore beat an instruction with a lower execution-unit rating, while an unrolled loop may lose because its extra bytes take longer to fetch than the removed branches saved.
| Event | Condition | Cost represented |
|---|---|---|
REFILL | Bus free and queue not full | One code byte enters after a four-clock transfer |
STARVE | Execution unit requests an absent byte | Execution waits for instruction fetch |
FLUSH | Taken control transfer | Old-path bytes are discarded; target fetch restarts empty |
| Refresh hold | DRAM refresh owns the bus | Neither code nor ordinary memory transfer can proceed |
| Regional fetch | Code executes from a slower memory tier | Qualified memory penalty applies to instruction bytes |
Lab 15.1 Account for the prefetch queue Step through refill, starvation, a taken-loop flush, and REP streaming on the four-byte queue.
This instrument shows queue state at instruction boundaries. It is a teaching view of the modeled mechanism, not a bus-cycle trace.
15.2Shorter can be slower
Encoded size matters because every instruction byte crosses the same eight-bit bus. It is not a sufficient score. Execution-unit work can give the bus interface time to refill, and an instruction that finishes sooner can expose the next fetch stall.
The 48-bit pulse product provided a controlled example. Replacing its
six-byte ADD/ADC doubling chain with a six-byte
SHL/RCL chain preserved results. The rotate form saved 9 to 33
clocks for several multipliers, tied for one, and lost 11 clocks in the
65,535-pulse all-add case: 1,993 clocks versus 1,982. The shorter
execution-unit work left the queue less refilled before the next loop fetch.
The ADD/ADC form remains.
A same-bank call rewrite produced the same lesson. A normal far
CALL plus RETF modeled at 88.943 clocks.
PUSH CS, near CALL, and RETF saved
two static bytes but modeled at 90.003 clocks. A true near
CALL/RET modeled at 62.473 clocks, but it requires
a proved local-only entry and caller set. That is an ABI change, not a safe
linker substitution.
Small mechanical changes still win when their fetch and flag contracts are clear. Removing one dead flag test cut a guarded path from 126.8 to 119.4 clocks and removed two bytes. Replacing variable-count shifts with fixed shifts cut one checked range sequence from 97.8 to 73.6 clocks.
15.3Move work out of loops
The largest assembly wins removed bookkeeping from each data item. IRQ0 originally copied a packet into the 96-byte MIDI ring with a byte load, store, pointer update, wrap normalization, decrement, and taken branch for every byte.
.copy:
mov al,[es:si]
mov [output_state+di],al
inc si
inc dl
cmp dl,MIDI_QUEUE_BYTES
sbb al,al
and dl,al
xor dh,dh
mov di,dx
dec cl
jnz .copy
mov dl,[output_state+MIDI_QUEUE_TAIL]
xor dh,dh
push ds
push es
pop ds ; DS = packet
pop es ; ES = resident ring
mov di,output_state
add di,dx
mov al,MIDI_QUEUE_BYTES
sub al,dl
cmp al,cl
jae .one_run
sub cl,al
push cx
mov cl,al
rep movsb
mov di,output_state
pop cx
.one_run:
rep movsb
push ds
push es
pop ds ; restore resident DS
pop es ; restore packet ES
The 12-byte microbenchmark fell from 1,765 to 548 clocks. The complete
in-image release path fell from 669 to 367 PIT counts. Wrap and non-wrap
cases retain byte order. IRQ0 saves and reverses DS and ES before the string
copy, then restores them before publication; IF remains clear while that
temporary map is visible. The current routine is in
src/asm/output_driver.inc
Modelled.
The provider checksum made the same change without REP. Its
original top-tested loop fetched comparison, branch, indexed XOR, pointer
add, count subtract, and jump for every word. The kept form lets
LODSW combine load and pointer advance and lets
LOOP own the bottom test.
.checksum:
cmp cx,2
jb .odd
xor dx,[di]
add di,2
sub cx,2
jmp .checksum
xor dx,dx
shr cx,1
pushf
jcxz .oddcheck
.words:
lodsw
xor dx,ax
loop .words
.oddcheck:
popf
jnc .checked
xor dl,[si]
A 4 KiB checksum fell from 182,315 to 57,139 clocks in the isolated
benchmark and from 46,720 to 27,326 PIT counts in the complete provider
path. Four-way unrolling was another 15% faster but did not fit the fixed
stage-2 space, so the plain streamed loop shipped in
src/asm/module_driver.inc.
The pulse-to-PIT product began as portable C with six live 16-bit words
repeatedly spilled around a shift-and-add loop. The current assembly leaf
keeps the three-word term in SI:DX:BP, result in
AX:BX:DI, and multiplier in CX. It leaves
interrupts enabled and avoids MUL, whose 144–184
uninterruptible clocks can exceed the IRQ-entry budget by itself.
u16 result0 = 0, result1 = 0, result2 = 0;
u16 term0 = value.lo, term1 = value.hi, term2 = 0;
while (pulses != 0) {
if (pulses & 1u)
add three words with carry;
pulses >>= 1;
if (pulses != 0)
shift three term words left;
}
.Lbit:
shrw %cx
jnc .Lnoadd
addw %si,%ax
adcw %dx,%bx
adcw %bp,%di
.Lnoadd:
jcxz .Lstore
addw %si,%si
adcw %dx,%dx
adcw %bp,%bp
jmp .Lbit
At 65,535 pulses the leaf fell from 2,376 to 495 PIT counts, a 4.8-fold
improvement. The public call including surrounding work fell from 2,611 to
730. The current leaf is src/asm/pulse_product.S.
Representative large advances save about 0.5 to 1.6 ms
Modelled.
15.4REP needs a segment contract
REP removes repeated instruction fetch and branch work, and
the 8088 recognizes interrupts between iterations. It does not choose the
right segments. The first IA-16 bytes_copy prototype assumed
DS still addressed resident data inside a medium-model far
body. GCC used DS as a temporary, and the fast copy deterministically
corrupted an arrangement generation.
cld
shrw $1,%cx
rep movsw
jnc .done
movsb
.done:
pushw %ds
pushw %es
pushw %ss
popw %ds
pushw %ss
popw %es
cld
shrw $1,%cx
rep movsw
jnc 1f
movsb
1: popw %es
popw %ds
Resident near data and the C stack share SS=0, so the kept helper
establishes both string segments from SS and restores compiler-owned DS and
ES. The inline implementation is in src/bytes.h. A 240-byte
copy fell from 30,414 to 3,474 clocks; a 376-byte arrangement
copy fell from 47,550 to 5,275. The matching SS-qualified
REP STOSW clear cut 64 bytes from 5,603 to 791 clocks and 512
bytes from 43,079 to 4,047. The 768-byte UI-arena initialization became
about 12 ms shorter Modelled.
15.5Measure the whole machine
The Black Book pass ranked mechanisms, built isolated A/B images, ran the cycle model, and then used nonce-bound profiler captures on the IBM 5160. The physical step is decisive because a local win can move work into another bank, alter bus traffic, or reduce the number of foreground turns.
| Candidate | Model result | Physical result | Decision |
|---|---|---|---|
| Park live arrangements between refill edges | kernel_poll entries 1,448→350; bank-2 share 47.62→10.42% | Empty polls/s 197.97→71.63; kernel share 71.55→39.03% | Keep |
| Compact productive event prefix | Mean event-prefix work fell 31–34% | Event mean 5.83→5.42 ms, but warm reboot missed its post-ACK execution budget | Reject |
| Event execution-index plan | Lookup work fell 42.6–61.7%; prefill 27.5–33.3% | Fixed-far event reads fell 15.71%; minimum window depth rose 4→5 | Keep |
| Batch event-window insertion | Local model improvement | Reads +3.59%; writes +7.17%; event time +0.83%; loops/s −8.61% | Reject |
| Compare MDA before writing | Fewer writes appeared desirable | Equal scan 1.13× slower than blind copy; all-changed scan 4.10× slower | Keep RAM shadow |
Arrangement parking also exposed a correctness requirement. The first C publication used interrupt masking and raised modeled jitter to 84.73 PIT counts. The kept assembly-owned set-and-recheck handshake leaves IF enabled: it publishes the parked proof, rechecks IRQ1's queue and overflow state, and lets IRQ1 clear the proof after publishing input. The monitor suite injects IRQ1 at each handoff boundary.
The MDA result is a direct bus measurement. Blind planar-to-MDA copy took
3.54 ms. Scanning an equal row took 4.00 ms and wrote nothing. Scanning an
all-changed row took 14.51 ms because each mismatch restarted
REP after a compare and control-flow change. The ordinary-RAM
shadow retained the useful comparison while avoiding MDA reads. In the
physical UI capture, UI calls fell 677→500 and full rows
458→304; kernel share fell 48.37→42.35%.
15.6The optimization ledger
| Change | Before | After | Reason it held |
|---|---|---|---|
| Monitor idle frame | 308.1 clocks | 185.3 clocks | Two-register frame removed repeated memory work; in-image 62→28 PIT |
| IRQ0 12-byte packet copy | 1,765 clocks | 548 clocks | At most two streamed runs replace per-byte bookkeeping |
| Provider checksum, 4 KiB | 46,720 PIT | 27,326 PIT | LODSW/LOOP folds pointer and loop work |
| Pulse product, 65,535 | 2,376 PIT | 495 PIT | All seven live words remain in registers |
| Fixed B-tree search | spilled scale factor | −151 to −204 PIT/page | Shift operand stays in a register; linked growth 3 bytes |
| 240-byte resident copy | 30,414 clocks | 3,474 clocks | SS-qualified REP MOVSW |
| 64-byte resident clear | 5,603 clocks | 791 clocks | SS-qualified REP STOSW |
| 16/32-bit stores | 307 / 420 clocks | 227 / 299 clocks | Direct SS-qualified word stores replace byte splitting |
| Network IPv4 equality | 617 clocks | 340 clocks | Two word comparisons replace a four-byte loop |
| Receive-ring bookkeeping | recomputed inside loop | 0.9–1.2 ms saved | Invariant address and capacity work moved out |
Lab 15.2 Apply the optimization ledger Compare local instruction changes with their complete-path measurement and keep or reject each candidate.
15.7What the gates prove
make test-prefetch checks effective
LODSW, STOSW, and MOVSW rates and
regional code fetch. Focused microbenchmarks assert output equality before
comparing cycles. In-image scopes call the linked routine through the real
gate and include argument, segment, and return work. The canonical
make test gate then checks that a local timing win did not
change boot, monitor, UI, storage, or musical behavior.
Physical A/B runs use separate nonce-bound images, aligned profiler windows, the same scripted workload, and the hardware lease. Model numbers guide the search; hardware captures decide claims about the IBM 5160. The accepted ledger preserves zero deadline, event, and logical underflows, zero scheduler overruns, and zero MIDI drops in its reported physical workloads Confirmed.
Appendix AOpen questions
The current software boundary is broad. The remaining items below require hardware qualification, a planned storage migration, or a new timing authority. They are not implied by the completed model and emulator gates.
| Boundary | Current state | Closure condition |
|---|---|---|
| Resident physical persistence | FAT16 discovery, catalog, transfer, extension, and command-88 adapter model exist. The boot image still uses the eight-page RAM store | Connect the installed PicoMEM command-88 path to the resident page-store poller, qualify firmware identity and shared-memory layout, then pass assembled storage scenarios on the 5160 |
| Physical serial destinations | Five connectors, four populated UARTs, and three responding bases are inventoried. Only COM1 is declared musical; individual card clocks and routing remain unqualified | Identify UART and oscillator parts, measure 31,250-baud output and IRQ behavior, then add only fixed declarations that pass the timing gate |
| Physical MIDI receive | The decoder, timestamp tuples, correlation, SysEx bounds, and receive model exist. No receive vector is installed and no IRQ is unmasked | Select an installed UART, crystal, base, and nonconflicting IRQ; derive disassembly timing; then run physical byte, realtime-interleave, and overrun tests |
| Parallel gate output | The logical eight-bit gate queue and transport flush are tested. The supported image does not execute OUT 3BCh |
Build the protected breakout, qualify electrical levels and timing, and add an explicit physical-enable declaration |
| Full paged arrangement semantics | The target keeps four committed tracks and eight placements near. Non-selected patterns retain structural playback but not every note, event, and control chain | Move the directory, placement index, records, restart pages, and arbitrary retained semantic chains through the canonical page graph while retaining 16 bounded active cursors |
| One global network clock | COM1 owns transport clock and commands. Network N02/N03 carry timestamped musical events with measured lookahead | Declare a single transport counter and queue clock, start, continue, stop, cancellation, and all-port timing against it; qualify multiport hardware or gateway behavior |
| Near-text forecast | The clean bfb60f2 image leaves 13,445 bytes; planned work and contingency consume 11,752, leaving 1,693. Fixed-far margin is 28,351 bytes |
Keep every new near owner in the generated manifest and rerun memory-scope. A far bank does not repair a near-text failure |
Capacity is a gate, not spare ownership
The two unallocated 16 KiB upper-memory fragments and conventional
far_reserve do not count as configured fixed-far capacity.
Chapter 3 reports only the thirteen boot-loaded banks. A future feature
must qualify and configure new capacity before charging code to it.
Appendix BGlossary
Glossary terms are interactive in the text. The first occurrence in each section is marked; selecting it opens a short definition and a link to the full entry. Later occurrences remain unmarked. Each entry lists its aliases in small print.
- 8087x87 · floating-point coprocessor
- Intel's floating-point coprocessor, socketed beside the 8088 and required here: the boot probe is a gate, not a capability check, and the machine halts on a fault banner rather than run without it (§2). It serves foreground arithmetic — tempo synthesis, pitch math — and is banned from IRQ0, the UART path, and every deadline seam.
- admission
- The compile-time bandwidth promise (§9.5): sustained output books at most 75% of the wire that remains after the clock, and no single instant may exceed the serializer's 72-byte burst ceiling. A workload over either face is rejected or routed to another destination before a token is published — never thinned mid-performance.
- boundary deadline
- A structural instant — a step edge, a Gbar edge — pushed into a 32-entry queue by the compiler ahead of playback. IRQ0 consumes the due head one per wake and publishes the boundary that the playhead and event windows hang from (§7). A push that undercuts the armed alarm retargets the one-shot under a brief interrupt mask (§9.2).
- crossed deadline
- A target that became due while the handler that chose it was still executing — inevitable in a single-interrupt design, since no handler can arm the one-shot inside its own remaining span (§9.4). Found within 2,048 counts of a fresh latch, it is served by a bounded follow-up wake and recorded as physics, not failure.
- declared backlog
- The scheduler's state for “more is due than one wake may do.” The handler performs its one bounded unit, marks the reservation, and re-arms a fixed 128 counts out, so a chord's eight packets drain at a measured rate instead of a loop with interrupts masked (§9.4). While declared, an empty boundary queue is continuation, not underflow.
- drift
- Long-term slide of the clock grid against ideal time. The F8 instant advances by an exact fixed-point period — from the previous ideal, never from “now” — so late service cannot accumulate into phase error; the gates hold fitted drift below 1 ppm (§9.6).
- epochextended time
- The upper word of extended time. The hardware counter is sixteen bits — 54.93 ms per wrap — so a single authority joins each fresh latch to an epoch word, advancing it exactly when a sample reads below its predecessor. Sound only while latches land less than one wrap apart, which the clock grid guarantees down to the 46.0 BPM floor (§9.3).
- far textfar bank
- Code placed in the fixed banks above the near segment and reached
through
retfgates: far is the default placement, near is a proved property or an exact waiver (§3, Lab 3.1). Thirteen configured banks contain linked code; a four-instruction stub is all a near caller sees. - foreground
- Everything that is not IRQ0: the kernel poll loop and its compilers, projection, the monitor, the network. It owns all policy, publishes queues tail-last, and is sliced so no single poll exceeds the 40,000-count gate even inside a chord's interrupt-dense stretch (§9.4).
- gatewaytools/seqgw
- The bounded host-side service between the XT's one TCP connection and the model server: eight sessions at most, contract dispatch on a four-byte key, prompts, generation schemas, normalizers, and the compute sandbox (§10.3). It holds preview-only authority — every proposal it forwards is validated again by the kernel.
- Gbar
- The global bar: the 256-slot structural cycle that placement, ratio conditions, and boundary generation phase against (§6). One Gbar pass is the unit a condition answers once per.
- ghost previewghost state · ghost row
- Preview state drawn over the committed pattern on the MDA: a proposal that has passed kernel validation but has not been accepted. Visible, never audible — playback continues from committed state until ENTER publishes the preview as a new revision (§10.1, §5.5).
- hot queuelogical token
- The 128-entry ring of 32-byte logical tokens — START, EXTEND, STOP — that compiled music waits in (§8.3). The foreground publishes tokens with their deadlines resolved; IRQ0 consumes the due head and releases the projected packet, one per wake.
- IRQ0
- The timer interrupt, and the only code that runs at a deadline. Each wake does one bounded unit — an F8, a queued byte, a released packet, a consumed boundary — re-arms the one-shot to the nearest deadline, and returns; nothing under it loops by queue length or calls far (§9.1). Entry latency is gated at 20 counts.
- jitter
- Wobble between when a wake was due and when it landed. Measured at interrupt entry against a 20-count gate, and as growth of clock-fit residuals under module and network flood (§9.6).
- logical gate
- A sounding logical note — opened by START, sustained by EXTEND, closed by STOP — with pitch and device still unresolved (§8). At most 24 are open at once; the 25th is refused at compile time.
- MDA
- IBM's Monochrome Display Adapter: 80×25 text cells at
segment
B000, one character and one attribute byte each, driving the 5151. The whole interface is rendered by writing that buffer directly — no BIOS call survives boot (§1). - MIDI clockF8
- The F8 realtime byte, 24 to a quarter note, first in priority at the wire: the guard reserves its slot a full byte-time ahead (§9.2). Its ideal instant advances by an exact fixed-point period, so the grid holds phase through any burst.
- monitor
- The framed-binary control channel on COM2 — ping, state, peek, poke, load, run — and the same packets over TCP through the NE2000 (§4). It is how tests, tools, and the inference gateway see a running machine without stopping it.
- NE2000
- The ISA Ethernet card at I/O
0x300, polled from the main loop — its interrupt stays masked. Carries the monitor transport and the inference gateway's TCP connection (§4). - one-shot
- A timer mode that counts a loaded value down once, fires, and stops. Channel 0 runs this way: armed at the close of every IRQ0 to the nearest known deadline, so the machine is interrupted exactly when something is due and never on a period (§9.1).
- overrunsched_overruns
sched_overrunsincrements only when a wanted wake is found more than 2,048 counts in the past with no declared backlog — a genuine scheduling loss, distinct from a crossed deadline (§9.4). The timing gates require every workload to finish at zero.- physical voice
- One of 24 fixed records binding a sounding note to its channel, note number, and bend state. Projection allocates it, STOP releases it, and panic tears every one down in a scheduled, bounded sequence — never a loop with interrupts masked (§8).
- PIC8259
- The 8259 interrupt controller. SEQ/OS masks it to
0xFC— timer and keyboard only — and every IRQ0 ends with the end-of-interrupt write at port0x20(§1). - PIT8253 · programmable interval timer
- The 8253 programmable interval timer: three 16-bit counters fed 1,193,182 Hz. Channel 0 is the one-shot alarm, channel 2 free-runs as the reference counter anything may latch, channel 1 is DRAM refresh and untouched (§1, §9.1). One count — 838 ns, four CPU cycles — is this manual's native unit of time.
- placementsong_placement_record
- The 24-byte record that puts one pattern on one arrangement track (§11.1): a 32-bit start pulse on the 960 PPQN grid, a pass count from 1 to 65,535, a mute state, and a revision policy. Its end is computed by checked shift-and-add and validated against overlap and overflow before it can exist; four tracks and eight placements form the current resident window.
- placement cursor
- The 64-byte far record that executes one placement on one active
track — sixteen of them at
0x37000(§11.3). It carries pinned revisions, pass and Gbar counters, the next compiled head, and a status word; a placement boundary always constructs a fresh one, and condition phase is recomputed from the pass counter rather than stored. - PPQN
- Pulses per quarter note. The wire clock is 24 PPQN — the MIDI standard — while patterns resolve time internally at 960 (§6).
- projection
- The foreground pass that turns a logical token into a physical packet: read the route, resolve pitch to note-plus-bend, allocate or steal a voice, and write the bytes IRQ0 will later release (§8, §9.4). Sliced so each poll carries a bounded number of far-window transfers. Ordinary built-in projection holds stopped lookahead until the pending-start handoff.
- semantic event
- A compiled occurrence window from the event pipeline (§7): 64 fit the queue, and IRQ0 consumes every due head each wake. Together with boundaries they carry the song's structure; the hot queue carries its notes.
- restart point
- Pulse 0, or any timeline instant no placement straddles — the only places playback may begin (§11.2). The resident keeps a nine-entry cache stamped with the generation it was computed at; a stale cache refuses everything except pulse 0, which is valid unconditionally, and the transport revalidates every requested start no matter who asked.
- pin-at-startfollow-latest · rev_policy
- A placement's answer to “which revision sounds?”
(§11.5).
FOLLOW_LATESTtracks the newest committed pattern revision;PIN_AT_STARTfreezes the revision current when the cursor started, carried in the far cursor so restart restores it. Overwriting a revision a live pin still owns is refused. - revision
- The monotonically increasing version carried by every contract state surface. A snapshot records it, a proposal must name it, and the kernel refuses a proposal whose base revision no longer matches committed state (§10.2). Accept advances it; undo republishes the before-image as a newer generation rather than moving it back.
- serializer
- The 96-byte ring between compiled music and the UART — the one elastic element in the output path (§9.2). Bursts land in it whole, bytes leave one per wake at wire pace, and admission books at most 72 of its bytes in any instant.
- state contractinference contract
- A versioned view of one resident state surface that a model may propose against. Each fixes a bounded snapshot encoding, the revision it was taken at, the operations a proposal may carry, and the preview semantics the kernel enforces (§10.2). Capability grows by adding contracts — nine resident contracts exist — not by widening any one of them.
- UART8250
- The 8250 serial chip. COM1 is the one declared physical musical port and its software contract uses the exact divisor for a 2 MHz input. The installed card clock and routing remain to be qualified. COM2 carries the monitor at 9600 (§1). The output paths use polling rather than UART interrupts.
- underflow
- A consumer finding a queue empty when structure says it should not be: the compiler fell behind its horizon. Counted per queue — logical, boundary, event window — and gated at zero; a declared backlog legitimately suppresses the boundary check while it drains (§9.4).