Repository navigation
misc fixes: Octane IOC3, 16550 loopback, idle CPU, SEEQ drop count - #367
Merged
Merged
Conversation
The REX3-Processor thread backed off with yield_now() forever when the FIFO was empty, so a Newport machine at an idle desktop kept a host core at ~99% (perf: all of it in sched_yield). The park-and-wake path already existed, but only under the idle-pause feature and in test builds. Every producer already checks `processor_parked` and unparks the thread after publishing, and the 2 ms park timeout stays as a backstop, so removing the feature gate adds no work to the push path and no draw latency beyond the wake-up.
The worker spun for any deadline under 200 us and only slept for the rest, with a 50 to 100 us margin. On the Indigo2 that left the hptimer thread at ~99% of a core (perf: the spin loop and clock_gettime); the Indy sat near 5%. Tick timers keep the waits short there, so most of the thread's life was inside the spin branch. Sleep whenever the wait exceeds 80 us, ending the sleep 50 us short, and spin only the last stretch. The margin covers the kernel's default 50 us timer slack and wake-up latency; a late wake costs at most the overshoot, a few tens of microseconds against guest ticks of 1 ms and up. Waits under 80 us (audio pacing at 44.1 kHz) still spin, as a sleep would outlast them. Indigo2 at the IRIX login screen, per-thread host CPU over 20 to 30 s: hptimer 100% before, 3% after.
A frame the address filter rejects is popped and discarded without a trace outside a developer build. That includes frames addressed to the guest that arrive while the driver has the receiver off, as IRIX does for a moment each time it resets the interface. Chasing a network install that stalled, 50 such frames went missing between the backend and the guest with nothing to say where they went. seeq status now reports how many frames the filter dropped and how many of those arrived with the receiver off.
A function's configuration dword 3 was read-only, so firmware's write of the latency timer was dropped and read back as zero. The cache line size and latency timer are the writable bytes of that dword on a PCI function; the header type and BIST bytes above them are not. The reference Octane2's PROM monitor reads the IOC3's latency timer back as 0x28 after the PROM programs it, which the IOC3 configuration-space check that follows depends on. Ported from the setup in QEMU sgi-origami ea56719b086d, hw/misc/sgi-ioc3: repeat the first 32 configuration bytes, whose test programs the latency timer through the PCI core and reads it back; that core stores it. The unit test fails without the change.
A reference Octane2's PROM monitor reads the onboard IOC3's type 0 configuration space as 32 bytes repeated: dwords 0x20-0x3f return 0x00-0x1f exactly, so offset 0x2c holds the latency word, not a subsystem id, and the interrupt pin byte at 0x3d reads zero. The chip is the same part on every board, so the repeat is chip behaviour. Its status register also reads fast back-to-back capable beside the medium decode timing (0x0280 in the top half of dword 1). Mainline Linux never reads those registers from the chip; its Bridge driver answers 0x2c from a table keyed on the board part and synthesises the pin. OpenBSD's ioc(4) reads only the command word and BAR 0, and treats every configuration register past BAR 0 as absent. Answer configuration reads through a new PciBusDevice::config_dword, which the Bridge now uses and which defaults to the stored header, so only the guest's view repeats: the Bridge still routes INTA from the stored pin and the subsystem id stays the key that picks the board's population. The dword at 0x1c reads the programmed BAR 0 value on the reference machine for a reason no public source gives, so it is not modelled. Ported from QEMU sgi-origami ea56719b086d, hw/misc/sgi-ioc3: repeat the first 32 configuration bytes, and the status bits from the chip's realize. The unit test is QEMU's octane2/ioc3-config-repeats and fails without the change.
…lock On a reference Octane2 the first five dwords of the onboard IOC3's register block read the PCI configuration header: 0x000310a9, 0x02800146, 0xff000001, 0x00002800 and 0x00500000, the same values the configuration reads return after the PROM's programming. The block read zero there. Read them through from the header; writes stay ignored. The same PROM monitor session reads pin control clear (0x38) as 0x002000f3, the value pin control set (0x34) holds, where the chip model returned zero. Both aliases now read the control word. The reference machine reads 0x00500010 at 0x14 with the record's valid bit clear, and no public source says what produces that, so it is left at zero. Ported from QEMU sgi-origami 3b55f1a386a2, hw/misc/sgi-ioc3: show the configuration header and pin control in the block. The unit test is QEMU's octane2/ioc3-register-header and fails without the change.
The status register returned one constant, nFault high and nothing else, whatever was behind the port. With bit 7 the inverse of the Busy pin, that is a printer holding Busy high, which no driver treats as ready, and a port with no peripheral looked like one with a printer. Read it from the cable instead. A connector with nothing on it reads the five input pull-ups, 0x78 (nAck, Select, PError and nFault high, Busy high so bit 7 is clear). A host backend reads as a printer that can take data, 0xd8, which is what the part's published status register, Linux's parport and OpenBSD's lpt all read as ready. Treating every backend as always ready is an approximation, since a file or pty never holds Busy. The pull direction on the real boards is inferred, and the part's PRINT indicator (bit 2) stays unmodelled. Ported from QEMU sgi-origami b360b2ad12b1, hw/char/sgi-ioc3-parallel: read an empty connector as nothing attached, and 72a4a07b7360, read a host backend as a ready printer. The unit test covers both values and fails without the change.
Receive-ring-full latched each time the engine refused to post an entry, and the engine retries a refused post on every pass. A guest that acknowledged the source while its ring stayed full saw it come straight back whenever anything ran the engine, even with no new character. Make it a level, like transmit-ring-empty: set while ring DMA is on and an entry waits that the ring had no room for, deaf to acknowledgement, and falling when the consumer makes room, whether or not the held entry has posted yet. A ring filled exactly, with nothing waiting to post, does not raise it. Level and acknowledgement handling follow the chip's other level-following bits. What the part really does is not known; this is a model choice. Ported from QEMU sgi-origami 6033a875b6f3, hw/char/sgi-ioc-serial-dma: make receive-ring-full a level. The unit test is QEMU's sn0/ioc3-serial/receive-ring-full-is-a-level and fails without the change.
With modem-control bit 4 set, a PC16550D disconnects its serial input: a character arriving on the line goes nowhere, and only looped-back characters reach the receiver. The core took what the host typed or its backend offered whatever the loopback bit said, so a character typed during a loopback self-test landed in the receiver, and on the IOC3 in the receive ring, beside the looped-back ones. Drop what the host offers while the part is in loopback and log it. The drop happens when the host delivers the character rather than a character time later on the line, an approximation. The O2's UARTs share the core and the PC16550D behaviour, so they follow. Ported from QEMU sgi-origami ada919589cd1, hw/char/sgi-ioc-serial-dma: discard outside input while in loopback. The unit test applies QEMU's sn0/ioc3-serial/loopback-discards-external-input to the 16550 core and fails without the change.
update() answered "a line is pending but masked" with a pace, a sleep until the guest could take the line. That check ran before the IE=0 check, so a guest with interrupts disabled and a masked line pending (a device poll such as the console's transmit-ready bit, with the SCC's own Tx interrupt pending and masked) was paced. pace() looped on a Status it cannot see change: the CPU thread is the only writer and it was asleep. It left only on a new unmasked line or when a monitor command cleared `running`. Observed on an Indy/Indigo2 with idle-pause: the guest stopped mid-word for ten minutes, twice, with MIPS-CPU at 0%, and resumed at the next monitor command. Test IE before the pending-line pace, as the other cases where sleeping cannot help already do, and make pace sleep at most one slice per call so it cannot wait on a condition only the guest changes.
The harness runs without a PROM and never wrote Config, so KSEG0 kept the CPU's reset cacheability. The R4400 and R5000 reset with K0 = 3, but the R10000 resets with K0 = 2 (uncached), so every R10000 kernel ran uncached through the slow memory path and measured the uncached bus, not the JIT. bench_init now sets K0 = 3 itself, so every CPU runs the same cached workload. Going from uncached to cached needs no cache maintenance, since nothing has been cached through KSEG0 yet. On the same image the R10000 suite goes from about 450 to 714 guest MIPS and now matches the R4400 (1.03, was 0.61). A new ignored bench_runner test checks the R10000 suite runs with K0 = 3. The prebuilt image was built with Debian's mips64-linux-gnuabi64-gcc 14.2.0; regenerate it with make -C bench prebuilt before landing.
The first switch after reset has no outgoing slot, so there is no context record to write and nothing is lost. The "nowhere to save" note fired on that switch, on every boot that starts graphics, and reported a gap that does not exist. A switch away from a slot outside ERAM still notes: that context has a slot number but no record to save into, which is the case the note exists for.
The "not modelled yet" notes for HQ4 register reads and writes gave the offset but not who made the access, so a boot log could not say which code touched an unmodelled register. They now include the PC from the CPU's last batch boundary, the same figure the unmapped-access log uses. The access happened within that batch, so the PC names the caller to within a few instructions, not exactly. Each distinct PC gets its own note, under the board's existing cap of 256.
jamesbraid
force-pushed
the
misc-fixups
branch
from
October 31, 2026 10:26
dc5f326 to
a851994
Compare
The PROM and kernel probe every Bridge slot's configuration space and rely on the master time-out to find the empty ones. Each probe makes the CPU print a bus error, and those lines should stay visible, but a bare "Bus error on uncached read32" reads as a fault when it is routine. Replace the fault_is_expected hook with BusDevice::describe_fault(addr, size), which returns a short reason for an access that already failed. The decode table forwards it to the device behind the address and prefixes that device's name; the bus wrappers delegate it. The Bridge names a configuration access that nothing answered, for example "PCI config master timeout: empty slot 4 function 0". The CPU always prints, appending the reason in brackets to the bus-error lines for instruction fetch, uncached reads, uncached writes and masked 64-bit writes. The status, the exception, BadVAddr and the Bridge's recorded master time-out are unchanged, and the hook runs on the failure path alone, so a good access costs nothing.
On the Octane the X server arms a board-to-host read with XFRCONTROL 8, kicks the host DMA, and then writes XFRCONTROL 9. With kick_starts_read the kick already runs the armed read, so the 9 finds nothing pending. That start was reported as "raster DMA read started with no host DMA pending", once per read: 109 times in one traced boot. The read itself was right, so the report was false and the boot log was misleading. A kick that runs an armed read now sets a flag, and the next XFRCONTROL start consumes it without a note. Any other XFRCONTROL write clears the flag, so a start that has no kick behind it still gets the note. The field comments put the kick last in the order. They now give arm, kick, start.
The comment on the raster write past register 0x1FF said the IP30 PROM sends 0x001A6804 before its first context switch. In a traced Octane boot the kernel sends the same word too, and some of the kernel's copies arrive after a context switch has started. The comments now say so. The behaviour is unchanged. The word is still dropped and reported, because what the board does with register 0x268 is not known.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Small fixes I picked up while bringing up the Octane2 and VPro, one per commit. I ported the IOC3 and 16550 ones from my QEMU sgi-origami branch, where they were checked against a real Octane2. The idle and SEEQ ones came out of chasing a stalled Indy install and cores pinned at an idle desktop.
seeq statuscounts frames the address filter drops, including those that arrive while the receiver is off.make -C bench prebuilt.Tested: the Octane, O2 and Indy boot to the IRIX login with
hinvunchanged, and an idle Indy at the login prompt drops from about two host cores to one.cargo test --libpasses, and every commit builds on its own.