Skip to content

cpu: fix R10000 TLB widths, refused stores, the O2 alias and partial LWR - #403

Draft
jamesbraid wants to merge 8 commits into
techomancer:mainfrom
jamesbraid:cpu-r10000-fixes
Draft

jamesbraid wants to merge 8 commits into
techomancer:mainfrom
jamesbraid:cpu-r10000-fixes

Conversation

@jamesbraid

@jamesbraid jamesbraid commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

I want the R10000-family machines (Octane2, O2, Fuel, Tezro) to run 64-bit IRIX and pass SGI's own PROM diagnostics. Today iris treats the R10000 like an R4400 in a few places, and each of these breaks one of them.

First of three CPU PRs that Fuel (#368) sits on: this, then #404, then #405.

  • TLB widths. The R10000 has 44-bit virtual and 40-bit physical addresses, not the R4400's 40 and 36, so 64-bit IRIX processes above 1 TiB took Address Errors. EntryHi now keeps VPN2 43:13, FrameMask (CP0 21) exists, and EntryLo keeps the full PFN and bits 63:62.
  • Overlapping TLB entries. A TLB write that overlaps an existing entry invalidates the old one and sets Status.TS, per the NEC R10000/R12000 manual (U10278EJ4V0UM pp. 223, 300). The 2-CPU Tezro needs this.
  • Uncached stores nothing answers now complete instead of raising a Data Bus Error. The R10000 gets no response to an uncached store, so it can't fault. The Octane's ide hits this.
  • O2 memory alias applies after the 32-bit physical mask, so pages with IRIX's software bits above the PFN stop landing in the PCI window.
  • Partial LWR keeps the upper word in 64-bit mode. Only IRIX's olperi CPU test cares. The manuals leave it to the implementation, so this is a model choice matching olperi and QEMU.

Where the manuals say nothing about the R14000 and R16000, they follow the R10000, matching QEMU.

Testing

  • IRIX boots to login and runs 64-bit programs on the Indigo2 (IP28, 6.5.22), Octane and O2 (6.5.30), all R10000.
  • The 2-CPU Tezro boots to 4Dwm on this stack.
  • cargo test --release --lib --features jitv2: 2897 passed. Each change has a test with an R4400 or R5000 control.

EntryHi, the TLB match and the xuseg bounds check all stopped at VA bit 39,
but the R10000 family implements 44 bits: VPN2 is 43:13 and the user segment
is 16 TiB (R10000 UM pp. 219, 288). A 64-bit kernel can put an n64 user
stack above 40 bits (0x0fff_ffff_aee4, for one). EntryHi dropped VA[43:40]
and such an address took an Address Error.

The width now comes from the part's VA_BITS in each place: the EntryHi write
mask, the TLB's 64-bit compare (Tlb::set_va_bits, installed by the executor
beside set_cache_attrs), the xuseg bounds check, the micro-TLB's cacheable
segment test and the EntryHi value a TLB exception loads. The mask was
spelled three times and is now one const fn, mips_tlb::vpn2_mask. All of it
is a const of the CPU model or set once at construction; the TLB lookup is
unchanged.

This reverses a comment in the executor (b7d2a18) that said widening the EntryHi mask
breaks the IP28 and Octane kernels. Those two machines and an O2 set to the
R10000 now see 44 bits. The R4400 and R5000 (Indy, Indigo2, O2 with an
R5000) stay at 40.

One test maps a page at 0x123_4000_0000 on the R10000 and the R4400: the
R10000 keeps EntryHi and reads the page, the R4400 masks it and takes
Address Error.
FrameMask (CP0 register 21) is new with the R10000. Bits 15:0 clear
EntryLo bits 33:18 as a TLB write passes them on, and the rest of the
register reads as zero (R10000 UM v2.0 p. 260). iris had no register 21: mfc0
read zero and mtc0 was dropped, so a kernel that sets FrameMask to keep
software flags out of the physical address got no effect.

The core holds the register; the executor sets which bits are writable,
all 16 on the R10000 and none on the R4400 and R5000. TLBWI and TLBWR pass
the masked EntryLo values, and TLBR returns the masked entry. The register
is saved with the rest of CP0 and loads as zero from an older save.
CpuModel::R10K_FAMILY says a part is in the R10000 family; for now that is
the R10000.

Nothing changes while FrameMask is zero, its reset value, and the write
path runs only on a TLB write.

Tests write FrameMask on the R10000 and the R4400, map a page and check the
physical address and the readback; a second writes with TLBWR as well as
TLBWI and reads the entry back with TLBR.
In 64-bit mode a partial LWR (offset 0 to 2) sign-extended the merged
word on every CPU. The manual calls the upper word of a partial LWR
implementation specific (NEC U12754E pp. 125-126), and the reference
results observed for the R10000 keep the old bits 63:32 at all
three partial offsets. With sign extension a test that compares those
results miscompares.

The interpreter and the JIT now merge the old upper word on the R10000; a
full word (offset 3) still sign-extends. The upper word is kept only when
64-bit operations are enabled for the current mode, matching QEMU;
otherwise it sign-extends. The later parts are not covered, since nothing
shows what they do. The choice is a JitConsts field set from
CpuModel::PARTIAL_LWR_KEEPS_UPPER, so compile workers see it. The code-cache
fingerprint gains lwr_upper=1 only when it is set, so the R4400 and
R5000 machines keep their warm caches.

Behaviour changes on the Octane, IP28 and an O2 with the R10000. The
R4400 and R5000 are unchanged.

Tests run LWR at every offset in the interpreter on the R4400, R5000
and R10000 in kernel, supervisor and user mode with the mode's own
64-bit bit set and clear, run the same offsets as compiled code on the
R10000 and R5000 and compare with the interpreter, and check the
fingerprint.
On the R10000 EntryLo holds a 28-bit PFN in bits 33:6, a 40-bit physical
address, and the two-bit uncached attribute in bits 63:62; 61:34 are
reserved (R10000 UM v2.0 pp. 239-240). iris kept the R4400's 30 bits, so the
attribute and PFN bits 33:30 read back as zero and never reached the TLB.

The core now has an EntryLo mask that the executor sets per part: the R4400
and R5000 keep 0x3FFF_FFFF, the R10000 gets 0xC000_0003_FFFF_FFFF. The TLB
carries the PFN up to physical 39:12, but a TLB write clears the PFN bits
below the page size, so TLBR does not return what was written to them
(R10000 UM v2.0 pp. 299-300, TLBWI and TLBWR; NEC VR10000 instruction set
manual U12754EJ1V0UM pp. 199-200). That is documented for the R10000;
for the R12000, R14000 and R16000 no manual describes a TLB write, so they
do the same as a model choice. The R4400 and R5000 keep the whole PFN. The
uncached attribute is stored and read back only; it is not carried to the
bus.

Bits 33:32 are physical 39:38, and the Octane's guest keeps software flags
there. FrameMask (previous commit) clears them on a TLB write, so the page
lands at its real address and the Octane's 40-bit physical mask needs no
narrowing.

On the Octane, the IP28 and an O2 set to the R10000, mfc0 of EntryLo now
returns bits 63:62 and PFN bits 33:30 if the guest wrote them; before they
read as zero. The R4400 and R5000 are unchanged.

A test checks the readback on the R10000 and the R4400, and that a page
with PFN bits 33:32 set maps to physical 39:38 and is brought back by
FrameMask.

The Octane PROM's tlb diagnostic writes 0xa5a5a5a4 and 0x5a5a5a58 to
EntryLo with 16 KB pages and expects 0xa5a5a524 and 0x5a5a5a18 back:
EntryLo bits 7:6 are physical bits 13:12, inside the page. It reported
320 compare errors. A write now clears EntryLo bits PageMask >> 7;
translation already ignored them, so only TLBR changes. A second test
checks the diagnostic's patterns through TLBWI and TLBWR on the R10000 and
the R4400, and that a 16 KB page translates the same on both.
A mapped page whose physical address has a bit set above bit 31 could
reach the PCI window instead of RAM on the O2. The O2 aliases RAM at
0x8000_0000, and fold_phys tested that window on the full translated
address before cutting it to the 32 bits the board decodes. IRIX keeps
software bits in its page-table entries just above the PFN, so such an
address missed the window, then lost the high bit in the mask and landed
on the PCI range at 0x8000_0000.

Cut to the decoded bits first, then apply the alias, then cut its target.
The window lies within the decoded bits, so a page already inside it
lands where it did. The extra AND runs only on an alias hit.

Only the O2 has a physical alias; the Indy, Indigo2 and Octane are
unchanged. A test maps a page at 0x1_8000_0000 with the O2's mask and
window and expects RAM at 0. It reads 0x8000_0000 without the fix.
The R10000 picks the TLB refill vector from the address space the
missing address lies in and that space's Status bit: user addresses
follow UX, xsseg SX, and kseg3, sseg/ksseg and xkseg KX. The current
mode only decides which space an address is in (R10000 UM v2.0 pp.
333-334, 342). The manual contrasts this with the R4400, which picks by
the current mode's bit. iris did the R4400 thing on every model.

So a 64-bit kernel (KX=1, UX=0) missing on a kuseg address went to the
XTLB vector at 0x080 on the R10000, where the part uses 0x000. The
Octane PROM's utlb diagnostic loads VA 0 in exactly that state and
waits at the 32-bit vector. It got "Read TLB miss exception" from the
XTLB vector instead and failed.

On the R10000 family a refill miss now reads the region of the address,
sign-extended from 32 bits in 32-bit mode, and tests UX, SX or KX. Only
the miss path changes. The TLB compare width still follows the mode.

The test misses in each space with each bit on the R10000, and checks
the R4400 keeps choosing by mode.
The R10000 and R12000 do not shut down on a multiple match. A TLB write
that would let one lookup match two entries first invalidates every
entry it overlaps, the wired ones included, then writes the new entry
and sets Status.TS (NEC U10278EJ4V0UM, Status register TS bit p.223 and
"Avoiding TLB Conflict" p.300). iris stored the entry and left the
duplicates matchable, so which one answered depended on how the lookup
map happened to be rebuilt. On the two-processor Tezro the firmware
flushes the TLB with 64 invalid entries on the kernel's page number,
and the kernel then writes the valid global 16 MiB entry over them;
a later write elsewhere left an invalid duplicate answering, and the
fetch took TLB Invalid.

The R14000 and R16000 get the same rule as a model choice, matching
QEMU's r10k_invalidate_conflicts(); no manual for them was read. The
R4400 and R5000 keep their behaviour.

An overlap is two entries a lookup could match together: same region
and page number under the wider of the two page masks, and equal ASIDs
or either entry global. Invalid entries count. The removed entry reads
back as an unwritten slot except for its PageMask, as in QEMU: the
kernel TLBRs its wired sentinel entries and reuses PageMask for the next
write, and a zeroed mask made it write a per-CPU mapping as 4 KiB.

The cost is confined to TLB writes on these models: a compare against
the (at most 63) entries written since power-on. The hit path, the vmap
fast path and the miss path are unchanged, and the rule is off on every
other model. TLBWI and TLBWR are never compiled by jitv2, so both reach
the same write and set TS in the interpreter.

Tests cover the Tezro sequence, wired entries, PageMask kept on a
read-back, entries that cannot match together, and TS from TLBWI and
TLBWR. They fail with the rule off, and the R4400 and R5000 test fails
if the rule is enabled for them.
A store that nothing answers raised a synchronous Data Bus Error with a
precise PC on every CPU. The R10000 cannot do that. It sends an uncached
store as a write request, and a write request gets no response; Bus Error
comes only from an error response to a read or upgrade request, or from
uncorrectable read data (R10000 UM pp. 104, 316). The processor never
learns that a store failed.

The R12000 manual documents the same no-response write path, and the later
parts are taken to match, so the test is the R10000 family and not the R10000
alone.

Add CpuModel::POSTS_UNCACHED_STORES, true for the R10000 family's caches. On such a
model a refused store logs and completes in write_data_impl and in the
masked-store path (SWL, SWR, SDL, SDR), which the interpreter and the
compiled code both go through. An uncached SC or SCD that is refused
completes and succeeds (rt = 1); that is a model choice, since the manual
says nothing about uncached store-conditional. Loads, instruction fetches,
cached accesses and the R4400 and R5000 keep the exception, and device
return values are untouched because BUS_ERR also tells non-CPU callers
that a request was declined.

The error goes no further. Nothing in iris raises an interrupt or sets an
error register for a store the Octane's bus refuses. The IP22/IP28 bus
already completes a write to an unanswered address, latching MC
CPU_ERROR/GIO_ERROR and raising no interrupt, so the stores that change
here are the ones a device refuses itself.

The log line stays visible ("Unsupported uncached write ... posted,
completes") but a probe loop must not flood stderr: it prints the first
four refused stores and then each power of two, with the running count.

The Octane's ide diagnostics store 0x591204 and then 0x4 to physical
0x1e130400, where nothing answers. Before, the first store took a Data Bus
Error; now both print one line and the run carries on.

Tests: the interpreter on the R10000 (full and shadow caches) and the
R5000 for SW, SWL and SC plus the load that still faults; the JIT against
the interpreter for SW and SWL on the R10000 and for the load and the
R5000 store that fault; the report schedule. cargo test --release
--features jitv2 --lib: 2422 passed, 0 failed.
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.

1 participant