Skip to content

kona: the InfiniteReality pipe's front end and a drawing back end, plus the Onyx2 plan - #454

Draft
jamesbraid wants to merge 190 commits into
techomancer:ip27-fixesfrom
jamesbraid:onyx2-kona-draft
Draft

jamesbraid wants to merge 190 commits into
techomancer:ip27-fixesfrom
jamesbraid:onyx2-kona-draft

Conversation

@jamesbraid

@jamesbraid jamesbraid commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #481 (the IP27 cleanup), which builds on #546 (IOC3) and #512 (PCI interrupts). Only the commits above ip27-fixes belong to this PR.

This is the InfiniteReality graphics pipe (the Onyx2's KONA board) as an Iris board model, fitted in the Onyx2's graphics slot, plus the plan notes for the Onyx2 and Onyx 3000 work.

What it does

  • On an Onyx2 booted in Iris: the PROM draws its menu on the pipe, and IRIX 6.5 reaches the xdm login and the 4Dwm desktop.
  • GL demos run: atlantis, ivview, tex_cube, Smoke and Mirrors, distort, mandel, solidview, cycles and others draw correctly.
  • Products: InfiniteReality2 and InfiniteReality2E ([graphics] board = "ir2" / "ir2e", with one or two raster managers). The InfiniteReality3's product row is in the table for when a machine fits it.
  • GL:
    • transform and lighting through src/dev/gl, user clip planes, texgen and the double-precision forms;
    • fog, polygon modes, and smoothed points, lines and polygons;
    • 2D and 3D textures, the detail and sharpen combine, and packed pixel types;
    • pixel operations and the accumulation buffer;
    • multisample;
    • queries, select and feedback, and attribute push and pop.
  • The X server's 2D drawing: fills, spans, points, lines and polylines.
  • Display: colour-index and direct colour through the gamma tables, the video channels' setup and queries, the DAC hardware cursor, and blanking.
  • Texture memory is addressed by bank and address as the board does it, capped at the product's texture memory.
  • Snapshots: the board's whole state can be saved and restored. (The Onyx2 machine itself doesn't snapshot yet.)

How it's built

  • The front end (host interface, command ring on GFifo, DMA, display lists) is ported from the sgi-origami model; each ported file says so.
  • The drawing side is new, written for Iris: one pixel store, tiles with one owner each, fog and texture paths chosen once per draw, and a display composed at retrace.
  • Board facts live in one defs module. Where a hardware detail isn't known, the code says so and makes a plain, labelled choice.

Testing

  • Replays: the PROM console, the 4Dwm desktop, the GL demos and Quake are replayed from recordings, with their frames or counts pinned. The recordings are too large for the repo, so those tests skip unless KONA_RECORDINGS and KONA_GL_RECORDING point at them.
  • The desktop matches its capture to within one colour level; the remaining level comes from the capturing model's colour widening, not this board.
  • The Quake replay loads every texture and refuses no binds.
  • cargo test --workspace passes.
  • Boot: an Onyx2 boots IRIX 6.5.30 to 4Dwm with [graphics] board = "ir2".

The pipe takes 32-bit words on a command page. A word opens a command,
or is an argument of the one open, and how many arguments follow depends
on a selector table per grammar: the textport after its sync word, the
kernel and graphics-process tokens, the GL client's commands and the
kernel FIFO's own. An unknown word between commands must be dropped
without losing the alignment of the commands after it, or one stray word
shifts every later command.

Add the framer and the four tables, with a lookup that is a flat array
indexed by selector, since every word between commands is looked up.
Host software finds the board by its widget id register, its one-wire
record and the configuration block the pipe answers a configuration
request with. A wrong word in the block shows up as a board set, a
raster-manager count or a channel count the board report and the
combination checker then trust.

Model an Onyx2 deskside's InfiniteReality2 as QEMU's model does: a GE14-4
geometry board, one or two RM7 raster managers with a 64 MB TM7 each and
an eight-channel DG5, with that model's part numbers, names and
revisions. ROM serials and barcodes are synthetic and marked as such.

The tests lay the deskside's configuration block out independently of
the builder and compare it byte for byte, for one and two raster managers.
The graphics install tool programs the pipe's 512 KiB flash by sending
command 0x1ec, an operation number and that operation's words, and reads
the answer from a reply buffer whose first word must go non-zero last.
The tool then reads everything back and compares it, so the flash has to
return exactly what was written, including the header word the PROM
checks.

Add the flash and the six operations, with QEMU's framing: a count above
0x7ffb fails before any data word, an unknown type takes its data and
stores none, and writes past the end are dropped. The identify word is
the same on every call, as the tool assumes. The contents can be held in
a file so they outlast the process as flash outlasts power-off.

The tests drive each operation through its words and replies, including
the read-back of an image group and of a header.
Most of the registers a processor touches while the pipe works are plain
storage or small state machines: the interrupt destination and vectors,
the cause register with its write-one-to-clear bits, the mailbox the
firmware answers on, the one-wire slot register. They live in one
register file behind a short lock that is never held across a call out
of the board. The interrupt-enable register holds its twelve bits and
reads them back, though software never reads it.

The board reaches the machine only through three handles it is given: an
interrupt packet sender, an inbound DMA handle and a FIFO-drained signal.
Nothing in the module names a Hub, HEART or Bedrock type, so a machine's
glue implements them over whatever its fabric is.
Everything that draws or changes display state is behind a typed seam,
so a later back end can implement it without touching the front end.
The front end hands it Jobs in stream order and gives it a BackPort for
the few things it may do: answer on the return page, post a completion
reason, queue mailbox replies, complete a retrace and move bytes through
the pipe's map RAM or the host's physical memory. NullBackend stands in
until one exists and completes retrace requests at once.

The state behind the port is the Host Interface Processor's: the pages
it answers on, the reason queue (one reason at a time, since a second
would overwrite one the driver has not read), contexts with their own
return pages, pool swaps, and the map RAM translation. The processor's
state is taken out of a slot while someone works on it, so no board lock
is held while the machine's DMA, interrupt and FIFO handles are called.

The thread spins for the slot, which a processor holds for a few
instructions. A monitor or reset sleeps on a condition variable.
The command page's words, the DMA push port, the bracket register, the
firmware load port and the map RAM make up the stream the Host Interface
Processor runs. Frame the textport, kernel and GL grammars that share the
page, set a user command aside when the privileged sender breaks in and
give it back whole, and answer what is about the board itself: the return
and reason pages, the configuration request, contexts, pool swaps and the
EEPROM operation. Everything that draws goes to the Backend as a Job.

Accesses meet the stream three ways. A stream store is run in order. A
register the driver polls while the pipe works (cause, mailbox, fill
level, activity) is served from the register file at once. Any other
register reads what the stream writes, so it waits for the stream. Here
stores run in the store; the next change moves them behind a thread.

The fill-level register counts 32-bit words, rounded up to two. DMA
packets and display lists are only framed and consumed so far.
The processor stores commands and goes on; the board decodes them behind
it, and a display list one store invokes can run for as long as its
words take to decode. Put stream stores into a GFifo and run them in
order on one thread standing for the Host Interface Processor. A full
ring answers the bus busy instead of spinning on the processor's thread,
as the VPro board does since 9ac76ff. A register the stream feeds waits
for the ring to drain, and an 8-byte access is all or nothing so a
retried store repeats no word.

The fill level adds the ring's backlog to the words framers hold, and the
published held count lags during a long run. stop runs what the thread
had not taken.

If the back end panics on the thread, the board is marked dead: the
bus answers an error instead of busy forever, and failure() says why.
Host software copies a display list into pipe memory with a list-write
record and a DMA packet, then invokes it from pipe memory or from a DMA
pool by (offset, word count) ranges. A compiled list holds the words
immediate mode writes to the command page, so the invocation feeds them
to the same decoder, in order, across the break between ranges. A list
that invokes a list fails and the outer one goes on, and offsets count
from the region the kernel placed for the current context.

A packet's high word with bit 28 clear is a device address shifted right
twelve, translated through the map RAM; with bit 28 set it is an absolute
host frame. Add the 15 MiB list memory, the packet's shape and tag
checks, the completion word its last word asks for, the pool and its pull
and move. A packet with no list record goes to the Backend as a DmaIn
job. The widget DMA goes to is a field of the widget control register,
which Kona::dma_widget exposes.
Record how the board attaches, which accesses wait for the command
stream, the slot the processor's state is taken from, and each
approximation or open question with its grade, so a back end or a
machine does not have to rediscover them.
BackPort works only inside Backend::job, on the Host Interface
Processor's thread. The display runs on a thread of its own at the frame
rate and has to complete retrace requests and raise the video interrupt,
so Backend::attach now hands the back end a DisplayHandle when the board
starts.

The handle holds the register file weakly. Host becomes its own Arc: the
pipe slot owns the back end, so a handle holding the whole board would be
a cycle that never drops. With the board gone every call is a no-op.

Calls carry an epoch. A reset begins a new one, so a display thread that
was inside a retrace completion when the reset happened cannot raise a
cause the reset cleared. activity() sets the driver's watchdog latch for
the drain loops and the raster thread; the display thread must not call
it or the watchdog could never fire. video_frame() raises the video cause
with no tag, and logs a missing interrupt destination once instead of
every frame.
A back end with a display thread, a raster thread and tile workers has
threads the board does not own, and Backend had only job and reset. Add
defaulted stop, quiesce and resume.

Device::stop calls Backend::stop once the Host Interface Processor's
thread has joined and the jobs it had not taken have run, so nothing
feeds the back end while it joins. It runs with the pipe slot taken and
no lock held, since a thread of the back end may be inside a handle call
that sends an interrupt packet. A board that never started does not call
it. One whose back end panicked does, so the next start does not attach
over live threads.

Kona::quiesce lets the ring run out and then quiesces the back end, for a
snapshot taken with the processors stopped; resume undoes it.
KonaBackend is the shell the drawing back end grows in. It takes every job
as NullBackend does, counts it by kind, completes retrace requests at
once, and carries what the pipe's later stages need to know about the
board: raster manager count, texture memory per manager, board set and
managed area. On firmware start it reports the managed area to the front
end.

It also holds the model-side bound on a pixel's record, from the host
driver's own budget arithmetic: the managed area's pixels (width rounded
up to a multiple of 160) divided into a per-board-set budget of words,
mapped to the 64, 32, 16, 8 or 4 word class. The guest derives its own
class from the raster manager count and the managed size, so the back end
never reports one; the bound only lets a later layout that needs more
words be refused.

Backend::monitor lets the back end answer the kona command, which Kona
routes to it: status and counters for now.
A word that opens no command was logged and forgotten. A token whose
length the selector tables lack arrives as its own word and the words
behind it, all of which the same path ate, so nothing showed which tokens
a guest sends that the front end cannot frame, or how much traffic they
carry.

Hand each such word to the back end as Job::Dropped, in stream order with
the commands around it. NullBackend ignores it; KonaBackend counts it
with the other jobs. The padding word after a command in a doubleword
store is one of these now, which the doubleword test says.
Which token families a stream really carries decides what each build
step must learn about their layouts, and the front end alone cannot say:
it frames only what its selector tables hold and hands the rest on as
dropped words.

Tap sits in front of NullBackend and tallies every job: commands by
grammar and selector (with the stream position of the first), data runs,
bitmaps, video commands, retrace tags, context changes, indirect stores
and DMA packets. Dropped words are put down to the command before them,
which is how a data run the tables cannot size shows up, and the census
is snapshotted where the first run of 64 of them begins, since past that
point a data word that equals a selector can open a command nobody sent.
It also lists the selectors QEMU's decoder leaves with an unresolved
argument layout against what the stream sent. Selector names come from the
comments beside the front end's tables, so there is one list to keep.

Test-only; the replay driver that feeds it recorded streams follows.
Replaying a stream another model of the board recorded checks the front
end and the file format before anything draws. The KONAREC reader takes
every record kind the recorder writes and refuses a bad magic, version,
tag, count or cut-off record. The replay builds the board from the
recording's board record, performs each store and load in order (retrying
while the bus is busy, comparing the loads whose answers do not depend on
timing), serves the board's DMA reads from the recorded ones and compares
what it wrote to guest memory, the geometry-engine interrupts and the
retrace and reset markers.

Both recordings run through NullBackend and the census tap. The PROM
stream is exact end to end. The IRIX stream is exact until the first run
of data the front end cannot size (an inline pixel write: only a back end
can answer Reply::Data for it), about 20,700 records into the graphics
traffic: every compared load and every byte the board wrote match there,
and the interrupts match. Past it the stream misframes, as it must, and
the test says so instead of claiming a match. The recordings stay out of
the tree; KONA_RECORDINGS names the directory and the tests skip without
it. The measurements are in clanker/kona/token-census.md.
The drawing back end keeps one array of 32-bit words per word position of
a pixel's record, one entry per pixel of the managed area, instead of
whole records. A fill that touches one or two words then writes contiguous
memory, and a window that uses 13 words of its 16-word class allocates 13
arrays.

An array is allocated whole, zero-filled, at a draw's or clear's set-up and
published through a fixed per-word AtomicPtr with release ordering, never
moved or grown; reset zeroes in place. A kernel resolves its arrays' bases
once, and the display can load a pointer once per row run and take null as
a run of zeros, so the pixel path checks nothing and a racing display read
sees the old or the new value. word_slot is the one definition of the
address function, whose physical form is unknown, for cold paths.

Tests: nothing allocates until a word is written; a reader racing a first
write sees zero or the value; two threads allocating one word share one
array; reset keeps the pointer.
From the buffer-configuration command's ten words (level, colour size,
index size, double buffer, stereo, sample count, depth, stencil,
accumulation and auxiliary buffers) work out where each plane starts in a
pixel's record: colour buffers, depth, 8-bit stencil, multisample colour,
accumulation, in that order, with the word counts the sheets give per plane
and the window offset moving every base together. The layout does not
depend on the raster manager count.

Configurations the sheets do not cover are refused with a reason instead
of guessed: an underlay level, a sample count other than 0, 1, 4 or 8, a
depth that does not round up to 16, 24 or 32 bits, a stencil other than 0,
1 or 8, a colour size of 13 to 15 or above 16 bits, an auxiliary buffer,
and accumulation on 16-bit colour (whose words depend on a firmware mode
value nothing defines). A layout that needs more
words than the host's budget gives a pixel is refused too.

The sheet's worked check is the first test: RGB10, double buffered, 4
samples, 24-bit depth, 8-bit stencil and accumulation ends at word 12.
A transfer crosses two sets of formats: the host's packed image (RGBA4,
bitmap, RGB5A1, R5G6B5, LA8, L16, I16, S16, Z16, RGBA8, RGB10A2, LA16, Z32,
RGBA16, RGBA12) and the 64-bit words of the raster bus, picked by a code.
The model holds a colour as four 16-bit components and a stencil or depth
as an integer of its width; HostFormat converts between those and packed
bytes, most significant component first and big-endian, the guest's memory
order. Widths round to nearest, narrowing as (v * max + 32767) / 65535,
so every value that fits survives a round trip.

Only what the sheets give is modelled. Of the bus words they place the
fields of RGBA12 alone (code 0), so only it packs; the other codes carry
pixels-per-word and the published table, with the five codes that have no
module refused. Host RGBA12 is refused: its field placement is not given.
Luminance formats take red as the luminance, an approximation.

Tests: every value of every component of every colour format round trips;
byte layouts; bitmap rows; scalars; the code table; the RGBA12 bus word.
…ack end

KonaBackend now owns a PixelStore sized to the managed area (nothing is
allocated until a word is written) and lays out every GL buffer
configuration it sees within the host's per-pixel budget, counting the
ones it refuses by reason. The token layer will keep a layout per context;
until then the last one shows in kona status. A reset zeroes the store in
place.

kona pix X Y lists the words of a pixel that have an array, so the store
can be looked at the way the later stages will write it.
The replay parser took the whole file as a slice, which a recording of
71 million records cannot be. Stream reads the same format from any
reader one item at a time (the board, a record, or the end with its
count), and Recording::parse is now a loop over it, so there is one decoder
for the format. Errors are as before (bad magic or version, unknown tag,
cut-off record, wrong count, second or missing board record), plus data
after the end record.
Counts which GL tokens the three demos in the GL recording send, per user
mark, replayed into NullBackend behind the census tap: vertex, array,
matrix, texture and query families, how many commands fall outside QEMU's
decoder table or in its unresolved set, and the dropped words by the
command before them. The 1 GB file does not fit memory, so census_spans
reads the board's DMA reads in a first pass and replays in a second,
cutting a census at every mark.

GL commands are also tallied by family from the front end's table names,
and the render says how many commands lack a decoder entry or sit in the
unresolved set. Results and the grammar still needed are in
clanker/kona/token-census.md. The test skips unless KONA_GL_RECORDING
names the file.
The integer primitives the textport and the X server share: plot a point,
fill a box and set a bitmap's bits into one word position of the store,
clipped to the managed area. A draw's set-up allocates the array it
writes. Coordinates are the board's (origin bottom left, y up) and the
store keeps them as given; the display flips.

A whole-pixel box lights columns x1+1..x2 and rows y1+1..y2: the left
column and bottom row are left out, so boxes that share an edge meet
with neither gap nor overlap. FillRule makes that a parameter, with a
variant that lights every edge, so the pending ruling on the rule's
grade changes a value and not the code. A reversed box draws nothing.

Tests: the four-corner packet's columns and rows, shared edges, the rule
as a parameter, clipping at the screen edges, off-screen plots and the
bit order of a bitmap.
The PROM's console draws before any window exists. Command 2 sets the
draw colour (a colour index, a byte), 4 the raster position, 5 fills a
box by the fill rule, 6 plots a point, and a bitmap blit draws a glyph or
icon at the raster position minus its origin and then advances the
position. 1, 3, 7 and 8 are framed and counted: 3 (a colour-map load)
belongs to the display tables, and nothing is known of the others' role.

Pixels go to the colour word of a single-buffered level-0 window, word 1
of the record, since no buffer configuration precedes the console; that
default, the colour-index reading of a textport pixel and the bitmap
placement are the model's own and graded approximated in the module doc.

Tests: colour and box, point and raster position, signed corners, bitmap
placement and advance, and the commands that only count.
KonaBackend takes the textport's commands and bitmap blits and draws them
into the store with the configured fill rule, shows the textport's state
in kona status, and clears both on reset. kona hash prints a hash of the
store (FNV-1a over each allocated word position and its entries), the way
the other boards' replay checkpoints do.

Replaying the PROM recording to its end draws the graphics menu: 5,528
draws, one word array, and a store whose hash is pinned in the replay
test as a regression net (class U: the hash says the drawing did not
change, not that the pixels are right). Rendering the store with the y
axis flipped puts every label where the recorded screen capture has it,
and the gradient, icons and logo are all there as colour indices, which
the display's colour map will resolve; so the board's y-up coordinates and
the placement rule hold for the textport. The test skips without
KONA_RECORDINGS.
The X server loads its drawing state as thirteen words: a colour, a
background, a write mask, a logic operation and a target. Decode them into
a Gc, or say why not (bad fields, colour or mask, a mixed display-ID form,
or a form whose meaning is not known) so a draw that uses an
undecoded context is refused and counted, never guessed.

The forms understood are the 8-bit and native 16-bit main-plane index,
24-bit and 30-bit RGB, the two overlay buffers, the popup planes, both
overlay buffers at once and the display-ID plane, with the 16 logic
operations and the write-mask arithmetic. Ported from the QEMU model's
decoder with its unit cases, and with its grades: the field layouts are
software expectations; that a repeated buffer selector means the same
selection, that the config-0x28 selector-3 form is the overlay window drawn
into both buffers, and that format 9 marks 30-bit colour are inferred.

Taken from jamesbraid/qemu-sgi-origami at dd935284c624 (hw/display/sgi/x-gc.c and
its unit test, tests/unit/test-sgi-x-gc.c).
The X server's state commands set what fills draw under: 0x129 the
context, 0x12a the drawable's position (the origin of rectangle
coordinates), 0x145 and 0x12b a clip list (a count and rectangles, the
first words arriving as bdata4 runs ahead of the clip), 0x12d a clip box.
XState keeps them and works out the bounds a fill clips to: the screen,
narrowed to the clip rectangles' box and the scissor, with a membership
test for a list of several rectangles.

Grades are in the module doc: each rectangle absolute with an exclusive
far corner, and drawing clipped to their union, are inferred; a list whose
length disagrees with its count clips to the screen only; the scissor ends
at the next clip load (a model choice, since how long it lasts is
unknown). Any other kernel command ends a waiting clip run, as the other
model does. The model keeps at most 64 rectangles. kona status shows the
decoded context and the state.
A box (two words, absolute, far corner exclusive) and a rectangle (x, y,
width, height, relative to the drawable's position) draw under the loaded
graphics context into the colour word: clipped to the screen, the clip
list and the scissor, the colour through the context's write mask and
logic operation. When a request has more boxes than a token holds, the
leading words arrive as data tokens (0x141 to 0x14b) that wait for the box
or rectangle token ending them; any other kernel command drops them, as an
odd-length run is dropped.

X shapes draw the pixels the request names and do not go through the
textport's fill rule (approximated). A request is checked whole before any
pixel is written, so a refusal draws nothing, and is counted by cause: a
context that did not decode or an opaque one with no stipple, a target held
by the display step (overlays, display-ID plane), or a destination word
wider than the target. The model carries no per-pixel tag for what a word
means; which colour map resolves it is the display's to settle.

Tests: box and rectangle geometry, data runs and their bounds, clipping to
screen, clip list and scissor, write masks and the no-op operation, and each
refusal.
Spans (a count, that many points, that many widths) draw one-pixel-high
runs from their points, absolute, with no position added (approximated: no
traced sequence tells that from a position-relative reading). A span token
whose count disagrees with its length is dropped and counted. Points draw
single pixels and, like boxes, take the words of a waiting data run first.
Both go through the same checked-then-drawn fill as boxes, under the same
context, clip and refusals.

Background and relative point forms and segments are not in either
recorded stream and are not read from the model's source yet.
Stippled fills are masked by a pattern the X server stores once and names.
0x13d stores an entry's width, height and rows (the rows follow the
command, so the back end answers Reply::Data for them and takes them as a
Job::Data), 0x13e sets the entry's origin and 0x13c loads an entry, or all
ones to end the masking. Box, rectangle, span and point fills then paint a
pixel only where the pattern's bit, measured from the entry's origin plus
the drawable's position, is set; a clear bit leaves the pixel alone unless
the context is opaque, in which case it takes the background. An opaque
context with no stipple loaded is still refused.

The cache's size (32 patterns of up to 32 by 32) and its refusals are the
model's own: a store outside it consumes its rows and leaves the entry
empty, a row count past 4096 is refused without asking for the rows. The
bit order is inferred from a traced I-beam; what a clear bit does and how
a store treats the origin are model choices, graded in the module doc.

Tests: tiling, wrap and origin shifts, load and unload, refusals, and a
checkerboard stored and loaded through Backend::job masking a box.
Replays the IRIX recording's graphics traffic up to the first run of data
only a back end can size (record 1,415,538, about 20,700 records in)
through the same path as the PROM test and pins the store's hash: 154
fills of the X server's state, boxes, rectangles, spans, points and
stipples at the greeter's start. A regression net (class U), not
evidence of the pixels: the pixel and image paths that fill the rest of
the desktop are the next step's.

Rendering the store of the whole replay with the X fill rows flipped puts
the toolchest, the console icon and the four desktop icons where the
recorded desktop capture has them, and the greeter's window, buttons and
text field where the greeter was; without the flip they land at the
bottom, so the X server's y runs down from the top of the screen while
the textport's runs up. The earlier commits' X fills carry that flip.

replay_drawing takes the number of records to run, so a test can stop at
a known point. The test skips without KONA_RECORDINGS.
Relative points (one, six or twenty-four per token) were framed and
dropped. Each word is a step from the previous point, the first from the
drawable's origin, and the walk runs on through a waiting data run and
the whole burst before starting again at the origin with the next token.
Once the points are placed they draw like absolute points, through the
clip, scissor, stipple, write mask and logic operation.
The X server's lines and arcs arrive as segment tokens (one or six
independent pairs of points) and polyline tokens (two or seven points,
absolute or as steps from the previous point). They were framed and
dropped, so every outline the X server drew was missing.

A line is cut into boxes before the request is planned: a run of pixels
along a row or column is one box and pixels off the screen are not made,
so the existing fill loops draw it, the destination check covers the
whole request, and nothing is looked up per pixel. A segment draws both
ends and a segment of no length one pixel; a polyline draws a shared
vertex once and is not closed unless its last point is its first.

No source gives the tie rule for a line that falls
exactly between two pixels. The model takes the integer line through
pixel centres and rounds an exact half toward the end point. That is an
approximation and is marked as one, with what would settle it.

Ported from the line shapes of hw/misc/sgi-kona.c in
jamesbraid/qemu-sgi-origami.
… and vertex tokens

The GL path counted these as unmodelled, so a client that submits
vertices with glVertex3d, colours with glColor3d or coordinates with
glTexCoord2d drew nothing. The texture-coordinate forms are established
as two words to a double, and the colour, normal and vertex forms are
inferred to be the same.

A double token is narrowed once, as the command arrives, into the words
of the matching float form, high word first as the depth clear's double
is, and then runs the float form's path. The vertex path gains no branch
and no second decode. The colour-index double stays unmodelled: the
model has no colour-index mode. A word count that does not match the
form is not narrowed and is counted under the float form.
The module comments and the back-end plan were written before the
fragment operations, texturing, lines and points landed, and still told
readers the stage has no stencil, blend, logic operation, write masks,
alpha test, polygon offset, stipples, texturing, lines or points, that
the GL path takes no texture commands, and that every draw runs one
unspecialised path. They also said swaps and retraces both go by
ticket, when only a retrace request carries one (the reset epoch it was
made in); a swap loads the display's bank table when its command is
reached in the stream.

Describe what the code does and keep the gaps that are real: no fog, no
texture on volume or detail binds, the model's own depth form,
fragment-operation order and line and point shapes.
The pipe's identity was a set of constants for the Onyx2 deskside's
InfiniteReality2, scattered through the configuration block, the
geometry board's one-wire record, the texture pool and the class word
the driver's attach reads. Adding another product would have meant
another copy of each.

Gather what differs by product into one row of a table: the board set,
the class word, the geometry, raster and texture boards, the texture
size and its code, three chip identity words, and the chassis the
product shipped in. The pipe's configuration names its product and
every reader takes its values from the row. The one row is the
InfiniteReality2 and every byte of the block is unchanged.
Add one page for the chip: the parent and each child against the Linux cell
it stands for, which parts are the IOC3 and which the external Super I/O, how
the interrupt words and DMA work, what each lock covers, each board's row and
wiring (including the Octane2's records and the Onyx2's MediaIO and RAD1),
what is modelled and what is approximated, and which snapshot keys remain.

Point the older notes that named src/dev/ioc3.rs and ioc3_eth.rs at the new
paths, and retitle the PS/2 page, which is not Octane-only. The clanker notes
keep only the traps (what the PROM insists on, the pacing rule, scan code
set 3).
The R10000 family puts a two-bit uncached attribute beside the address
of every uncached reference. An unmapped one takes it from XKPHYS bits
58:57, which already reach the board in those bits; a mapped one takes
it from EntryLo bits 63:62, which the TLB stored and then dropped when
it made the physical address. Every mapped uncached reference therefore
went out with attribute 0.

The Onyx2's Hub picks node memory, its own registers or a crossbar
widget by that attribute. IRIX maps the InfiniteReality pipe's command
page into the kernel and into the graphics start-up and X server as an
uncached accelerated page with attribute 1, I/O. With attribute 0 the
stores landed in the Hub-special view at node memory past the fitted
128 MiB and vanished: the kernel's pipe start never reached the pipe,
and the driver gave up with "IR0: mopup timeout" and death reason 17.

A TLB entry with cache algorithm 2 (uncached) or 7 (uncached
accelerated) now carries its attribute in bits 58:57 of the physical
address it gives, the same place an unmapped reference carries it.
Cached entries are unchanged. The attribute is folded in when the entry
is written, so translation does no extra work. The other processors
cannot store those EntryLo bits, and the boards that do not decode the
attribute cut it off with the executor's physical mask.
The Hub still asked the machine four things through a hooks trait: the
node number, the cached alias, a processor soft reset and a restart. A
trait that a board implements is the wrong direction for a chip, and
each call hid ordering: the effect carried the new value, so two writers
that raced could leave the machine's routing at the older one.

The node number and the cached-alias size are now published values the
owning unit stores under its own lock, as the register changes. The
Hub's address decode reads them from there with no lock, as before
and through one pointer. The machine's node-zero routing is handed to
the Hub once and re-reads both under its own lock when a store, a
neighbour's vector write or a reset has changed one, so the last value
written wins.

A soft reset is a line, not a call: the Hub sets bit 63 of the
processor's interrupt word, which is what the processor's reset signal
did, without a lock and without the machine. A restart goes out on the
machine's event channel as a hard reset, wired the way the Indy IOC's
power events are. Enable and NMI stay stored and logged.

The node number stays 16 bits because the register field is 9 bits wide.
No per-access cost is added: the claim decode loads the number and the
alias through a pointer it already followed.
src/ip35.rs held the bounded MMIO windows, their sparse map and
map_device, but the only code that uses them upstream is the SN0 node
bus, so IP27 imported its bus library from a module named for the Fuel's
software platform. The file becomes src/window_map.rs, named for what
is in it, and takes the physical address masks that ip27.rs and a Fuel
bus both define (PA_MASK and CPU_PHYS_MASK), so there is one definition.
A bus that wraps the four views differently stays with the machine that
has it. The move is a rename; a test now pins what the window map did
without one: a window answers exactly its bounds, the rest of the page is
refused, and a later map over a page replaces the earlier.
The X server sets bit 31 of the graphics context's fourth word for an
opaque stipple or a tiled fill, and nothing else on the wire tells the
two stipples apart. Whether the board honours the bit is unknown: the
engine forwards the word whole and the chip probably masks it to 24
bits, which would leave the bit no effect. The model keys an opaque fill
on it anyway, so call that an approximation that contradicts the mask,
and say what would settle it.
The kernel puts the pointer in the video DAC when an X session owns a
DAC-capable channel: the display request loads a 64 x 64 two-bit image
and two colours, and writes to the DAC block's index and data registers
set the control value and the position. The load was already decoded and
kept; nothing drew it, because the composed frame had no stage after
gamma.

Follow the block's registers (index low then high byte, data to register
2) for the control value at 0x204 and the four position bytes at 0x200,
and draw channel 0's cursor last, over the XMAP glyph and the frame. Its
colours are the DAC's 10-bit values narrowed to eight and do not go
through the gamma RAM, the DAC being after it. It shows in X11 mode with
the enable bit set, which is what the kernel writes, 5. Pixel value 3
takes the foreground and 2 the background, 0 and 1 leave the frame. The
host's pointer follows it the way it follows the glyph cursor.

The kernel sends the index again before each position byte and the bytes
go to register 2 of the owning channel's block like every other data
write; no other address carries them. The index advances after each
access in 0x200 to 0x204 and in the image's 0x400 to 0x7ff, as the
published part does; whether it advances in 0x000 to 0x00f is unknown and
the model leaves it there. The position is a signed 16-bit top-left in
output pixels with no bias, and which active pixel is (0, 0) depends on
the board's blanking edge, which is unknown. The colour assignment is the
part's, inferred to be this board's DAC. The XGA mode, the interlace
bit's effect and the load for channel 0xde are not drawn. The module
header and the board page say which is which.

The DAC state is saved in the snapshot. The recorded desktop and console
frames do not move: the recording's pointer is the XMAP glyph and the
stream never loads or enables a DAC cursor, which the replay test now
asserts.
The display generator's ChannelBlanking and TempBlankAllChannels
commands were kept as state and logged, and the screen ignored them.
A guest that blanks a channel while it changes video format, or blanks
everything around a mode switch, still saw the old picture.

The board shows one channel's frame, channel 0's, so that is the one
that matters: the frame is black while channel 0 is blanked or every
channel is temporarily blanked. Other channels' blanking stays state.
The composer takes the choice once per frame, from a flag in the baked
tables, so an unblanked frame pays nothing per pixel. The pixel store
is not touched; drawing carries on underneath and shows again when the
blank ends.

The recorded desktop and console frames do not move: channel 0 is
unblanked in both.
On a two-node Onyx2 or Origin 2000 the PROM numbers one node 1. That
node's software addresses its own memory by its number (bit 32 up), and
node zero's memory beyond the cached alias belongs to the other node.
The processor's cache and the JIT reach RAM through the node's ppmem
window, but the window covered only node-relative offsets below 4 GiB,
and NodeZero cleared it entirely once the node held a nonzero number.
Every cached access of that node's processor then went through the
decode table, the Hub's node view and its bank lookup, and every
instruction fetch missed the decode cache's window pointer and re-read
its word over that path. A per-thread count in a throwaway build showed
it: during `runon 1 awk 'BEGIN{for(i=0;i<1000000;i++)s+=i}'` on the
Onyx2, all 912 million instruction fetches and 458 million data
accesses of that processor took the bus path, 86 percent of the
fetches to its own memory at its number and the rest to node zero's
memory. During runon 0 the other processor fetched 665 million of its
804 million instructions from node 1's memory the same way.

Windows now lays every node's window from the numbers and aliases the
Hubs publish, as the decode table routes them: node zero's window from
the node's own banks below the alias and the zero holder's above it,
and each node's memory at its number, the other node's included,
sharing its storage and generation counters. A two-node board gets an
8 GiB window, so numbers 0 and 1 fit. Any node's change relays both
windows, laying only the difference: a page that stays mapped is
replaced in one mapping call, and a range that goes leaves the region
bitmap before it is scrubbed, so a renumbering never clears a window
the other processor is executing from. The access path is unchanged.

With the fix the same count shows no cached access on the bus path on
either processor, from power-on to the end of the runs. On the Onyx2
at this branch's previous tip, the awk loop took 29.9 s on processor 0
and 57.9 s on processor 1 (means of five runs each, alternating in one
boot). With the fix it takes 27.9 s and 28.2 s. Boot to the login
prompt went from 293 s to 208 s. The two-node Origin 2000 runs the
loop in 35.2-36.5 s on processor 0 and 37.7-39.6 s on processor 1.

An 8 GiB window has 128 MiB regions in its bitmap, so on two nodes a
64 or 256 MiB bank, which fills no region whole, stays on the bus path
for both processors. A number above 1 is left to the bus path too.
The Origin 2000 guide said a two-node machine stops in IRIX with
"Processor #1 did not start", the second node's processor taking a TLB
exception in the PROM. It no longer does. At this branch's tip, with and
without the node window change, a two-node Origin 2000 (180 MHz R10000,
128 MB per node, stock IP27 PROM 6.156 and I/O PROM) boots IRIX 6.5.30
from the BaseIO's SCSI disk to a root login on the serial console, and
hinv lists two IP27 processors. runon 0 and runon 1 both complete.
The texture arena only grew. A level reloaded at an address in another
size, a level a later load overlapped, and every level of a context whose
slot was activated afresh all stayed in the arena, so it filled at the
product's size (16.7 million 32-bit texels on an InfiniteReality2) and
every later load was refused as past texture memory and every bind of a
level that never loaded failed. A TyrQuake replay hit that wall partway
through.

The pipe has no allocator of its own: a load writes where its address
names, a level reloaded at the same place overwrites it, and a later
load that overlaps a level replaces it. The model now stores texels the
same way. Texture memory is a plane of words for each bank of each
context (and for its detail images), and a level at address a of a bank
is the words from 16 a of that plane, a unit of 256 bits being room for
at most sixteen texels. Nothing is allocated, so nothing fragments, and
a load is refused and counted only when its address and extent reach
past the product's bank, which is the model's own limit since the pipe
has nothing to refuse with. A load of a new shape or an overlapping
address retires the levels it overlaps and zeroes its own words. A fresh
activation of a context slot drops that context's planes after draining
the tiles. A first-fit allocator would have refused loads the pipe takes
once the pool fragmented, which a random stress shows.

The owner's block offset the kernel writes on a reserve is not modelled,
so each context has planes of its own and contexts neither contend for
the pool nor alias; that is stated as an approximation. A plane is as
long as the highest word written. The sampler takes a level's plane once
for a sample and indexes it, so a texel read costs what it did. Snapshots
carry each plane that holds words.

Tests: a long run of reloads at the same addresses, of alternating sizes
at one address and of overlapping addresses each stays bounded; a load
that fits no hole is not refused; 20000 random loads inside the bank are
never refused and never overlap; a load past the bank is refused with
the levels there untouched; two contexts at the same addresses do not
alias; planes survive a snapshot and a bad one is refused. A gated
replay of the Quake recording prints the texture counts
(`KONA_QUAKE_RECORDING`, `KONA_PROGRESS`, `KONA_STOP_AT`).
The desktop frame test compared with a threshold of 8 levels, so a
difference of one level on 97% of the pixels (1274736 of 1310720) passed
unseen. Measured against the recorded capture, every pixel is within one
level and none is further.

The one-level differences come from two conventions that nothing known
settles, and the recorder settles each the other way. This model widens
a 10-bit colour-map entry to the 12-bit gamma index by replicating its top
bits; the recorder rounds it to 8 bits first. This model rounds the 12-bit
gamma output to 8 bits; the recorder drops the low four bits. With both
switched to the recorder's way the frame matches exactly, so nothing else
(stipple, text, cursor placement) differs. The widening is graded
as inferred and nothing yet settles the output narrowing, so neither side is
shown wrong and neither is changed.

Assert no pixel is further than one level, and name the two conventions in
the test's comment.
The gamma RAM's 12-bit output reached the DAC's 8-bit inputs rounded to
nearest. The colour map's output chips drive the video-output chips from
bits 11:4 of each colour alone and nothing on that link rounds or dithers
(read from the board's wiring), so the top eight bits are the value. The model now
truncates. That is established for that link and inferred end to end,
since a chip between the video-output chips and the DAC is not read to
add anything.

Grade the other two choices in the display chain where they are made: the
10-bit colour-map entry reaches the 12-bit gamma index by replicating its
top two bits (inferred: the only rule the board's gamma addressing is
known to fit), the glyph cursor passes through gamma (inferred) and the
DAC cursor does not (established by where it sits). The gamma address's
colour field 3 is rewritten to green by a hardware test program, not to blue; what
the board does with 3 is unknown.
The kernel writes 1 to the host interface's status register at 0x8003a8
once per video interrupt. The model treated every bit but 2 as an
unimplemented write and logged a warning for each, about 85 a second
while a guest ran. Nothing in the kernel or the firmware reads bit 0
back, so there is nothing to model beyond the write itself.

Count the writes, expose the count, and leave the busy latch alone.
Other bits still warn.
A GL client that pushes attributes waits for word 0 of its return page
before it goes on, whatever the mask. The model did not claim the push
token, so the word was never set and the cycles demo sat in its wait
loop printing "GL:Wait Rdata timed out" after its text helper pushed
with only the list bit set.

The pipe opens a read-back before it looks at the mask and closes it on
every path, a push onto a full 16-entry stack included, and the close
writes the completion word. Post a bare completion (a zero count, then
the word) for each push. Nothing is read back for the list bit, and the
model leaves the data for the other tested bits empty and counts those
pushes. A push onto a full stack sets the stack-overflow error (inferred
from the handler calling its overflow routine) and still completes. A
pop takes an entry back and posts nothing; the light-state step is not
sent by the host and still posts nothing.
The two-node SN0 window was 8 GiB, so its one-word region bitmap counted
128 MiB regions. The bitmap only names a region that is RAM end to end,
and a 64 MiB bank fills half of one, as does each 64 MiB run of a
256 MiB bank. On a two-node Origin 2000 or Onyx2 with those banks every
cached access of both processors took the bus path again, a regression
for the first node, which had used 64 MiB regions before. A node number
above 1 fell outside the window altogether.

The bitmap test runs on every load, store and fetch, so it stays as it
is. A space now keeps a fine table beside it, one bit per 64 MiB region
of the whole window, and the shadow cache consults it on the bitmap's
miss branch before taking the bus. A space's bitmap region size can be
set apart from its window size. A cabled SN0 board gets a window spanning
the node numbers up to the next power of two above its node count, 8 GiB
for two nodes and 16 GiB for three or four, with 64 bitmap regions of
128 MiB over numbers 0 and 1. One node keeps its 4 GiB window and 64 MiB
regions. The window is reserved address space that commits nothing
(mmap PROT_NONE and MAP_NORESERVE on Unix, a VirtualAlloc2 placeholder
on Windows, read from the code and run only on Linux), and sizing it by
the node count rather than for all 256 node numbers keeps the
reservation at 8 GiB per node on a two-node board. A node a guest
gives a number past the window still works: the bus path answers every
number, at the speed it had before the window.

The decode cache keeps window pointers for pages either test finds. It
used to key them on the bitmap and start over when it moved, which
misses a change only the fine table sees, so it now keys them on a value
the space bumps whenever either changes (PpMemSpace::add_key_sink). Every
machine that wires a shadow cache to a window registers it.

With a throwaway per-thread counter, a two-node Onyx2 with two 64 MiB
banks per node made no cached access on the bus path on either
processor, from power-on through a runon 0 and a runon 1 awk loop. The
iris-bench R10000 suite, deterministic clock, quick, A B B A: identical
guest work (621448231 instructions) on every run, and 341.12e9,
337.38e9, 337.29e9, 341.19e9 host user instructions, so the shadow
cache's hit path costs no more.

On a one-node Origin 200 under IRIX 6.5.30, the processor thread's user
instructions over one awk loop, base and branch alternating over six
boots (eleven runs), were 243e9 to 279e9 for the base and 239e9 to
292e9 for the branch, means 264e9 and 251e9. That count is not fixed
work, since the guest's timers run on the host clock, so it shows no
regression beyond its own spread and nothing finer.
A 512 MiB bank 0 spans the whole 512 MiB stride, boot flash's reset
address included. The node memory mapped the flash over the bank in the
cached decode and listed both in the memory map, so MemoryMap::new
panicked ("bank 0 overlaps boot flash") for any machine built with one,
such as a two-node deskside with 512 MiB in bank 0.

Cached references now reach the bank, as the PROM's memory test expects
when it walks that address as RAM, and the reset fetch, which is
uncached, reaches the flash through the Hub-special space, where it
already is. With a smaller bank 0 the flash stays in the cached decode
as before. The QEMU sgi-origami node model makes the same split. That a
real machine does is inferred from the PROM's memory test, not measured.

A test builds every size bank 0 can be, and checks that a 512 MiB one
keeps its RAM at the reset address and every smaller one the flash.
The IP27 and IP35 platform work and the KONA board are spread over
several stacked branches that join late. Write down the goal, the order,
the decisions taken so far, the finish line and the rules every PR
follows, so a later session can pick the port up from the repository.
A texture load sent in a packed type (the library's codes 8 to 12:
3-3-2, 4-4-4-4, 5-5-5-1, 8-8-8-8 and 10-10-10-2) was refused as a type
the model does not decode, so the level never reached texture memory and
every bind that listed its address found nothing to bind and was refused.
A replay of the Onyx2 Quake session sent its 4-4-4-4 textures this way
and lost 478 binds to 30 such loads.

A packed pixel is the packed type's own size, 3-3-2 with RGB and the
others with RGBA (the library refuses any other pairing, and so does the
model). The sizes are established. The first component is taken from the
most significant bits and a field narrower than eight bits is scaled to
255, as OpenGL defines its packed types; the pipe's unpacker produces an
unsigned byte for each, but its bit order and scaling are not read, so
those are inferred. The stream is big-endian unless the unpack state
swaps bytes, like the plain types.

Tests: each packed type decodes to the texels expected, the wrong format
for a type is no load, and swapped bytes read the word little-endian.
Both notes still said no machine fits the board and that lines, points
and cursors were undone. The Onyx2 deskside now takes the pipe, the
4Dwm desktop runs on it, and the back end draws lines and points, fog,
volume textures and the DAC cursor. The notes say so, and name what is
still open: stereo, the crosshair, the cursor on the other channels and
any comparison with hardware pixels.

KONA now lands stacked on the SN0 machine branches it joins, not after
a single Onyx2 machine PR, and only the IP27 machines refuse a
snapshot.
@jamesbraid
jamesbraid changed the base branch from main to ip27-fixes November 10, 2026 21:32
@jamesbraid
jamesbraid force-pushed the ip27-fixes branch 4 times, most recently from 3fac6c4 to ca677fd Compare November 11, 2026 02:31
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