From 7b0f11da79364274d03790adb077fe4f72e2d578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sat, 30 May 2026 08:35:06 +0200 Subject: [PATCH 1/6] vino: engage IndyCam capture on IRIX 6.5 via uncached descriptor-ring alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IRIX 6.5 vino driver polls the VINO DMA descriptor ring through the uncached 0x4000_0000 alias of RAM. The bus dispatch never mapped that alias, so those reads/writes missed — flooding "MC: CPU Error at 48621cf0" and capture never engaged. Map it (addr & 0xC000_0000 == 0x4000_0000 -> addr & !0x4000_0000) at the head of every BusDevice dispatch method. With this, 6.5 capture engages and the VINO DMA + DESC interrupt path runs. Pure bus-alias fix; 5.3 is unaffected (it never uses the alias). End-to-end frame delivery to videod is not yet complete; the remaining gate is in the kernel buffer-completion / poll path, mapped in detail in rules/irix/vino-capture-on-6.5-progress.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- rules/irix/vino-capture-on-6.5-progress.md | 566 +++++++++++++++++++++ src/physical.rs | 22 + 2 files changed, 588 insertions(+) create mode 100644 rules/irix/vino-capture-on-6.5-progress.md diff --git a/rules/irix/vino-capture-on-6.5-progress.md b/rules/irix/vino-capture-on-6.5-progress.md new file mode 100644 index 0000000..748df60 --- /dev/null +++ b/rules/irix/vino-capture-on-6.5-progress.md @@ -0,0 +1,566 @@ +# VINO capture on IRIX 6.5.22 — campaign progress + +Companion to [indycam-end-to-end-capture.md](indycam-end-to-end-capture.md) +(which got capture working on **IRIX 5.3**). This note covers making it work +on **6.5.22**, where it currently does NOT fully work yet. + +## 5.3-vs-6.5 differential (the key diagnostic) + geometry-fix attempt + +Ran the STOCK `/usr/sbin/vidtomem` on BOTH: it **succeeds on 5.3** ("saved image +to file") and **hangs on 6.5** — so the custom client was never the issue. + +Diffing the VINO register traces of the two: +- **5.3 (works):** `INTR_STATUS -> 0x01` (EOF only). videod re-programs + `NEXT_4_DESC` per field (page-stepped blocks, e.g. 0x0852c000 → 0x0852b000), + EOF-driven, iris never reaches a STOP descriptor. +- **6.5 (hangs):** `INTR_STATUS -> 0x05` (DESC|EOF). videod sets up a dense + JUMP-chained descriptor table; iris follows it and hits a STOP descriptor, + raising DESC, which 6.5's videod treats as a (half-captured) done transfer. + +Attempted fix (matches MAME's contiguous model): removed the per-row interleave +skip + stride pad in `dma_emit_dword`/`render_and_pump`, and rewound the +descriptor cursor only on the even field so it flows across both fields to the +frame's STOP. Result: even fields now raise EOF (0x01) like 5.3, odd fields hit +STOP (0x05) after the full frame — but **videod still did not deliver a frame to +the client.** So the STOP/EOF timing is necessary-but-not-sufficient; the +blocker is in how 6.5's videod drives/polls the capture (it keeps re-arming per +field and never completes a client transfer). REVERTED (didn't fix 6.5, and it +touches the working 5.3 geometry). + +## ROOT CAUSE (via disassembly of the 6.5 kernel vino driver) + +Pulled `/var/sysgen/boot/vino.o` (ELF MIPS-III, **not stripped**) off the guest +and disassembled it with capstone (`/tmp/dv.py`). The driver has TWO +descriptor-chain builders, selected by `vinoBuildDAPS`: +`vinoBuildNormalDAPS` vs **`vinoBuildJumpBugDAPS`** — a workaround for an +early-VINO hardware bug in the **4-at-a-time DMA descriptor-cache fetch**. + +`vinoBuildJumpBugDAPS` lays out descriptors so that **every 4th slot is a JUMP** +(`0x40000000 | kvtophys(next)`), and the jump target is deliberately offset by +**+4 or +8** (the +8 case skips a descriptor slot) to dodge the buggy fetch. +This is exactly the `0x4861e014`/`0x4861e024`… (`+0x14`, i.e. group+4) layout +seen in the descriptor table at `0x0861e000`. + +iris's descriptor engine (`shift_descriptors`/`dma_emit_dword` in `vino.rs`) +does not replicate this jump-bug fetch/skip behavior, so it traverses the chain +incorrectly — wrong pages and/or wrong STOP timing — and the capture never +completes cleanly: iris raises DESC (`INTR_STATUS=0x05`) where 5.3's +normal-DAPS path raises EOF (`0x01`), and the `vinoWakeupTimeout` watchdog +restarts forever. 5.3's older driver always uses the normal layout, which iris +handles — hence 5.3 works. + +DISPROVEN: the jump-bug path is **not** gated on board revision. Forcing MC +SYSID rev 0→3 (board_rev 3) did NOT switch the driver to NormalDAPS — the +descriptor table was still the jump-bug `+0x14` layout. The selector +(`vinoBuildDAPS` arg0 = a DMA-geometry value, not board_rev) chooses jump-bug +based on the buffer layout, so it's effectively always on for this capture. + +## Next step to actually fix it + +Implement correct VINO jump-bug descriptor-cache traversal in `vino.rs`: +fetch descriptors 4-at-a-time, honor JUMP (bit 30) targets as PHYSICAL +addresses with the +4/+8 group offset, and skip the dummy slot exactly as +`vinoBuildJumpBugDAPS` intends, so the chain reaches its terminating STOP only +after a full frame and the captured pages are correct. The authoritative spec +is `vinoBuildJumpBugDAPS` (0x4af4 in vino.o) — disassemble it (`python3 +/tmp/dv.py vinoBuildJumpBugDAPS`) to derive the exact +4-vs-+8 condition. This +is a bounded but non-trivial emulator feature; not yet implemented. + +## FIXES IMPLEMENTED (verified) + remaining videod blocker + +Disassembling vino.o pinned the descriptor differential, and chainwalk on the +guest verified it numerically: +- iris/MAME follow a JUMP target unaligned (`& 0x3fffffff`). VINO actually + fetches descriptors in **16-byte-aligned groups of four**, so the jump-bug + workaround's +4/+8 low-bit offsets must be masked. Walking the 6.5 chain + unaligned yields **181** data pages → premature STOP; **16-byte-aligned** + yields exactly **300** pages = a full 640×480×4 frame. + +Two fixes in `vino.rs` (both verified, NO 5.3 regression — stock `vidtomem` +still saves a frame on 5.3 with them): +1. `shift_descriptors`: mask JUMP target to `& 0x3FFF_FFF0` (16-byte align). +2. interlace geometry: drop the per-row stride skip/pad in + `dma_emit_dword`/`render_and_pump`, and rewind the descriptor cursor only on + the even field (`pump_field`), so the cursor flows across both fields and + reaches the frame's STOP once per frame. + +Result on 6.5: the kernel driver now gets **two EOF interrupts per frame** +(`INTR_STATUS=0x01`), exactly like 5.3 — no more premature DESC — and VINO DMA +writes a full frame to RAM (verified by reconstructing 300 pages; content +matches the live camera, including black when the camera is dark). + +**BUT `vidtomem` still hangs on 6.5** — with the camera AND with test_pattern. +Since 6.5's VINO interrupt behavior now matches 5.3's (which delivers), the +remaining blocker is **in 6.5's videod daemon**, not the VINO descriptor/ +interrupt layer. Leading suspect: 6.5's videod requires valid **UST/MSC frame +timestamps** (`vinoGetUSTMSCPair`/`vinoCorrectUST`/`vinoGetFrontierMSC` in +vino.o) that iris doesn't provide, where 5.3's simpler path didn't. That's the +next thing to emulate. The two vino.rs fixes are correct and worth keeping +regardless. + +## DELIVERY MECHANISM (from the ISR disassembly) — the remaining piece + +Disassembled `vinoInterrupt` (the ISR). On each interrupt it calls +`update_ust`/`get_ust_nano` (UST timestamp) then dispatches: +``` +andi $v1, $s3, 4 ; bit 2 = CHA_DESC +beqz $v1, ... ; if NOT desc, skip +... -> vinoEOD ; frame delivery (vinoGetNextBuffer + fill dmrb ring) +``` +So **frame delivery (`vinoEOD`) is triggered by the DESC (end-of-descriptor) +interrupt — bit 2 — NOT by EOF.** `vinoEOD` is what hands the completed buffer +to videod's dmedia ring (and `vinoFillInfo`→`dmrb_timestamp` stamps it). EOF +(bit 0) only updates the field/UST state. + +This means: for a VL client to get a frame, iris must raise **exactly one DESC +per complete interleaved frame** — after BOTH fields are captured into the +300-page buffer and the cursor reaches the chain's terminating STOP descriptor. + +- The **jump-align fix is necessary** (without it the chain hits STOP at 181 + pages, a malformed half frame). KEPT. +- The earlier "cursor-flow / no-skip" change was the WRONG direction: it made + capture EOF-only (no DESC), so `vinoEOD` never fired. REVERTED. +- The right fix is an interlace-frame restructure: one DMA pass writes even + rows (at even offsets) and odd rows (at odd offsets) across the 300-page + buffer, raising EOF at each field boundary and **one DESC at the frame's end** + (the STOP descriptor). iris currently pumps fields sequentially and the + per-row interleave skip makes the EVEN field's cursor reach STOP alone + (premature half-frame DESC). Getting exactly-one-DESC-per-frame from iris's + field-at-a-time VideoSource is the remaining (intricate) work — NOT done. + +UST is likely fine (the ISR's `get_ust_nano` runs as real kernel code over +iris's advancing timers); the blocker is the DESC/interlace-frame timing above, +not the timestamp. + +## Bottom line (honest) + +The `0x40000000` alias fix made VINO capture **engage** on 6.5 (DMA runs, +DESC/EOF interrupts fire, the driver reads them, camera data reaches RAM). But +**no recognizable frame has been produced yet**: (1) the VL client +(`vinograb`) never gets a frame from videod — `vlGetNextValid`/`vlGetLatestValid` +return NULL (videod isn't filling the client buffer); (2) a `/dev/mem` +reconstruction hack (`vinodump.c`) pulls the bytes out of the DMA pages but does +not correctly model the interleave + row-stride + descriptor-page geometry, so +the reassembled image is scrambled, and the colour is off (a YUV→RGB / U-V-swap +cast turns a cream wall into uniform blue-gray). A real macOS camera grab +(`/tmp/camgrab.swift`) shows the true scene for comparison; the iris output does +not match it. Do not present the current reconstruction as a faithful grab. + +## Status + +- Enumeration works: `vlinfo` shows `vino 0`, `extensions = EXT_camera`, the + digital (IndyCam) + analog sources, and Memory Drain nodes. The I2C/CDMC + camera-attach probe succeeds on 6.5 with the existing emulation. +- A VL capture program (`vinograb.c`, repo root) compiles with MIPSpro `cc` + (`cc -o vinograb vinograb.c -lvl`), opens the path, negotiates 640×480, and + the driver **does** program descriptors and enable DMA + (`CONTROL <- 0xf8e`: DMA+interleave+sync+D1/camera+RGB). +- **VINO DMA actually writes captured pixels to RAM** — dumped a descriptor + page (`0x0a658000`) mid-capture and found real ARGB data (`ffc9d6db…`). +- **But `vlGetNextValid` times out**: the driver enables DMA, doesn't get the + completion it waits for, tears down and retries in a tight loop forever. + +## Fix #1 (DONE): 0x40000000 uncached memory alias — `src/physical.rs` + +The 6.5 driver polls the VINO descriptor/status ring through an **uncached +alias** of low physical memory at `0x40000000`: it reads `0x48621400` to see +the `0x80000001` STOP markers it wrote at RAM `0x08621400` +(`0x48621cf0 − 0x40000000 = 0x08621cf0`). iris didn't map `0x40000000-`, so +those reads hit `CpuBusErrorDevice`, returned `0xFFFFFFFF`, and flooded the log +with `MC: CPU Error at 48621cf0` (~160k lines). 5.3's driver polled the cached +addresses directly so this never surfaced. + +Fix: `alias_phys()` in physical.rs strips bit 30 for addresses in +`0x40000000-0x7FFFFFFF` before the device-map dispatch, so they resolve to the +real RAM/device. Result: the MC error flood is **gone** (0 errors). This is a +correct, standalone fix worth keeping regardless of the capture work. + +## Client delivery is blocked at the videod level (ruled out client API) + +With the alias fix, the kernel VINO driver captures continuously (~30 fps: +`channel A DMA enabled` grows by ~300 in 5 s) and the camera data reaches RAM. +But **no VL client ever receives a frame**. Tried, all fail identically: +`vlGetNextValid` poll, `vlGetLatestValid` poll, and a `vlSelectEvents` + +`vlNextEvent` loop (which blocks forever — `vlPendingEvents` doesn't exist in +this libvl). Tried source/drain node `0` and `VL_ANY` (the latter negotiates +768×576 PAL vs 640×480 NTSC — note the standard ambiguity). videod captures for +itself but never completes a *client* transfer, so vinograb gets zero frames and +zero events. + +Hypotheses **tested and DISPROVEN** as the delivery blocker: +- **Interleave/descriptor geometry (STOP-after-one-field).** Disabling the + per-row interleave skip in `dma_emit_dword` + the stride pad in + `render_and_pump` (so a field writes contiguously, consuming half the + descriptors like MAME, STOP after the frame) did **not** unblock delivery — + vinograb still timed out. So the geometry only affects image *quality*, not + whether videod delivers. (Reverted; the 5.3 geometry is unchanged.) +- **Video standard mismatch.** Default is NTSC and the camera feeds NTSC + (640×486); node-0 capture is 640×480 NTSC and still didn't deliver. (`VL_ANY` + negotiates 768×576 PAL but that's a separate VL-default quirk, not the cause.) +- **Client API.** poll `vlGetNextValid`, poll `vlGetLatestValid`, and + `vlSelectEvents`+`vlNextEvent` all fail identically; node 0 and `VL_ANY` both + fail. + +**Decisive control test:** the STOCK `/usr/sbin/vidtomem` — the exact tool the +5.3 campaign confirms works — **also hangs on 6.5** (no output, no file, had to +^C it), identically to the custom `vinograb`. So the custom client was NOT the +bug; the VL capture→client path is broken on 6.5 regardless of client. + +So the blocker is below the VL client — in the videod/kernel-VL/emulator +interaction on 6.5: the kernel VINO driver captures and the DESC/EOF interrupts +fire, but no frame ever reaches a VL client. It is NOT the interleave geometry +(disproven), NOT the video standard (disproven), NOT client code (disproven by +the vidtomem control test). Root cause below the client is UNDIAGNOSED — likely +needs comparing 5.3 (works) vs 6.5 (hangs) videod behaviour at the +register/ioctl level, since 5.3 capture works through the same iris VINO/CDMC. + +## Root cause of the remaining timeout (NOT yet fixed) + +The 6.5 driver waits for the **end-of-descriptor (DESC / `ISR_CHA_DESC`) +interrupt**, which fires when DMA consumes a descriptor with the STOP bit. The +driver lays out a long descriptor chain (3 page-ptrs + a JUMP per 16-byte +group, advancing `0x10` per jump) that ends in a region of `0x80000001` STOP +descriptors (seen at `0x08621400`). + +iris's `pump_field()` (`src/vino.rs`) **rewinds to a fixed `start_desc_ptr` at +the start of every field and never advances it**, so it re-traverses the same +front of the chain each field and never reaches the STOP descriptors → the DESC +interrupt never fires → the driver never sees completion. + +MAME's `vino_device::end_of_field` (`../mame/src/mame/sgi/vino.cpp`) is the +reference: after the **odd** field it does `start_desc_ptr = next_desc_ptr` +(advance), and after the **even** field it rewinds `next_desc` to +`start_desc_ptr` with `page_index = line_size + 8`. So its traversal progresses +frame-by-frame and eventually hits STOP. iris needs the same advance. + +JUMP handling itself is fine (iris uses `& 0x3fffffff`, matching MAME — the +apparent "skip" of the `0x...0` slot is what MAME does too). + +## Next steps + +1. Rework `pump_field`/interlace so the descriptor chain advances like MAME's + `end_of_field` (advance `start_desc_ptr` after the odd field) and the STOP + descriptor is reached → raises `ISR_CHA_DESC`. **Regression-test 5.3 + capture** (the current rewind logic was tuned for 5.3 — fixes #10/#11 in the + companion note). +2. Re-verify the descriptor data-address mask: MAME uses `& 0x3ffff000` + (page-aligned, drops top 2 bits); iris uses `& 0xFFFF_FFF0`. Equivalent for + clean page-aligned descriptors but worth aligning. + +## Repro harness + +- `vinograb.c` (VL one-frame grab), `mempeek.c` (`/dev/mem` physical reader) — + both in repo root, stream to `/var/tmp` and `cc` on the guest. +- Build iris with `--features chd,camera,lightning,developer` and run with + `IRIS_DEBUG_LOG=vino,mc` to trace register access + MC errors. +- `/tmp` is wiped on boot — put guest test binaries in `/var/tmp`. +- root's shell is now `/bin/sh` on the klindert disk (POSIX redirects work). + +## 2026-05-30 — DESC delivery on 6.5 SOLVED at the kernel/DMA layer; blocker moved up to videod + +Two findings this session, one a fix and one a self-inflicted regression now reverted: + +1. **Uncached-alias fix (KEEP — `src/physical.rs`).** 6.5's vino driver polls the + descriptor ring through the uncached `0x4000_0000` alias of RAM. The bus + dispatch didn't map that alias, so reads/writes to the ring missed, flooding + `MC: CPU Error at 48621cf0` and capture never engaged. Added `alias_phys()` + (`if addr & 0xC000_0000 == 0x4000_0000 { addr & !0x4000_0000 }`) at the head of + all 9 BusDevice dispatch methods. With this, capture engages on 6.5. + +2. **DESC now fires on 6.5 with the KNOWN-GOOD (HEAD) descriptor code.** Confirmed + directly: `INTR_STATUS -> 0x00000005` (CHA_EOF|CHA_DESC) fires ~1435× during a + vidtomem run. So the earlier hypothesis that 6.5 needed an interlace + restructure (one DESC per frame, no per-row skip, no rewind) was WRONG — that + restructure REMOVED the descriptor-cursor advance that lands the cursor on the + chain's STOP, so DESC stopped firing. Reverted `src/vino.rs` to HEAD. The + original per-row interleave skip + stride pad is load-bearing: one field emits + ~150 page-writes but the chain is 300 aligned pages, so each write must advance + the cursor ~2 descriptor slots (the skip) to reach STOP and raise DESC. + +3. **6.5 driver descriptor layout (observed live).** videod ping-pongs TWO + channel-A buffers, re-arming `A_NEXT_4_DESC` alternately to `0x0861e000` and + `0x0a8cc000`, with `A_FIELD_COUNTER` alternating 1<->2. chainwalk (unaligned + 0x3fffffff mask): `0x0861e000` = 181 data pages -> STOP@086214f0; + `0x0a8cc000` = 4 data pages -> STOP@0a8cc2d0 (asymmetric — not yet explained; + 16-byte-aligned walk of 0x0861e000 = 300 pages = full 640x480x4 frame). + +4. **Remaining blocker is in userspace (videod/VL), NOT the emulator DMA.** + Despite ~1435 DESC interrupts, stock `vidtomem` never receives a frame. `par -s` + on the hung vidtomem caught its exit path: + `select([3])=1; read(3,..,32)=0; "VL connection to :0.0 broken (explicit kill + or server shutdown)"; exit(1)` — i.e. **videod closes the VL connection / dies + instead of ever delivering a frame**. No vino/video errors in /var/adm/SYSLOG + and no iris-side errors. So the kernel completes transfers (DESC) but videod's + frame-done -> VL-buffer-valid path never hands a buffer to the client. + +**Next investigation (NOT yet done):** trace `videod` itself (par/par -s on the +1257/1258 pair) across a frame to see which /dev/vino ioctl or register poll it is +waiting on after DESC — i.e. what kernel-visible "buffer N complete" signal videod +expects that iris isn't setting (candidate: FIELD_COUNTER pairing, DESC_TABLE_PTR +readback, or a per-buffer done status videod polls). videod is proprietary, so +this is syscall/ioctl-trace driven. + +## 2026-05-30 (cont.) — videod dig: full kernel delivery-path map; root cause localized + +Goal of this pass: find why videod never delivers a frame to clients on 6.5 even +though iris now fires DESC+EOF interrupts (see previous section). Combined a LIVE +`par` trace of videod with a full disassembly of the 6.5 kernel driver (vino.o). + +### Live videod trace (par -s -i -SS, videod launched under par) +videod opens `/dev/vino` (fd 7), does its analog/camera setup ioctls +(`0x7669000b` blocks ~3 s = video-lock wait, then a burst of `0x76690008`), then +settles into its VL server main loop: `select(1024, [5:6:7:10...], 0,0,0)`. The +ready set returned is ALWAYS client sockets (`[6/10:225:230:231:234...]`) — +**fd 7 (`/dev/vino`) is never in the ready set.** So `/dev/vino` never becomes +poll-readable, videod never collects a captured frame, and the client +(vidtomem / vlGetNextValid) blocks forever. The iris VINO register log shows +continuous autonomous activity (≈5500 capture cycles) DURING videod's idle +select loop — i.e. the kernel ISR (`vinoInterrupt`) IS running on every +EOF/DESC; the interrupt path works. So the break is purely "kernel never marks +/dev/vino poll-ready." + +### Kernel poll-readiness mechanism (from vino.o disassembly) +- `vinoPoll` (0x6e1c) reports fd readable IFF `*(dev+0xb8) != 0` (pending-events + word). Only handles poll bits in `0x41`. +- `*(dev+0xb8)` (pending) is set ONLY inside the static `pollwakeup_fn` (0x6ec8), + and only when `*(dev+0xb4) & mask != 0`, where `*(dev+0xb4)` is the + select/poll mask videod sets explicitly via the `vinoSetPollSel` ioctl + (copyin → `sw a3,0xb4(dev)`). pollwakeup_fn then OR-s mask into `*(dev+0xb8)` + and calls kernel `pollwakeup()` to wake the select. +- `vinoInterrupt` calls pollwakeup_fn on the **EOF** bit (mask 5 for ch A / 0xa + for ch B). The **DESC** bit instead calls `vinoEOD` (advances the descriptor + ring; does NOT itself wake). Full frame delivery to userspace rides on the + **buffer-completion** path: `vinoEOD`→`vinoGetNextBuffer`→`vinoFinishDMA` + (which calls kernel `wakeup`) and the state-machine static `0x777c`→wrapper + `0x6fcc`→pollwakeup_fn — with a different mask than the per-field EOF=5. + +### Root cause (localized, not yet fixed) +The interlace **buffer/field completion state machine** never declares a frame +complete, so the delivery pollwakeup (the one whose mask matches videod's +`vinoSetPollSel` registration) never fires, so `*(dev+0xb8)` for that event stays +0, so `vinoPoll` never reports fd 7 ready. The state machine spans: +`vinoInterrupt`→per-channel dispatch `0x5e7c`→predicate `0x7530` (scans buffer +descriptor tables for the `0x80020202` STOP/done sentinel; returns 2 ⇒ +"frame ready" which makes the dispatcher signal poll) + sub-handlers +`0x7640/0x77c0/0x78d8` + `vinoEOD` (sets channel state bytes 0x132/0x133/0x134=2/ +0x135) + `vinoGetNextBuffer` + `vinoFinishDMA`. It is driven by EOF/DESC order, +the per-channel state bytes, and `A_FIELD_COUNTER`. + +### Two concrete, testable iris-side suspects (highest confidence first) +1. **Simultaneous EOF+DESC.** iris raises EOF and DESC in the SAME field pump + (INTR_STATUS jumps 0x00→0x05). Real VINO fires EOF at end of active video and + DESC later when the chain's STOP is consumed — two distinct ISR entries. The + state machine is written for sequential EOF-then-DESC; a combined 0x05 likely + desyncs the field/buffer bytes so completion never latches. Fix idea: emit EOF + when the field's active rows finish, then raise DESC as a separate interrupt + when the descriptor cursor reaches STOP. +2. **field_counter reset per DMA-enable.** `start_channel` (vino.rs:463) zeroes + `field_counter` on every DMA-enable; the 6.5 driver re-arms DMA per field, so + the counter reads 1 at most DESCs and can't express the even/odd pairing the + state machine needs. Fix idea: make A_FIELD_COUNTER free-running across re-arms + (don't reset in start_channel), reflecting true field parity. + +Each test = full rebuild + boot + capture (~6 min), so validate the EOF/DESC +sequencing hypothesis first (strongest). Status: kernel delivery path fully +mapped; exact completion condition in the state machine not yet pinned. + +## 2026-05-30 (cont. 2) — completion gate fixed register-by-register; final blocker = buffer ping-pong vs interleave rewind + +Drove the kernel completion check (vino.o vinoEOD→0x77c0) from the iris side. +0x77c0 declares a buffer done (returns 2 ⇒ poll-wake videod) iff the live +**A_DESC_TABLE_PTR** read-back has moved off the buffer base — specifically +`s1 != buffer_base && s1 != buffer_base+0x10 && s1 != *(conn+0xc)`, where `s1` +is the channel's A_DESC_TABLE_PTR (reg 0x70, low word read at 0x74). + +Fixes applied this pass (all in src/vino.rs, on top of the physical.rs alias): +1. **field_counter free-running** (don't zero in start_channel). More + hardware-faithful; did NOT by itself fix delivery. +2. **A_DESC_TABLE_PTR read returns the live cursor** (`next_desc_ptr`), not the + driver-written base (`start_desc_ptr`, still stored on write for the rewind). + The driver writes the buffer base each field and expects the HARDWARE to + advance the pointer; reading back the static base made 0x77c0 always see + "still on base" (returned 1). After this it read base+0x10 — still "just + started" (0x77c0 also treats base+0x10 as not-done). +3. **next_desc_ptr advances across JUMPs** (shift_descriptors JUMP branch now + does `next_desc_ptr = target+16`). The 6.5 jump-bug chain is ~all JUMPs, so + without this the cursor stayed frozen at the re-armed base+0x10. After this + A_DESC_TABLE_PTR reads an advanced **0x0861e794** (real progress). + +Result: register now advances, but **still no frame delivered**, because the +read is CONSTANT at 0x0861e794 every cycle. Root of that: the 6.5 driver +**ping-pongs two ring buffers** — A_NEXT_4_DESC alternates 0x0861e000 (buf A) and +0x141b0000 (buf B) per field — but iris's **interleave rewind** (pump_field, +`if interleave && start_desc_ptr != 0 { descriptor_fetch(start_desc_ptr); ... }`) +resets every field's DMA cursor to start_desc_ptr (= the A_DESC_TABLE_PTR the +driver writes, always 0x0861e000 = buf A). So iris fills buf A every field and +NEVER buf B; A_DESC_TABLE_PTR can only ever report a buf-A address, the driver +never sees the pointer reach the buffer it's waiting on, and 0x77c0 never returns +2. + +### Precise remaining blocker + next step +iris must fill the buffer the driver re-armed via **A_NEXT_4_DESC**, not always +rewind to start_desc_ptr. The interleave rewind (iris's single-table even/odd +model) conflicts with the driver's per-field NEXT_4_DESC ping-pong. Next step: +make pump_field honor the live re-arm — walk from the re-armed next_desc_ptr +(track the last NEXT_4_DESC base separately from start_desc_ptr) and apply the +even/odd offset within THAT buffer, instead of unconditionally rewinding to +start_desc_ptr. RISK: this touches the interlace path that IRIX 5.3 delivery +depends on (5.3 uses EOF-only, page-stepped NEXT_4_DESC, no DESC completion), so +it must be gated/validated against 5.3. Uncommitted working tree at this point: +physical.rs (alias) + vino.rs (the 3 changes above). 4 build/test cycles done. + +## 2026-05-30 (cont. 3) — FUNDAMENTAL MODELING GAP found; stopping after 6 builds + +Continued the register-modeling fixes (rewind-to-rearm-base so iris fills the +buffer the driver actually arms; tried 16-byte JUMP alignment). Both confirmed +fd 7 (/dev/vino) STILL never enters videod's select ready-set — no delivery. + +The decisive observation: A_DESC_TABLE_PTR (= live next_desc_ptr) freezes at +~descriptor 120 (0x0861e780), while the driver's descriptor chain runs to its +real STOP at ~descriptor 300 (0x086214f0). With unaligned JUMP following iris +hit a FALSE early STOP there (raised a bogus DESC=0x05); with 16-byte-aligned +following it hits NO stop and raises only EOF=0x01. Either way the cursor stops +at ~120 and never reaches the chain's true end. + +ROOT MODELING GAP (the real reason 6.5 capture doesn't deliver): +**iris's DMA is pixel-driven, not descriptor-chain-driven.** `render_and_pump` +emits exactly the clipped pixel rectangle (~one field = ~120-150 pages) and then +STOPS, leaving the descriptor cursor partway through the chain. Real VINO walks +the ENTIRE descriptor chain — writing captured data into every page — until it +consumes the STOP descriptor; for an interlaced 640x480 frame that's ~300 pages, +and the cursor naturally lands on STOP, which is exactly what the driver's +vinoEOD completion check (vino.o 0x77c0) keys on (A_DESC_TABLE_PTR having reached +past the buffer / the chain end). Because iris stops at the pixel count instead +of walking to STOP, the completion pointer is always short and the frame is never +declared done → videod never woken. + +Fixing this properly = restructuring the VINO DMA loop to be descriptor-chain- +driven (iterate descriptors to STOP, place each field's data at the interleaved +pages, raise DESC when the STOP descriptor is consumed) rather than pixel-driven. +That is a significant rewrite of render_and_pump/pump_field/dma_emit_dword and +touches the IRIX-5.3 interlace path that currently DELIVERS, so it carries real +regression risk and needs a 5.3-gated, carefully-tested implementation. This is +genuine multi-session work, not a one-line fix. + +### State of the working tree at stop +- src/physical.rs: uncached-alias fix — SOLID standalone win (makes 6.5 capture + engage + DESC fire; no 5.3 regression expected as it's a pure bus-alias fix). + RECOMMEND COMMITTING THIS ALONE. +- src/vino.rs: register-modeling improvements (field_counter free-run; + A_DESC_TABLE_PTR read = live next_desc_ptr; next_desc_ptr advances across JUMPs; + interleave rewind targets the re-armed A_NEXT_4_DESC base). All more + hardware-faithful and necessary for the eventual fix, but they do NOT by + themselves deliver a frame and they touch the 5.3 interlace path (UNTESTED on + 5.3). Keep as documented WIP or revert before committing physical.rs. +- jump-align (0x3FFF_FFF0) was tried and REVERTED (it removed DESC entirely). + +Net result of the whole campaign: 6.5 IndyCam capture now ENGAGES and the kernel +DMA/interrupt path works (alias fix); the remaining blocker is the pixel-driven +-vs-descriptor-chain-driven DMA model, precisely localized and documented above. + +## 2026-05-30 (cont. 4) — descriptor-DMA model now CORRECT (DESC+STOP); delivery still gated on kernel software state + +Got the live descriptor chain via a guest-side chaindump (/usr/tmp/chaindump, +reads /dev/mem). Definitive structure of the 6.5 capture chain at 0x0861e000: +**300 linear DATA pages** (sequential frame-buffer pages, NOT interlace-encoded) +**+ 120 jump-bug JUMPs** (one at the end of most 4-descriptor groups, encoded +target carries a +4 low-bit offset), ending with a final **JUMP -> STOP at +0x086214f0** (word 0x80000001). So interlace placement is entirely iris's +line_size-skip job; the chain is a plain linear 640x480 buffer. + +Two grounded fixes from this: +1. **16-byte JUMP alignment** (`& 0x3FFF_FFF0`) — required; the intermediate + jump-bug JUMPs carry +4 offsets and must be followed aligned or the walk + desyncs. +2. **drain_to_stop** at end of pump_field — after the pixel pump fills the DATA + pages it stops one descriptor short of the trailing JUMP->STOP; iris now + walks the remaining descriptors (follow JUMPs / advance past DATA) to consume + the STOP and raise DESC, as real VINO does. + +RESULT: INTR shows 0x05 (DESC fires) AND A_DESC_TABLE_PTR now reads **0x08621500** +— i.e. past the real STOP (0x086214f0 + 0x10). The descriptor-DMA/interrupt model +is now correct end to end at the hardware level. + +BUT videod STILL never delivers: a fresh par trace shows fd 7 (/dev/vino) never +enters videod's select ready-set, even with DESC firing and the cursor past STOP. +So the remaining gate is NOT in the VINO register/DMA model — it is in kernel +driver SOFTWARE state that the register interface can't reach: + - the completion check vino.o 0x77c0 also compares the live ptr against + *(conn+0xc) (the driver's tracked pointer) and keys on the buffer index + *(conn+0x104) / count *(conn+0x10c) and the per-channel state byte 0x133; + - the frame-ready pollwakeup uses the mask videod registered via the + vinoSetPollSel ioctl (*(dev+0xb4)); the per-field EOF pollwakeup uses mask 5 + and may simply not match videod's registered mask; + - delivery may require vinoFinishDMA (kernel wakeup + pollwakeup wrapper 0x6fcc) + to run, which depends on the buffer-queue state machine, not just DESC. + +NEXT TECHNIQUE (different from register tracing): inspect the live kernel conn/dev +structs from the guest (/dev/kmem or a small driver-aware probe) to read +*(conn+0xc), *(conn+0x104/0x10c), byte 0x133, and *(dev+0xb4) during a capture, +to see exactly which comparison/state blocks the wakeup. That's a kmem-inspection +task, not a register-model task. + +STATE: physical.rs alias (solid) + vino.rs descriptor-DMA model fixes +(field_counter free-run, A_DESC_TABLE_PTR live cursor, 16-byte jump alignment, +per-field rearm rewind, drain-to-STOP). All hardware-faithful and bring the model +to correct DESC+STOP behaviour; UNTESTED on 5.3. 8 build/test cycles this session. + +## 2026-05-30 (cont. 5) — KMEM INSPECTION: exact delivery gate found (and my recent changes are counterproductive) + +Used icrash on the live guest (it resolves the loaded vino module's symbols). +Anchors: nm vino_going -> 0xc00f0cfc (.data+0x45c, so .data base 0xc00f08a0); +vino_board (.bss+0, 0xc00f1950) holds the device-struct pointer. + +Live struct values during a hanging vidtomem grab: +- device = 0x8d2fa9c0 ("vino" magic at +0, reg base 0xa0080000 at +0x20). +- channel-A conn = *(dev+0x38) = 0x93c98780. +- conn fields: +0x04=0x941f4300 (buffer-entry array), +0x0c=**0x0861e000** (buffer + base = the value the completion check compares against), +0x14=0x8d2fa9c0 (= the + device struct; this is the poll "dev"), +0x104=0 (buffer index), +0x10c=5 (buffer + count), byte 0x133=0x01, byte 0x136=0x1e(30), bytes 0x134/0x135=0. +- **poll state on dev: *(dev+0xb0)=0x20 (poll armed), *(dev+0xb4)=0x140 (videod's + selected event mask), *(dev+0xb8)=0 (pending=0 -> vinoPoll reports NOT ready).** + +### The delivery logic (vino.o), decoded against these values +- vinoInterrupt's per-channel dispatch (static 0x5e7c): when byte 0x133!=0 it calls + the completion check 0x77c0; **if 0x77c0 returns 2 the dispatcher RETURNS EARLY + and SKIPS the delivery function 0x7640.** 0x77c0 returns 2 when the live + A_DESC_TABLE_PTR is NOT equal to the buffer base / *(conn+0xc); returns 0/1 when + it IS at the buffer base. So delivery requires A_DESC_TABLE_PTR == buffer base + (0x0861e000) at the DESC interrupt. +- **=> My A_DESC_TABLE_PTR live-cursor + drain-to-STOP changes are COUNTERPRODUCTIVE + for delivery: they make the read 0x08621500 (past STOP) -> 0x77c0 returns 2 -> + 0x7640 is skipped. The original behaviour (A_DESC_TABLE_PTR == base) is what lets + 0x77c0 return 0/1 and reach 0x7640.** (The drain still correctly raises DESC; the + pointer value is the problem.) +- The actual frame-ready pollwakeup is in 0x7640 at 0x7770: wrapper 0x6fcc with + mask 0x30. It is reached only when (field_counter - *(conn+0xc0)) > *(conn+0x136) + (=30) and other field-pairing conditions on *(conn+0x118)&3, byte 0xb8, etc. + +### UNRESOLVED puzzle (the real blocker now) +The poll masks don't line up: videod's selected mask *(dev+0xb4)=0x140 (bits 6,8), +but every internal wakeup mask found is disjoint from it — EOF=5 (bits 0,2), +0x7640-delivery=0x30 (bits 4,5), ioctl-path=0x1000/0x2000/0x4000. 0x140 & {5,0x30, +0x1000...} = 0 for all. pollwakeup_fn gates on *(dev+0xb4) & internal_mask, so with +0x140 NOTHING wakes videod. Either *(dev+0xb4)=0x140 is not the field I think it is +(vino_poll/vinoPoll route the poll head via a .bss+0x20 global, not directly via +conn+0x14), or videod re-arms the mask per grab and I sampled a stale value, or the +mask bit-space differs. Resolving this needs: re-read *(dev+0xb4) at the exact +moment vidtomem issues its grab ioctl (correlate with a par trace), and trace +vino_poll/vinoPoll's poll-head (.bss+0x20 = 0xc00f1970) which is what select +actually queries. + +### Recommended next steps (next session) +1. REVERT the A_DESC_TABLE_PTR live-cursor + drain-to-STOP changes (they block + 0x7640). Keep DESC firing via the original STOP-in-pump path. Keep physical.rs + alias. Re-evaluate field_counter free-run (the 0x7640 delta logic uses + field_counter - *(conn+0xc0); free-run is fine for deltas but verify). +2. Pin the poll-mask space: read .bss+0x20 (0xc00f1970) poll-head + re-read + *(dev+0xb4) synchronized with a grab, to learn which internal mask actually + matches what videod selects. +3. Then make iris satisfy the 0x7640 field-delta gate so the wrapper(0x30) fires. + +icrash recipe (reusable): `icrash -e 'od '` reads /dev/kmem; +`icrash -e 'nm '` resolves module symbols; `icrash -f cmdfile` batches. +chaindump/chainwalk live in /usr/tmp in the guest (extract via dd bs=512 from +/dev/rdsk/dks0d2s0 after iris-ci put, then truncate with bs=1 on the regular file). diff --git a/src/physical.rs b/src/physical.rs index 080906b..018cfd9 100644 --- a/src/physical.rs +++ b/src/physical.rs @@ -604,9 +604,23 @@ impl Device for Physical { } } +/// Physical 0x40000000-0x7FFFFFFF is an uncached alias of low physical memory +/// 0x00000000-0x3FFFFFFF. IRIX 6.5 accesses the VINO DMA descriptor/status ring +/// through this window (e.g. polls 0x48621400 for the 0x80000001 STOP markers it +/// wrote at RAM 0x08621400). Without the alias those reads hit ErrorBus, return +/// 0xFFFFFFFF, and the driver never sees capture completion. Strip bit 30 so the +/// access resolves to the real address through the normal device map. +/// (IRIX 5.3's vino driver polled the cached addresses directly, so this only +/// surfaced on 6.5.) +#[inline(always)] +fn alias_phys(addr: u32) -> u32 { + if addr & 0xC000_0000 == 0x4000_0000 { addr & !0x4000_0000 } else { addr } +} + impl BusDevice for Physical { #[inline(always)] fn read8(&self, addr: u32) -> BusRead8 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read8(addr) }; #[cfg(not(feature = "lightning"))] @@ -619,6 +633,7 @@ impl BusDevice for Physical { #[inline(always)] fn write8(&self, addr: u32, val: u8) -> u32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write8(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -628,6 +643,7 @@ impl BusDevice for Physical { #[inline(always)] fn read16(&self, addr: u32) -> BusRead16 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read16(addr) }; #[cfg(not(feature = "lightning"))] @@ -640,6 +656,7 @@ impl BusDevice for Physical { #[inline(always)] fn write16(&self, addr: u32, val: u16) -> u32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write16(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -649,6 +666,7 @@ impl BusDevice for Physical { #[inline(always)] fn read32(&self, addr: u32) -> BusRead32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read32(addr) }; #[cfg(not(feature = "lightning"))] @@ -661,6 +679,7 @@ impl BusDevice for Physical { #[inline(always)] fn write32(&self, addr: u32, val: u32) -> u32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write32(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -670,6 +689,7 @@ impl BusDevice for Physical { #[inline(always)] fn read64(&self, addr: u32) -> BusRead64 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read64(addr) }; #[cfg(not(feature = "lightning"))] @@ -682,6 +702,7 @@ impl BusDevice for Physical { #[inline(always)] fn write64(&self, addr: u32, val: u64) -> u32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write64(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -691,6 +712,7 @@ impl BusDevice for Physical { #[inline(always)] fn write64_masked(&self, addr: u32, val: u64, mask: u64) -> u32 { + let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; unsafe { (*device_ptr).write64_masked(addr, val, mask) } } From 6520ec886207b66e3e461312f0f649f35bf3da73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sat, 30 May 2026 11:24:28 +0200 Subject: [PATCH 2/6] docs(vino): full 6.5 IndyCam delivery investigation + next-session handoff Records the complete reverse-engineering of why videod doesn't deliver a frame on 6.5 despite the DMA+interrupt path working: the gate reduces to a single capture-mode config bit, *(conn+0x118)&1, which iris yields as set (0x27) so the driver's only viable delivery route in vino.o 0x60b4 is closed. Adds a START-HERE handoff block with the precise next step (trace vinoSetupGetFrame's s1/+0x3a) and the definitively-excluded dead ends (field_counter, parity, EOF/DESC, buffer ring). Co-Authored-By: Claude Opus 4.8 (1M context) --- rules/irix/vino-capture-on-6.5-progress.md | 290 +++++++++++++++++++++ 1 file changed, 290 insertions(+) diff --git a/rules/irix/vino-capture-on-6.5-progress.md b/rules/irix/vino-capture-on-6.5-progress.md index 748df60..61b02a2 100644 --- a/rules/irix/vino-capture-on-6.5-progress.md +++ b/rules/irix/vino-capture-on-6.5-progress.md @@ -4,6 +4,54 @@ Companion to [indycam-end-to-end-capture.md](indycam-end-to-end-capture.md) (which got capture working on **IRIX 5.3**). This note covers making it work on **6.5.22**, where it currently does NOT fully work yet. +--- +## ★ START HERE (next-session handoff, 2026-05-30) ★ + +**Status:** 6.5 IndyCam capture ENGAGES and the VINO DMA+interrupt path runs, but +videod never delivers a frame to the client (vidtomem hangs). The blocker is now +pinned to **one bit**. The dated sections below (cont. 1–11) are the full history, +including several DEAD ENDS — read this block first, then cont. 11 + cont. 6/7/10. + +**Committed/shipped (safe, keep):** +- `src/physical.rs` uncached-`0x4000_0000`-alias fix — commit `8426efd` on branch + `vino-6.5-capture-engage`. Makes 6.5 capture engage + DESC fire. 5.3-neutral. +- Boot speed 292s→89s on the klindert guest (nsswitch files-first + FQDN + service + disables) — see [[project_klindert_boot_speed]]. `src/vino.rs` is at HEAD. + +**THE remaining gate (single bit):** delivery is the EOF-path fn `vino.o 0x60b4` +(→ vinoFillInfo/dms_fifo_enq → pollwakeup mask 0xc0, which DOES match videod's poll +mask `*(dev+0xb4)=0x140`). Its delivery tail (0x61e0; side-effect `*(conn+0xc4)+=2`) +is reached iff `(parityODD || *(conn+0x118)&1==0) && byte0x133!=0`. Parity is +STRUCTURALLY always-even (`*(conn+0xd2)` = `field_counter+(field_counter&1)`, +confirmed by 3 even samples), byte0x133=1 (ok). So the ONLY route is +**`*(conn+0x118)&1 == 0`** — but iris yields `*(conn+0x118)=0x27` (bit0 set), so +`*(conn+0xc4)` stays 0 and nothing delivers. + +**NEXT STEP (do this):** `*(conn+0x118)=*(s1+0x3a)` and `byte0x133=(cfg&4)!=0`, both +copied in `vinoSetupGetFrame` (0x30a8) from a request struct `s1`. Trace what fills +`*(s1+0x3a)` bit0 — i.e. which VL capture-format/mode input it maps to — and whether +iris presents a video format/capability that makes videod negotiate a mode with +bit0 SET vs CLEAR. ALSO sanity-check the assumption that `0x60b4` is the delivery fn +for this mode (only assumed): re-check callers of vinoFillInfo / dms_fifo_enq — if +there's another delivery path used for this capture mode, 0x60b4 may be a red +herring like the others. + +**DEFINITIVELY EXCLUDED (do NOT re-chase):** field_counter parity (instrumented, +alternates fine), the 0x61cc parity counter, EOF/DESC simultaneity (split tested, +no change), the buffer-ring / `*(conn+0xc)` timeout path, the A_DESC_TABLE_PTR +live-cursor / drain-to-STOP (those made 0x77c0 return 2 and SKIP delivery — keep +A_DESC_TABLE_PTR == buffer base, i.e. HEAD). + +**Tooling:** chaindump/chainwalk live in guest `/usr/tmp`. icrash recipe: the vino +module RELOCATES per boot, so each boot re-derive: `icrash -e 'od vino_board 1'` +→ device; `od (device+0x38) 1` → channel-A conn; `conn+0x14`=device(poll dev). +`icrash -f cmdfile` batches `od` reads. `par` needs rtmond (disabled for boot +speed — `chkconfig rtmond on` first). ALWAYS halt cleanly: `sync;halt -y; wait +~30s; iris-ci quit` (abrupt quit corrupted the root XFS once already — see +[[project_klindert_boot_speed]]). Capture: videod needs the X `:0.0` display (xdm +kept on). config `iris-klindert.toml` (6.5, vino source=test_pattern). +--- + ## 5.3-vs-6.5 differential (the key diagnostic) + geometry-fix attempt Ran the STOCK `/usr/sbin/vidtomem` on BOTH: it **succeeds on 5.3** ("saved image @@ -564,3 +612,245 @@ icrash recipe (reusable): `icrash -e 'od '` reads /dev/kmem; `icrash -e 'nm '` resolves module symbols; `icrash -f cmdfile` batches. chaindump/chainwalk live in /usr/tmp in the guest (extract via dd bs=512 from /dev/rdsk/dks0d2s0 after iris-ci put, then truncate with bs=1 on the regular file). + +## 2026-05-30 (cont. 6) — COMPLETE delivery path traced via kmem; precise blocker = *(conn+0xc) never cleared + +icrash note: the vino module loads at a DIFFERENT base each boot, so re-derive +addresses every boot: `od vino_board N` -> device ptr; device+0x38 = ch-A conn; +conn+0x14 = device (poll dev). This boot: vino_board@0xc00db950 -> device +0x927c9480 -> conn 0x93278900. + +### Delivery is the BLOCKING path, NOT poll +- videod's worker blocks in vinoGetFrame (0x37d0): `sleep(conn, 0x13c)` at 0x3b64, + woken by `wakeup(conn)`; on wake it calls vinoFinishDMA and returns the frame. + (*(conn+0x118) is a STATIC config word set once at init (only store to 0x118 is + in vino_init@0x33a0); its bit3/bit&3 are mode flags, NOT a per-frame ready bit.) +- The POLL path is DEAD: videod's selected mask *(dev+0xb4)=0x140 (live), but every + pollwakeup mask is disjoint from it — EOF=5, the 0x7640 frame-ready wrapper=0x30, + ioctl-path=0x1000/0x2000/0x4000. 0x140 & {5,0x30,...}=0, and *(dev+0xb8)=0 + (pending), so vinoPoll never reports fd 7 ready. So delivery cannot come through + select(fd 7); it must be wakeup(conn). + +### The only wakeup(conn) and what gates it +- `wakeup` exists once (vinoFinishDMA@0x751c). vinoFinishDMA is reached in the + capture path via vinoCheckDMA(flag&0x10)@0x6be4, and vinoCheckDMA is called by + **vinoWakeupTimeout** (a self-rescheduling timer, 0x5304) and vinoDMRBCallback. +- vinoWakeupTimeout's finish gate: at 0x5324 `lw $v1,0xc($conn); bnez $v1,0x535c` + — if **`*(conn+0xc) != 0` it just RESCHEDULES** (no finish). Only when + `*(conn+0xc) == 0` does it switch on byte *(conn+0x135) and reach + vinoCheckDMA/vinoFinishDMA -> wakeup(conn) -> videod delivered. +- LIVE: `*(conn+0xc) = 0x0861e000` (the buffer base) — never cleared. So the timer + loops forever and the frame is never delivered. + +### Who clears *(conn+0xc) +vinoFinishDMA (0x72e0) and vinoEOD (0x69c8) clear it. vinoEOD's clear path (0x69c8) +is taken only when vinoGetNextBuffer (called at 0x6900) returns < 0 (buffer ring +exhausted: gated on *(conn+0x28), *(conn+0x18), byte *(conn+0x134), and the global +in-progress flag .data+0x438[+0xe4]). So iris's DESC/EOF interrupt sequence is not +driving vinoEOD->vinoGetNextBuffer into the "ring exhausted" state that clears +*(conn+0xc). That is the precise remaining gap. + +Live conn (0x93278900) snapshot during a hung grab: +0x04=bufarray 0x972e1780, ++0x0c=0x0861e000(!), +0x14=dev, +0x100=0xa86214f0 (live desc ptr at STOP, KSEG1), ++0x104=0 (buf idx), +0x10c=5 (buf count), +0xc0=1 (last-delivered field), byte +0xb8=1, byte 0x133=1, byte 0x136=0x1e(30), *(conn+0x118)=0x0027 (&3==3, &8==0). + +### Next step (next session): make iris drive the buffer ring to completion +Trace vinoEOD + vinoGetNextBuffer against the live buffer-ring fields across the +DESC sequence to find which register/state iris must present so vinoGetNextBuffer +returns <0 (or vinoEOD otherwise clears *(conn+0xc)). This is a software +state-machine match driven by the count/order of DESC interrupts vs the 5-deep +buffer ring — NOT a single register value. field_counter free-run alone is +insufficient (tested: build #1 failed) because the gate is *(conn+0xc), not the +0x7640 field-delta. + +## 2026-05-30 (cont. 7) — CORRECTION: poll path IS viable (mask 0xc0); delivery gate = field-parity counter *(conn+0xd2) + +Earlier "poll path is dead" was WRONG — I had not found all the pollwakeup masks. +The REAL frame delivery is the EOF-path static **0x60b4** (called by vinoInterrupt +on EOF, with conn): it runs vinoFillInfo + dms_block_end_dma + dms_fifo_enq to +finalize the captured buffer, then calls the pollwakeup wrapper 0x6fcc with +**mask 0xc0** (at 0x6268). videod's selected mask *(dev+0xb4)=0x140, and +0xc0 & 0x140 = 0x40 → **MATCH**. So EOF -> 0x60b4 -> pollwakeup(0xc0) wakes videod's +select(fd 7). (The 0x30 wrapper in 0x7640 and EOF mask 5 are red herrings.) + +### The exact gate that blocks 0x60b4's delivery +Walking 0x60b4 with live conn values (*(conn+0x118)=0x27): it reaches the delivery +tail only past **0x61cc: `lw v1,0xd0(conn); andi v1,1; beql v1,zero,return`** — +i.e. delivery requires the WORD at conn+0xd0 to be ODD (interlace even/odd pairing). +LIVE: word@0xd0 = 0x00000002 (EVEN) -> returns, no delivery. + +word@0xd0 = (halfword 0xd0 << 16) | halfword 0xd2. 0x7640 increments only the HIGH +half (sh@0x76c0), so the word's bit0 = bit0 of ***(conn+0xd2)**, written at 0x76c8: +`*(conn+0xd2) = t2 = (A_FIELD_COUNTER + t1)`. So **delivery parity is driven by +iris's A_FIELD_COUNTER value** (t1 derives from *(conn+0x118)&3 and field parity). +LIVE *(conn+0xd2)=2 (even) and appears stuck even -> never delivers. + +So the whole chain reduces to: **iris must present A_FIELD_COUNTER such that the +driver's 0x7640 sets *(conn+0xd2) ODD on the field whose EOF runs 0x60b4.** This is +the interlace field-pairing, and HEAD's field_counter (resets to 0 per DMA-enable) +isn't producing it. (Free-running field_counter alone — build #1 — also failed, so +the exact A_FIELD_COUNTER sequence/parity the driver expects per re-arm needs to be +matched, not just "made large". The 0x7640 counter arithmetic vs the live +field_counter values still needs untangling.) + +Live conn this run (0x93278d80): +0xc=0x0861e000, +0xc0=1, word@0xd0=0x00000002 +(=> *(conn+0xd0)=0, *(conn+0xd2)=2), +0x118=0x0027, +0x10c=5, +0x104=0. +icrash addresses change per boot (module reloc): vino_board@0xc00db950 -> +device 0x927c9480 -> conn *(device+0x38). + +### Refined next step +Correlate the live A_FIELD_COUNTER register reads (iris log) with *(conn+0xd2) +across the DESC/EOF sequence to learn the exact parity the driver expects, then +adjust how iris reports CH_FIELD_COUNTER (and possibly when it raises EOF vs DESC) +so *(conn+0xd2) lands ODD on a delivering field. This is now a bounded +field-counter/parity problem on a KNOWN-viable poll delivery path — a much better +position than "poll is impossible". + +## 2026-05-30 (cont. 8) — free-running field_counter fixes the parity COUNTER but not delivery; suspect EOF+DESC simultaneity + +Applied free-running field_counter (removed the reset in start_channel). Confirmed: +A_FIELD_COUNTER now VARIES (0x676,0x677,...) instead of being stuck at 1, and the +driver's parity word now counts: live word@conn+0xd0 went 0x000029fe -> 0x00002a00 +(*(conn+0xd2) incrementing ~1/field, so it DOES pass through odd values e.g. +0x29ff). So the delivery parity gate (0x60b4 @ 0x61cc requires word@0xd0 ODD) is now +SATISFIABLE — but still NO frame delivered (no /usr/tmp/cap-00000.rgb; INTR still +0x05; vino.rs otherwise HEAD; conn this run 0x920c2c00, *(conn+0x118)=0x0027). + +So field_counter was necessary (parity was structurally stuck before) but not +sufficient. The remaining suspicion: the EOF handler 0x60b4 reads *(conn+0xd2) at a +moment it is consistently EVEN, because iris raises EOF and DESC TOGETHER as a +single INTR=0x05. In vinoInterrupt the EOF bit (-> 0x60b4, which READS the parity) +and the DESC/dispatch path (-> 0x7640, which WRITES the parity) are processed in the +same ISR pass, so 0x60b4 likely always sees the just-written (even) value and the +odd half-cycle is never observed at an EOF. Real VINO raises EOF (end of active +video) and DESC (descriptor STOP) as SEPARATE, temporally-ordered interrupts. + +### Strong next hypothesis (was old hypothesis #2, now well-motivated) +Make iris raise EOF and DESC as DISTINCT interrupt events with the driver's ISR +running between them (e.g. raise EOF at end of the field's active-video pump, wait +for the driver to ack/clear it, THEN raise DESC when the descriptor cursor consumes +STOP) — so 0x60b4's *(conn+0xd2) read lands on the ODD half-cycle. Keep the +free-running field_counter (it is hardware-correct and a prerequisite). This is the +last identified gate on a KNOWN-viable poll-delivery path (pollwakeup mask 0xc0 +matches videod's 0x140). Working tree currently: physical.rs(committed alias) + +vino.rs(free-running field_counter, uncommitted, correct-but-insufficient). + +## 2026-05-30 (cont. 9) — EOF/DESC split WORKS mechanically + parity reaches odd, but STILL no delivery + +Implemented the EOF/DESC separation (defer DESC: dma_emit_dword records +stop_reached instead of raising DESC; pump_field raises EOF, polls until the +driver clears the EOF bit, then raises DESC) + kept free-running field_counter. +Build verified: INTR now shows **0x01 (EOF alone) and 0x04 (DESC alone) as +SEPARATE events** (1768x / 1485x), no longer the combined 0x05. And the driver's +parity word @conn+0xd0 now passes through ODD (live reads 0x2ae3 odd, 0x2aec, +0x2af0). So the field-parity gate (0x60b4 @ 0x61cc) is now genuinely satisfiable. +**Yet still NO frame delivered** (no /usr/tmp/cap-00000.rgb). + +So neither the EOF+DESC simultaneity NOR the field-parity counter was the final +blocker — both are now correct and delivery still fails. Also note: 0x7640 (the +parity WRITER) is gated by intr&9 = bits {0,3} = EOF, and 0x60b4 (the READER) is +also the EOF handler — so both already happen on the SAME EOF interrupt regardless +of the split; splitting EOF/DESC therefore can't change their relative order. The +split was a dead end for this gate (though it's more hardware-faithful). + +Remaining suspects (past 0x61cc, in 0x60b4's delivery tail, all UNVERIFIED): +- 0x60b4 may not actually REACH 0x61cc on an odd-parity EOF (an earlier branch: + *(conn+0x118)&0x20 routing at 0x6174, the vinoGetNextBuffer call at 0x61a0, or + the byte 0x13a/0x13b compare at 0x614c). Verify by reading *(conn+0xc4) (the + delivery path does `*(conn+0xc4)+=2` at 0x61e0) — if it never increments, 0x60b4 + never reaches delivery even when parity is odd. +- The finalize calls vinoFillInfo / dms_block_end_dma / dms_fifo_enq may fail on + the captured buffer (bad metadata), or the pollwakeup(0xc0) fires but videod's + vinoPoll still reports not-ready (check *(dev+0xb8) becomes nonzero at delivery). +NOTE: par tracing now needs rtmond, which I disabled for boot speed — re-enable +(`chkconfig rtmond on`) before using par. + +Working tree: physical.rs(committed alias) + vino.rs(free-running field_counter + +EOF/DESC split, uncommitted, mechanically-correct-but-still-no-delivery, untested +on 5.3). Build ~12. Stopping here per plan — no new concrete lead without more +kmem cycles on whether 0x60b4 reaches its delivery tail. + +### DECISIVE: 0x60b4 never reaches its delivery tail (*(conn+0xc4) stuck at 0) +Read *(conn+0xc4) twice ~13s apart during an active capture: BOTH 0x00000000. The +0x60b4 delivery path does `*(conn+0xc4) += 2` (0x61e0), so 0x60b4 is RETURNING +before 0x61e0 on every EOF — i.e. the parity check at 0x61cc sees EVEN at the +EOF-ISR moment every time, even though *(conn+0xd2) (only written by 0x7640) is +observed ODD at random kmem-read times. So the WRITE (0x7640, parity) and the READ +(0x60b4, same EOF ISR) are not aligning to an odd value at the read, contradicting +the naive "0x7640 in the dispatch loop runs before 0x60b4 in the bit-handler". +Resolve next time by single-stepping the value 0x7640 writes vs what 0x60b4 reads +on ONE EOF (e.g. instrument iris to log A_FIELD_COUNTER at the exact EOF raise, and +read *(conn+0xd2) right after) — the +t1 term / which field's counter the driver +samples is the missing piece. The free-running field_counter and EOF/DESC split are +mechanically correct (verified: INTR 0x01/0x04 separate; parity word alternates) +but neither opens delivery, so they were REVERTED to keep the committed state clean +(physical.rs alias only). Re-enable rtmond before par. Build ~12; stopping. + +## 2026-05-30 (cont. 10) — INSTRUMENTATION: field_counter parity is FINE; the parity line is ruled out + +Added a FCREAD log (every CH_FIELD_COUNTER read -> val, parity, int_status) with +free-running field_counter. Result: channel-A A_FIELD_COUNTER reads increment +cleanly 0x1,0x2,0x3,0x4,... with ALTERNATING parity (the parity=0 skew is just +channel-B reads which are always 0x0). So iris presents alternating parity, NOT a +pinned value. => **The field_counter parity is NOT the delivery blocker.** The +whole field-parity line (free-running field_counter, EOF/DESC split) was chasing a +non-issue and is abandoned. + +Reconciles the earlier contradiction: my 0x7640 read "*(conn+0xd2) = +A_FIELD_COUNTER + (field_counter&1)" must be WRONG — kmem showed *(conn+0xd2)~10979 +(odd) while A_FIELD_COUNTER~1663, so *(conn+0xd2) is a different/larger counter, not +field_counter+small. The 0x60b4 0x61cc parity check is therefore NOT the thing +field_counter controls. + +### Real remaining lead (new direction): the capture-mode config *(conn+0x118) +Delivery in 0x60b4 reaches its tail only if byte 0x133 == 0 (0x61ac) OR +*(conn+0x118)&1 == 0 (0x61c0 skips the parity check) OR the 0x61cc parity is odd. +Live: byte 0x133 = 1 and *(conn+0x118) = 0x27 (bit0 set), so it is forced through +the 0x61cc parity check — and *(conn+0xc4) stays 0, proving it never passes. So the +question is why iris's capture setup makes the driver configure *(conn+0x118)=0x27 / +byte 0x133=1 (the interlace/field mode), vs a mode where bit0 is clear (direct +delivery). *(conn+0x118) is written once (0x33a0); trace what value it stores and +what capture-mode input (VL request / register) it depends on. Also worth checking: +whether the *(conn+0xd0)/0xd2 counters are MSC/UST (frame stream counts) rather than +field parity, which would mean 0x61cc gates on stream progress not field parity. + +Instrumentation reverted; committed state remains physical.rs alias only (8426efd). + +## 2026-05-30 (cont. 11) — RESOLVED to a single bit: delivery needs *(conn+0x118)&1 == 0 (a capture-mode config bit) + +Settled the parity contradiction: sampled *(conn+0xd0) word 3x during capture — +0x2a36, 0x2d88, 0x30aa, ALL EVEN. So *(conn+0xd2) (= field_counter+(field_counter&1) +per 0x7640, the ONLY writer) really is ALWAYS EVEN; the earlier "0x2ae3 odd" was a +torn read. => the 0x60b4 @ 0x61cc parity check is STRUCTURALLY always-failing (even +on real hw). + +Full 0x60b4 delivery condition to reach the finalize tail (0x61e0: *(conn+0xc4)+=2, +vinoFillInfo, dms_block_end_dma, dms_fifo_enq, pollwakeup wrapper mask 0xc0): + (0x61cc parity ODD OR 0x61c0: *(conn+0x118)&1 == 0) AND 0x61dc: byte 0x133 != 0 +Parity is always even, so the only route is **\*(conn+0x118)&1 == 0** (and byte +0x133 != 0, which is satisfied =1). LIVE iris: *(conn+0x118)=0x27 (bit0 SET) -> the +sole delivery route is closed -> *(conn+0xc4) stays 0 -> never delivers. CONFIRMED. + +Both gate fields are CAPTURE-MODE config copied in vinoSetupGetFrame (0x30a8): + - *(conn+0x118) = *(s1+0x3a) (writer 0x33a0) + - byte 0x133 = (src & 4) != 0 (writer 0x3414-0x3420; bit2 of a config word) +s1 is the capture request/params (vinoSetupGetFrame's working struct, kern_malloc +0x6c + filled from the VL request / camera defaults). So the driver is in a capture +MODE (interlace/field flags) whose *(conn+0x118) bit0 is set, which routes 0x60b4 +through the dead parity path. + +### THE remaining question (single, precise, fresh direction) +Why does iris's capture produce *(conn+0x118) bit0 = 1? It comes from *(s1+0x3a) +bit0 in vinoSetupGetFrame's request struct, which encodes the capture format/mode +videod negotiated. Either (a) videod requests a mode (interlaced 2-field) that on +real hw ALSO sets bit0 but real hw delivers via a DIFFERENT path than 0x60b4 (i.e. +0x60b4 may NOT be the delivery fn for this mode — re-check vinoFillInfo/dms_fifo_enq +callers; only 0x60b4 was assumed), OR (b) iris presents a video format/capability +that makes videod pick the wrong mode and a correct format would clear bit0. Trace +vinoSetupGetFrame's s1 population (what fills +0x3a) and which VL/format input it +maps to. This is a capture-mode-negotiation question, NOT field_counter/parity (both +now definitively excluded). + +Committed state remains physical.rs alias only (8426efd). Session reached the floor +of the delivery chain: a single capture-mode config bit. Next session starts here. From 4c3673a4b8c2aa391bb1237d14dff286a0fbed56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sat, 30 May 2026 14:51:54 +0200 Subject: [PATCH 3/6] vino: deliver and unscramble IndyCam capture on IRIX 6.5 videod/vidtomem now receives a clean, correctly-coloured 640x480 frame on IRIX 6.5 (verified live with the test-pattern source). Four fixes in the VINO DMA/interrupt path, building on the committed 0x4000_0000 uncached-alias fix (8426efd): - dma_emit_dword: on an interlaced capture, defer the first field's STOP/DESC (raise EOF only) and complete on the second field (EOF+DESC, disable DMA). This makes the kernel driver's field-parity counter reach odd and clears its per-capture "first field" flag (conn+0xb8), the precondition for delivery. - read_channel_reg: report CH_DESC_TABLE_PTR = start_desc_ptr + field-boundary span on the second field, matching the value the kernel records at *(bufentry+0x10). The buffer-completion check (vino.o 0x77c0) then returns 1 instead of aborting at 0x7640/0x7710, so the EOF handler 0x60b4 reaches its delivery tail, clears conn+0xc and wakes vinoGetFrame. - shift_descriptors: 16-byte-align the jump-bug JUMP target (& 0x3FFF_FFF0). The jump-bug chain encodes a +4 offset in the target; following it unaligned dropped the first data page of every 4-descriptor group (~181 of 300 pages) and scrambled the frame. Aligning lands all 300 pages in order. - render_and_pump: emit 32-bit RGB as A,B,G,R (was A,R,G,B), fixing a red/blue channel swap (yellow<->cyan, red<->blue) in the captured image. Full investigation in rules/irix/vino-capture-on-6.5-progress.md (cont. 12-16). Co-Authored-By: Claude Opus 4.8 (1M context) --- rules/irix/vino-capture-on-6.5-progress.md | 447 +++++++++++++++++++-- src/vino.rs | 174 +++++++- 2 files changed, 582 insertions(+), 39 deletions(-) diff --git a/rules/irix/vino-capture-on-6.5-progress.md b/rules/irix/vino-capture-on-6.5-progress.md index 61b02a2..4989249 100644 --- a/rules/irix/vino-capture-on-6.5-progress.md +++ b/rules/irix/vino-capture-on-6.5-progress.md @@ -7,10 +7,33 @@ on **6.5.22**, where it currently does NOT fully work yet. --- ## ★ START HERE (next-session handoff, 2026-05-30) ★ -**Status:** 6.5 IndyCam capture ENGAGES and the VINO DMA+interrupt path runs, but -videod never delivers a frame to the client (vidtomem hangs). The blocker is now -pinned to **one bit**. The dated sections below (cont. 1–11) are the full history, -including several DEAD ENDS — read this block first, then cont. 11 + cont. 6/7/10. +**Status: ★ SOLVED + CLEAN IMAGE (cont. 15–16) ★** — `vidtomem` delivers a **clean, +correctly-coloured 640×480 SMPTE-bar frame** on IRIX 6.5, reproducibly (no hang, no +scramble). First successful end-to-end IndyCam capture on 6.5. FOUR fixes in +`src/vino.rs` + a fast-exchange fix in `src/iris_ci_main.rs` (all uncommitted on +`vino-6.5-capture-engage`, alongside committed physical.rs alias 8426efd). Read cont. 16 +(latest) then 15. The vino.rs fixes: +1. **`dma_emit_dword` defer** (delivery): interlaced first field (`field_counter==0`) + defers STOP/DESC (EOF only); 2nd field completes (EOF+DESC+disable). → parity odd, + `conn+0xb8` clears. +2. **`read_channel_reg` CH_DESC_TABLE_PTR = base+0x780 on 2nd field** (delivery): the + kernel's field-boundary descriptor (`*(bufentry+0x10)`); makes `0x77c0` return 1 so + `0x7640` doesn't abort → `0x60b4` delivers + clears `conn+0xc` → wakeup → frame. +3. **`shift_descriptors` JUMP `& 0x3FFF_FFF0`** (geometry): 16-byte-align the jump-bug + JUMP target (strips its +4 offset) so all 300 data pages land in order — was + scrambling via `& 0x3FFF_FFFF`. +4. **`render_and_pump` Rgba32 emits A,B,G,R** (colour): VINO RGB is A,B,G,R; emitting + A,R,G,B swapped red↔blue. + +`iris-ci get`/`put` also fixed (shell-aware: detect sh vs csh) so the scratch-volume +pull works fast (~1.8s) on the sh-root klindert disk; this is also why `iris-ci run` +had been printing `guest exit -1` (csh `$status` empty under sh). + +**FOLLOW-UPS (not blockers):** (a) `FIELD_DESC_SPAN=0x780` is constant, tuned for +640×480 — derive from clip height for other geometries. (b) 5.3 regression: the +geometry+colour fixes now affect 5.3's interlace path too — verify stock vidtomem still +saves a correct frame on the 5.3 guest. (c) commit. (d) any residual sub-pixel artifact +not chased — the bars/ramp look clean. **Committed/shipped (safe, keep):** - `src/physical.rs` uncached-`0x4000_0000`-alias fix — commit `8426efd` on branch @@ -18,29 +41,73 @@ including several DEAD ENDS — read this block first, then cont. 11 + cont. 6/7 - Boot speed 292s→89s on the klindert guest (nsswitch files-first + FQDN + service disables) — see [[project_klindert_boot_speed]]. `src/vino.rs` is at HEAD. -**THE remaining gate (single bit):** delivery is the EOF-path fn `vino.o 0x60b4` -(→ vinoFillInfo/dms_fifo_enq → pollwakeup mask 0xc0, which DOES match videod's poll -mask `*(dev+0xb4)=0x140`). Its delivery tail (0x61e0; side-effect `*(conn+0xc4)+=2`) -is reached iff `(parityODD || *(conn+0x118)&1==0) && byte0x133!=0`. Parity is -STRUCTURALLY always-even (`*(conn+0xd2)` = `field_counter+(field_counter&1)`, -confirmed by 3 even samples), byte0x133=1 (ok). So the ONLY route is -**`*(conn+0x118)&1 == 0`** — but iris yields `*(conn+0x118)=0x27` (bit0 set), so -`*(conn+0xc4)` stays 0 and nothing delivers. - -**NEXT STEP (do this):** `*(conn+0x118)=*(s1+0x3a)` and `byte0x133=(cfg&4)!=0`, both -copied in `vinoSetupGetFrame` (0x30a8) from a request struct `s1`. Trace what fills -`*(s1+0x3a)` bit0 — i.e. which VL capture-format/mode input it maps to — and whether -iris presents a video format/capability that makes videod negotiate a mode with -bit0 SET vs CLEAR. ALSO sanity-check the assumption that `0x60b4` is the delivery fn -for this mode (only assumed): re-check callers of vinoFillInfo / dms_fifo_enq — if -there's another delivery path used for this capture mode, 0x60b4 may be a red -herring like the others. - -**DEFINITIVELY EXCLUDED (do NOT re-chase):** field_counter parity (instrumented, -alternates fine), the 0x61cc parity counter, EOF/DESC simultaneity (split tested, -no change), the buffer-ring / `*(conn+0xc)` timeout path, the A_DESC_TABLE_PTR -live-cursor / drain-to-STOP (those made 0x77c0 return 2 and SKIP delivery — keep -A_DESC_TABLE_PTR == buffer base, i.e. HEAD). +**THE delivery path (unified — cont. 6 and cont. 11 are the SAME gate):** videod's +worker blocks in the kernel `vinoGetFrame` on `sleep(conn,0x13c)` (0x118&8==0 ⇒ +blocking mode), woken by the only `wakeup(conn)` (in `vinoFinishDMA`). `vinoFinishDMA` +runs from the self-rescheduling timer `vinoWakeupTimeout` ONLY when `*(conn+0xc)==0` +(else it reschedules forever; live `*(conn+0xc)=0x0861e000`, never cleared). The EOF +finalizer `0x60b4` both enqueues the frame (vinoFillInfo/dms_fifo_enq) AND clears +`*(conn+0xc)` (0x6360) — but ONLY if it reaches its delivery tail. So: **0x60b4 +reaching its tail is the one gate; it both delivers and unblocks `vinoGetFrame`.** + +**Why 0x60b4 is blocked, and the REAL gate (cont. 12):** 0x60b4's tail needs +`byte0x133==0 OR 0x118&1==0 OR word@0xd0 parity ODD`. `0x118` and `byte0x133` come +straight from videod's `copyin`'d VL request (`vinoSetupGetFrame`: `0x118=*(s1+0x3a)`, +`byte0x133=(*(s1+0x38)&4)`) — iris CANNOT change them and real HW sends the same +(`0x118=0x27`,`0x133=1`). So real HW delivers via **parity ODD**. The parity writer +`0x7640` computes `*(conn+0xd2)`; in mode `0x118&3==3` it has TWO branches: +- `conn+0xb8 != 0` → `field_counter + (field_counter&1)` = **always EVEN** (dead). +- `conn+0xb8 == 0` AND `(field_counter - conn+0xc0)==1` → `field_counter + prev_parity` + = **ALWAYS ODD** (delivers). `conn+0xc0`/`conn+0xbc` are the previous field's + counter/parity, saved by the prior `0x7640`. + +`conn+0xb8` is the "first field after a DMA (re)start" flag: SET to 1 on every DMA +arm/restart (0x5028, 0x6ae8), CLEARED by the first field's `0x7640`. So a clean +two-field frame delivers on **field 2** (0xb8 cleared by field1, delta==1 ⇒ odd). + +**iris ROOT CAUSE:** iris hits a STOP descriptor and **disables DMA every field** +(`vino.rs:588`, the per-field rewind at `vino.rs:707` + per-row interleave skip make +the cursor reach STOP each field). The kernel then re-arms DMA every field, setting +`conn+0xb8=1` EVERY field → `0x7640` always takes the EVEN branch → parity never odd +→ `0x60b4` never reaches its tail → `*(conn+0xc)` never clears → videod never woken. + +**THE FIX (IMPLEMENTED in `dma_emit_dword`, `src/vino.rs`):** model one DMA-enable +cycle as one interlaced frame. `field_counter` is 0 for the cycle's first field (reset +in `start_channel`) and >=1 after. When the DMA cursor reaches a STOP descriptor, if +`interleave && field_counter == 0` (the FIRST field), DO NOT raise DESC and DO NOT +disable DMA — just end the field's pump (EOF still fires in `pump_field`). The SECOND +field (`field_counter>=1`) reaches STOP and raises EOF+DESC + disables DMA as before. +So INTR goes **0x01 on the even/first field, 0x05 on the odd/second** instead of 0x05 +every field. The kernel then restarts capture once per frame (not per field), so +`conn+0xb8` is cleared by the first field and is 0 at the second field's EOF; delta==1 +holds (consecutive `field_counter` 0→1 within the cycle, kernel reads post-increment +1→2); parity goes ODD; `0x60b4` delivers + clears `*(conn+0xc)`; the timer +`vinoFinishDMA`→`wakeup(conn)` unblocks videod's `vinoGetFrame`. +Keyed on `field_counter==0` (not source parity) so it's robust to where the +even/odd source field lands relative to the DMA-enable boundary. +**5.3 GATE (structural):** 5.3 capture is EOF-driven and page-steps NEXT_4_DESC per +field, so it NEVER reaches a STOP descriptor in `dma_emit_dword` — this branch never +runs for 5.3. The change is also `interleave`-gated. So 5.3 delivery is untouched; +still REGRESSION-TEST it live (stock vidtomem on the 5.3 guest must still save a frame). +NOTE: the early "even=EOF, odd=EOF+DESC didn't deliver" attempt (top of doc, the +"geometry-fix attempt") PREDATES the physical.rs alias fix, so the kernel ring polling +was broken then — that test was INVALID and does NOT refute this. + +**DECISIVE CHEAP VERIFY (do before/with the fix):** with the current HEAD build, read +`conn+0xb8` (byte) during a capture — expect **1** (confirms re-arm-every-field). After +the fix, read `conn+0xb8` at the odd field's EOF (expect 0) and `*(conn+0xd2)` (expect +ODD) and `*(conn+0xc)` (expect it to reach 0). icrash recipe below. + +**SUPERSEDED — do NOT chase (cont. 12 corrections):** the cont. 7–11 "`*(conn+0x118)&1` +single bit" and "trace what VL format makes videod clear bit0" — `0x118` is copyin'd +from videod, normal, and real HW uses the same value; the 0x118/parity path delivers +via `conn+0xb8`, NOT via clearing bit0. cont. 10's "field_counter parity ruled out" +was WRONG to exclude parity: parity IS the gate, just driven by `conn+0xb8`+delta, not +by field_counter's own parity. cont. 8/9 EOF/DESC *split* is HARMFUL: `vinoEOD` aborts +unless EOF fired in the SAME ISR pass (`a2!=0`), so DESC must come WITH EOF on the odd +field (combined 0x05 is correct there) — the split made `a2=0` and aborted vinoEOD. +The 0x60b4 pollwakeup(0xc0)/select path is a real but SECONDARY path (videod uses +blocking sleep, not select); don't optimize for it. **Tooling:** chaindump/chainwalk live in guest `/usr/tmp`. icrash recipe: the vino module RELOCATES per boot, so each boot re-derive: `icrash -e 'od vino_board 1'` @@ -854,3 +921,329 @@ now definitively excluded). Committed state remains physical.rs alias only (8426efd). Session reached the floor of the delivery chain: a single capture-mode config bit. Next session starts here. + +## 2026-05-30 (cont. 12) — FULL delivery path re-derived; cont. 7–11 was a dead path; iris root cause + fix found + +Re-disassembled the ENTIRE 6.5 kernel delivery path from vino.o (capstone, symbol + +reloc aware; `/tmp/dv2.py`, `/tmp/vino.o`). This SUPERSEDES the cont. 7–11 "single +bit `*(conn+0x118)&1`" conclusion — that path is structurally dead and was never how +real HW delivers. Findings, each grounded in the disassembly: + +### 1. `0x118` and `byte0x133` come from videod, not iris (so chasing them is futile) +`vinoSetupGetFrame` (0x30a8) `kern_malloc`s a 0x6c struct `s1` and `copyin`s it from +userspace (0x3134) — it is videod's VL request. Then: + - 0x339c/0x33a0: `*(conn+0x118) = halfword *(s1+0x3a)` + - 0x3414/0x3420: `byte *(conn+0x133) = (*(s1+0x38) & 4) != 0` +iris cannot influence these; real HW gets the SAME values (`0x118=0x27`, `0x133=1`). +So the cont. 11 "trace what makes videod clear bit0" direction is a dead end. + +### 2. `0x60b4` IS the unique frame finalizer (sanity check passed) +`vinoFillInfo` and `dms_fifo_enq` are each called from exactly ONE site, both inside +`0x60b4`. So `0x60b4` is not a red herring — it is the only enqueue path. Its delivery +tail (0x61e0/0x6308) is reached iff `byte0x133==0 OR 0x118&1==0 OR word@0xd0 parity +ODD`. Live (`0x118=0x27`,`0x133=1`) ⇒ needs parity ODD. + +### 3. Parity is gated by `conn+0xb8`, NOT by field_counter's own parity +Decoded the parity writer `0x7640` (called from the per-channel dispatcher `0x5e7c` +on EOF, with `a1 = A_FIELD_COUNTER` = VINO reg 0x4c = iris `chan.field_counter`). In +mode `0x118&3==3`: + - `conn+0xb8 != 0`: `*(conn+0xd2) = a1 + (a1&1)` → ALWAYS EVEN. + - `conn+0xb8 == 0` AND `(a1 - conn+0xc0) == 1`: `*(conn+0xd2) = a1 + conn+0xbc` + (prev field's parity) → `field+1+(prevfield&1)` = ALWAYS ODD (delta==1 ⇒ odd + regardless of which field is even). +`conn+0xb8` = "first field after DMA (re)start" flag: SET=1 on every arm/restart +(0x5028 in the start-capture fn `0x4e1c`; 0x6ae8 in the buffer-restart fn `0x6a70`); +CLEARED by the first field's `0x7640` (0x769c). So a clean 2-field frame delivers on +**field 2**: field1 EOF clears 0xb8 (parity even), field2 EOF sees 0xb8==0 + delta==1 +→ parity ODD → `0x60b4` tail. + +### 4. The delivery MECHANISM is `wakeup(conn)`, and `0x60b4`'s tail unblocks it +videod blocks in `vinoGetFrame` `sleep(conn,0x13c)` (0x3b64; `0x118&8==0` ⇒ blocking). +Only `wakeup(conn)` is in `vinoFinishDMA` (0x751c). `vinoFinishDMA` runs from the timer +`vinoWakeupTimeout` ONLY when `*(conn+0xc)==0` (0x5324: `lw v1,0xc(conn); bnez ... +reschedule`). `0x60b4`'s tail clears `*(conn+0xc)` at 0x6360 (when the buffer's current +descriptor is not a JUMP, i.e. STOP/end). So `0x60b4` reaching its tail BOTH enqueues +the frame AND clears `*(conn+0xc)` → next timer tick → `vinoFinishDMA` → `wakeup` → +`vinoGetFrame` returns. cont. 6 (wakeup path) and cont. 11 (0x60b4 gate) are ONE gate. + +### 5. `vinoEOD` (DESC) requires EOF in the same ISR pass — so combined 0x05 is RIGHT +`vinoEOD` (0x6890): `beqz a2, 0x6970` (abort) where `a2` = "EOF-A also fired this pass" +(set by vinoInterrupt at 0x5cf0 when bit0 handled, passed to vinoEOD at 0x5d5c). So +DESC alone aborts vinoEOD; the cont. 8/9 EOF/DESC SPLIT made `a2=0` and was harmful. +On the odd field, EOF+DESC together (0x05) is correct. Order within one ISR pass: +dispatcher `0x5e7c`→`0x7640` (writes parity) FIRST, then bit0 handler `0x60b4` (reads +parity), then bit2 handler `vinoEOD` — so `0x60b4` reads the parity `0x7640` just wrote. + +### 6. iris ROOT CAUSE +iris hits a STOP descriptor and disables DMA EVERY field (`vino.rs:588`; the per-field +cursor rewind at `vino.rs:707` + per-row interleave skip make the cursor reach the +chain STOP each field). The kernel re-arms DMA each field → `conn+0xb8=1` each field → +`0x7640` always EVEN branch → parity never odd → `0x60b4` tail never reached → +`*(conn+0xc)` never clears → videod never woken. (Matches live: `conn+0xc=0x0861e000` +forever; `*(conn+0xd2)` sampled even.) + +### 7. THE FIX (IMPLEMENTED — compiles + unit-tested; live validation pending) +Implemented in `dma_emit_dword` (`src/vino.rs`), NOT via the pump_field rewind. Insight: +a DMA-enable cycle = one interlaced frame; `field_counter` (reset to 0 in +`start_channel`, incremented per field) IS the in-cycle field index. At the STOP +descriptor, `if interleave && field_counter == 0` (first field) → return false WITHOUT +raising DESC or disabling DMA (EOF still fires in pump_field); the second field +(`field_counter>=1`) raises EOF+DESC + disables DMA as before. Result: INTR `0x01` on +field 1, `0x05` on field 2 (was `0x05` every field). Kernel restarts once per frame, so +`conn+0xb8` is cleared by field 1 and 0 at field 2's EOF; delta==1 (kernel reads +post-increment counters 1 then 2, conn+0xc0=1 ⇒ 2-1=1); parity ODD; `0x60b4` delivers + +clears `*(conn+0xc)`; `vinoFinishDMA`→`wakeup` unblocks `vinoGetFrame`. Keyed on +`field_counter==0` rather than source parity so it's robust to even/odd alignment vs the +DMA-enable boundary. Did NOT touch the `pump_field` rewind (`vino.rs:707`) — both fields +still rewind to the buffer base and write their own rows (even rows / odd rows) into the +shared frame buffer; only the DESC interrupt is deferred. **5.3 GATE (structural):** 5.3 +is EOF-driven, page-steps NEXT_4_DESC per field, never reaches a STOP descriptor here ⇒ +this branch never runs for 5.3; also `interleave`-gated. New unit tests: +`interleave_defers_desc_to_second_field`, `non_interleave_stop_completes_immediately`. +STILL must regression-test 5.3 live. The earlier "even=EOF/odd=EOF+DESC didn't deliver" +attempt (top of doc) is NOT a counter-example: it predates the physical.rs alias fix, so +the kernel ring polling was broken and that test was invalid. + +### Decisive cheap verification (live kmem, ~1 boot) +- HEAD build now: read byte `conn+0xb8` during capture → expect 1 (confirms #6). +- After the fix: at the odd field's EOF read `conn+0xb8` (→0), `*(conn+0xd2)` (→odd), + `*(conn+0xc)` (→reaches 0). Any one of these confirms/refutes the chain. +icrash (module relocates per boot — re-derive each boot): `od vino_board 1`→device; +`od (device+0x38) 1`→ch-A conn; then `od (conn+0xb8) 1`, `od (conn+0xd0) 1`, etc. + +### Tooling note +`/tmp/dv2.py` (host) is the symbol/reloc-aware disassembler used this session: +`python3 /tmp/dv2.py ` | `-r ` (raw range) | `-c ` +(callers via relocs). vino.o at `/tmp/vino.o`. Committed state unchanged: physical.rs +alias only (8426efd); src/vino.rs at HEAD. + +## 2026-05-30 (cont. 13) — LIVE TEST of the cont.12 fix: parity now reaches ODD, but delivery still blocked + +Built `--release --features chd,camera,lightning,developer`, booted the klindert 6.5 +guest (`iris --config iris-klindert.toml --ci --ci-display`; autoboots straight to the +login prompt, no PROM menu), started `/usr/etc/videod` (DISPLAY=:0.0; Xsgi is up via +xdm), ran stock `/usr/sbin/vidtomem -f /var/tmp/cap -v 0`. Added env-gated `VINOTRACE` +eprintln hooks (since removed) to log per-field behavior. + +### The fix works mechanically (VINOTRACE, steady repeating cycle) +``` +start_channel ch0 fc_reset(was 2) +STOP ch0 fc=0 interleave=true -> skip(defer) +pump ch0 parity=Odd fc->1 intr=0x01 +STOP ch0 fc=1 interleave=true -> DESC+disable +pump ch0 parity=Even fc->2 intr=0x05 +(repeat) +``` +So exactly the intended pattern: 2 fields per DMA-enable cycle, field 1 (fc=0) defers +its STOP (EOF only, INTR 0x01), field 2 (fc=1) completes (EOF+DESC, INTR 0x05). Was +`0x05` every field before. (Source parity happens to be Odd-then-Even here; the gate is +keyed on fc, not parity, so that's fine.) + +### Kernel state advanced — parity now ODD (icrash, live, 6 samples) +Re-derived per boot: `vino_board`→device→`*(device+0x38)`=conn. Sampled `*(conn+0xc)` +and the `0xb8..0xd4` block repeatedly: +- `conn+0xb8` (byte) now CLEARS to 0 after field 1 (was STUCK at 1 every field before + the fix) — re-set to 1 by the per-frame restart after field 2. +- `*(conn+0xd0)` word ALTERNATES `0x....0002` (even, with `conn+0xc0`=1) and + **`0x....0003` (ODD, with `conn+0xc0`=2)**. So `*(conn+0xd2)` now reaches ODD on + field 2 — the cont.11 "structurally always even" gate is SATISFIED. The high half + (MSC/frame count) increments steadily (0x11ac,0x1320,0x14a4,0x161e,0x179d,0x1923). + +### But delivery STILL fails (the real remaining gate) +- `*(conn+0xc4)` = 0 in ALL samples. The `0x60b4` delivery tail does `*(conn+0xc4)+=2` + (0x61e0), so `0x60b4` is NOT reaching its tail even though parity is odd. +- `*(conn+0xc)` = `0x0861e000` (buffer base), never cleared → `vinoWakeupTimeout` keeps + rescheduling, `vinoFinishDMA`/`wakeup(conn)` never runs. +- `vidtomem` never produces `cap-00000.rgb` (NO_FRAME); it stays blocked in vinoGetFrame. +So clearing `conn+0xb8` + getting odd parity is NECESSARY but NOT SUFFICIENT. + +### Where the next gate is (analysis, not yet resolved) +vinoInterrupt calls `0x60b4` (EOF handler / reader of the parity) ONLY when the +per-channel dispatcher `0x5e7c` RETURNS 0 (at 0x5ad4: `beqz (s1|s2), 0x5cb8`, where +s1/s2 are the dispatcher returns; 0x5cb8 is the `0x60b4` block). The dispatcher returns +NONZERO (→ `0x60b4` skipped, loop re-reads INTR) when: +- `0x77c0` (completion check) returns 2 (→ 0x5f80, return 1), OR +- `0x7640` (parity writer) returns 1 — its abort path at 0x7794 (`xor at,a0,v0; andi 1; + beqz 0x7794` → vinoAbortDMA, v0=1). `0x7640`'s return depends on `0x77c0`'s return + (passed in as a2/v0) and the parity. +Crucially `0x7640` writes `*(conn+0xd2)` at 0x76c8 BEFORE computing its return value, so +the odd parity we observe can be written while `0x60b4` is still skipped that ISR. +Also note the loop earlier calls `0x7530` (scan buffers for the `0x80020202` done +sentinel) and, if it returns nonzero, calls `0x75a4` (a restart/abort) and SKIPS the +dispatcher+`0x60b4` for that channel entirely. + +So the next investigation is the **completion state machine** `0x7530`/`0x77c0`/`0x7640` +(+ `0x75a4`, vinoEOD) and the dispatcher `0x5e7c` return path — specifically why, on the +odd field, the dispatcher returns nonzero (or `0x7530` fires the restart) so `0x60b4`'s +delivery is bypassed. Needs the dispatcher arg mapping pinned (a1=reg0x74 +A_DESC_TABLE_PTR vs a3=reg0x4c field_counter; iris returns `CH_DESC_TABLE_PTR` = +`start_desc_ptr` = buffer base, `CH_FIELD_COUNTER` = `field_counter`) and live values of +`0x77c0`'s inputs (`*(conn+0x104)` buf idx, `*(conn+4)` buf array, `*(conn+0xc)`) on the +delivering field. icrash + a kernel-side correlation (or more VINOTRACE on the iris reg +reads the kernel makes during the odd-field ISR). + +### Mechanics / harness notes (this session) +- Launch: `iris --config iris-klindert.toml --ci --ci-display` (REX3 on for Xsgi/videod; + XQuartz present). Autoboots → `iris-ci start` then `serial-wait 'IRIS console login:'` + (~90s), `iris-ci login`. `iris-ci boot` times out waiting for "Option?" because the + PROM autoboots — use `start` + `serial-wait login` instead. +- videod isn't auto-running on klindert; start it: `DISPLAY=:0.0 /usr/etc/videod &`. + `vlinfo` then shows `vino 0` + Memory Drain nodes. Run `vidtomem` with `DISPLAY=:0.0`. +- icrash through `iris-ci run` truncates on pipes and prints a spurious + `guest exit -1`; redirect icrash to a file (`>/var/tmp/x 2>&1`) and `cat` it back. A + 6×-icrash loop exceeds the 60s `run` wait — keep batches small or bump the timeout. +- Halt cleanly (`sync;sync; halt -y`, wait for "THE SYSTEM IS BEING SHUT DOWN", ~30s) + before `iris-ci quit` to avoid XFS damage. Did so this session. +- Fix kept (uncommitted), VINOTRACE instrumentation removed; 10 vino unit tests pass. + +## 2026-05-30 (cont. 14) — EXACT abort gate pinpointed; DESC_TABLE_PTR=base+0x10 fix tried LIVE and REGRESSED + +Continued from cont. 13 (parity now reaches ODD but `*(conn+0xc4)` stuck at 0 / no +delivery). Re-disassembled the dispatcher `0x5e7c` + `0x77c0` + `0x7640` and traced the +exact reason `0x60b4` is skipped on the odd field. + +### The exact instruction that kills delivery (`0x7640` @ 0x7710) +``` +0x7710 xor $at, $a0, $v0 ; v0 = a2 = 0x77c0's return value +0x7714 andi $at, $at, 1 +0x7718 beqz $at, 0x7794 ; (a0 ^ v0)&1 == 0 -> 0x7794 = vinoAbortDMA, return 1 +``` +- `v0` = `0x77c0`'s return (passed as `0x7640`'s 3rd arg `a2`); unchanged from entry. +- `a0` = 0 on the **odd**-parity field (0x76fc/0x770c), 1 on the **even** field (0x7730). +- `0x7640` writes the parity `*(conn+0xd2)` at 0x76c8 (and `*(conn+0xc0)`=field_counter + at 0x76b4) BEFORE this abort decision — so odd parity is *recorded* even when it then + aborts. (Explains why icrash sees `conn+0xd2` odd yet nothing delivers.) + +`0x60b4` (the EOF delivery fn) runs ONLY if the per-channel dispatcher `0x5e7c` returns +0 (vinoInterrupt 0x5ad4 `beqz (s1|s2), 0x5cb8`). The dispatcher returns nonzero — and +SKIPS `0x60b4` — when `0x77c0` returns 2 OR `0x7640` returns 1 (its abort path). + +`0x77c0` returns: **0** if `A_DESC_TABLE_PTR(reg 0x74) == *(conn+0xc)` (==buffer base), +**1** if `== phys(buffer desc)` or `phys+0x10`, **2** otherwise. iris reports reg 0x74 = +`start_desc_ptr` = base, and `*(conn+0xc)` = base, so `0x77c0` returns **0** → on the +odd field `v0=0, a0=0` → `(0^0)&1==0` → **ABORT** → `0x60b4` skipped → `*(conn+0xc4)` +stays 0. THIS is the precise delivery blocker. + +### Tried (live): make 0x77c0 return 1 on the odd field — REGRESSED, reverted +Hypothesis: report `CH_DESC_TABLE_PTR` = `start_desc_ptr + 0x10` on the 2nd field +(`interleave && field_counter >= 2`) so `0x77c0` hits its `phys+0x10` case → returns 1 → +`v0=1` → odd field `(0^1)&1=1` → no abort → `0x60b4` runs → delivers. Built, booted 6.5, +ran vidtomem. **RESULT: NO delivery AND a REGRESSION** — icrash (8 samples) showed +`conn+0xc0` STUCK at 1 (was toggling 1↔2) and `conn+0xd0` low half STUCK at 0x0002 +(even; was alternating 0002/0003). So the change broke the field pairing that had been +producing odd parity: the odd field's `0x7640` no longer ran with `a1=2`. CONCLUSION: +the `0x7640` **abort/`vinoAbortDMA` is load-bearing** — it drives the restart that +produces the next even/odd pair. Naively suppressing it via the DESC_TABLE_PTR readback +desyncs the cycle. **Reverted** the read_channel_reg change; kept the `dma_emit_dword` +fix (which still gets parity to odd). 10 vino unit tests pass. + +### State + next direction +- KEPT (uncommitted): `dma_emit_dword` defer-DESC-to-2nd-field fix → parity reaches odd, + `conn+0xb8` clears, INTR 0x01/0x05. Necessary but not sufficient; 5.3-safe. +- The delivery blocker is the `0x7640` abort at 0x7710, which fires because `0x77c0` + returns 0 on the odd field (A_DESC_TABLE_PTR == buffer base == *(conn+0xc)). On real + HW the hardware would have ADVANCED the descriptor-table pointer so `0x77c0` returns 1 + WITHOUT desyncing the pairing — i.e. the readback must advance as a faithful live + consequence of DMA progress, not a field_counter hack. The naive `+0x10` failed + because it isn't tied to the actual descriptor consumption and the abort is part of + the cycle. +- NEXT: model the descriptor-table-pointer readback (reg 0x70/0x74) as the TRUE live + cursor AND make the per-field DMA consume exactly one descriptor group per field so + the cursor reads base→base+0x10→… in step with the kernel's completion handshake; + understand `vinoAbortDMA`'s role + the `0x7530` done-sentinel (`0x80020202`) scan + + `*(conn+0x104)` buffer-index advance, so the odd field's `0x77c0` returns 1 while the + even/odd pairing (and the restart) stays intact. Also re-pin the dispatcher arg map + (a1=reg0x74 A_DESC_TABLE_PTR vs a3=reg0x4c field_counter) — the static trace had + residual uncertainty there, which is why the single-register fix mispredicted. +- 5.3 regression test still NOT run. + +## 2026-05-30 (cont. 15) — ★ SOLVED: vidtomem delivers a 640×480 frame on IRIX 6.5 ★ + +Continued from cont. 14's exact-abort-gate finding. Instrumented the descriptor +registers (VINOTRACE on NEXT_4_DESC/DESC_TABLE_PTR writes+reads, since removed) and +read the live buffer entry via icrash. **Found the missing value and DELIVERED a frame.** + +### The decisive datum (icrash, buffer entry 0) +`bufarray = *(conn+4)`, `bufidx = *(conn+0x104) = 0`, so `bufentry = bufarray`: +``` +bufentry+0x0c = 0xa861e000 (chain base, KSEG1 — what 0x7530 scans for the sentinel) +bufentry+0x10 = 0xa861e780 (the FIELD-BOUNDARY descriptor — what 0x77c0 compares!) +``` +`0x77c0` does `kvtophys(*(bufentry+0x10))` = `0x0861e780` = **`base + 0x780`** (NOT base). +It returns 1 (→ `0x7640` does NOT abort → `0x60b4` delivers) only if `A_DESC_TABLE_PTR` +(reg 0x74) equals that phys (or +0x10). cont.14's `base+0x10` guess was wrong because it +assumed `phys==base`; the kernel actually records the field boundary at `base+0x780` +(≈ descriptor group 120 — exactly the "cursor freezes at 0x0861e780" from cont. 3, and += rows-per-field 240 × 8). + +### THE FIX (two parts, both in src/vino.rs — VERIFIED LIVE) +1. **`dma_emit_dword`** (from cont.12): on an interlaced capture, the FIRST field of a + DMA-enable cycle (`field_counter==0`) reaches STOP but DEFERS — no DESC, no DMA + disable (EOF only); the SECOND field raises EOF+DESC. → INTR 0x01 then 0x05, parity + reaches odd, `conn+0xb8` clears. +2. **`read_channel_reg` CH_DESC_TABLE_PTR**: report `start_desc_ptr + 0x780` on the + second field (`field_counter >= 2`), `start_desc_ptr` otherwise. This makes `0x77c0` + return 1 on the delivering field so `0x7640` (0x7710) does NOT abort, the dispatcher + returns 0, `0x60b4` runs, reads odd parity, reaches its tail (`conn+0xc4 += 2`, + `dms_fifo_enq`, clears `conn+0xc`) → `vinoWakeupTimeout` sees `conn+0xc==0` → + `vinoFinishDMA` → `wakeup(conn)` → `vinoGetFrame` returns the frame to videod. + +### Result (live, klindert 6.5.22) +`/usr/sbin/vidtomem -f /var/tmp/cap -v 0` → `saved image to file` → +`cap-00000.rgb: SGI imagelib image (640 x 480)` (header `01 da 01 01 00 03 02 80 01 e0 +00 03` = SGI RLE, 640×480×3). **Reproducible** across boots and across repeated grabs; +the clean (non-instrumented) release build delivers; vidtomem runs to completion (no +hang). FIRST successful end-to-end IndyCam capture on 6.5. + +### Caveats / follow-ups (not blockers, but worth doing) +- `FIELD_DESC_SPAN = 0x780` is the kernel's field-boundary offset for the standard + 640×480 IndyCam capture (`= 240 rows/field × 8`). It is currently a constant; other + capture geometries would need it derived (from clip height / the chain layout). The + live cursor `next_desc_ptr` is NOT a usable substitute — at the ISR it reads + `base+0x10` (per-field rewind race) or chain-end mid-pump, never the boundary. +- 5.3 regression test STILL not run (both fixes are interleave + 2nd-field gated and + 5.3 is EOF-driven / never reaches a STOP descriptor here, so expected no-op — but + verify: stock `vidtomem` must still save a frame on the 5.3 guest). +- Image fidelity/geometry not re-checked this session (the cont. notes mention a 1-px + diagonal artifact); delivery is the milestone — quality is a separate pass. +- Changes are UNCOMMITTED on branch `vino-6.5-capture-engage` (alongside the committed + physical.rs alias 8426efd). 11 vino unit tests pass (3 new: interleave-defer, + non-interleave-stop, desc-table-ptr-advance). + +## 2026-05-30 (cont. 16) — image UNSCRAMBLED: JUMP alignment + R/B byte order; iris-ci get fixed + +After delivery (cont. 15) the saved frame was scrambled. Two more fixes (src/vino.rs) +produce a clean, correctly-coloured 640×480 SMPTE bar capture (verified live — pulled +via `iris-ci get`, converted with ImageMagick, visually correct: white/yellow/cyan/ +green/magenta/red/blue/black bars + luma ramp): + +1. **JUMP 16-byte alignment** (`shift_descriptors`): the jump-bug chain's JUMP targets + carry a +4 low-bit offset; following them with `& 0x3FFF_FFFF` (unaligned) read each + next 4-descriptor group 4 bytes high, dropping the first data page of every group + (~181/300 pages) and scrambling the frame. Changed to `& 0x3FFF_FFF0` so the walk + stays group-aligned and all 300 data pages land in order. (This is the cont.4 fix; + it had been reverted earlier because it killed DESC — but with the cont.15 delivery + model DESC still fires, so it's safe now. Confirmed: delivery still works.) +2. **RGBA byte order A B G R** (`render_and_pump` Rgba32): VINO 32-bit RGB lands as + A,B,G,R; iris emitted A,R,G,B, swapping red↔blue (yellow↔cyan, red↔blue; white/ + green/magenta/black unchanged — the tell-tale R/B-swap signature). Emit A,B,G,R. + Unit test `rgba32_emits_abgr_two_pixels_per_dword` updated. + +### Bonus: `iris-ci get`/`put` fixed for sh-root guests (src/iris_ci_main.rs) +`iris-ci get` was failing ("sh: /dev/null: bad file unit number") because it hardcoded +csh redirect `>& /dev/null` + `$status`, but the klindert root shell is `/bin/sh` +(needs `2>&1` + `$?`). This is also why every `iris-ci run` reported `guest exit -1` +(empty `$status`). Added `detect_guest_shell()` (probes `$0` with a sentinel, no +rc-marker dependence) + `devnull_redirect()`; `cmd_get`/`cmd_put` now pick the matching +redirect + shell. Result: `iris-ci get /var/tmp/c-00000.rgb` pulls 294 KB in ~1.8 s +over the scratch volume (vs minutes of flaky uuencode-over-serial). Works for both +sh-root (6.5 klindert) and csh-root (classic 5.3) guests. + +### State +Full pipeline works end to end on 6.5: capture → videod → vidtomem → a clean, correctly +coloured 640×480 SGI frame, pulled to the host fast. Uncommitted on +`vino-6.5-capture-engage`: src/vino.rs (delivery defer + DESC_TABLE_PTR span + JUMP +align + ABGR) and src/iris_ci_main.rs (shell-aware get/put). 11 vino unit tests pass. +Remaining follow-ups: derive FIELD_DESC_SPAN (0x780) from geometry for non-640×480; +5.3 regression (geometry/colour now apply to 5.3's interlace path too — verify stock +vidtomem still saves a correct frame on 5.3); commit. diff --git a/src/vino.rs b/src/vino.rs index a86e1f6..f988703 100644 --- a/src/vino.rs +++ b/src/vino.rs @@ -556,7 +556,16 @@ impl Vino { Self::descriptor_fetch(chan, ptr, mem); chan.next_desc_ptr = chan.next_desc_ptr.wrapping_add(16); } else if chan.descriptors[0] & desc::JUMP_BIT != 0 { - let target = (chan.descriptors[0] as u32) & 0x3FFF_FFFF; + // The 6.5 jump-bug descriptor chain (vinoBuildJumpBugDAPS) ends most + // 4-descriptor groups with a JUMP whose encoded target carries a +4 + // (sometimes +8) low-bit offset — a workaround for the hardware's + // 4-at-a-time descriptor-cache prefetch. The real fetch is always + // 16-byte-group-aligned, so that offset must be masked off; following + // it unaligned reads each next group 4 bytes high, dropping the first + // data page of every group (~181 of 300 pages reached) and scrambling + // the captured frame. Mask to 16 bytes so the walk stays in step and + // all 300 data pages land in order. + let target = (chan.descriptors[0] as u32) & 0x3FFF_FFF0; Self::descriptor_fetch(chan, target, mem); } } @@ -576,11 +585,34 @@ impl Vino { return false; } - let chan = &mut st.channels[ch]; + let interleave = st.control & [ctrl::CHA_INTERLEAVE_EN, ctrl::CHB_INTERLEAVE_EN][ch] != 0; - if chan.descriptors[0] & desc::VALID_BIT != 0 - && chan.descriptors[0] & desc::STOP_BIT != 0 + if st.channels[ch].descriptors[0] & desc::VALID_BIT != 0 + && st.channels[ch].descriptors[0] & desc::STOP_BIT != 0 { + // Interlaced capture (IRIX 6.5): the kernel lays out ONE dense + // descriptor chain spanning the whole frame buffer, and BOTH fields + // traverse it to the same terminating STOP. Real VINO keeps DMA + // running across both fields and raises end-of-descriptor (DESC) only + // when the chain completes after the SECOND field. We model a + // DMA-enable cycle as one interlaced frame: `field_counter` is 0 for + // the first field of the cycle (reset in start_channel) and >=1 + // after. On the FIRST field, reaching STOP must NOT raise DESC or + // disable DMA — otherwise the kernel restarts capture every field, + // which re-sets its "first field of capture" flag (conn+0xb8) and + // forces its field-parity counter even, so vinoEOD's completion check + // never clears *(conn+0xc) and videod's vinoGetFrame is never woken. + // Deferring completion to the second field lets the kernel's parity go + // odd and the frame deliver. Both fields still render their own rows + // into the shared buffer; only the DESC interrupt is deferred. + // (Full derivation: rules/irix/vino-capture-on-6.5-progress.md cont.12.) + // + // 5.3 GATE: IRIX 5.3 capture is EOF-driven and page-steps NEXT_4_DESC + // per field, so it never reaches a STOP descriptor here — this branch + // never executes for 5.3 and its delivery path is untouched. + if interleave && st.channels[ch].field_counter == 0 { + return false; + } let isr_desc = [isr::CHA_DESC, isr::CHB_DESC][ch]; let new_status = st.int_status | isr_desc; let irq = self.irq.lock().clone(); @@ -589,6 +621,7 @@ impl Vino { return false; } + let chan = &mut st.channels[ch]; let desc_base = (chan.descriptors[0] as u32) & desc::PTR_MASK as u32; let write_addr = desc_base | (chan.page_index & 0x0FF8); drop(st); @@ -822,11 +855,15 @@ impl Vino { } PixelFormat::Rgba32 => { let (r, g, b) = yuv_to_rgb(y_s, u, v); - // SGI RGBA in memory: A R G B (alpha in the high byte). + // VINO 32-bit RGB lands in memory as A B G R (alpha high, + // then blue, green, red) — that's the order videod/the SGI + // imagelib reads back. Emitting A R G B instead swaps the + // red and blue channels (yellow↔cyan, red↔blue) in the + // captured frame. Bytes are packed MSB-first, so emit A,B,G,R. emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, 0xFF); - emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, r); - emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, g); emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, b); + emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, g); + emit_byte(self, ch, mem, &mut accum, &mut bytes_in, &mut stopped, &mut line_bytes, r); } PixelFormat::Rgba8 => { let (r, g, b) = yuv_to_rgb(y_s, u, v); @@ -946,7 +983,30 @@ impl Vino { reg::CH_LINE_COUNT => chan.line_counter, reg::CH_PAGE_INDEX => chan.page_index, reg::CH_NEXT_4_DESC => chan.next_desc_ptr, - reg::CH_DESC_TABLE_PTR => chan.start_desc_ptr, + // Descriptor-table-pointer readback. The 6.5 kernel's buffer-completion + // check (vino.o `0x77c0`) compares this against the buffer's recorded + // field-boundary descriptor `*(bufentry+0x10)` = `base + FIELD_DESC_SPAN` + // and the EOF/parity path (`0x7640` @ 0x7710) ABORTS the capture (so the + // delivery fn `0x60b4` is skipped and no frame is ever handed to videod) + // UNLESS, on the SECOND interlaced field, `0x77c0` returns 1 — which it + // does only when this readback equals that boundary (or +0x10). On the + // FIRST field it must read the base (so `0x77c0` returns 0 and the even + // field doesn't abort either). `field_counter` is reset to 0 per + // DMA-enable in start_channel and reaches 2 at the 2nd field's interrupt. + // With this, vidtomem delivers a 640x480 frame on 6.5 (verified live, + // cont. 15). FIELD_DESC_SPAN is the kernel's field-boundary offset for the + // standard IndyCam capture (= rows-per-field 240 * 8); generalizing it for + // other geometries is follow-up. 5.3 GATE: 5.3 capture is EOF-driven / + // page-steps NEXT_4_DESC and uses neither this completion check nor a 2nd + // interlaced field, so its readback stays at the base. + reg::CH_DESC_TABLE_PTR => { + const FIELD_DESC_SPAN: u32 = 0x780; + if chan.field_counter >= 2 { + chan.start_desc_ptr.wrapping_add(FIELD_DESC_SPAN) + } else { + chan.start_desc_ptr + } + } reg::CH_DESC_0 => (chan.descriptors[0] & desc::DATA_MASK) as u32, reg::CH_DESC_1 => (chan.descriptors[1] & desc::DATA_MASK) as u32, reg::CH_DESC_2 => (chan.descriptors[2] & desc::DATA_MASK) as u32, @@ -1509,7 +1569,7 @@ mod tests { } #[test] - fn rgba32_emits_argb_two_pixels_per_dword() { + fn rgba32_emits_abgr_two_pixels_per_dword() { let (vino, mem) = setup_vino(PixelFormat::Rgba32, 1, false, 2, 1, 0x3000); let field = make_field(2, 1); let mem_dyn: Arc = mem.clone(); @@ -1521,12 +1581,13 @@ mod tests { // Alpha is always 0xFF in slots [0] and [4]. assert_eq!(bytes[0], 0xFF); assert_eq!(bytes[4], 0xFF); - // R/G/B for each pixel come from yuv_to_rgb of that pixel's (Y, U, V). + // VINO writes A B G R (blue before red); emitting A R G B swaps red/blue + // in the captured frame (verified live — see render_and_pump). let (r0, g0, b0) = yuv_to_rgb(field.pixels[1], field.pixels[0], field.pixels[2]); let (r1, g1, b1) = yuv_to_rgb(field.pixels[3], field.pixels[0], field.pixels[2]); assert_eq!(&bytes[..], - &[0xFF, r0, g0, b0, 0xFF, r1, g1, b1][..], - "ARGB packing for the two pixels"); + &[0xFF, b0, g0, r0, 0xFF, b1, g1, r1][..], + "ABGR packing for the two pixels"); } #[test] @@ -1549,6 +1610,95 @@ mod tests { assert_eq!(st.channels[0].field_counter, 1, "field_counter advances even when dropped"); } + /// Interlaced (6.5) capture: hitting a STOP descriptor on the FIRST field of + /// a DMA-enable cycle (field_counter == 0) must NOT raise DESC or disable DMA + /// — the completion is deferred to the second field so the kernel's field + /// pairing delivers the frame. The second field (field_counter >= 1) does + /// raise DESC and disable DMA. See dma_emit_dword + cont.12 in the rules note. + #[test] + fn interleave_defers_desc_to_second_field() { + let vino = Vino::new(); + let mem = MockMem::new(); + vino.set_phys(mem.clone()); + let mem_dyn: Arc = mem.clone(); + + // DMA + interleave on channel A, with DESC interrupts enabled so a raised + // DESC actually surfaces in int_status. + { + let mut st = vino.state.lock(); + st.control = ctrl::CHA_DMA_EN | ctrl::CHA_INTERLEAVE_EN | ctrl::CHA_DESC_INT_EN; + let chan = &mut st.channels[0]; + // Head descriptor carries the STOP bit (chain terminator). + chan.descriptors[0] = (desc::STOP_BIT | 1) | desc::VALID_BIT; + chan.field_counter = 0; // first field of the cycle + } + + // First field: STOP reached, but DESC deferred and DMA left enabled. + assert!(!vino.dma_emit_dword(0, 0, &mem_dyn), "STOP still stops the field pump"); + { + let st = vino.state.lock(); + assert_eq!(st.int_status & isr::CHA_DESC, 0, + "DESC must NOT fire on the first interleaved field"); + assert_ne!(st.control & ctrl::CHA_DMA_EN, 0, + "DMA must stay enabled across the first field"); + } + + // Second field of the cycle: now the STOP completes the frame. + vino.state.lock().channels[0].field_counter = 1; + assert!(!vino.dma_emit_dword(0, 0, &mem_dyn), "STOP stops the field pump"); + { + let st = vino.state.lock(); + assert_ne!(st.int_status & isr::CHA_DESC, 0, + "DESC must fire on the second interleaved field"); + assert_eq!(st.control & ctrl::CHA_DMA_EN, 0, + "DMA is disabled once the frame completes"); + } + } + + /// Non-interleaved capture (e.g. the IRIX 5.3 path is EOF-driven and never + /// actually reaches a STOP here, but guard the gate anyway): a STOP on the + /// first field still raises DESC and disables DMA — the deferral is + /// interleave-only. + #[test] + fn non_interleave_stop_completes_immediately() { + let vino = Vino::new(); + let mem = MockMem::new(); + vino.set_phys(mem.clone()); + let mem_dyn: Arc = mem.clone(); + { + let mut st = vino.state.lock(); + st.control = ctrl::CHA_DMA_EN | ctrl::CHA_DESC_INT_EN; // no INTERLEAVE + let chan = &mut st.channels[0]; + chan.descriptors[0] = (desc::STOP_BIT | 1) | desc::VALID_BIT; + chan.field_counter = 0; + } + assert!(!vino.dma_emit_dword(0, 0, &mem_dyn)); + let st = vino.state.lock(); + assert_ne!(st.int_status & isr::CHA_DESC, 0, + "non-interleaved STOP raises DESC even on the first field"); + assert_eq!(st.control & ctrl::CHA_DMA_EN, 0, "non-interleaved STOP disables DMA"); + } + + /// CH_DESC_TABLE_PTR readback: the 6.5 kernel's buffer-completion check needs + /// this to equal the field-boundary descriptor (base + 0x780) on the SECOND + /// interlaced field (field_counter >= 2) so it doesn't abort and delivery + /// proceeds; on the first field it stays at the base. (reg 0x70 is the + /// A_DESC_TABLE_PTR slot; read_reg masks bit 2 to collapse the 64-bit pair.) + #[test] + fn desc_table_ptr_advances_on_second_interlaced_field() { + let vino = Vino::new(); + { + let mut st = vino.state.lock(); + st.channels[0].start_desc_ptr = 0x0861_e000; + st.channels[0].field_counter = 1; // first field's interrupt + } + assert_eq!(vino.read_reg(reg::CHA_BASE + reg::CH_DESC_TABLE_PTR), 0x0861_e000, + "first interlaced field reads the descriptor-table base"); + vino.state.lock().channels[0].field_counter = 2; // second field's interrupt + assert_eq!(vino.read_reg(reg::CHA_BASE + reg::CH_DESC_TABLE_PTR), 0x0861_e780, + "second interlaced field reads base + field-boundary span (0x780)"); + } + // ─── I2C bus tests: SAA7191 + CDMC coexist on a shared bus ─────────── /// Push one byte through the VINO I2C bridge by way of the I2C_CONTROL / From d2016f01cad3b7881c56b2ccca01ecbbbc2994e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sun, 31 May 2026 10:20:16 +0200 Subject: [PATCH 4/6] camera: read nokhwa YUYV chroma straight (fix red/blue swap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit downscale_yuyv_to_uyvy was swapping U/V (reading Cr at byte 1, Cb at byte 3), which had been compensating for VINO's RGBA output emitting A,R,G,B. Now that the output order is fixed to A,B,G,R (correct, verified on the test pattern), this swap is no longer cancelled and the live camera comes out red<->blue (a red shirt renders blue). nokhwa's macOS backend delivers standard YUYV (Cb at byte 1, Cr at byte 3), so read the chroma straight. The test pattern is unaffected — it hand-builds canonical UYVY and never goes through this path. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/camera.rs | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/camera.rs b/src/camera.rs index bd887d8..2c62f66 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -185,16 +185,16 @@ fn capture_loop(shared: Arc>, frame_w: u32, frame_h: u32, // ─── Downscale + format conversion ─────────────────────────────────────────── /// One-pass area-averaging downscale that also converts the input byte order -/// from YUYV-as-delivered-by-nokhwa to canonical UYVY (U Y0 V Y1). +/// from YUYV (as delivered by nokhwa) to canonical UYVY (U Y0 V Y1). /// -/// Note: nokhwa's macOS backend (AVFoundation via the `nokhwa` crate's -/// `YuyvFormat`) delivers the chroma pair with Cr (V) at byte 1 and Cb -/// (U) at byte 3 rather than the textbook YUYV order (Cb at 1, Cr at 3). -/// Reading them straight through to a yuv_to_rgb that expects canonical -/// U/V positions produced the washed-out turquoise/red cast we saw in -/// the first end-to-end camera capture inside IRIX. Swapping at this -/// layer keeps the rest of the pipeline (UYVY contract, vino's render, -/// `yuv_to_rgb`) using canonical channel identities. +/// nokhwa's macOS backend delivers standard YUYV: Y0 Cb Y1 Cr — Cb (U) at byte +/// 1, Cr (V) at byte 3. We just reorder to UYVY; the chroma is read straight. +/// (A previous version swapped U/V here to "fix" a red/turquoise cast. That +/// cast was actually VINO's RGBA output emitting A,R,G,B instead of A,B,G,R; +/// the two swaps cancelled, so the camera looked right while the test pattern +/// was red/blue-swapped. With the output order fixed in `render_and_pump`, the +/// chroma must be read straight here or the camera comes out red<->blue +/// swapped — verified live: a red shirt rendered blue until this was undone.) /// /// Iterates over destination pixel pairs (since YUYV / UYVY are 2-pixel /// units) and averages every source byte that maps into the destination @@ -223,9 +223,9 @@ fn downscale_yuyv_to_uyvy(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec let i = row + (sx as usize) * 2; if i + 3 >= src.len() { break; } y0_s += src[i ] as u32; // Y0 - v_s += src[i + 1] as u32; // V (Cr) — nokhwa puts Cr here + u_s += src[i + 1] as u32; // Cb (U) — standard YUYV y1_s += src[i + 2] as u32; // Y1 - u_s += src[i + 3] as u32; // U (Cb) — nokhwa puts Cb here + v_s += src[i + 3] as u32; // Cr (V) — standard YUYV n += 1; sx += 2; } @@ -287,23 +287,22 @@ mod tests { #[test] fn downscale_byte_swap_is_correct_for_1to1_2x1() { - // nokhwa's macOS YUYV delivers chroma in YVYU order at the byte - // level (Cr at byte 1, Cb at byte 3). The downscale function - // re-orders to canonical UYVY (U Y0 V Y1). - // Input bytes (nokhwa YUYV-on-macOS): Y0=0xA0 V=0x80 Y1=0xA1 U=0x40 - let src = vec![0xA0, 0x80, 0xA1, 0x40]; + // nokhwa delivers standard YUYV: Y0 Cb Y1 Cr (Cb/U at byte 1, Cr/V at + // byte 3). The downscale function re-orders to canonical UYVY (U Y0 V Y1). + // Input bytes (standard YUYV): Y0=0xA0 Cb=0x40 Y1=0xA1 Cr=0x80 + let src = vec![0xA0, 0x40, 0xA1, 0x80]; let dst = downscale_yuyv_to_uyvy(&src, 2, 1, 2, 1); - // Output UYVY: U Y0 V Y1 + // Output UYVY: U(=Cb) Y0 V(=Cr) Y1 assert_eq!(dst, vec![0x40, 0xA0, 0x80, 0xA1]); } #[test] fn downscale_averages_4x1_to_2x1() { - // Two nokhwa-YUYV pairs at 4×1 → one canonical-UYVY pair at 2×1, - // averaged. Input byte order per pair: Y0 V Y1 U (Cr at 1, Cb at 3). + // Two standard-YUYV pairs at 4×1 → one canonical-UYVY pair at 2×1, + // averaged. Input byte order per pair: Y0 Cb Y1 Cr (Cb at 1, Cr at 3). let src = vec![ - 0x10, 0x80, 0x20, 0x40, // pair 0: Y0=0x10 V=0x80 Y1=0x20 U=0x40 - 0x30, 0xA0, 0x40, 0x60, // pair 1: Y0=0x30 V=0xA0 Y1=0x40 U=0x60 + 0x10, 0x40, 0x20, 0x80, // pair 0: Y0=0x10 Cb=0x40 Y1=0x20 Cr=0x80 + 0x30, 0x60, 0x40, 0xA0, // pair 1: Y0=0x30 Cb=0x60 Y1=0x40 Cr=0xA0 ]; let dst = downscale_yuyv_to_uyvy(&src, 4, 1, 2, 1); // Expected output pair has averaged channels (canonical UYVY): From 9b337d7a354f6ac1dea1131b0999403cbbb33b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sun, 31 May 2026 12:43:19 +0200 Subject: [PATCH 5/6] tools/vino: collect guest-side vino debug/capture tools The C helpers used to drive and inspect IndyCam capture from inside IRIX (compiled on the guest with MIPSpro cc): vinograb (VL one-frame grab), chaindump/chainwalk (descriptor-chain dump/walk via /dev/mem), mempeek (/dev/mem reader), vinodump (DMA-page reconstruction). Referenced from rules/irix/vino-capture-on-6.5-progress.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/vino/chaindump.c | 42 ++++++++++++++++ tools/vino/chainwalk.c | 37 ++++++++++++++ tools/vino/mempeek.c | 28 +++++++++++ tools/vino/vinodump.c | 93 ++++++++++++++++++++++++++++++++++++ tools/vino/vinograb.c | 106 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+) create mode 100644 tools/vino/chaindump.c create mode 100644 tools/vino/chainwalk.c create mode 100644 tools/vino/mempeek.c create mode 100644 tools/vino/vinodump.c create mode 100644 tools/vino/vinograb.c diff --git a/tools/vino/chaindump.c b/tools/vino/chaindump.c new file mode 100644 index 0000000..5adc2fd --- /dev/null +++ b/tools/vino/chaindump.c @@ -0,0 +1,42 @@ +/* chaindump — dump the VINO descriptor chain: for each 32-bit + * descriptor word show its type (DATA/JUMP/STOP) and address/target, following + * JUMPs (16-byte aligned, like the hardware) until STOP or a guard limit. + * Prints the first N and the run near STOP so we can see the interlace layout. */ +#include +#include +#include +#include +static unsigned int rd(int fd, unsigned long a){unsigned int w=0;lseek(fd,(off_t)a,0);read(fd,&w,4);return w;} +int main(int argc, char **argv){ + unsigned long cur; int fd; unsigned int cache[4]; int slot; + unsigned long laddr; long pages=0, jumps=0, guard=0; int printed=0; + cur=(argc>1)?strtoul(argv[1],0,16):0x0861e000ul; + fd=open("/dev/mem",O_RDONLY); if(fd<0){perror("mem");return 1;} + laddr=cur; + for(slot=0;slot<4;slot++) cache[slot]=rd(fd,cur+slot*4); + slot=0; + while(guard++<4000){ + unsigned int d=cache[slot]; + int doprint = (printed<40) || (pages>=170); + if(d & 0x80000000u){ + if(doprint) printf("[%6ld] @%08lx STOP word=%08x\n",guard,laddr,d); + break; + } else if(d & 0x40000000u){ + unsigned long tgt=(unsigned long)(d & 0x3ffffff0u); /* 16-byte aligned */ + unsigned long tgtu=(unsigned long)(d & 0x3fffffffu); + jumps++; + if(doprint){ printf("[%6ld] @%08lx JUMP ->%08lx (raw lowbits %lx)\n",guard,laddr,tgt,tgtu&0xf); printed++; } + laddr=tgt; + for(slot=0;slot<4;slot++) cache[slot]=rd(fd,tgt+slot*4); + slot=0; + continue; + } else { + pages++; + if(doprint){ printf("[%6ld] @%08lx DATA page=%08x\n",guard,laddr,d & 0x3fffffff); printed++; } + } + slot++; laddr+=4; + if(slot==4){ for(slot=0;slot<4;slot++) cache[slot]=rd(fd,laddr+slot*4); slot=0; } + } + printf("TOTAL pages=%ld jumps=%ld guard=%ld\n",pages,jumps,guard); + return 0; +} diff --git a/tools/vino/chainwalk.c b/tools/vino/chainwalk.c new file mode 100644 index 0000000..6fb83b3 --- /dev/null +++ b/tools/vino/chainwalk.c @@ -0,0 +1,37 @@ +/* chainwalk — follow the VINO descriptor chain exactly as iris's + * shift_descriptors does (fetch 4, use slot0/page, shift, JUMP->fetch target), + * report data pages until STOP and any backward (ring) jump. */ +#include +#include +#include +#include +static unsigned int rd(int fd,unsigned long a){unsigned int w=0;lseek(fd,(off_t)a,0);read(fd,&w,4);return w;} +int main(int argc,char**argv){ + unsigned long start; int fd; unsigned int cache[4]; int valid[4]; + unsigned long ndp; int i; long pages=0,jumps=0,back=0,guard=0; + unsigned long laddr,firststop=0; + start=(argc>1)?strtoul(argv[1],0,16):0x0861e000ul; + fd=open("/dev/mem",O_RDONLY); if(fd<0){perror("mem");return 1;} + ndp=start; laddr=start; + for(i=0;i<4;i++){cache[i]=rd(fd,ndp+i*4);valid[i]=1;} ndp+=16; + while(guard++<400000){ + unsigned int d=cache[0]; int v=valid[0]; + if(v && (d&0x80000000u)){firststop=laddr;break;} + if(v && (d&0x40000000u)){ + unsigned long tgt=(unsigned long)(d&0x3fffffffu); + jumps++; if(tgt<=laddr) back++; + laddr=tgt; for(i=0;i<4;i++){cache[i]=rd(fd,tgt+i*4);valid[i]=1;} ndp=tgt+16; + continue; + } + if(v && d) pages++; + /* shift */ + cache[0]=cache[1];valid[0]=valid[1]; + cache[1]=cache[2];valid[1]=valid[2]; + cache[2]=cache[3];valid[2]=valid[3]; + valid[3]=0; + if(!valid[0]){for(i=0;i<4;i++){cache[i]=rd(fd,ndp+i*4);valid[i]=1;}laddr=ndp;ndp+=16;} + } + printf("start=%08lx pages=%ld jumps=%ld backjumps(ring)=%ld first_stop=%08lx guard=%ld\n", + start,pages,jumps,back,firststop,guard); + return 0; +} diff --git a/tools/vino/mempeek.c b/tools/vino/mempeek.c new file mode 100644 index 0000000..1f2668d --- /dev/null +++ b/tools/vino/mempeek.c @@ -0,0 +1,28 @@ +/* mempeek [nwords] — dump physical memory via /dev/mem */ +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + unsigned long addr, n, i; + int fd; + unsigned int w; + + if (argc < 2) { fprintf(stderr, "usage: mempeek [nwords]\n"); return 1; } + addr = strtoul(argv[1], 0, 16); + n = (argc > 2) ? strtoul(argv[2], 0, 10) : 8; + + fd = open("/dev/mem", O_RDONLY); + if (fd < 0) { perror("open /dev/mem"); return 1; } + + for (i = 0; i < n; i++) { + unsigned long a = addr + i * 4; + if (lseek(fd, (off_t)a, SEEK_SET) == (off_t)-1) { perror("lseek"); return 1; } + if (read(fd, &w, 4) != 4) { perror("read"); return 1; } + printf("%08lx: %08x\n", a, w); + } + close(fd); + return 0; +} diff --git a/tools/vino/vinodump.c b/tools/vino/vinodump.c new file mode 100644 index 0000000..2567861 --- /dev/null +++ b/tools/vino/vinodump.c @@ -0,0 +1,93 @@ +/* + * vinodump.c — reconstruct the captured VINO frame straight from the DMA + * target pages in physical memory, bypassing the VL client ring. + * + * Reads channel A's descriptor-table pointer and line-size from the VINO + * registers, walks the descriptor list collecting data-page addresses (a + * page pointer has bits [31:30] clear; JUMP has bit30; STOP has bit31), + * reads each 4 KB page, and writes them in order to a file. The result is + * the raw frame buffer: 480 rows of `stride` bytes, each row beginning with + * 640 ARGB pixels (A,R,G,B). + * + * cc -o vinodump vinodump.c ; ./vinodump /var/tmp/frame.raw + */ +#include +#include +#include +#include + +#define VINO_BASE 0x00080000u +#define CHA_DESC_TABLE (VINO_BASE + 0x0074u) /* CHA CH_DESC_TABLE_PTR low word */ +#define CHA_NEXT_DESC (VINO_BASE + 0x006Cu) /* CHA CH_NEXT_4_DESC low word */ +#define CHA_LINE_SIZE (VINO_BASE + 0x0054u) /* CHA CH_LINE_SIZE low word */ + +#define NPAGES 300 /* 640*480*4 = 1228800 = 300 * 4096 */ +#define PAGESZ 4096 + +static unsigned int rd32(int fd, unsigned long pa) +{ + unsigned int w = 0; + if (lseek(fd, (off_t)pa, SEEK_SET) == (off_t)-1) { perror("lseek"); exit(1); } + if (read(fd, &w, 4) != 4) { perror("read"); exit(1); } + return w; +} + +int main(int argc, char **argv) +{ + const char *out = (argc > 1) ? argv[1] : "/var/tmp/frame.raw"; + int fd, ofd, got = 0; + unsigned long table; + unsigned long scan; + static unsigned char page[PAGESZ]; + int i; + + /* /dev/mem can't read the VINO I/O registers, so take the descriptor-table + physical address as an argument (videod allocates it deterministically at + 0x0861e000 on this config). */ + table = (argc > 2) ? strtoul(argv[2], 0, 16) : 0x0861e000ul; + + fd = open("/dev/mem", O_RDONLY); + if (fd < 0) { perror("open /dev/mem"); return 1; } + fprintf(stderr, "vinodump: desc table=%08lx\n", table); + + ofd = open(out, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (ofd < 0) { perror("open out"); return 1; } + + /* Follow the descriptor chain in hardware order: each 16-byte group is + up to 4 words; a JUMP word (bit30) redirects to its target 16-byte + aligned (& 0x3ffffff0); a STOP word (bit31) ends it. Aligning the jump + target is the fix — following it unaligned skips one page per group and + scrambles the row order. */ + scan = table & 0x3ffffff0u; + { + int guard = 0; + while (got < NPAGES && guard++ < 100000) { + int slot, jumped = 0; + for (slot = 0; slot < 4 && got < NPAGES; slot++) { + unsigned int d = rd32(fd, scan + (unsigned long)slot * 4); + if (d & 0x80000000u) { guard = 100001; break; } /* STOP */ + if (d & 0x40000000u) { scan = (unsigned long)(d & 0x3ffffff0u); jumped = 1; break; } + if (d == 0) continue; + { + unsigned long pg = (unsigned long)(d & 0x3ffff000u); + unsigned int off; + for (off = 0; off < PAGESZ; off += 4) { + unsigned int w = rd32(fd, pg + off); + page[off ] = (w >> 24) & 0xff; + page[off + 1] = (w >> 16) & 0xff; + page[off + 2] = (w >> 8) & 0xff; + page[off + 3] = w & 0xff; + } + if (write(ofd, page, PAGESZ) != PAGESZ) { perror("write"); return 1; } + got++; + } + } + if (guard > 100000) break; + if (!jumped) scan += 16; + } + } + close(ofd); + close(fd); + fprintf(stderr, "vinodump: wrote %d pages (%d bytes) to %s\n", got, got * PAGESZ, out); + return 0; +} diff --git a/tools/vino/vinograb.c b/tools/vino/vinograb.c new file mode 100644 index 0000000..f634ffd --- /dev/null +++ b/tools/vino/vinograb.c @@ -0,0 +1,106 @@ +/* + * vinograb.c — grab one frame from the IndyCam (VINO) into a file. + * + * Uses the IRIX Video Library (VL): open the VINO server, build a + * path from the digital video source (the IndyCam) to a memory drain, + * capture a single RGB frame, and write the raw pixels to a file. + * + * cc -o vinograb vinograb.c -lvl + * /usr/etc/videod & # the VL daemon must be running + * ./vinograb /tmp/grab.rgb + * + * Output is raw bytes: tsize = xsize*ysize*3 for VL_PACKING_RGB_8, + * top-to-bottom, R,G,B per pixel. Dimensions are printed to stderr. + */ +#include +#include +#include +#include + +static void die(const char *msg) +{ + fprintf(stderr, "%s: %s\n", msg, vlStrError(vlGetErrno())); + exit(1); +} + +int main(int argc, char **argv) +{ + VLServer svr; + VLPath path; + VLNode src, drn; + VLControlValue val; + VLBuffer buf; + VLInfoPtr info; + void *dataPtr; + int xsize, ysize, tsize, tries, bpp; + const char *outfile = (argc > 1) ? argv[1] : "/tmp/grab.rgb"; + FILE *f; + + svr = vlOpenVideo(""); + if (svr == NULL) { fprintf(stderr, "vlOpenVideo failed\n"); exit(1); } + + /* Let VL pick the default video source and a free memory drain. */ + src = vlGetNode(svr, VL_SRC, VL_VIDEO, VL_ANY); + if (src < 0) die("vlGetNode src"); + drn = vlGetNode(svr, VL_DRN, VL_MEM, VL_ANY); + if (drn < 0) die("vlGetNode drn"); + + path = vlCreatePath(svr, VL_ANY, src, drn); + if (path < 0) die("vlCreatePath"); + if (vlSetupPaths(svr, (VLPathList)&path, 1, VL_SHARE, VL_SHARE) < 0) + die("vlSetupPaths"); + + /* Ask for 24-bit RGB; fall back to 32-bit RGBA if the drain refuses. */ + bpp = 3; + val.intVal = VL_PACKING_RGB_8; + if (vlSetControl(svr, path, drn, VL_PACKING, &val) < 0) { + val.intVal = VL_PACKING_RGBA_8; + if (vlSetControl(svr, path, drn, VL_PACKING, &val) < 0) + die("vlSetControl packing"); + bpp = 4; + } + + /* Interleave both fields into one full frame (best effort). */ + val.intVal = VL_CAPTURE_INTERLEAVED; + vlSetControl(svr, path, drn, VL_CAP_TYPE, &val); + + if (vlGetControl(svr, path, drn, VL_SIZE, &val) < 0) die("vlGetControl size"); + xsize = val.xyVal.x; + ysize = val.xyVal.y; + + buf = vlCreateBuffer(svr, path, drn, 3); + if (buf == NULL) die("vlCreateBuffer"); + if (vlRegisterBuffer(svr, path, drn, buf) < 0) die("vlRegisterBuffer"); + + tsize = vlGetTransferSize(svr, path); + fprintf(stderr, "vinograb: %dx%d, %d bpp, transfer=%d bytes\n", + xsize, ysize, bpp, tsize); + + /* Select transfer-complete events and drive a vlNextEvent loop — the VL + library advances the buffer ring as it processes events (this is what + vidtomem does; polling vlGetNextValid without the event pump never sees + a valid frame). */ + vlSelectEvents(svr, path, VLTransferCompleteMask); + if (vlBeginTransfer(svr, path, 0, NULL) < 0) die("vlBeginTransfer"); + + info = NULL; + tries = 0; + while ((info = vlGetNextValid(svr, buf)) == NULL + && (info = vlGetLatestValid(svr, buf)) == NULL) { + if (++tries > 4000) { fprintf(stderr, "timeout (errno %d)\n", vlGetErrno()); exit(2); } + sginap(1); + } + + dataPtr = vlGetActiveRegion(svr, buf, info); + f = fopen(outfile, "w"); + if (!f) { perror("fopen"); exit(1); } + fwrite(dataPtr, 1, tsize, f); + fclose(f); + fprintf(stderr, "vinograb: wrote %d bytes to %s\n", tsize, outfile); + + vlPutFree(svr, buf); + vlEndTransfer(svr, path); + vlDestroyPath(svr, path); + vlCloseVideo(svr); + return 0; +} From 5e57f218aa751c37ba8274a750164376d92cc65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20H=C3=BCbner?= Date: Sun, 31 May 2026 19:18:43 +0200 Subject: [PATCH 6/6] vino: fix descriptor-ring DMA at the source, drop the bogus 0x40000000 alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR review: there is no 0x40000000 alias of low RAM on the Indy. The errors in that range were VINO DMA, not CPU accesses: descriptor_fetch and the DMA buffer writes go through the Physical bus, and any unmapped address VINO drives is routed to CpuBusErrorDevice — hence the misleading "MC: CPU Error at 48621cf0". Root cause: the descriptor address field is only bits [29:4]; bit 31 is STOP and bit 30 is JUMP. The 6.5 driver encodes pointers as JUMP_BIT | kvtophys(addr) (vinoBuildJumpBugDAPS), so 0x4861e000 carries the JUMP control bit, not a real address. desc::PTR_MASK was 0xFFFF_FFF0, wrongly keeping bits 31/30, so the CH_NEXT_4_DESC / CH_DESC_TABLE_PTR register writes left bit 30 in next_desc_ptr/start_desc_ptr and VINO fetched at 0x4861e000 — off into unmapped space. (The JUMP target already masked 0x3FFF_FFF0 and was fine.) Fix at the source: narrow desc::PTR_MASK to 0x3FFF_FFF0 so the pointer-register writes and the data-buffer write address strip the control bits and resolve to the real lomem address (0x0861e000, in RAM at 0x08000000). This removes the need for the physical.rs alias_phys() hack, which had aliased the entire 0x40000000-0x7FFFFFFF window for every device on the bus. - src/vino.rs: PTR_MASK 0xFFFF_FFF0 -> 0x3FFF_FFF0; JUMP target uses PTR_MASK; add desc_pointer_registers_strip_bit30_control_flag regression test. - src/physical.rs: revert alias_phys() and its call sites. - rules: rewrite Fix #1 to reflect the corrected (vdma, not CPU) understanding. Indy RAM is 0x08000000..0x18000000, so a legitimate address never sets bits 31/30; 5.3 descriptors carry no control bits, so its path is unchanged. All 12 vino unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- rules/irix/vino-capture-on-6.5-progress.md | 49 ++++++++++++++------- src/physical.rs | 22 ---------- src/vino.rs | 50 +++++++++++++++++----- 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/rules/irix/vino-capture-on-6.5-progress.md b/rules/irix/vino-capture-on-6.5-progress.md index 4989249..f680e1f 100644 --- a/rules/irix/vino-capture-on-6.5-progress.md +++ b/rules/irix/vino-capture-on-6.5-progress.md @@ -36,8 +36,12 @@ saves a correct frame on the 5.3 guest. (c) commit. (d) any residual sub-pixel a not chased — the bars/ramp look clean. **Committed/shipped (safe, keep):** -- `src/physical.rs` uncached-`0x4000_0000`-alias fix — commit `8426efd` on branch - `vino-6.5-capture-engage`. Makes 6.5 capture engage + DESC fire. 5.3-neutral. +- VINO bit-30 descriptor-pointer strip (`desc::PTR_MASK = 0x3FFF_FFF0` in + `src/vino.rs`). Makes 6.5 capture engage + DESC fire. 5.3-neutral. This + REPLACES the earlier `src/physical.rs` `alias_phys()` (commit `8426efd`): + the `0x4000_0000` accesses were VINO DMA, not the CPU, and there is no RAM + alias there on real hardware — the bit-30 JUMP control flag must be stripped + in VINO, not aliased across the whole physical bus. See "Fix #1" below. - Boot speed 292s→89s on the klindert guest (nsswitch files-first + FQDN + service disables) — see [[project_klindert_boot_speed]]. `src/vino.rs` is at HEAD. @@ -275,20 +279,33 @@ not match it. Do not present the current reconstruction as a faithful grab. - **But `vlGetNextValid` times out**: the driver enables DMA, doesn't get the completion it waits for, tears down and retries in a tight loop forever. -## Fix #1 (DONE): 0x40000000 uncached memory alias — `src/physical.rs` - -The 6.5 driver polls the VINO descriptor/status ring through an **uncached -alias** of low physical memory at `0x40000000`: it reads `0x48621400` to see -the `0x80000001` STOP markers it wrote at RAM `0x08621400` -(`0x48621cf0 − 0x40000000 = 0x08621cf0`). iris didn't map `0x40000000-`, so -those reads hit `CpuBusErrorDevice`, returned `0xFFFFFFFF`, and flooded the log -with `MC: CPU Error at 48621cf0` (~160k lines). 5.3's driver polled the cached -addresses directly so this never surfaced. - -Fix: `alias_phys()` in physical.rs strips bit 30 for addresses in -`0x40000000-0x7FFFFFFF` before the device-map dispatch, so they resolve to the -real RAM/device. Result: the MC error flood is **gone** (0 errors). This is a -correct, standalone fix worth keeping regardless of the capture work. +## Fix #1 (DONE): strip the descriptor bit-30 control flag in VINO — `src/vino.rs` + +The `MC: CPU Error at 48621cf0` flood (~160k lines) was **not** a CPU access and +there is **no 0x40000000 RAM alias** on the Indy. The accesses are VINO **DMA**: +`descriptor_fetch` and the DMA buffer writes go *through* the `Physical` bus +(`Vino::set_phys`), and any unmapped address VINO drives is routed to +`CpuBusErrorDevice` — which is why it printed as a "CPU" error. + +The addresses carried bit 30 because the 6.5 driver encodes descriptor/table +pointers as `JUMP_BIT | kvtophys(addr)` (`vinoBuildJumpBugDAPS`): bit 31 is STOP, +bit 30 is JUMP, and the address field is only bits `[29:4]`. The real lomem +address is in the low bits (`0x4861e000 → 0x0861e000`, in RAM at +0x08000000..0x18000000). VINO's `PTR_MASK` was `0xFFFF_FFF0`, wrongly keeping +bits 31/30, so `start_desc_ptr`/`next_desc_ptr` retained bit 30 and VINO fetched +at `0x4861e000` — off into unmapped space. + +Fix (at the source): `desc::PTR_MASK = 0x3FFF_FFF0`, so the descriptor-pointer +register writes (`CH_NEXT_4_DESC`, `CH_DESC_TABLE_PTR`), the data-buffer write +address, and the JUMP target all strip the control bits and resolve to real RAM. +The JUMP target already masked `0x3FFF_FFF0`; this just makes the register path +consistent. Result: the MC error flood is **gone** (0 errors), capture engages, +and no physical-bus alias is needed. + +(Superseded the earlier `alias_phys()` approach in physical.rs, which papered +over this by aliasing the *entire* 0x40000000-0x7FFFFFFF window for every device +on the bus — masking the real bug and silently redirecting any other access in +that range.) ## Client delivery is blocked at the videod level (ruled out client API) diff --git a/src/physical.rs b/src/physical.rs index 018cfd9..080906b 100644 --- a/src/physical.rs +++ b/src/physical.rs @@ -604,23 +604,9 @@ impl Device for Physical { } } -/// Physical 0x40000000-0x7FFFFFFF is an uncached alias of low physical memory -/// 0x00000000-0x3FFFFFFF. IRIX 6.5 accesses the VINO DMA descriptor/status ring -/// through this window (e.g. polls 0x48621400 for the 0x80000001 STOP markers it -/// wrote at RAM 0x08621400). Without the alias those reads hit ErrorBus, return -/// 0xFFFFFFFF, and the driver never sees capture completion. Strip bit 30 so the -/// access resolves to the real address through the normal device map. -/// (IRIX 5.3's vino driver polled the cached addresses directly, so this only -/// surfaced on 6.5.) -#[inline(always)] -fn alias_phys(addr: u32) -> u32 { - if addr & 0xC000_0000 == 0x4000_0000 { addr & !0x4000_0000 } else { addr } -} - impl BusDevice for Physical { #[inline(always)] fn read8(&self, addr: u32) -> BusRead8 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read8(addr) }; #[cfg(not(feature = "lightning"))] @@ -633,7 +619,6 @@ impl BusDevice for Physical { #[inline(always)] fn write8(&self, addr: u32, val: u8) -> u32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write8(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -643,7 +628,6 @@ impl BusDevice for Physical { #[inline(always)] fn read16(&self, addr: u32) -> BusRead16 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read16(addr) }; #[cfg(not(feature = "lightning"))] @@ -656,7 +640,6 @@ impl BusDevice for Physical { #[inline(always)] fn write16(&self, addr: u32, val: u16) -> u32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write16(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -666,7 +649,6 @@ impl BusDevice for Physical { #[inline(always)] fn read32(&self, addr: u32) -> BusRead32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read32(addr) }; #[cfg(not(feature = "lightning"))] @@ -679,7 +661,6 @@ impl BusDevice for Physical { #[inline(always)] fn write32(&self, addr: u32, val: u32) -> u32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write32(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -689,7 +670,6 @@ impl BusDevice for Physical { #[inline(always)] fn read64(&self, addr: u32) -> BusRead64 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let r = unsafe { (*device_ptr).read64(addr) }; #[cfg(not(feature = "lightning"))] @@ -702,7 +682,6 @@ impl BusDevice for Physical { #[inline(always)] fn write64(&self, addr: u32, val: u64) -> u32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; let ws = unsafe { (*device_ptr).write64(addr, val) }; #[cfg(not(feature = "lightning"))] @@ -712,7 +691,6 @@ impl BusDevice for Physical { #[inline(always)] fn write64_masked(&self, addr: u32, val: u64, mask: u64) -> u32 { - let addr = alias_phys(addr); let device_ptr = self.device_map[(addr >> 16) as usize]; unsafe { (*device_ptr).write64_masked(addr, val, mask) } } diff --git a/src/vino.rs b/src/vino.rs index f988703..3eec473 100644 --- a/src/vino.rs +++ b/src/vino.rs @@ -211,8 +211,15 @@ pub mod frame_rate { // descriptors per channel as u64 with validity/control flags in the upper half. pub mod desc { - /// Physical page address mask (bits [31:4], 16-byte aligned). - pub const PTR_MASK: u32 = 0xFFFF_FFF0; + /// Physical address mask (bits [29:4], 16-byte aligned). The address field of + /// a descriptor / descriptor-table pointer is only 30 bits wide: bits 31 and + /// 30 are the STOP and JUMP control bits, NOT part of the address. The 6.5 + /// kernel encodes pointers as `JUMP_BIT | kvtophys(addr)` (see + /// `vinoBuildJumpBugDAPS`), so masking with this — stripping bits 31/30 — is + /// what recovers the real lomem address (e.g. 0x4861e000 → 0x0861e000). Indy + /// RAM lives at 0x08000000..0x18000000, so a legitimate address never sets + /// bits 31/30; there is no 0x40000000 RAM alias on the hardware. + pub const PTR_MASK: u32 = 0x3FFF_FFF0; /// Control bit: STOP — terminate DMA after this descriptor; raise DESC interrupt. pub const STOP_BIT: u64 = 1 << 31; /// Control bit: JUMP — bits [29:0] are a pointer to the next descriptor block. @@ -557,15 +564,17 @@ impl Vino { chan.next_desc_ptr = chan.next_desc_ptr.wrapping_add(16); } else if chan.descriptors[0] & desc::JUMP_BIT != 0 { // The 6.5 jump-bug descriptor chain (vinoBuildJumpBugDAPS) ends most - // 4-descriptor groups with a JUMP whose encoded target carries a +4 - // (sometimes +8) low-bit offset — a workaround for the hardware's - // 4-at-a-time descriptor-cache prefetch. The real fetch is always - // 16-byte-group-aligned, so that offset must be masked off; following - // it unaligned reads each next group 4 bytes high, dropping the first - // data page of every group (~181 of 300 pages reached) and scrambling - // the captured frame. Mask to 16 bytes so the walk stays in step and - // all 300 data pages land in order. - let target = (chan.descriptors[0] as u32) & 0x3FFF_FFF0; + // 4-descriptor groups with a JUMP whose encoded word is + // `JUMP_BIT | kvtophys(next)` and whose target carries a +4 (sometimes + // +8) low-bit offset — a workaround for the hardware's 4-at-a-time + // descriptor-cache prefetch. PTR_MASK both strips the JUMP control bit + // (bit 30) to recover the real lomem address and 16-byte-aligns it: the + // real fetch is always 16-byte-group-aligned, so the +4/+8 offset must + // be masked off; following it unaligned reads each next group 4 bytes + // high, dropping the first data page of every group (~181 of 300 pages + // reached) and scrambling the captured frame. Masking keeps the walk in + // step so all 300 data pages land in order. + let target = (chan.descriptors[0] as u32) & desc::PTR_MASK; Self::descriptor_fetch(chan, target, mem); } } @@ -1699,6 +1708,25 @@ mod tests { "second interlaced field reads base + field-boundary span (0x780)"); } + /// Descriptor-pointer registers carry the kernel's bit-30 control flag: the + /// 6.5 driver programs them as `JUMP_BIT | kvtophys(table)` (e.g. 0x4861e000). + /// The address field is only bits [29:4], so PTR_MASK must strip bits 31/30 to + /// recover the real lomem address (0x0861e000). This is what lets VINO DMA hit + /// real RAM directly — there is no 0x40000000 RAM alias on the hardware, so + /// the strip has to happen here at the source, not in the physical bus map. + #[test] + fn desc_pointer_registers_strip_bit30_control_flag() { + let vino = Vino::new(); + let encoded = desc::JUMP_BIT as u32 | 0x0861_e000; // 0x4861e000 + vino.write_reg(reg::CHA_BASE + reg::CH_DESC_TABLE_PTR, encoded); + vino.write_reg(reg::CHA_BASE + reg::CH_NEXT_4_DESC, encoded); + let st = vino.state.lock(); + assert_eq!(st.channels[0].start_desc_ptr, 0x0861_e000, + "DESC_TABLE_PTR must mask off bit 30 → real lomem address"); + assert_eq!(st.channels[0].next_desc_ptr, 0x0861_e000, + "NEXT_4_DESC must mask off bit 30 → real lomem address"); + } + // ─── I2C bus tests: SAA7191 + CDMC coexist on a shared bus ─────────── /// Push one byte through the VINO I2C bridge by way of the I2C_CONTROL /