Skip to content

misc fixes: Octane IOC3, 16550 loopback, idle CPU, SEEQ drop count - #367

Merged
techomancer merged 16 commits into
techomancer:mainfrom
jamesbraid:misc-fixups
Oct 31, 2026
Merged

techomancer merged 16 commits into
techomancer:mainfrom
jamesbraid:misc-fixups

Conversation

@jamesbraid

@jamesbraid jamesbraid commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • The Octane2 PROM checks the IOC3's configuration space, so it now reads as on real hardware: the first 32 bytes repeat, status is 0x0280, and the register block shows the header and pin control. PCI now keeps the latency timer and cache line size the PROM writes.
  • IOC3 parallel status reads 0x78 with nothing attached and a ready printer with a host backend, instead of a printer that's always busy.
  • IOC3 receive-ring-full is a level that drops when the ring has room, so a guest that acknowledges it doesn't see it come straight back.
  • The 16550 ignores line input in loopback, so a key typed during a loopback self-test no longer lands in the receiver. The O2's UARTs share the core.
  • The hptimer sleeps up to a deadline instead of spinning, which takes the Indigo2's idle login screen from a full core to 3%.
  • REX3's processor thread parks on an empty queue in every build, so an idle Newport desktop no longer burns a core.
  • Idle-park no longer stalls a guest that polls with interrupts off. With idle-pause on, an Indy could freeze mid-word until the next monitor command.
  • seeq status counts frames the address filter drops, including those that arrive while the receiver is off.
  • The bench harness sets KSEG0 cached itself. It runs without a PROM, and the R10000 resets with KSEG0 uncached, so every R10000 kernel measured the uncached bus instead of the JIT. The R10000 suite goes from about 450 to 714 guest MIPS and now matches the R4400. The prebuilt image was built with Debian's gcc 14.2, so regenerate it with make -C bench prebuilt.
  • IMPACT no longer logs "nowhere to save" on the first context switch, when there is nothing to save yet. A switch away from a slot outside ERAM still reports.
  • IMPACT notes for HQ4 reads and writes it does not model now name the CPU PC, so a boot log shows who made the access.
  • Bus errors from reads of empty PCI slots still print, but now say why: the Bridge names the configuration master timeout and the slot, so the PROM and kernel probes of slots 4 to 7 read as expected rather than as faults.
  • On the Octane, IMPACT no longer warns about the X server's raster read start after the host DMA kick has already run the read, which happened on every boot. A start with no kick behind it still reports.

Tested: the Octane, O2 and Indy boot to the IRIX login with hinv unchanged, and an idle Indy at the login prompt drops from about two host cores to one. cargo test --lib passes, and every commit builds on its own.

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.
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.
@techomancer
techomancer merged commit ad89773 into techomancer:main Oct 31, 2026
14 checks passed
@jamesbraid
jamesbraid deleted the misc-fixups branch October 31, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants