Skip to content

IRIX installer fixes, initial Indycam support, usability improvements#23

Merged
techomancer merged 15 commits into
techomancer:mainfrom
hanshuebner:main
May 28, 2026
Merged

IRIX installer fixes, initial Indycam support, usability improvements#23
techomancer merged 15 commits into
techomancer:mainfrom
hanshuebner:main

Conversation

@hanshuebner
Copy link
Copy Markdown
Contributor

@hanshuebner hanshuebner commented May 26, 2026

I tried installing IRIX on the iris emulator but experienced hangs while the miniroot was loaded. It took me quite some convincing of Claude Code to make it get to the bottom of the issue, which was caused by differences in how the miniroot kernel's drivers acknowledge interrupts. This PR also includes Indycam support and a couple of debugability improvements. Let me know if you'd rather want to see them as separate PRs.

Thanks for the emulator, it hit me at the exact right moment when I needed one!

hanshuebner and others added 8 commits May 26, 2026 05:52
CiSerialBackend gains an optional file mirror: when `serial_log` is set
in iris.toml or `--serial-log <FILE>` is passed on the command line,
every byte the guest emits on ttyd1 (the IRIX serial console) is
appended to the file in addition to the in-memory ring buffer.

Useful for keeping a continuously-updated, tailable transcript of an
install or test run — `tail -f` works against the file even between
iris-ci `serial-read` calls, so bursts that arrive during a
`wait-serial` block stay ordered and visible.

Host-injected bytes are echoed back by the guest tty driver, so mirroring
only guest output already captures the full transcript without
duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI control socket replaces the monitor port for orchestration, but
the monitor's debug helpers (status, regs, bt, dis, cop0, tlb, etc.)
are still useful while iris-ci drives the guest from the serial console.
Skipping the monitor server in CI mode meant connecting GDB or pausing
the CPU mid-run required a non-CI rebuild.

Drop the `ci_serial.is_none()` gate around the start_server call.
Parallel `--ci` instances on the same host will see a bind failure on
the second one (and the rest of CI mode keeps working), which is the
same behavior an interactive iris already has.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new CI socket commands matched by iris-ci subcommands:

- `iris-ci rtc-save [--path nvram.bin]`
  Persists the emulated DS1386 NVRAM/RTC state to disk (calls
  Ds1x86::save_nvram). Equivalent to the monitor's `rtc save`, but
  reachable from the CI socket — needed for unattended workflows that
  configure PROM env vars and then want them to survive across runs.

- `iris-ci cdrom-eject <id>`
  Cycles the CD changer on the given SCSI ID to the next disc in its
  `discs` list. Wraps Wd33c93a::eject_disc. Lets automation drive
  multi-disc installs without needing a human to swap discs.

A new `Machine::hpc3()` accessor exposes the HPC3 controller to the CI
dispatcher (HPC3 already owns `rtc()` and `scsi()`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring up the VINO Video-In ASIC end-to-end so IRIX vlcam/videopanel see
a real image, with three selectable sources:

- TestPatternSource: SMPTE-style colour bars + animated luma ramp.
  Default; no host capture, no permission prompts, works everywhere.
- CameraSource: live host UVC camera via nokhwa + AVFoundation on macOS.
  YUYV input, area-average downscaled to the broadcast frame size,
  YUYV→UYVY in the same pass, split into even/odd fields, paced at the
  field rate. Falls back to a black field if the camera open fails.
  Gated behind `--features camera` to keep default builds light.
- BlackSource: solid black field for headless / permission-free runs.

CDMC (the IndyCam camera control codec) gets a register-storage stub on
VINO's shared I2C bus so IRIX drivers can probe and write its registers
without errors; pixels still come from the VideoSource regardless.

SAA7191 (DMSD) gains an `is_active()` predicate so VINO can route I2C
traffic to the correct target on the shared bus, and stops complaining
when an address it doesn't own goes by — another device on the bus may
pick it up.

Wire-up: new `[vino]` block in iris.toml selects `source` and
`standard`; `Machine::new` builds the appropriate VideoSource at startup
and hands it to VINO.

Docs: HELP.md gains the [vino] config block + `vino status` / `vino debug`
monitor commands. README.md documents `--features camera`. TODO.md
replaces the old "vino - all, hook up uvc camera" line with the
remaining open work (per-port D0/D1 routing, I2C repeated-start, IRIX
visual verification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone binary that decompresses a SCSI HD CHD to a raw disk image.
Useful for setting up an Indy install on a writable raw scsi1.raw when
the compressed CHD parent must stay untouched, or for inspecting the
on-disk contents of MAME-shipped IRIX CHDs from host-side tools that
don't speak the CHD format.

Reads the input via libchdman-rs's HdImage in 1024-sector chunks (512 KB
at a time), reports progress every 5 % to stderr. Gated behind the
existing `chd` cargo feature.

Usage: cargo build --release --features chd && \
       ./target/release/chd_extract irix65.chd scsi1.raw

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: from-scratch IRIX install boots miniroot, prints the "root on ..."
kernel banner, then hangs forever — IP2 storms at ~98 k/s while the
kernel idles, no further serial output.  Two cooperating root causes:

1. WD33C93 worker thread had a classic lost-wakeup race.  `cond.wait`
   was called unconditionally on every iteration; if the kernel wrote a
   new COMMAND between the previous `process_wd_command` finishing and
   the thread re-entering `cond.wait`, the `notify_one` was dropped and
   the command stranded.  Fixed by guarding the wait with a
   `while !has_pending_command` predicate loop.

2. HPC3 kept the paired PDMA `INTSTAT_SCSI0_DMA` bit asserted forever.
   IRIX miniroot's SCSI ISR acks only at the chip level (`SCSI_STATUS`
   read clears `ASR.INT`); the PDMA-side bit stayed set, kept the IOC
   SCSI0 line raised, kept IP2 raised, kernel re-entered the ISR over
   and over.  Extended `ScsiCallback` with `clear_pdma_int()`; the
   `SCSI_STATUS` ack now calls it, and `Hpc3Irq` for the chip-IRQ
   carries a `pdma_paired` handle (set on scsi0_irq construction) so it
   can clear `chan.ctrl.INT` and `intstat.SCSI*_DMA` together.

Verified end-to-end: 5.3, 6.5.18, 6.5.22 miniroots all boot through to
the Inst 4.1 main menu.  Findings doc with reproducer + IRQ-trace
methodology at rules/irix/miniroot-install-hang-scsi0-dma-irq-storm.md.

Also defensive: MISC_INTSTAT now has a W1C write handler (was silently
dropped before), and the SCSI_CTRL FLUSH branch no longer raises a
spurious PDMA IRQ that the kernel doesn't expect.

Diagnostics that found the bug (kept as production tooling):

  - `ioc status`  — was a stub; now dumps L0/L1/MAP stat+mask+effective,
    CPU IP lines, raw interrupts atomic word
  - `hpc3 status` — was a stub; now dumps intstat (with decoded bit
    names), gio_misc, eeprom
  - `scsi wdt`    — new; dedup-coalesced ring buffer of WD33C93 register
    accesses.  Long polling loops compress to one "(xN)" line so a 4000-
    entry ring covers millions of events.  Indispensable for diffing
    the access pattern of a working boot against a hung one — which is
    how the two root causes above were tracked down.
Previously --serial-log was only honored when --ci was also passed; in
normal (non-CI) mode it was silently dropped because the SCC channel B
backend was bound to a TCP listener that didn't know about the log
path.  Running `iris --serial-log boot.log` quietly produced no file.

Add a `TeeBackend` wrapper that owns the log file and delegates
send_byte/recv_byte to the inner backend.  In `machine.rs` non-CI
branch, when `serial_log` is set, wrap the live TCP backend so guest-
emitted bytes get teed to the file in addition to whatever client is
attached to port 8881.  CI path unchanged (CiSerialBackend already had
its own set_log_file).
docs/irix-6.5.22-install.md walks through the full fresh install on
iris from PROM env → fx → miniroot → mkfs → inst, with the prefs that
make it usable (`set page_output off`) and a note on the missing
6.5.22-era Foundations 1.1 CD that constrains which optional packages
can install with a given media set.

tools/inst-resolve.py parses inst's `conflicts` output and emits a
single-line `conflicts N{a,b,c}...` resolution.  Picks the highest-
letter option whose prereqs are on a loaded distribution; falls back
to 'a' (don't install) and reports which packages got dropped and why.
Cross-references "incompat" conflicts so packages on the skip-list
aren't reintroduced via someone else's missing-prereq option.

tools/inst-drive.sh runs a parse → apply → go loop until the install
starts or convergence is reached, capturing each round's conflicts to
/tmp/cf.<round>.txt for inspection.
@hanshuebner hanshuebner changed the title IRIX installer fixes IRIX installer fixes, initial Indycam support, usability improvements May 26, 2026
hanshuebner and others added 5 commits May 27, 2026 06:54
Both serial ports (8880/tty2, 8881/tty1) and the monitor (8888) were raw TCP
sockets — telnet clients stayed in NVT line-buffered mode with local echo
and CR/LF mangling. Add a small RFC 854 state machine that, on accept,
offers WILL ECHO + WILL SGA + WILL BINARY + DO BINARY so the client drops
into char-at-a-time, host-echo, 8-bit-clean mode for the serial endpoints.
Inbound IAC sequences are filtered out; outbound 0xFF on the serial side is
escaped as IAC IAC.

The monitor uses a passive variant (no initial offers, just declines what
the client offers) so the client keeps its local line editor and echo. All
monitor write paths route through a CrlfWriter so bare \n renders as CRLF
on the wire, as NVT requires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/physical.rs: fix vino_gio_alias offset (0xFF000000 → 0xE1000000).
  0xFF000000 subtracted 16 MB (-(0x01000000)), so GIO-aperture reads at
  0x1F080000 landed at 0x1E080000 (gio_err_ptr region) and returned
  0xFFFFFFFF instead of vino's REV_ID 0xB0. The intended translation
  is 0x1F080000 → 0x00080000, which needs wrapping-subtraction of
  0x1F000000 = 0xE1000000.

- rules/irix/vino-gio-alias-offset.md: document the alias bug + fix.

- rules/irix/vino-attach-stops-after-rev-id.md: capture the deeper
  diagnostic — after the alias fix the IRIX vino driver reads REV_ID
  once and silently bails before registering a board. PROM POST does
  not touch vino (verified by reaching the maintenance menu with zero
  vino reads); the read is the IRIX kernel's vino_init. Why it stops
  is open; rule lists candidates and next-step investigation paths.

- tools/inst-watch.py: classified stall-detector for driving the
  IRIX inst program from iris-ci. Emits per-prompt events (cd_swap,
  yn_confirm, numbered_choice, inst_ready, etc.) with one-line hints.
  Polls the serial-log; fires when output goes quiet for N seconds
  AND the last line looks like a prompt. Skips false stalls during
  progress output.

- docs/irix-6.5.22-install.md: lessons from a full run-through —
  - headless + nvram console=g panics on Install System Software
    (UTLB miss at PC 0x97f9c39c) because REX3 isn't mapped; must
    setenv -f console d before option 2 if nvram was inherited from
    a graphical session.
  - from-loop discipline: scan all 6 CDs before any install/keep,
    or inst's "switch distributions" prompt evicts the catalog.
  - don't send q after the Applications CD scan — page_output off
    auto-quits the license pager, and q gets eaten as quit-from-loop.
  - new step 9: dvhtool -v delete ide /dev/rdsk/dks0d1vh removes
    the leftover miniroot stub so sash stops asking
    "miniroot install state reset" on every boot.

- rules/irix/miniroot-install-hang-...md, README.md, TODO.md:
  remove references to the MAME pre-installed CHD as a shortcut /
  fallback. Install-from-original-media is the supported path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IRIX 5.3 vino driver's vino_init() gates the entire attach path on
SYSID bit 4 at 0xBFA0001C. Disassembly of /var/sysgen/boot/vino.a's
vino_init shows:

    lui  $t6, 0xbfa0           ; MC base in KSEG1
    lw   $v0, 0x1c($t6)        ; v0 = MC[0x1C] = SYSID
    andi $t7, $v0, 0x10        ; t7 = SYSID & 0x10
    beql $t7, $zero, epilogue  ; if t7==0, bail silently

With iris's previous SYSID (0x3 for Guinness, 0x0 otherwise), bit 4 was
clear, the branch was taken, and vino_init returned without ever calling
the chip-probe / board-allocation / vid_register_board path. videod
consequently reported `Fatal server error: no boards found`.

docs/mc.pdf §5.4 documents bit 4 as "EISA bus present" — which on Indy
should be clear per spec. Real IndyCam-equipped Indys clearly do attach
vino under IRIX 5.3, so either the bit is repurposed as a
vino-board-present strap on Indy MC silicon, or there's silicon/spec
drift. Either way, the pragmatic fix for the emulator is to report the
bit set — with it set, vlinfo enumerates the vino device cleanly:

    device: vino 0
            nodes = 5
            VINO Device Controls
            Digital Video Input (= IndyCam)
            Analog Video Input
            Memory Drain 0
            Memory Drain 1

Frame DMA (vidtomem) still doesn't complete — that's a separate vino
emulation gap, not covered by this commit.

Full diagnostic / disassembly process documented in
rules/irix/vino-attach-via-sysid-bit4.md (renamed from
vino-attach-stops-after-rev-id.md, which captured the open question
before this fix was found).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related fixes that together let the IRIX 5.3 vino driver
successfully read IndyCam VERSION (= 0x12) over the VINO I2C bus:

1. cdmc.rs: IndyCam I2C address is 0x56 (write) / 0x57 (read), not
   0xAE/0xAF. Verified by scanning all vino_*.o objects from the IRIX
   5.3 driver: 0x56 / 0x57 / 0x2b immediates appear dozens of times,
   0xAE appears exactly once. The previous "0xAE/0xAF" was a stale
   reading of `IRIX 6.5 indycam.h` carried over without verification.

2. vino.rs (I2C_DATA write): real VINO triggers an I2C byte transfer
   on EACH I2C_DATA write while NOT_IDLE is asserted. IRIX writes
   I2C_CONTROL once at start-of-transaction (NOT_IDLE | HOLD_BUS) and
   then streams data bytes via I2C_DATA, polling I2C_CONTROL.XFER_BUSY
   between them. Before this, only the first byte ever reached the
   slave; the SAA7191 / CDMC saw a single addr byte and then went
   silent.

3. vino.rs (I2C_CONTROL write): only re-trigger from I2C_CONTROL when
   NOT_IDLE just transitioned clear→set, OR the READ flag is set.
   Without the edge check, the kernel's "poll I2C_CONTROL after every
   data byte" pattern would re-fire the byte transfer and every kernel
   write would be sent twice (mid-transaction state corruption).

4. cdmc.rs: recognise REPEATED START. If the I2C state machine is
   non-Idle and sees its own write- or read-address byte arrive,
   transition to SubaddrWrite or DataRead respectively (preserving the
   subaddr set earlier). IRIX reads use the canonical pattern
   START → 0x56 → subaddr → REPEATED-START → 0x57 → read; without
   repeated-start handling the 0x57 byte fell into DataWrite and
   silently overwrote the VERSION register with whatever came next.

Tests:
- Existing vino::tests cdmc_* tests updated for the new I2C address
  and the canonical write-subaddr-repeated-start-read protocol; all 8
  vino lib tests pass.
- Manual verification on the running IRIX 5.3 install: vlinfo enumerates
  the vino device, vidtomem now sees CDMC respond with VERSION=0x12
  (visible as `VINO R [0x0020] I2C_DATA -> 0x00000012`). Frame DMA still
  doesn't complete (driver sets up A_NEXT_4_DESC but doesn't enable
  CHA_DMA_EN); that's the next investigation, separate from this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes that together let `vidtomem -f /tmp/cap -v 0` complete and
write a 640x480 RGB SGI-image-format frame to disk from the IndyCam
test-pattern source on a fresh IRIX 5.3-for-Indy install:

1. cdmc: add CAMERA_ID register at subaddress 0x0E = 0x10.

   The IRIX 5.3 vino driver's vinoCameraAttached() reads CDMC subaddr
   0x0E (not the VERSION at 0x00) and considers the camera present iff
   the byte is exactly 0x10. Disassembly of vinoCameraAttached in
   vino_main.o makes this explicit:

     addiu $a1, $zero, 0x56     ; CDMC I2C write addr
     addiu $a2, $zero, 0x0e     ; subaddr
     jal   vinoI2cReadReg
     ...
     addiu $at, $zero, 0x10
     bnel  $v0, $at, not_attached

   Without it the kernel prints "IndyCam not attached.
   [HELP=VINONOCAMERA_WARN]" and vidtomem hangs forever waiting for a
   frame the driver refuses to schedule. CDMC's register space grows
   from subaddrs 0x00–0x08 (COUNT = 9) to 0x00–0x0E (COUNT = 15).

2. physical: black-hole the HPC1 region (0x1FB00000..HPC3_BASE).

   iris doesn't emulate HPC1. The CPU-bus-error path that catches
   accesses there is fine during normal IRIX boot (the kernel installs
   handlers and tolerates the error), but once vidtomem walks the vino
   capture path the same access escalates to a hard panic
   ("PANIC: IRIX Killed due to Bus Error" at 0x1FB02000). Mapping the
   range to the existing BlackHoleRegion (reads as 0, writes silently
   accepted) keeps the bus quiet without implementing HPC1 semantics.

3. vino: implement read8/read16/write8/write16.

   The IRIX vino driver issues at least one 16-bit read at
   `VINO_BASE + 0x16` as part of the vidtomem path. Vino's BusDevice
   impl only had read32/read64, so byte/halfword accesses fell through
   to the trait default of err() → CPU bus error →
   `PANIC: KERNEL FAULT … Bad addr: 0xa0080016`. Now read8/read16
   extract the appropriate sub-field of the underlying 32-bit register
   and write8/write16 are silently accepted (no partial register
   update, matching the chip's actual semantics).

End-to-end verification on the running IRIX 5.3:
    IRIS # /usr/etc/videod &
    IRIS # vidtomem -f /tmp/cap -v 0
    vidtomem: saved image to file /tmp/cap-00000.rgb
    IRIS # ls -la /tmp/cap-00000.rgb
    -rw-r--r--    1 root     sys   162322 May 27 06:14 /tmp/cap-00000.rgb
    IRIS # od -c /tmp/cap-00000.rgb | head -1
    0000000 001 332 001 001  \0 003 002 200 001 340  \0 003  \0  \0  \0  \0

Magic 0x01DA = SGI image format, 1 BPC, 3 dimensions, 640x480 RGB.
File content is the iris vino test_pattern source, captured through
the full IRIX video stack (videod → VL → kernel vino driver → DMA).

This is the first capture-to-file success on iris. Earlier sessions got
as far as videod enumerating the vino device but no frame ever flowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hanshuebner
Copy link
Copy Markdown
Contributor Author

hanshuebner commented May 27, 2026

I needed to make a couple more patches to iris for IndyCam support, but it works now:

image

I'll try to update the PR tomorrow.

hanshuebner and others added 2 commits May 27, 2026 17:53
Two related fixes that together turn raw vidtomem output from
"every-other-row-black, washed-out turquoise" into a real-colour
640×480 RGB capture from the host camera. A 1-pixel-per-row diagonal
artifact in Even-field rows remains; geometry and colour are otherwise
correct.

1. vino: reset DMA cursor at field boundaries (interlace).

   pump_field() now reloads descriptors from start_desc_ptr and sets
   page_index to either 0 (Even field) or line_size + 8 (Odd field, =
   actual row stride since CH_LINE_SIZE is encoded as stride-minus-8)
   at the start of every captured field. Previously the Odd field
   continued from wherever Even left off (past the end of the frame
   buffer), so the captured image's odd memory rows stayed zero —
   alternating bright/black scanlines in the output.

   dma_emit_dword's interleave-trigger condition tightened from `>=`
   to `>`. The line-size register encodes the *last dword's* offset
   within the line (one dword short of the actual stride), so the
   trigger must fire after line_counter exceeds line_size, not after
   it equals it. With `>=`, the last dword of every row went to the
   *next* row's territory and a 2-px-per-row drift accumulated.

2. camera: nokhwa-on-macOS YUYV byte order is YVYU.

   downscale_yuyv_to_uyvy was assuming canonical YUYV (Y0 Cb Y1 Cr)
   but the nokhwa macOS backend (AVFoundation via YuyvFormat) actually
   delivers Cr at byte 1 and Cb at byte 3. Reading them through to a
   yuv_to_rgb that expects canonical channel positions swapped U and
   V in every pixel, producing the washed-out turquoise/red cast in
   the first end-to-end camera capture inside IRIX. Swap on read at
   the camera-source layer so the rest of the pipeline (UYVY contract,
   vino's render, yuv_to_rgb) uses canonical channel identities.

   Tests updated to match (the canonical-byte-order assumption was
   baked into downscale_*_to_2x1 tests).

Verification:
  IRIS # /usr/etc/videod &
  IRIS # vidtomem -f /tmp/cap -v 0
  vidtomem: saved image to file /tmp/cap-00000.rgb
  # → 640×480 RGB SGI image with all rows populated and real colour.
  # Visible artifact: thin dark diagonal line in even-numbered output
  #   rows, drifting -1 col per row from col 638 at row 0. Tracked as
  #   a follow-up — likely a stride/descriptor mismatch we don't yet
  #   reproduce from line_size alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sage

- rules/irix/indycam-end-to-end-capture.md: chronological writeup of the
  12 fixes that took iris from "vino_init silently exits before the
  REV_ID read" to "vidtomem -f /tmp/cap -v 0 writes a real 640×480 RGB
  frame from the host camera." Each item names the commit that closed it,
  the kernel symptom that surfaced it, and the iris-side surface area
  touched. Two open issues called out (diagonal artifact, two-row clip).

- src/ci.rs: scratch-volume header comment had stale slot numbers (slot
  7 vs slot 10 for "vol", slot 8 unchanged) and pointed the typical
  guest read at /dev/rdsk/dks0d2vol — which overlaps the SGI VH and
  whose first write clobbers the partition table iris pre-formatted.
  Updated to point at /dev/rdsk/dks0d2s0 (the payload partition,
  sectors 8..end) and call out the gotcha explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@techomancer
Copy link
Copy Markdown
Owner

all right, that is a big awesome pile of ai slop ;-)
the only comment i have is i would probably merge both scsi callback calls that clear int into one. but we can do it later. also, nice monitor improvements.
uvc->vino was on my todo list, so glad that is now crossed out ;-)
next big challenge - parallel port!
kidding aside, thank you.
also i thought i fixed the installer when i fixed chunking large requests but i guess that wasnt enough.

@techomancer techomancer merged commit 90bf518 into techomancer:main May 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants